diff --git a/API.md b/API.md index 8e4ab5194e..3a506b0202 100644 --- a/API.md +++ b/API.md @@ -8,28 +8,32 @@ iD supports several URL parameters. When constructing a URL to a standalone inst of iD (e.g. `http://preview.ideditor.com/release/`), the following parameters are available in the hash portion of the URL: -* `map` - A slash separated `zoom/latitude/longitude`. Example: - `map=20.00/38.90085/-77.02271` -* `id` - The character 'n', 'w', or 'r', followed by the OSM ID of a node, +* __`map`__ - A slash separated `zoom/latitude/longitude`.
+ _Example:_ `map=20.00/38.90085/-77.02271` +* __`id`__ - The character 'n', 'w', or 'r', followed by the OSM ID of a node, way or relation, respectively. Selects the specified entity, and, unless a `map` parameter is also provided, centers the map on it. -* `background` - The value from a `sourcetag` property in iD's +* __`background`__ - The value from a `sourcetag` property in iD's [imagery list](https://github.com/openstreetmap/iD/blob/master/data/imagery.json), or a custom tile URL. A custom URL is specified in the format `custom:`, where the URL can contain the standard tile URL placeholders `{x}`, `{y}` and `{z}`/`{zoom}`, `{ty}` for flipped TMS-style Y coordinates, and `{switch:a,b,c}` for - DNS multiplexing. Example: - `background=custom:https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png` -* `gpx` - A custom URL for loading a gpx track. Specifying a `gpx` parameter will - automatically enable the gpx layer for display. Example: - `gpx=https://tasks.hotosm.org/project/592/task/16.gpx` -* `offset` - imagery offset in meters, formatted as `east,north`. Example: - `offset=-10,5` -* `comment` - Prefills the changeset comment box, for use when integrating iD with - external task management or quality assurance tools. Example: - `comment=CAR%20crisis%2C%20refugee%20areas%20in%20Cameroon%20%23hotosm-task-592`. -* `rtl=true` - Force iD into right-to-left mode (useful for testing). -* `walkthrough=true` - Start the walkthrough automatically + DNS multiplexing.
+ _Example:_ `background=custom:https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png` +* __`gpx`__ - A custom URL for loading a gpx track. Specifying a `gpx` parameter will + automatically enable the gpx layer for display.
+ _Example:_ `gpx=https://tasks.hotosm.org/project/592/task/16.gpx` +* __`offset`__ - imagery offset in meters, formatted as `east,north`.
+ _Example:_ `offset=-10,5` +* __`comment`__ - Prefills the changeset comment. Pass a url encoded string.
+ _Example:_ `comment=CAR%20crisis%2C%20refugee%20areas%20in%20Cameroon` +* __`hashtags`__ - Prefills the changeset hashtags. Pass a url encoded 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`).
+ _Example:_ `hashtags=%23hotosm-task-592,%23MissingMaps` +* __`rtl=true`__ - Force iD into right-to-left mode (useful for testing). +* __`walkthrough=true`__ - Start the walkthrough automatically ##### iD on openstreetmap.org (Rails Port) @@ -37,14 +41,15 @@ When constructing a URL to an instance of iD embedded in the OpenStreetMap Rails Port (e.g. `http://www.openstreetmap.org/edit?editor=id`), the following parameters are available as regular URL query parameters: -* `map` - same as standalone -* `lat`, `lon`, `zoom` - Self-explanatory. -* `node`, `way`, `relation` - Select the specified entity. -* `background` - same as standalone -* `gpx` - same as standalone -* `offset` - same as standalone -* `comment` - same as standalone -* `walkthrough` - same as standalone +* __`map`__ - same as standalone +* __`lat`__, __`lon`__, __`zoom`__ - Self-explanatory. +* __`node`__, __`way`__, __`relation`__ - Select the specified entity. +* __`background`__ - same as standalone +* __`gpx`__ - same as standalone +* __`offset`__ - same as standalone +* __`comment`__ - same as standalone +* __`hashtags`__ - same as standalone +* __`walkthrough`__ - same as standalone ## CSS selectors diff --git a/css/80_app.css b/css/80_app.css index fc334eaa1c..e652cbecf1 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -225,6 +225,12 @@ input[type="radio"] { margin-right: 5px; margin-top: 3px; } +[dir='rtl'] input[type="checkbox"], +[dir='rtl'] input[type="radio"] { + float: right; + margin-left: 5px; + margin-right: 0; +} /* remove bottom border radius when combobox is open */ .combobox + * textarea:focus, @@ -453,23 +459,37 @@ button.minor:hover { .button-wrap:last-of-type { padding-right: 0; } +[dir='rtl'] .button-wrap:last-of-type { + padding-left: 0; + padding-right: 10px; +} .joined button { border-radius:0; border-right: 1px solid rgba(0,0,0,.5); } +[dir='rtl'] .joined button { + border-left: 1px solid rgba(0,0,0,.5); + border-right: none; +} .fillL .joined button { border-right: 1px solid white; } .joined button:first-child { - border-radius:4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +[dir='rtl'] .joined button:first-child { + border-radius: 0 4px 4px 0; } .joined button:last-child { border-right-width: 0; - border-radius:0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +[dir='rtl'] .joined button:last-child { + border-radius: 4px 0 0 4px; } button.action { @@ -516,6 +536,10 @@ button.save.has-count .count { margin: auto; margin-left: 9.3333%; } +[dir='rtl'] button.save.has-count .count { + margin-left: auto; + margin-right: 8%; +} button.save.has-count .count::before { content: ""; @@ -532,6 +556,12 @@ button.save.has-count .count::before { border-right-style: solid; border-right-color: inherit; } +[dir='rtl'] button.save.has-count .count::before { + border-left: 6px solid rgba(255,255,255,.5); + border-right: none; + left: auto; + right: -6px; +} /* Icons */ @@ -551,6 +581,10 @@ button.save.has-count .count::before { .icon.pre-text { margin-right: 5px; } +[dir='rtl'] .icon.pre-text { + margin-left: 5px; + margin-right: 0; +} .icon.light { color: #fff; @@ -583,14 +617,20 @@ button.save.has-count .count::before { #bar { position: fixed; padding: 10px 0; - left:0; - top:0; - right:0; - height:60px; + left: 0; + top: 0; + right: 0; + height: 60px; z-index: 9; min-width: 768px; } +[dir='rtl'] #bar .spacer, +[dir='rtl'] #bar .button-wrap, +[dir='rtl'] #bar .button-wrap button { + float: right; +} + /* Header for modals / panes ------------------------------------------------------- */ @@ -609,6 +649,10 @@ button.save.has-count .count::before { overflow: hidden; padding: 20px 20px 20px 40px; } +[dir='rtl'] .header h3 { + text-align: right; + padding: 20px 40px 20px 20px; +} .header button, .modal > button { @@ -629,12 +673,21 @@ button.save.has-count .count::before { right: 0; top: 0; } +[dir='rtl'] .entity-editor-pane .header button.preset-close, +[dir='rtl'] .preset-list-pane .header button.preset-choose { + left: 0; + right: auto; +} .entity-editor-pane .header button.preset-choose { position: absolute; left: 0; top: 0; } +[dir='rtl'] .entity-editor-pane .header button.preset-choose { + left: auto; + right: 0; +} .preset-choose { font-size: 16px; @@ -649,6 +702,10 @@ button.save.has-count .count::before { height: 60px; z-index: 50; } +[dir='rtl'] .modal > button { + left: 0; + right: unset; +} .footer { position: absolute; @@ -680,6 +737,9 @@ button.save.has-count .count::before { background: #f6f6f6; -ms-user-select: element; } +[dir='rtl'] #sidebar { + float: right; +} .sidebar-component { position: absolute; @@ -736,6 +796,10 @@ button.save.has-count .count::before { top: 80px; pointer-events: none; } +[dir='rtl'] #sidebar .search-header .icon { + left: auto; + right: 10px; +} #sidebar .search-header input { position: absolute; @@ -789,6 +853,9 @@ button.save.has-count .count::before { overflow: hidden; border-left: 1px solid rgba(0, 0, 0, .1); } +[dir='rtl'] .feature-list-item .label { + text-align: right; +} .feature-list-item .label .icon { opacity: .5; @@ -816,6 +883,11 @@ button.save.has-count .count::before { color: #666; padding-left: 10px; } +[dir='rtl'] .feature-list-item .entity-name { + padding-left: 0; + padding-right: 10px; +} + /* Presets ------------------------------------------------------- */ @@ -940,6 +1012,19 @@ button.save.has-count .count::before { height: 24px; } +[dir='rtl'] .preset-list-button-wrap .preset-icon { + left: auto; + right: auto; +} + +[dir='rtl'] .preset-list-button-wrap .preset-icon-28 { + right: 16px; +} + +[dir='rtl'] .preset-list-button-wrap .preset-icon-24 { + right: 18px; +} + .preset-list-button .label { background-color: #f6f6f6; text-align: left; @@ -955,7 +1040,15 @@ button.save.has-count .count::before { overflow: hidden; border-left: 1px solid rgba(0, 0, 0, .1); border-radius: 0 3px 3px 0; - } +} +[dir='rtl'] .preset-list-button .label { + text-align: right; + left: 0; + right: 60px; + border-left: none; + border-right: 1px solid rgba(0, 0, 0, .1); + border-radius: 3px 0 0 3px; +} .preset-list-button:hover .label { background-color: #ececec; @@ -971,6 +1064,11 @@ button.save.has-count .count::before { width: 32px; background: #fafafa; } +[dir='rtl'] .preset-list-item button.tag-reference-button { + left: 0; + right: auto; + border-radius: 3px 0 0 3px; +} .preset-list-item button.tag-reference-button:hover { background: #f1f1f1; @@ -1033,13 +1131,13 @@ button.save.has-count .count::before { margin: 0 20px 10px 20px; } -.preset-editor .preset-form { +.preset-editor .form-fields-container { padding: 10px; margin: 0 10px 10px 10px; border-radius: 8px; } -.preset-editor .preset-form:empty { +.preset-editor .form-fields-container:empty { display: none; } @@ -1272,6 +1370,9 @@ button.save.has-count .count::before { padding: 0 20px 20px 20px; font-weight: bold; } +.changeset-editor .more-fields { + padding: 15px 20px 0 20px; +} .more-fields label { display: flex; @@ -1295,6 +1396,9 @@ button.save.has-count .count::before { padding: 5px 10px; } +[dir='rtl'] .preset-input-wrap .col6 { + float: right; +} /* preset form access */ @@ -1754,6 +1858,10 @@ div.combobox { margin-left: -30px; vertical-align: top; } +[dir='rtl'] .combobox-caret { + margin-left: 0; + margin-right: -30px; +} .combobox-caret::after { content:""; @@ -1926,18 +2034,17 @@ button.minor.tag-reference-loading { clear: both; } -.tag-reference-body p, -.tag-reference-body img { - margin-top: 20px; -} - -.tag-reference-body p:last-child { - padding-bottom: 10px; +.tag-reference-body .tag-reference-description { + margin: 10px 5px 0 5px; } .tag-reference-body a { display: block; - padding-bottom: 10px; +} + +.tag-reference-body .tag-reference-description:last-child, +.tag-reference-body a:last-child { + margin-bottom: 15px; } .preset-list .tag-reference-body { @@ -1945,41 +2052,35 @@ button.minor.tag-reference-loading { width: 100%; } -.preset-list .tag-reference-body a { - padding-bottom: 20px; -} - -.preset-list .tag-reference-body p, -.preset-list .tag-reference-body img { - margin-top: 10px; -} - .raw-tag-editor .tag-reference-body { - border-bottom: 1px solid #ccc; float: left; width: 100%; } -.raw-tag-editor .tag-reference-body p:last-child { - padding-bottom: 20px; +.raw-tag-editor .tag-row.readonly .tag-reference-body { + background: #f6f6f6; + color: #333; } -.raw-tag-editor .tag-reference-body a { - padding-bottom: 20px; +.raw-tag-editor .tag-row:not(:last-child) .tag-reference-body { + border-bottom: 1px solid #ccc; } -img.wiki-image { +.raw-tag-editor .tag-row.readonly .tag-reference-body.expanded { + border-top: 1px solid #ccc; +} + +img.tag-reference-wiki-image { float: right; width: 33.3333%; width: -webkit-calc(33.3333% - 10px); width: calc(33.3333% - 10px); - margin-left: 20px; - margin-right: 10px; border-radius: 4px; max-height: 200px; - margin-bottom: 20px; + margin: 10px 5px 15px 20px; } + /* Raw relation membership editor */ .raw-member-editor .member-list li:first-child, @@ -2057,6 +2158,10 @@ div.full-screen > button:hover { position: fixed; z-index: 100; } +[dir='rtl'] .map-controls { + left: 0; + right: auto; +} .map-control > button { width: 40px; @@ -2078,6 +2183,9 @@ div.full-screen > button:hover { .zoombuttons button.zoom-in { border-radius: 4px 0 0 0; } +[dir='rtl'] .zoombuttons button.zoom-in { + border-radius: 0 4px 0 0; +} /* Background / Map Data Settings */ @@ -2088,6 +2196,9 @@ div.full-screen > button:hover { .background-control button { border-radius: 4px 0 0 0; } +[dir='rtl'] .background-control button { + border-radius: 0 4px 0 0; +} .map-data-control, .background-control { @@ -2151,6 +2262,10 @@ div.full-screen > button:hover { float: right; } +[dir='rtl'] .list-item-gpx-browse svg { + transform: rotateY(180deg); +} + /* make sure tooltip fits in map-control panel */ /* if too wide, placement will be wrong the first time it displays */ .layer-list li.best .tooltip-inner { @@ -2188,9 +2303,13 @@ div.full-screen > button:hover { .hide-toggle { display: block; - padding-left:12px; + padding-left: 12px; position: relative; } +[dir='rtl'] .hide-toggle { + padding-left: 0; + padding-right: 12px; +} .hide-toggle:before { content: ''; @@ -2204,6 +2323,12 @@ div.full-screen > button:hover { border-bottom: 4px solid transparent; border-left: 8px solid #7092ff; } +[dir='rtl'] .hide-toggle:before { + left: auto; + right: 0; + border-left: none; + border-right: 8px solid #7092ff; +} .hide-toggle.expanded:before { border-top: 8px solid #7092ff; @@ -2211,6 +2336,11 @@ div.full-screen > button:hover { border-right: 4px solid transparent; border-left: 4px solid transparent; } +[dir='rtl'] .hide-toggle.expanded:before { + border-left: 4px solid transparent; + border-right: 4px solid transparent; +} + /* Adjust Alignment controls */ @@ -2341,13 +2471,17 @@ div.full-screen > button:hover { .opacity-options { background: url(img/background-pattern-opacity.png) 0 0 repeat; - height:20px; - width:82px; + height: 20px; + width: 82px; position: absolute; right: 50px; top: 20px; border: 1px solid #ccc; } +[dir='rtl'] .opacity-options { + left: 50px; + right: auto; +} .opacity-options li { height: 100%; @@ -2357,8 +2491,8 @@ div.full-screen > button:hover { .opacity-options li .select-box{ position: absolute; - width:20px; - height:18px; + width: 20px; + height: 18px; z-index: 9999; } @@ -2393,6 +2527,12 @@ div.full-screen > button:hover { border-left: 1px solid #CCC; border-radius: 0; } +[dir='rtl'] .map-data-control .layer-list button, +[dir='rtl'] .background-control .layer-list button { + float: left; + border-left: none; + border-right: 1px solid #CCC; +} .map-data-control .layer-list button .icon, .background-control .layer-list button .icon { @@ -2403,6 +2543,10 @@ div.full-screen > button:hover { .background-control .layer-list button:first-of-type { border-radius: 0 3px 3px 0; } +[dir='rtl'] .map-data-control .layer-list button:first-of-type, +[dir='rtl'] .background-control .layer-list button:first-of-type { + border-radius: 3px 0 0 3px; +} .map-data-control .map-overlay, .background-control .map-overlay, @@ -2410,7 +2554,6 @@ div.full-screen > button:hover { z-index: -1; } - /* Geolocator */ .geolocate-control { @@ -2420,6 +2563,9 @@ div.full-screen > button:hover { .geolocate-control button { border-radius: 0 0 0 4px; } +[dir='rtl'] .geolocate-control button { + border-radius: 0 0 4px 0; +} .map-overlay.content { position: fixed; @@ -2429,12 +2575,20 @@ div.full-screen > button:hover { right: 0; overflow: auto; } +[dir='rtl'] .map-overlay.content { + padding: 20px 20px 20px 50px; + left: 0; + right: auto !important; +} /* Help */ .help-control button { border-radius: 0 0 0 4px; } +[dir='rtl'] .help-control button { + border-radius: 0 0 4px 0; +} .help-wrap p { font-size: 15px; @@ -3348,32 +3502,50 @@ img.tile-removing { float: left; height: 12px; min-width: 12px; - font-size:12px; + font-size: 12px; line-height: 12px; - border-radius:24px; - padding:5px; - background:#7092ff; - color:#fff; + border-radius: 24px; + padding: 5px; + background: #7092ff; + color: #fff; +} + +.mode-save .field-warning { + background: #ffb; + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; } +.mode-save .field-warning:empty { + display: none; +} + +.mode-save .field-warning, +.mode-save .changeset-info, +.mode-save .request-review, .mode-save .commit-info { margin-bottom: 10px; } .mode-save .changeset-list { - border:1px solid #ccc; + border: 1px solid #ccc; border-radius: 4px; - background:#fff; + background: #fff; +} + +.mode-save .warning-section { + background: #ffb; } .mode-save .warning-section .changeset-list button { - border-left: 1px solid #CCC; + border-left: 1px solid #ccc; } .mode-save .changeset-list li { position: relative; - border-top:1px solid #ccc; - padding:5px 10px; + border-top: 1px solid #ccc; + padding: 5px 10px; cursor: pointer; } @@ -3386,8 +3558,8 @@ img.tile-removing { } .changeset-list li span.count { - font-size:10px; - color:#555; + font-size: 10px; + color: #555; } .mode-save .commit-section .changeset-list button { @@ -3478,8 +3650,12 @@ img.tile-removing { } .notice .zoom-to .icon { - margin-top:10px; - margin-right:10px; + margin-top: 10px; + margin-right: 10px; +} +[dir='rtl'] .notice .zoom-to .icon { + margin-left: 10px; + margin-right: 0; } /* Tooltips @@ -3691,6 +3867,10 @@ img.tile-removing { .add-point .tooltip { left: 33.3333% !important; } +[dir='rtl'] .add-point .tooltip { + left: inherit !important; +} + .add-point .tooltip .tooltip-arrow { left: 60px; } @@ -4023,204 +4203,3 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { color: #7092FF; } - -/* Right-to-left localization settings */ - -[dir='rtl'] #sidebar { - float: right; -} - -[dir='rtl'] #sidebar .search-header .icon { - left: auto; - right: 10px; -} - -/* header */ -[dir='rtl'] .header h3 { - text-align: right; - padding: 20px 40px 20px 20px; -} - -[dir='rtl'] .entity-editor-pane .header button.preset-choose { - left: auto; - right: 0; -} - -[dir='rtl'] .entity-editor-pane .header button.preset-close, [dir='rtl'] .preset-list-pane .header button.preset-choose { - left: 0; - right: auto; -} - -[dir='rtl'] .map-data-control .layer-list button, [dir='rtl'] .background-control .layer-list button { - float: left; - border-left: none; - border-right: 1px solid #CCC; -} - -[dir='rtl'] .map-data-control .layer-list button:first-of-type, [dir='rtl'] .background-control .layer-list button:first-of-type { - border-radius: 3px 0 0 3px; -} - -/* search */ -[dir='rtl'] .feature-list-item .label { - text-align: right; -} - -[dir='rtl'] .feature-list-item .entity-name { - padding-left: 0; - padding-right: 10px; -} - -/* preset form */ - -[dir='rtl'] .combobox-caret { - margin-left: 0; - margin-right: -30px; -} - -[dir='rtl'] .icon.pre-text { - margin-left: 5px; - margin-right: 0; -} - -[dir='rtl'] .notice .zoom-to .icon { - margin-left: 10px; - margin-right: 0; -} - -[dir='rtl'] .preset-list-button .label { - text-align: right; - left: 0; - right: 60px; - border-left: none; - border-right: 1px solid rgba(0, 0, 0, .1); - border-radius: 3px 0 0 3px; -} - -[dir='rtl'] .preset-list-item button.tag-reference-button { - left: 0; - right: auto; - border-radius: 3px 0 0 3px; -} - -[dir='rtl'] .preset-list-button-wrap .preset-icon { - left: auto; - right: auto; -} - -[dir='rtl'] .preset-list-button-wrap .preset-icon-28 { - right: 16px; -} - -[dir='rtl'] .preset-list-button-wrap .preset-icon-24 { - right: 18px; -} - -[dir='rtl'] input[type="checkbox"], [dir='rtl'] input[type="radio"] { - float: right; - margin-left: 5px; - margin-right: 0; -} - -[dir='rtl'] .preset-input-wrap .col6 { - float: right; -} - -/* map control buttons */ -[dir='rtl'] .map-controls { - left: 0; - right: auto; -} - -[dir='rtl'] .background-control button, -[dir='rtl'] .zoombuttons button.zoom-in { - border-radius: 0 4px 0 0; -} - -[dir='rtl'] .help-control button, -[dir='rtl'] .geolocate-control button { - border-radius: 0 0 4px 0; -} - -[dir='rtl'] .list-item-gpx-browse svg { - transform: rotateY(180deg); -} - -/* map control button overlays */ -[dir='rtl'] .map-overlay { - padding: 20px 20px 20px 50px; - left: 0; - right: auto !important; -} - -[dir='rtl'] .opacity-options { - left: 50px; - right: auto; -} - -[dir='rtl'] .hide-toggle { - padding-left: 0; - padding-right: 12px; -} - -[dir='rtl'] .hide-toggle:before { - left: auto; - right: 0; - border-left: none; - border-right: 8px solid #7092ff; -} - -[dir='rtl'] .hide-toggle.expanded:before { - border-left: 4px solid transparent; - border-right: 4px solid transparent; -} - -/* navbar */ -[dir='rtl'] #bar .spacer, -[dir='rtl'] #bar .button-wrap, -[dir='rtl'] #bar .button-wrap button { - float: right; -} - -[dir='rtl'] .add-point .tooltip { - left: inherit !important; -} - -[dir='rtl'] .button-wrap:last-of-type { - padding-left: 0; - padding-right: 10px; -} - -[dir='rtl'] button.save.has-count .count { - margin-left: auto; - margin-right: 8%; -} - -[dir='rtl'] button.save.has-count .count::before { - border-left: 6px solid rgba(255,255,255,.5); - border-right: none; - left: auto; - right: -6px; -} - -[dir='rtl'] .joined button { - border-left: 1px solid rgba(0,0,0,.5); - border-right: none; -} - -[dir='rtl'] .joined button:first-child { - border-radius: 0 4px 4px 0; -} - -[dir='rtl'] .joined button:last-child { - border-radius: 4px 0 0 4px; -} - -/* modal */ -[dir='rtl'] .modal > button { - position: absolute; - left: 0; - right: unset; - top: 0; -} - diff --git a/data/core.yaml b/data/core.yaml index 44767d2fa8..9f45ae26c3 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -254,10 +254,9 @@ en: rateLimit: The API is limiting anonymous connections. You can fix this by logging in. commit: title: Upload to OpenStreetMap - description_placeholder: Brief description of your contributions (required) - message_label: Changeset Comment upload_explanation: "The changes you upload will be visible on all maps that use OpenStreetMap data." upload_explanation_with_user: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data." + request_review: "I would like someone to review my edits." save: Upload cancel: Cancel changes: "{count} Changes" diff --git a/data/presets.yaml b/data/presets.yaml index 5895a11b2c..aede647a20 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -339,6 +339,11 @@ en: collection_times: # collection_times=* label: Collection Times + comment: + # comment=* + label: Changeset Comment + # comment field placeholder + placeholder: Brief description of your contributions (required) communication_multi: # 'communication:=*' label: Communication Types @@ -571,6 +576,11 @@ en: handrail: # handrail=* label: Handrail + hashtags: + # hashtags=* + label: Hashtags + # hashtags field placeholder + placeholder: '#example' height: # height=* label: Height (Meters) @@ -1218,7 +1228,7 @@ en: label: People Served source: # source=* - label: Source + label: Sources sport: # sport=* label: Sports diff --git a/data/presets/fields.json b/data/presets/fields.json index e099f5416d..2a0ea2a53e 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -437,6 +437,12 @@ "type": "text", "label": "Collection Times" }, + "comment": { + "key": "comment", + "type": "textarea", + "label": "Changeset Comment", + "placeholder": "Brief description of your contributions (required)" + }, "communication_multi": { "key": "communication:", "type": "multiCombo", @@ -788,6 +794,12 @@ "type": "check", "label": "Handrail" }, + "hashtags": { + "key": "hashtags", + "type": "semiCombo", + "label": "Hashtags", + "placeholder": "#example" + }, "height": { "key": "height", "type": "number", @@ -1624,10 +1636,17 @@ }, "source": { "key": "source", - "type": "text", + "type": "semiCombo", "icon": "source", "universal": true, - "label": "Source" + "label": "Sources", + "options": [ + "survey", + "local knowledge", + "gps", + "aerial imagery", + "streetlevel imagery" + ] }, "sport_ice": { "key": "sport", diff --git a/data/presets/fields/comment.json b/data/presets/fields/comment.json new file mode 100644 index 0000000000..71072e1d45 --- /dev/null +++ b/data/presets/fields/comment.json @@ -0,0 +1,6 @@ +{ + "key": "comment", + "type": "textarea", + "label": "Changeset Comment", + "placeholder": "Brief description of your contributions (required)" +} diff --git a/data/presets/fields/hashtags.json b/data/presets/fields/hashtags.json new file mode 100644 index 0000000000..d3045507c3 --- /dev/null +++ b/data/presets/fields/hashtags.json @@ -0,0 +1,6 @@ +{ + "key": "hashtags", + "type": "semiCombo", + "label": "Hashtags", + "placeholder": "#example" +} diff --git a/data/presets/fields/source.json b/data/presets/fields/source.json index 0cad9fa73a..c9b746500c 100644 --- a/data/presets/fields/source.json +++ b/data/presets/fields/source.json @@ -1,7 +1,14 @@ { "key": "source", - "type": "text", + "type": "semiCombo", "icon": "source", "universal": true, - "label": "Source" -} \ No newline at end of file + "label": "Sources", + "options": [ + "survey", + "local knowledge", + "gps", + "aerial imagery", + "streetlevel imagery" + ] +} diff --git a/dist/locales/en.json b/dist/locales/en.json index 2f4cfb3f95..618af1d786 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -328,10 +328,9 @@ }, "commit": { "title": "Upload to OpenStreetMap", - "description_placeholder": "Brief description of your contributions (required)", - "message_label": "Changeset Comment", "upload_explanation": "The changes you upload will be visible on all maps that use OpenStreetMap data.", "upload_explanation_with_user": "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + "request_review": "I would like someone to review my edits.", "save": "Upload", "cancel": "Cancel", "changes": "{count} Changes", @@ -1354,6 +1353,10 @@ "collection_times": { "label": "Collection Times" }, + "comment": { + "label": "Changeset Comment", + "placeholder": "Brief description of your contributions (required)" + }, "communication_multi": { "label": "Communication Types" }, @@ -1566,6 +1569,10 @@ "handrail": { "label": "Handrail" }, + "hashtags": { + "label": "Hashtags", + "placeholder": "#example" + }, "height": { "label": "Height (Meters)" }, @@ -2094,7 +2101,7 @@ "label": "Type" }, "source": { - "label": "Source" + "label": "Sources" }, "sport_ice": { "label": "Sports" diff --git a/modules/behavior/hash.js b/modules/behavior/hash.js index 431594a7c9..63cece2dfa 100644 --- a/modules/behavior/hash.js +++ b/modules/behavior/hash.js @@ -37,7 +37,9 @@ export function behaviorHash(context) { var center = map.center(), zoom = map.zoom(), precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)), - q = _.omit(utilStringQs(window.location.hash.substring(1)), ['comment', 'walkthrough']), + q = _.omit(utilStringQs(window.location.hash.substring(1)), + ['comment', 'hashtags', 'walkthrough'] + ), newParams = {}; delete q.id; @@ -99,6 +101,10 @@ export function behaviorHash(context) { context.storage('commentDate', Date.now()); } + if (q.hashtags) { + context.storage('hashtags', q.hashtags); + } + if (q.walkthrough === 'true') { hash.startWalkthrough = true; } diff --git a/modules/modes/save.js b/modules/modes/save.js index 766f667799..391fe2f47a 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -1,6 +1,7 @@ import * as d3 from 'd3'; import _ from 'lodash'; +import { d3keybinding } from '../lib/d3.keybinding.js'; import { t } from '../util/locale'; import { JXON } from '../util/jxon'; @@ -34,6 +35,8 @@ export function modeSave(context) { id: 'save' }; + var keybinding = d3keybinding('select'); + var commit = uiCommit(context) .on('cancel', cancel) .on('save', save); @@ -360,6 +363,12 @@ export function modeSave(context) { context.ui().sidebar.show(commit); } + keybinding + .on('⎋', cancel, true); + + d3.select(document) + .call(keybinding); + context.container().selectAll('#content') .attr('class', 'inactive'); @@ -381,6 +390,8 @@ export function modeSave(context) { mode.exit = function() { + keybinding.off(); + context.container().selectAll('#content') .attr('class', 'active'); diff --git a/modules/ui/changeset_editor.js b/modules/ui/changeset_editor.js new file mode 100644 index 0000000000..f04b47e3d7 --- /dev/null +++ b/modules/ui/changeset_editor.js @@ -0,0 +1,141 @@ +import * as d3 from 'd3'; +import _ from 'lodash'; +import { d3combobox } from '../lib/d3.combobox.js'; +import { t } from '../util/locale'; +import { svgIcon } from '../svg'; +import { uiField } from './field'; +import { uiFormFields } from './form_fields'; +import { utilRebind, utilTriggerEvent } from '../util'; + + +export function uiChangesetEditor(context) { + var dispatch = d3.dispatch('change'), + formFields = uiFormFields(context), + fieldsArr, + tags, + changesetId; + + + + function changesetEditor(selection) { + render(selection); + } + + + function render(selection) { + var initial = false; + + if (!fieldsArr) { + initial = true; + var presets = context.presets(); + + fieldsArr = [ + uiField(context, presets.field('comment'), null, { show: true, revert: false }), + uiField(context, presets.field('source'), null, { show: false, revert: false }), + uiField(context, presets.field('hashtags'), null, { show: false, revert: false }), + ]; + + fieldsArr.forEach(function(field) { + field + .on('change', function(t, onInput) { + dispatch.call('change', field, t, onInput); + }); + }); + } + + fieldsArr.forEach(function(field) { + field + .tags(tags); + }); + + + selection + .call(formFields.fieldsArr(fieldsArr)); + + + if (initial) { + var commentField = selection.select('#preset-input-comment'), + commentNode = commentField.node(); + + if (commentNode) { + commentNode.focus(); + commentNode.select(); + } + + // trigger a 'blur' event so that comment field can be cleaned + // and checked for hashtags, even if retrieved from localstorage + utilTriggerEvent(commentField, 'blur'); + + var osm = context.connection(); + if (osm) { + osm.userChangesets(function (err, changesets) { + if (err) return; + + var comments = changesets.map(function(changeset) { + return { + title: changeset.tags.comment, + value: changeset.tags.comment + }; + }); + + commentField + .call(d3combobox() + .container(context.container()) + .caseSensitive(true) + .data(_.uniqBy(comments, 'title')) + ); + }); + } + } + + // Add warning if comment mentions Google + var hasGoogle = tags.comment.match(/google/i); + var commentWarning = selection.select('.form-field-comment').selectAll('.comment-warning') + .data(hasGoogle ? [0] : []); + + commentWarning.exit() + .transition() + .duration(200) + .style('opacity', 0) + .remove(); + + var commentEnter = commentWarning.enter() + .insert('div', '.tag-reference-body') + .attr('class', 'field-warning comment-warning') + .style('opacity', 0); + + commentEnter + .append('a') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#icon-alert', 'inline')) + .attr('href', t('commit.google_warning_link')) + .append('span') + .text(t('commit.google_warning')); + + commentEnter + .transition() + .duration(200) + .style('opacity', 1); + } + + + changesetEditor.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + // Don't reset fieldsArr here. + return changesetEditor; + }; + + + changesetEditor.changesetID = function(_) { + if (!arguments.length) return changesetId; + if (changesetId === _) return changesetEditor; + changesetId = _; + fieldsArr = null; + return changesetEditor; + }; + + + return utilRebind(changesetEditor, dispatch, 'on'); +} diff --git a/modules/ui/commit.js b/modules/ui/commit.js index 088d282e70..9c54a6d4fa 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -1,187 +1,140 @@ import * as d3 from 'd3'; import _ from 'lodash'; import { t } from '../util/locale'; -import { d3combobox } from '../lib/d3.combobox.js'; import { osmChangeset } from '../osm'; -import { modeSelect } from '../modes'; -import { svgIcon } from '../svg'; -import { tooltip } from '../util/tooltip'; +import { uiChangesetEditor } from './changeset_editor'; +import { uiCommitChanges } from './commit_changes'; +import { uiCommitWarnings } from './commit_warnings'; import { uiRawTagEditor } from './raw_tag_editor'; import { utilDetect } from '../util/detect'; -import { - utilDisplayName, - utilDisplayType, - utilEntityOrMemberSelector, - utilRebind, - utilTriggerEvent -} from '../util'; +import { utilRebind } from '../util'; var changeset; -var readOnlyTags = ['created_by', 'imagery_used', 'host', 'locale']; +var readOnlyTags = [ + /^changesets_count$/, + /^created_by$/, + /^ideditor:/, + /^imagery_used$/, + /^host$/, + /^locale$/ +]; export function uiCommit(context) { - var dispatch = d3.dispatch('cancel', 'save'); + var dispatch = d3.dispatch('cancel', 'save'), + userDetails, + _selection; + + var changesetEditor = uiChangesetEditor(context) + .on('change', changeTags); + var rawTagEditor = uiRawTagEditor(context) + .on('change', changeTags); + var commitChanges = uiCommitChanges(context); + var commitWarnings = uiCommitWarnings(context); function commit(selection) { + _selection = selection; + var osm = context.connection(); if (!osm) return; - if (!changeset) { - var detected = utilDetect(), - tags = { - created_by: ('iD ' + context.version).substr(0, 255), - imagery_used: context.history().imageryUsed().join(';').substr(0, 255), - host: detected.host.substr(0, 255), - locale: detected.locale.substr(0, 255) - }; - - changeset = new osmChangeset({ tags: tags }); - } - - - var changes = context.history().changes(), - summary = context.history().difference().summary(), - rawTagEditor = uiRawTagEditor(context).on('change', changeTags), - comment = context.storage('comment') || '', + var comment = context.storage('comment') || '', commentDate = +context.storage('commentDate') || 0, + hashtags = context.storage('hashtags'), currDate = Date.now(), cutoff = 2 * 86400 * 1000; // 2 days - // expire the stored comment if it is too old - #3947 + // expire stored comment and hashtags after cutoff datetime - #3947 if (commentDate > currDate || currDate - commentDate > cutoff) { comment = ''; + hashtags = undefined; } - selection - .append('div') - .attr('class', 'header fillL') - .append('h3') - .text(t('commit.title')); - - var body = selection - .append('div') - .attr('class', 'body'); - - var commentSection = body - .append('div') - .attr('class', 'modal-section form-field commit-form'); - - commentSection - .append('label') - .attr('class', 'form-label') - .text(t('commit.message_label')); - - var commentField = commentSection - .append('textarea') - .attr('class', 'commit-form-comment') - .attr('placeholder', t('commit.description_placeholder')) - .attr('maxlength', 255) - .property('value', comment) - .on('input.save', change(true)) - .on('change.save', change()) - .on('blur.save', function() { - context.storage('comment', this.value); - context.storage('commentDate', Date.now()); - }); - - - commentField.node().select(); - - osm.userChangesets(function (err, changesets) { - if (err) return; - - var comments = changesets.map(function(changeset) { - return { - title: changeset.tags.comment, - value: changeset.tags.comment - }; - }); - - commentField - .call(d3combobox() - .container(context.container()) - .caseSensitive(true) - .data(_.uniqBy(comments, 'title')) - ); - }); - - var clippyArea = commentSection.append('div') - .attr('class', 'clippy-area'); + var tags; + if (!changeset) { + var detected = utilDetect(); + tags = { + comment: comment, + created_by: ('iD ' + context.version).substr(0, 255), + imagery_used: context.history().imageryUsed().join(';').substr(0, 255), + host: detected.host.substr(0, 255), + locale: detected.locale.substr(0, 255) + }; - var changeSetInfo = commentSection.append('div') - .attr('class', 'changeset-info'); + if (hashtags) { + tags.hashtags = hashtags; + } - changeSetInfo.append('a') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#icon-out-link', 'inline')) - .attr('href', t('commit.about_changeset_comments_link')) - .append('span') - .text(t('commit.about_changeset_comments')); + changeset = new osmChangeset({ tags: tags }); + } + tags = _.clone(changeset.tags); - // Warnings - var warnings = body.selectAll('div.warning-section') - .data([context.history().validate(changes)]); + var header = selection.selectAll('.header') + .data([0]); - warnings = warnings.enter() + header.enter() .append('div') - .attr('class', 'modal-section warning-section fillL2') - .style('display', function(d) { return _.isEmpty(d) ? 'none' : null; }) - .style('background', '#ffb') - .merge(warnings); - - warnings + .attr('class', 'header fillL') .append('h3') - .text(t('commit.warnings')); + .text(t('commit.title')); - warnings - .append('ul') - .attr('class', 'changeset-list'); + var body = selection.selectAll('.body') + .data([0]); - var warningLi = warnings.select('ul').selectAll('li') - .data(function(d) { return d; }); + body = body.enter() + .append('div') + .attr('class', 'body') + .merge(body); - warningLi = warningLi.enter() - .append('li') - .on('mouseover', mouseover) - .on('mouseout', mouseout) - .on('click', warningClick) - .merge(warningLi); - warningLi - .call(svgIcon('#icon-alert', 'pre-text')); + // Changeset Section + var changesetSection = body.selectAll('.changeset-editor') + .data([0]); - warningLi - .append('strong') - .text(function(d) { return d.message; }); + changesetSection = changesetSection.enter() + .append('div') + .attr('class', 'modal-section changeset-editor') + .merge(changesetSection); - warningLi.filter(function(d) { return d.tooltip; }) - .call(tooltip() - .title(function(d) { return d.tooltip; }) - .placement('top') + changesetSection + .call(changesetEditor + .changesetID(changeset.id) + .tags(tags) ); + // Warnings + body.call(commitWarnings); + + // Upload Explanation - var saveSection = body + var saveSection = body.selectAll('.save-section') + .data([0]); + + saveSection = saveSection.enter() .append('div') - .attr('class','modal-section save-section fillL cf'); + .attr('class','modal-section save-section fillL cf') + .merge(saveSection); - var prose = saveSection + var prose = saveSection.selectAll('.commit-info') + .data([0]); + + prose = prose.enter() .append('p') .attr('class', 'commit-info') - .html(t('commit.upload_explanation')); - + .text(t('commit.upload_explanation')) + .merge(prose); osm.userDetails(function(err, user) { if (err) return; var userLink = d3.select(document.createElement('div')); + userDetails = user; + if (user.image_url) { userLink .append('img') @@ -202,217 +155,217 @@ export function uiCommit(context) { }); + var requestReview = saveSection.selectAll('.request-review') + .data([0]); + + requestReview = requestReview.enter() + .append('p') + .attr('class', 'request-review') + .text(t('commit.request_review')) + .merge(requestReview); + + var requestReviewField = requestReview.selectAll('input') + .data([0]); + + requestReviewField = requestReviewField.enter() + .append('input') + .attr('type', 'checkbox') + .merge(requestReviewField); + + requestReviewField + .property('checked', isReviewRequested(changeset.tags)) + .on('change', toggleRequestReview); + + // Buttons - var buttonSection = saveSection + var buttonSection = saveSection.selectAll('.buttons') + .data([0]); + + // enter + var buttonEnter = buttonSection.enter() .append('div') .attr('class', 'buttons fillL cf'); - var cancelButton = buttonSection + buttonEnter .append('button') .attr('class', 'secondary-action col5 button cancel-button') - .on('click.cancel', function() { - dispatch.call('cancel'); - }); - - cancelButton .append('span') .attr('class', 'label') .text(t('commit.cancel')); - var saveButton = buttonSection + buttonEnter .append('button') .attr('class', 'action col5 button save-button') + .append('span') + .attr('class', 'label') + .text(t('commit.save')); + + // update + buttonSection = buttonSection + .merge(buttonEnter); + + buttonSection.selectAll('.cancel-button') + .on('click.cancel', function() { + dispatch.call('cancel'); + }); + + buttonSection.selectAll('.save-button') .attr('disabled', function() { - var n = d3.select('.commit-form textarea').node(); + var n = d3.select('#preset-input-comment').node(); return (n && n.value.length) ? null : true; }) .on('click.save', function() { dispatch.call('save', this, changeset); }); - saveButton - .append('span') - .attr('class', 'label') - .text(t('commit.save')); - // Raw Tag Editor - var tagSection = body - .append('div') - .attr('class', 'modal-section tag-section raw-tag-editor'); + var tagSection = body.selectAll('.tag-section.raw-tag-editor') + .data([0]); - - // Changes - var changeSection = body + tagSection = tagSection.enter() .append('div') - .attr('class', 'commit-section modal-section fillL2'); - - changeSection.append('h3') - .text(t('commit.changes', { count: summary.length })); - - var li = changeSection - .append('ul') - .attr('class', 'changeset-list') - .selectAll('li') - .data(summary); - - li = li.enter() - .append('li') - .on('mouseover', mouseover) - .on('mouseout', mouseout) - .on('click', zoomToEntity) - .merge(li); - - li.each(function(d) { - d3.select(this) - .call(svgIcon('#icon-' + d.entity.geometry(d.graph), 'pre-text ' + d.changeType)); - }); + .attr('class', 'modal-section tag-section raw-tag-editor') + .merge(tagSection); + + var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty(); + tagSection + .call(rawTagEditor + .expanded(expanded) + .readOnlyTags(readOnlyTags) + .tags(_.clone(changeset.tags)) + ); - li.append('span') - .attr('class', 'change-type') - .text(function(d) { return t('commit.' + d.changeType) + ' '; }); - li.append('strong') - .attr('class', 'entity-type') - .text(function(d) { - var matched = context.presets().match(d.entity, d.graph); - return (matched && matched.name()) || utilDisplayType(d.entity.id); - }); + // Change summary + body.call(commitChanges); - li.append('span') - .attr('class', 'entity-name') - .text(function(d) { - var name = utilDisplayName(d.entity) || '', - string = ''; - if (name !== '') string += ':'; - return string += ' ' + name; - }); - li.style('opacity', 0) - .transition() - .style('opacity', 1); + function toggleRequestReview() { + var rr = requestReviewField.property('checked'); + updateChangeset({ review_requested: (rr ? 'yes' : undefined) }); + var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty(); - // Call change() off the bat, in case a changeset - // comment is recovered from localStorage - utilTriggerEvent(commentField, 'input'); + tagSection + .call(rawTagEditor + .expanded(expanded) + .readOnlyTags(readOnlyTags) + .tags(_.clone(changeset.tags)) + ); + } + } - function mouseover(d) { - if (d.entity) { - context.surface().selectAll( - utilEntityOrMemberSelector([d.entity.id], context.graph()) - ).classed('hover', true); + function changeTags(changed, onInput) { + if (changed.hasOwnProperty('comment')) { + if (changed.comment === undefined) { + changed.comment = ''; + } + if (!onInput) { + context.storage('comment', changed.comment); + context.storage('commentDate', Date.now()); } } + updateChangeset(changed, onInput); - function mouseout() { - context.surface().selectAll('.hover') - .classed('hover', false); + if (_selection) { + _selection.call(commit); } + } - function warningClick(d) { - if (d.entity) { - context.map().zoomTo(d.entity); - context.enter(modeSelect(context, [d.entity.id])); - } - } - + function findHashtags(tags) { + return _.unionBy(commentTags(), hashTags(), function (s) { + return s.toLowerCase(); + }); - function zoomToEntity(change) { - var entity = change.entity; - if (change.changeType !== 'deleted' && - context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') { - context.map().zoomTo(entity); - context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph())) - .classed('hover', true); - } + // Extract hashtags from `comment` + function commentTags() { + return tags.comment.match(/#[^\s\#]+/g); } - - function checkComment(comment) { - // Save button disabled if there is no comment.. - d3.selectAll('.save-section .save-button') - .attr('disabled', (comment.length ? null : true)); - - // Warn if comment mentions Google.. - var googleWarning = clippyArea - .html('') - .selectAll('a') - .data(comment.match(/google/i) ? [true] : []); - - googleWarning.exit() - .remove(); - - googleWarning.enter() - .append('a') - .attr('target', '_blank') - .attr('tabindex', -1) - .call(svgIcon('#icon-alert', 'inline')) - .attr('href', t('commit.google_warning_link')) - .append('span') - .text(t('commit.google_warning')); + // Extract and clean hashtags from `hashtags` + function hashTags() { + var t = tags.hashtags || ''; + return t + .split(/[,;\s]+/) + .map(function (s) { + if (s[0] !== '#') { s = '#' + s; } // prepend '#' + var matched = s.match(/#[^\s\#]+/g); // match valid hashtags + return matched && matched[0]; + }).filter(Boolean); // exclude falsey } + } - function change(onInput) { - return function() { - var comment = commentField.property('value').trim(); - if (!onInput) { - commentField.property('value', comment); - } - - checkComment(comment); + function isReviewRequested(tags) { + var rr = tags.review_requested; + if (rr === undefined) return false; + rr = rr.trim().toLowerCase(); + return !(rr === '' || rr === 'no'); + } - var changeset = updateChangeset({ comment: comment }); - var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty(); - tagSection - .call(rawTagEditor - .expanded(expanded) - .readOnlyTags(readOnlyTags) - .tags(_.clone(changeset.tags)) - ); - }; - } + function updateChangeset(changed, onInput) { + var tags = _.clone(changeset.tags); + _.forEach(changed, function(v, k) { + k = k.trim().substr(0, 255); + if (readOnlyTags.indexOf(k) !== -1) return; - function changeTags(changed) { - if (changed.hasOwnProperty('comment')) { - if (changed.comment === undefined) { - changed.comment = ''; + if (k !== '' && v !== undefined) { + if (onInput) { + tags[k] = v; + } else { + tags[k] = v.trim().substr(0, 255); } - changed.comment = changed.comment.trim(); - commentField.property('value', changed.comment); + } else { + delete tags[k]; } - updateChangeset(changed); - utilTriggerEvent(commentField, 'input'); - } + }); + if (!onInput) { + var arr = findHashtags(tags); + if (arr.length) { + tags.hashtags = arr.join(';').substr(0, 255); + context.storage('hashtags', tags.hashtags); + } else { + delete tags.hashtags; + context.storage('hashtags', null); + } + } - function updateChangeset(changed) { - var tags = _.clone(changeset.tags); + // always update userdetails, just in case user reauthenticates as someone else + if (userDetails && userDetails.changesets_count !== undefined) { + tags.changesets_count = String(userDetails.changesets_count); - _.forEach(changed, function(v, k) { - k = k.trim().substr(0, 255); - if (readOnlyTags.indexOf(k) !== -1) return; + // first 100 edits - new user + if (parseInt(tags.changesets_count, 10) < 100) { + var s; + s = context.storage('walkthrough_completed'); + if (s) { + tags['ideditor:walkthrough_completed'] = s; + } - if (k !== '' && v !== undefined) { - tags[k] = v.trim().substr(0, 255); - } else { - delete tags[k]; + s = context.storage('walkthrough_progress'); + if (s) { + tags['ideditor:walkthrough_progress'] = s; } - }); - if (!_.isEqual(changeset.tags, tags)) { - changeset = changeset.update({ tags: tags }); + s = context.storage('walkthrough_started'); + if (s) { + tags['ideditor:walkthrough_started'] = s; + } } - - return changeset; + } else { + delete tags.changesets_count; } + if (!_.isEqual(changeset.tags, tags)) { + changeset = changeset.update({ tags: tags }); + } } diff --git a/modules/ui/commit_changes.js b/modules/ui/commit_changes.js new file mode 100644 index 0000000000..e47fd81b2e --- /dev/null +++ b/modules/ui/commit_changes.js @@ -0,0 +1,116 @@ +import * as d3 from 'd3'; +import { t } from '../util/locale'; +import { svgIcon } from '../svg'; +import { + utilDisplayName, + utilDisplayType, + utilEntityOrMemberSelector +} from '../util'; + + +export function uiCommitChanges(context) { + + function commitChanges(selection) { + + var summary = context.history().difference().summary(); + + var container = selection.selectAll('.modal-section.commit-section') + .data([0]); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'commit-section modal-section fillL2'); + + containerEnter + .append('h3') + .text(t('commit.changes', { count: summary.length })); + + containerEnter + .append('ul') + .attr('class', 'changeset-list'); + + container = containerEnter + .merge(container); + + + var items = container.select('ul').selectAll('li') + .data(summary); + + var itemsEnter = items.enter() + .append('li') + .attr('class', 'change-item'); + + itemsEnter + .each(function(d) { + d3.select(this) + .call(svgIcon('#icon-' + d.entity.geometry(d.graph), 'pre-text ' + d.changeType)); + }); + + itemsEnter + .append('span') + .attr('class', 'change-type') + .text(function(d) { return t('commit.' + d.changeType) + ' '; }); + + itemsEnter + .append('strong') + .attr('class', 'entity-type') + .text(function(d) { + var matched = context.presets().match(d.entity, d.graph); + return (matched && matched.name()) || utilDisplayType(d.entity.id); + }); + + itemsEnter + .append('span') + .attr('class', 'entity-name') + .text(function(d) { + var name = utilDisplayName(d.entity) || '', + string = ''; + if (name !== '') { + string += ':'; + } + return string += ' ' + name; + }); + + itemsEnter + .style('opacity', 0) + .transition() + .style('opacity', 1); + + items = itemsEnter + .merge(items); + + items + .on('mouseover', mouseover) + .on('mouseout', mouseout) + .on('click', zoomToEntity); + + + function mouseover(d) { + if (d.entity) { + context.surface().selectAll( + utilEntityOrMemberSelector([d.entity.id], context.graph()) + ).classed('hover', true); + } + } + + + function mouseout() { + context.surface().selectAll('.hover') + .classed('hover', false); + } + + + function zoomToEntity(change) { + var entity = change.entity; + if (change.changeType !== 'deleted' && + context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') { + context.map().zoomTo(entity); + context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph())) + .classed('hover', true); + } + } + } + + + return commitChanges; +} diff --git a/modules/ui/commit_warnings.js b/modules/ui/commit_warnings.js new file mode 100644 index 0000000000..1dfba52cb7 --- /dev/null +++ b/modules/ui/commit_warnings.js @@ -0,0 +1,95 @@ +import { t } from '../util/locale'; +import { modeSelect } from '../modes'; +import { svgIcon } from '../svg'; +import { tooltip } from '../util/tooltip'; +import { utilEntityOrMemberSelector } from '../util'; + + +export function uiCommitWarnings(context) { + + function commitWarnings(selection) { + + var changes = context.history().changes(); + var warnings = context.history().validate(changes); + + var container = selection.selectAll('.warning-section') + .data(warnings.length ? [0] : []); + + container.exit() + .remove(); + + var containerEnter = container.enter() + .append('div') + .attr('class', 'modal-section warning-section fillL2'); + + containerEnter + .append('h3') + .text(t('commit.warnings')); + + containerEnter + .append('ul') + .attr('class', 'changeset-list'); + + container = containerEnter + .merge(container); + + + var items = container.select('ul').selectAll('li') + .data(warnings); + + items.exit() + .remove(); + + var itemsEnter = items.enter() + .append('li') + .attr('class', 'warning-item'); + + itemsEnter + .call(svgIcon('#icon-alert', 'pre-text')); + + itemsEnter + .append('strong') + .text(function(d) { return d.message; }); + + itemsEnter.filter(function(d) { return d.tooltip; }) + .call(tooltip() + .title(function(d) { return d.tooltip; }) + .placement('top') + ); + + items = itemsEnter + .merge(items); + + items + .on('mouseover', mouseover) + .on('mouseout', mouseout) + .on('click', warningClick); + + + function mouseover(d) { + if (d.entity) { + context.surface().selectAll( + utilEntityOrMemberSelector([d.entity.id], context.graph()) + ).classed('hover', true); + } + } + + + function mouseout() { + context.surface().selectAll('.hover') + .classed('hover', false); + } + + + function warningClick(d) { + if (d.entity) { + context.map().zoomTo(d.entity); + context.enter(modeSelect(context, [d.entity.id])); + } + } + + } + + + return commitWarnings; +} diff --git a/modules/ui/field.js b/modules/ui/field.js index 148369f1d0..ee70f98c55 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -10,7 +10,10 @@ import { utilRebind } from '../util'; export function uiField(context, presetField, entity, options) { options = _.extend({ show: true, - wrap: true + wrap: true, + remove: true, + revert: true, + info: true }, options); var dispatch = d3.dispatch('change'), @@ -24,7 +27,7 @@ export function uiField(context, presetField, entity, options) { dispatch.call('change', field, t, onInput); }); - if (field.impl.entity) { + if (entity && field.impl.entity) { field.impl.entity(entity); } @@ -34,6 +37,7 @@ export function uiField(context, presetField, entity, options) { function isModified() { + if (!entity) return false; var original = context.graph().base().entities[entity.id]; return _.some(field.keys, function(key) { return original ? tags[key] !== original.tags[key] : tags[key]; @@ -51,6 +55,7 @@ export function uiField(context, presetField, entity, options) { function revert(d) { d3.event.stopPropagation(); d3.event.preventDefault(); + if (!entity) return false; var original = context.graph().base().entities[entity.id], t = {}; @@ -96,19 +101,23 @@ export function uiField(context, presetField, entity, options) { .append('div') .attr('class', 'form-label-button-wrap'); - wrap - .append('button') - .attr('class', 'remove-icon') - .attr('tabindex', -1) - .call(svgIcon('#operation-delete')); - - wrap - .append('button') - .attr('class', 'modified-icon') - .attr('tabindex', -1) - .call( - (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') - ); + if (options.remove) { + wrap + .append('button') + .attr('class', 'remove-icon') + .attr('tabindex', -1) + .call(svgIcon('#operation-delete')); + } + + if (options.revert) { + wrap + .append('button') + .attr('class', 'modified-icon') + .attr('tabindex', -1) + .call( + (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') + ); + } } @@ -126,7 +135,7 @@ export function uiField(context, presetField, entity, options) { .classed('modified', isModified()) .classed('present', isPresent()) .each(function(d) { - if (options.wrap) { + if (options.wrap && options.info) { var referenceKey = d.key; if (d.type === 'multiCombo') { // lookup key without the trailing ':' referenceKey = referenceKey.replace(/:$/, ''); @@ -141,7 +150,7 @@ export function uiField(context, presetField, entity, options) { d3.select(this) .call(d.impl); - if (options.wrap) { + if (options.wrap && options.info) { d3.select(this) .call(reference.body) .select('.form-label-button-wrap') diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index 1f61bf0bd0..baef52aa07 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -161,12 +161,17 @@ export function uiFieldCombo(field, context) { query = country + ':'; } - taginfo[fn]({ + var params = { debounce: (q !== ''), key: field.key, - geometry: context.geometry(entity.id), query: query - }, function(err, data) { + }; + + if (entity) { + params.geometry = context.geometry(entity.id); + } + + taginfo[fn](params, function(err, data) { if (err) return; if (hasCountryPrefix) { data = _.filter(data, function(d) { diff --git a/modules/ui/form_fields.js b/modules/ui/form_fields.js new file mode 100644 index 0000000000..bcb2345580 --- /dev/null +++ b/modules/ui/form_fields.js @@ -0,0 +1,122 @@ +import * as d3 from 'd3'; +import { d3combobox } from '../lib/d3.combobox.js'; +import { t } from '../util/locale'; +import { utilGetSetValue, utilNoAuto } from '../util'; + + +export function uiFormFields(context) { + var fieldsArr; + + + function formFields(selection, klass) { + render(selection, klass); + } + + + function render(selection, klass) { + + var shown = fieldsArr.filter(function(field) { return field.isShown(); }), + notShown = fieldsArr.filter(function(field) { return !field.isShown(); }); + + var container = selection.selectAll('.form-fields-container') + .data([0]); + + container = container.enter() + .append('div') + .attr('class', 'form-fields-container ' + (klass || '')) + .merge(container); + + + var fields = container.selectAll('.wrap-form-field') + .data(shown, function(d) { return d.id; }); + + fields.exit() + .remove(); + + // Enter + var enter = fields.enter() + .append('div') + .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.id; }); + + // Update + fields = fields + .merge(enter); + + fields + .order() + .each(function(d) { + d3.select(this) + .call(d.render); + }); + + + notShown = notShown.map(function(field) { + return { + title: field.label(), + value: field.label(), + field: field + }; + }); + + + var more = selection.selectAll('.more-fields') + .data((notShown.length > 0) ? [0] : []); + + more.exit() + .remove(); + + more = more.enter() + .append('div') + .attr('class', 'more-fields') + .append('label') + .text(t('inspector.add_fields')) + .merge(more); + + + var input = more.selectAll('.value') + .data([0]); + + input.exit() + .remove(); + + input = input.enter() + .append('input') + .attr('class', 'value') + .attr('type', 'text') + .call(utilNoAuto) + .merge(input); + + input + .call(utilGetSetValue, '') + .attr('placeholder', function() { + var placeholder = []; + for (var field in notShown) { + placeholder.push(notShown[field].title); + } + return placeholder.slice(0,3).join(', ') + ((placeholder.length > 3) ? '…' : ''); + }) + .call(d3combobox() + .container(context.container()) + .data(notShown) + .minItems(1) + .on('accept', function (d) { + var field = d.field; + field.show = true; + render(selection); + if (field.type !== 'semiCombo' && field.type !== 'multiCombo') { + field.focus(); + } + }) + ); + } + + + formFields.fieldsArr = function(_) { + if (!arguments.length) return fieldsArr; + fieldsArr = _; + return formFields; + }; + + + return formFields; +} diff --git a/modules/ui/index.js b/modules/ui/index.js index 3abe1ebb27..742aa7b1ca 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -2,8 +2,11 @@ export { uiInit } from './init'; export { uiAccount } from './account'; export { uiAttribution } from './attribution'; export { uiBackground } from './background'; +export { uiChangesetEditor } from './changeset_editor'; export { uiCmd } from './cmd'; export { uiCommit } from './commit'; +export { uiCommitChanges } from './commit_changes'; +export { uiCommitWarnings } from './commit_warnings'; export { uiConfirm } from './confirm'; export { uiConflicts } from './conflicts'; export { uiContributors } from './contributors'; @@ -15,6 +18,7 @@ export { uiFeatureInfo } from './feature_info'; export { uiFeatureList } from './feature_list'; export { uiField } from './field'; export { uiFlash } from './flash'; +export { uiFormFields } from './form_fields'; export { uiFullScreen } from './full_screen'; export { uiGeolocate } from './geolocate'; export { uiHelp } from './help'; diff --git a/modules/ui/intro/intro.js b/modules/ui/intro/intro.js index bb351c452a..f87bb9b162 100644 --- a/modules/ui/intro/intro.js +++ b/modules/ui/intro/intro.js @@ -1,4 +1,5 @@ import * as d3 from 'd3'; +import _ from 'lodash'; import { t, textDirection } from '../../util/locale'; import { localize } from './helper'; @@ -88,6 +89,13 @@ export function uiIntro(context) { var curtain = uiCurtain(); selection.call(curtain); + // store that the user started the walkthrough.. + context.storage('walkthrough_started', 'yes'); + + // restore previous walkthrough progress.. + var storedProgress = context.storage('walkthrough_progress') || ''; + var progress = storedProgress.split(';').filter(Boolean); + var chapters = chapterFlow.map(function(chapter, i) { var s = chapterUi[chapter](context, curtain.reveal) .on('done', function() { @@ -102,11 +110,25 @@ export function uiIntro(context) { d3.select('button.chapter-' + next) .classed('next', true); } + + // store walkthrough progress.. + progress.push(chapter); + context.storage('walkthrough_progress', _.uniq(progress).join(';')); }); return s; }); chapters[chapters.length - 1].on('startEditing', function() { + // store walkthrough progress.. + progress.push('startEditing'); + context.storage('walkthrough_progress', _.uniq(progress).join(';')); + + // store if walkthrough is completed.. + var incomplete = _.difference(chapterFlow, progress); + if (!incomplete.length) { + context.storage('walkthrough_completed', 'yes'); + } + curtain.remove(); navwrap.remove(); d3.selectAll('#map .layer-background').style('opacity', opacity); diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js index beeb67cc5d..c597256c56 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/preset_editor.js @@ -1,18 +1,15 @@ import * as d3 from 'd3'; -import { d3combobox } from '../lib/d3.combobox.js'; import { t } from '../util/locale'; import { modeBrowse } from '../modes'; import { uiDisclosure } from './disclosure'; import { uiField } from './field'; -import { - utilGetSetValue, - utilNoAuto, - utilRebind -} from '../util'; +import { uiFormFields } from './form_fields'; +import { utilRebind } from '../util'; export function uiPresetEditor(context) { var dispatch = d3.dispatch('change'), + formFields = uiFormFields(context), expandedPreference = (context.storage('preset_fields.expanded') !== 'false'), state, fieldsArr, @@ -80,106 +77,18 @@ export function uiPresetEditor(context) { .tags(tags); }); - var shown = fieldsArr.filter(function(field) { return field.isShown(); }), - notShown = fieldsArr.filter(function(field) { return !field.isShown(); }); + selection + .call(formFields.fieldsArr(fieldsArr), 'inspector-inner fillL3'); - var form = selection.selectAll('.preset-form') - .data([0]); - form = form.enter() - .append('div') - .attr('class', 'preset-form inspector-inner fillL3') - .merge(form); - - - var fields = form.selectAll('.wrap-form-field') - .data(shown, function(d) { return d.id; }); - - fields.exit() - .remove(); - - // Enter - var enter = fields.enter() - .append('div') - .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.id; }); - - // Update - fields = fields - .merge(enter); - - fields - .order() - .each(function(d) { - d3.select(this) - .call(d.render) - .selectAll('input') - .on('keydown', function() { - // if user presses enter, and combobox is not active, accept edits.. - if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) { - context.enter(modeBrowse(context)); - } - }); - }); - - - notShown = notShown.map(function(field) { - return { - title: field.label(), - value: field.label(), - field: field - }; - }); - - - var more = selection.selectAll('.more-fields') - .data((notShown.length > 0) ? [0] : []); - - more.exit() - .remove(); - - more = more.enter() - .append('div') - .attr('class', 'more-fields') - .append('label') - .text(t('inspector.add_fields')) - .merge(more); - - - var input = more.selectAll('.value') - .data([0]); - - input.exit() - .remove(); - - input = input.enter() - .append('input') - .attr('class', 'value') - .attr('type', 'text') - .call(utilNoAuto) - .merge(input); - - input - .call(utilGetSetValue, '') - .attr('placeholder', function() { - var placeholder = []; - for (var field in notShown) { - placeholder.push(notShown[field].title); + selection.selectAll('.wrap-form-field input') + .on('keydown', function() { + // if user presses enter, and combobox is not active, accept edits.. + if (d3.event.keyCode === 13 && d3.select('.combobox').empty()) { + context.enter(modeBrowse(context)); } - return placeholder.slice(0,3).join(', ') + ((placeholder.length > 3) ? '…' : ''); - }) - .call(d3combobox() - .container(context.container()) - .data(notShown) - .minItems(1) - .on('accept', function (d) { - var field = d.field; - field.show = true; - render(selection); - field.focus(); - }) - ); - + }); } diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index c3bbb4f8a1..2991895d6e 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -175,7 +175,12 @@ export function uiRawTagEditor(context) { function isReadOnly(d) { - return readOnlyTags.indexOf(d.key) !== -1; + for (var i = 0; i < readOnlyTags.length; i++) { + if (d.key.match(readOnlyTags[i]) !== null) { + return true; + } + } + return false; } diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 77531839fc..b5e5e5ec87 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -9,8 +9,8 @@ import { svgIcon } from '../svg/index'; export function uiTagReference(tag) { var taginfo = services.taginfo, tagReference = {}, - button, - body, + button = d3.select(null), + body = d3.select(null), loaded, showing; @@ -46,7 +46,8 @@ export function uiTagReference(tag) { function load(param) { if (!taginfo) return; - button.classed('tag-reference-loading', true); + button + .classed('tag-reference-loading', true); taginfo.docs(param, function show(err, data) { var docs; @@ -56,22 +57,23 @@ export function uiTagReference(tag) { body.html(''); - if (!docs || !docs.title) { if (param.hasOwnProperty('value')) { load(_.omit(param, 'value')); // retry with key only } else { - body.append('p').text(t('inspector.no_documentation_key')); + body + .append('p') + .attr('class', 'tag-reference-description') + .text(t('inspector.no_documentation_key')); done(); } return; } - if (docs.image && docs.image.thumb_url_prefix) { body .append('img') - .attr('class', 'wiki-image') + .attr('class', 'tag-reference-wiki-image') .attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix) .on('load', function() { done(); }) .on('error', function() { d3.select(this).remove(); done(); }); @@ -81,16 +83,31 @@ export function uiTagReference(tag) { body .append('p') + .attr('class', 'tag-reference-description') .text(docs.description || t('inspector.documentation_redirect')); body .append('a') + .attr('class', 'tag-reference-link') .attr('target', '_blank') .attr('tabindex', -1) .attr('href', 'https://wiki.openstreetmap.org/wiki/' + docs.title) .call(svgIcon('#icon-out-link', 'inline')) .append('span') .text(t('inspector.reference')); + + // Add link to info about "good changeset comments" - #2923 + if (param.key === 'comment') { + body + .append('a') + .attr('class', 'tag-reference-comment-link') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#icon-out-link', 'inline')) + .attr('href', t('commit.about_changeset_comments_link')) + .append('span') + .text(t('commit.about_changeset_comments')); + } }); } @@ -98,9 +115,12 @@ export function uiTagReference(tag) { function done() { loaded = true; - button.classed('tag-reference-loading', false); + button + .classed('tag-reference-loading', false); - body.transition() + body + .classed('expanded', true) + .transition() .duration(200) .style('max-height', '200px') .style('opacity', '1'); @@ -109,12 +129,15 @@ export function uiTagReference(tag) { } - function hide(selection) { - selection = selection || body.transition().duration(200); - - selection + function hide() { + body + .transition() + .duration(200) .style('max-height', '0px') - .style('opacity', '0'); + .style('opacity', '0') + .on('end', function () { + body.classed('expanded', false); + }); showing = false; } @@ -158,7 +181,7 @@ export function uiTagReference(tag) { .merge(body); if (showing === false) { - hide(body); + hide(); } };