From 521c1f9799d291261a35af8223f1c558147d7e5b Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Mon, 16 Mar 2020 17:46:51 -0700 Subject: [PATCH 01/43] Re-implement v3 toolbar on the latest working branch --- css/80_app.css | 296 ++++++++- data/core.yaml | 40 +- dist/locales/en.json | 54 +- modules/behavior/add_way.js | 20 +- modules/behavior/draw_way.js | 69 +- modules/modes/add_area.js | 87 ++- modules/modes/add_line.js | 92 ++- modules/modes/add_point.js | 85 ++- modules/modes/draw_area.js | 53 +- modules/modes/draw_line.js | 72 ++- modules/operations/continue.js | 2 +- modules/osm/tags.js | 32 + modules/presets/index.js | 153 ++++- modules/presets/preset.js | 31 + modules/ui/index.js | 2 + modules/ui/preset_browser.js | 684 ++++++++++++++++++++ modules/ui/preset_favorite_button.js | 54 ++ modules/ui/preset_icon.js | 10 +- modules/ui/tools/add_feature.js | 163 +++++ modules/ui/tools/adding_geometry.js | 95 +++ modules/ui/tools/index.js | 14 +- modules/ui/tools/modes.js | 160 ----- modules/ui/tools/notes.js | 133 ++-- modules/ui/tools/power_support.js | 77 +++ modules/ui/tools/quick_presets.js | 354 ++++++++++ modules/ui/tools/quick_presets_addable.js | 33 + modules/ui/tools/quick_presets_favorites.js | 37 ++ modules/ui/tools/quick_presets_generic.js | 32 + modules/ui/tools/quick_presets_recent.js | 74 +++ modules/ui/tools/repeat_add.js | 74 +++ modules/ui/tools/save.js | 65 +- modules/ui/tools/segmented.js | 115 ++++ modules/ui/tools/sidebar_toggle.js | 31 - modules/ui/tools/simple_button.js | 40 ++ modules/ui/tools/stop_draw.js | 65 ++ modules/ui/tools/structure.js | 192 ++++++ modules/ui/tools/toolbox.js | 172 +++++ modules/ui/tools/undo_redo.js | 81 ++- modules/ui/tools/way_segments.js | 44 ++ modules/ui/top_toolbar.js | 143 +++- modules/validations/disconnected_way.js | 2 +- modules/validations/impossible_oneway.js | 2 +- scripts/build_data.js | 3 +- svg/fontawesome/fas-toolbox.svg | 1 + svg/iD-sprite/icons/icon-repeat.svg | 4 + svg/iD-sprite/tools/presets-grid.svg | 4 + svg/iD-sprite/tools/segment-orthogonal.svg | 4 + svg/iD-sprite/tools/segment-straight.svg | 4 + svg/iD-sprite/tools/structure-none.svg | 4 + 49 files changed, 3533 insertions(+), 525 deletions(-) create mode 100644 modules/ui/preset_browser.js create mode 100644 modules/ui/preset_favorite_button.js create mode 100644 modules/ui/tools/add_feature.js create mode 100644 modules/ui/tools/adding_geometry.js delete mode 100644 modules/ui/tools/modes.js create mode 100644 modules/ui/tools/power_support.js create mode 100644 modules/ui/tools/quick_presets.js create mode 100644 modules/ui/tools/quick_presets_addable.js create mode 100644 modules/ui/tools/quick_presets_favorites.js create mode 100644 modules/ui/tools/quick_presets_generic.js create mode 100644 modules/ui/tools/quick_presets_recent.js create mode 100644 modules/ui/tools/repeat_add.js create mode 100644 modules/ui/tools/segmented.js delete mode 100644 modules/ui/tools/sidebar_toggle.js create mode 100644 modules/ui/tools/simple_button.js create mode 100644 modules/ui/tools/stop_draw.js create mode 100644 modules/ui/tools/structure.js create mode 100644 modules/ui/tools/toolbox.js create mode 100644 modules/ui/tools/way_segments.js create mode 100644 svg/fontawesome/fas-toolbox.svg create mode 100644 svg/iD-sprite/icons/icon-repeat.svg create mode 100644 svg/iD-sprite/tools/presets-grid.svg create mode 100644 svg/iD-sprite/tools/segment-orthogonal.svg create mode 100644 svg/iD-sprite/tools/segment-straight.svg create mode 100644 svg/iD-sprite/tools/structure-none.svg diff --git a/css/80_app.css b/css/80_app.css index 8b723d9ddd..52c6474176 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -389,6 +389,10 @@ button[disabled].action:hover { width: 20px; height: 20px; } +.icon-30 { + width: 30px; + height: 30px; +} .icon.monochrome use { fill: currentColor; @@ -518,20 +522,48 @@ button.bar-button { white-space: nowrap; display: flex; } +button.bar-button.wide { + padding: 0 15px; +} button.bar-button .icon { - flex: 0 0 20px; + flex: 0 0 auto; } button.bar-button .label { flex: 0 1 auto; padding: 0 5px; } +.toolbar-item button.bar-button .disclosure-icon { + width: 18px; + color: rgba(0, 0, 0, 0.5); +} +[dir='ltr'] .toolbar-item .disclosure-icon { + margin-left: 6px; + margin-right: -2px; +} +[dir='rtl'] .toolbar-item .disclosure-icon { + margin-left: -2px; + margin-right: 6px; +} +[dir='ltr'] .toolbar-item.add-feature .disclosure-icon { + margin-left: 4px; +} +[dir='rtl'] .toolbar-item.add-feature .disclosure-icon { + margin-right: 4px; +} + +#bar .drag-placeholder { + width: 41px; + height: 40px; + visibility: hidden; +} button.bar-button.dragging { opacity: 0.75; z-index: 200; } +.toolbar-item.disclosing button.bar-button.active .tooltip, button.bar-button.dragging .tooltip { - display: none; + visibility: hidden; } button.bar-button.dragging.removing { cursor: url(img/cursor-select-remove.png), pointer; @@ -586,15 +618,9 @@ button.add-note svg.icon { -ms-filter: "FlipH"; } - -#bar.narrow .spinner, #bar.narrow button.bar-button .label { display: none; } -#bar.narrow button .count { - border-left-width: 0; - border-right-width: 0; -} [dir='ltr'] .undo-redo button:first-of-type { margin-right: 1px; @@ -5281,6 +5307,260 @@ li.hide + li.version .badge .tooltip .popover-arrow { } +/* Poplist +------------------------------------------------------- */ +.poplist { + border: 0.5px solid #DCDCDC; + border-radius: 6px; + max-height: 600px; + min-width: 160px; + max-width: 325px; + flex-direction: column; + -webkit-box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); + -moz-box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); + box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); +} +.poplist.in { + display: flex; +} +.poplist input[type='search'] { + position: relative; + width: 100%; + height: 100%; + border: none; + font-size: 14px; + text-indent: 25px; + padding: 5px 10px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} +.poplist input[type='search'], +.poplist input[type='search']:focus { + background: #f6f6f6; +} +.poplist .search-icon { + color: #333; + display: block; + position: absolute; + left: 10px; + top: 10px; + pointer-events: none; +} +[dir='rtl'] .poplist .search-icon { + left: auto; + right: 10px; +} +.poplist .poplist-content { + overflow-y: auto; + max-height: 60vh; +} +.poplist-content:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.poplist-content:last-child { + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; +} +/* ensure corners are rounded in Chrome +.poplist .poplist-content { + -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); +} + */ +.poplist::-webkit-scrollbar { + /* don't overlap rounded corners */ + background: transparent; +} +.poplist .list { + height: 100%; +} +.poplist .list-item > .row { + display: flex; + position: relative; + padding: 2px; + min-height: 40px; + align-items: center; +} +.poplist .list-item:not(:last-of-type) .row, +.poplist .subsection.subitems .list-item .row { + border-bottom: 1px solid #DCDCDC; +} +.poplist .list-item .item-icon { + flex: 0 0 auto; + position: relative; + padding: 0 8px; +} +.poplist .list-item .item-icon.icon-30 { + padding: 0 3px; +} +.poplist .list-item .label { + font-weight: bold; + font-size: 12px; + padding-left: 2px; + top: 0; + bottom: 0; + position: relative; + display: flex; + align-items: center; + line-height: 1.3em; + width: 100%; +} +.poplist .list-item .label .namepart:nth-child(2) { + font-weight: normal; +} +.poplist .list-item.disabled .preset-icon-container, +.poplist .list-item.disabled .label { + opacity: 0.55; +} +[dir='ltr'] .poplist .list-item .label .icon.inline { + margin-left: 0; +} +[dir='rtl'] .poplist .list-item .label .icon.inline { + margin-right: 0; +} +.poplist .list-item .row > *:not(button) { + pointer-events: none; +} +.poplist .list-item button.choose { + position: absolute; + border-radius: 0; + height: 100%; + width: 100%; + top: 0; + left: 0; +} +.poplist .list-item button.choose:hover, +.poplist .list-item button.choose:focus { + background: #fff; +} +.poplist .list-item.focused:not(.disabled) button.choose { + background: #e8ebff; +} +.poplist .list-item button.choose.disabled { + background-color: #ececec; +} +.poplist .subsection .list-item button.choose { + opacity: 0.85; +} +.poplist .list-item .accessory { + position: relative; + flex: 0 0 auto; + color: #808080; + background: transparent; + padding-right: 3px; + padding-left: 3px; +} +.poplist .list-item button.accessory:hover { + color: #666; +} +.poplist .list-item .checkmark { + color: #7092ff; + padding: 0 3px; +} +.poplist .subsection { + background-color: #CBCBCB; + display: flex; + flex-direction: column; +} +[dir='ltr'] .poplist .subitems { + padding-left: 6px; +} +[dir='rtl'] .poplist .subitems { + padding-right: 6px; +} +/* Preset browser +------------------------------------------------------- */ +.preset-browser.poplist { + min-width: 300px; +} +.assistant .preset-browser.poplist { + top: 84px; + max-height: 300px +} +.assistant .preset-browser.poplist .poplist-content { + max-height: 30vh; +} +[dir='ltr'] .assistant .preset-browser.poplist { + left: 20px; +} +[dir='rtl'] .assistant .preset-browser.poplist { + right: 20px; +} +.preset-browser .poplist-header { + height: 40px; + border-bottom: 2px solid #DCDCDC; + flex: 0 0 auto; +} +.preset-browser .poplist-footer { + padding: 5px 10px 5px 10px; + background: #f6f6f6; + border-top: 1px solid #DCDCDC; + flex: 0 0 auto; + display: flex; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} +.preset-browser .poplist-footer .message { + color: #666666; + flex-grow: 1; +} +.preset-browser .poplist-footer button.filter { + height: 20px; + background: transparent; + color: #666; +} +.preset-browser .poplist-footer button.filter.active { + color: #7092ff; +} +.preset-browser .poplist-footer button.filter:hover { + color: #333; +} +.preset-browser .poplist-footer button.filter.active:hover { + color: #597be7; +} +.preset-browser .subsection .tag-reference-body { + background: rgba(255, 255, 255, 0.85); + padding: 10px; +} +.preset-browser .list-item button.tag-reference-open path { + fill: #000; +} +.preset-browser .subsection > .tag-reference-body { + border-bottom: 1px solid #DCDCDC; +} + +button.preset-favorite-button .icon { + fill-opacity: 0; + stroke-width: 1; +} +button.preset-favorite-button.active .icon { + fill-opacity: inherit; +} + +/* Add a preset mode buttons +------------------------------------------------------- */ +button.bar-button.add-preset { + border-radius: 4px; +} +[dir='ltr'] button.bar-button.add-preset { + margin-left: 1px; +} +[dir='rtl'] button.bar-button.add-preset { + margin-right: 1px; +} +[dir='ltr'] button.bar-button.add-preset.first-recent { + margin-left: 10px; +} +[dir='rtl'] button.bar-button.add-preset.first-recent { + margin-right: 10px; +} +button.bar-button.add-preset { + padding: 0; +} +button.add-preset.disabled .preset-icon-container { + opacity: 0.5; +} + /* Contextual Edit Menu ------------------------------------------------------- */ .edit-menu-tooltip { diff --git a/data/core.yaml b/data/core.yaml index 1539f0d7a1..12cf2a7454 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -12,16 +12,48 @@ en: text: text deselect: deselect toolbar: - inspect: Inspect undo_redo: Undo / Redo recent: Recent favorites: Favorites add_feature: Add Feature + finish: Finish + generic: + title: Geometries + geometry: + key: T + repeat: + title: Repeat + tooltip: + point: "Add another {feature} after this one." + way: "Start another {feature} after finishing this one." + key: R + segments: + title: Segments + straight: + title: Straight + orthogonal: + title: Rectangular + key: A + structure: + none: + title: None + key: S + support: + title: Support + pole: + title: Pole + tower: + title: Tower + key: S + toolbox: + title: Tools + tooltip: Customize the toolbar. modes: add_feature: - title: Add a feature - description: "Search for features to add to the map." - key: Tab + search_placeholder: Search feature types + description: "Browse features to add to the map." + # The hotkey to open the Add Feature preset browser. Expect the key adjacent to the number row. + key: "`" result: "{count} result" results: "{count} results" add_area: diff --git a/dist/locales/en.json b/dist/locales/en.json index b7d55ab423..1d8e1ddfff 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -14,17 +14,61 @@ "deselect": "deselect" }, "toolbar": { - "inspect": "Inspect", "undo_redo": "Undo / Redo", "recent": "Recent", "favorites": "Favorites", - "add_feature": "Add Feature" + "add_feature": "Add Feature", + "finish": "Finish", + "generic": { + "title": "Geometries" + }, + "geometry": { + "key": "T" + }, + "repeat": { + "title": "Repeat", + "tooltip": { + "point": "Add another {feature} after this one.", + "way": "Start another {feature} after finishing this one." + }, + "key": "R" + }, + "segments": { + "title": "Segments", + "straight": { + "title": "Straight" + }, + "orthogonal": { + "title": "Rectangular" + }, + "key": "A" + }, + "structure": { + "none": { + "title": "None" + }, + "key": "S" + }, + "support": { + "title": "Support", + "pole": { + "title": "Pole" + }, + "tower": { + "title": "Tower" + }, + "key": "S" + }, + "toolbox": { + "title": "Tools", + "tooltip": "Customize the toolbar." + } }, "modes": { "add_feature": { - "title": "Add a feature", - "description": "Search for features to add to the map.", - "key": "Tab", + "search_placeholder": "Search feature types", + "description": "Browse features to add to the map.", + "key": "`", "result": "{count} result", "results": "{count} results" }, diff --git a/modules/behavior/add_way.js b/modules/behavior/add_way.js index a4b0461ae6..8ab1432d2b 100644 --- a/modules/behavior/add_way.js +++ b/modules/behavior/add_way.js @@ -6,15 +6,15 @@ import { utilRebind } from '../util/rebind'; export function behaviorAddWay(context) { - var dispatch = d3_dispatch('start', 'startFromWay', 'startFromNode'); + var dispatch = d3_dispatch('start', 'startFromWay', 'startFromNode', 'cancel', 'finish'); var draw = behaviorDraw(context); function behavior(surface) { draw.on('click', function() { dispatch.apply('start', this, arguments); }) .on('clickWay', function() { dispatch.apply('startFromWay', this, arguments); }) .on('clickNode', function() { dispatch.apply('startFromNode', this, arguments); }) - .on('cancel', behavior.cancel) - .on('finish', behavior.cancel); + .on('cancel', function() { dispatch.apply('cancel', this, arguments); }) + .on('finish', function() { dispatch.apply('finish', this, arguments); }); context.map() .dblclickZoomEnable(false); @@ -22,26 +22,16 @@ export function behaviorAddWay(context) { surface.call(draw); } - behavior.off = function(surface) { - surface.call(draw.off); - }; - + context.map().dblclickZoomEnable(true); - behavior.cancel = function() { - window.setTimeout(function() { - context.map().dblclickZoomEnable(true); - }, 1000); - - context.enter(modeBrowse(context)); + surface.call(draw.off); }; - behavior.tail = function(text) { draw.tail(text); return behavior; }; - return utilRebind(behavior, dispatch, 'on'); } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 421eb0ef86..8a2477a995 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -11,15 +11,24 @@ import { actionMoveNode } from '../actions/move_node'; import { actionNoop } from '../actions/noop'; import { behaviorDraw } from './draw'; import { geoChooseEdge, geoHasSelfIntersections } from '../geo'; -import { modeBrowse } from '../modes/browse'; -import { modeSelect } from '../modes/select'; import { osmNode } from '../osm/node'; import { utilRebind } from '../util/rebind'; import { utilKeybinding } from '../util'; -export function behaviorDrawWay(context, wayID, index, mode, startGraph) { +export function behaviorDrawWay(context, wayID, index, startGraph) { - var dispatch = d3_dispatch('rejectedSelfIntersection'); + var dispatch = d3_dispatch( + // completed the drawing session + 'finish', + // aborted the drawing session; graph was reset + 'revert', + + // this particular segment is complete; drawing should continue + 'doneSegment', + + // the drawing of a self-intersecting way was attempted but not performed + 'rejectedSelfIntersection' + ); var _origWay = context.entity(wayID); @@ -197,21 +206,17 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { context.pauseChangeDispatch(); - var nextMode; + var shouldContinue = false; - if (context.graph() === startGraph) { // we've undone back to the beginning - nextMode = modeSelect(context, [wayID]); - } else { + if (context.graph() !== startGraph) { // not yet at the beginning context.history() .on('undone.draw', null); // remove whatever segment was drawn previously context.undo(); - if (context.graph() === startGraph) { // we've undone back to the beginning - nextMode = modeSelect(context, [wayID]); - } else { + if (context.graph() !== startGraph) { // not yet at the beginning // continue drawing - nextMode = mode; + shouldContinue = true; } } @@ -220,7 +225,12 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { context.pop(1); context.resumeChangeDispatch(); - context.enter(nextMode); + + if (shouldContinue) { + dispatch.call('doneSegment', this); + } else { + dispatch.call('finish', this); + } } @@ -246,7 +256,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { .on('clickWay', drawWay.addWay) .on('clickNode', drawWay.addNode) .on('undo', context.undo) - .on('cancel', drawWay.cancel) + .on('cancel', drawWay.revert) .on('finish', drawWay.finish); d3_select(window) @@ -279,6 +289,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { } context.map() + .dblclickZoomEnable(true) .on('drawn.draw', null); surface.call(behavior.off) @@ -322,12 +333,12 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { } context.pauseChangeDispatch(); - doAdd(); + var finalNode = doAdd(); // we just replaced the temporary edit with the real one _didResolveTempEdit = true; context.resumeChangeDispatch(); - context.enter(mode); + dispatch.call('doneSegment', this, context.entity(finalNode.id)); } @@ -335,6 +346,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { drawWay.add = function(loc, d) { attemptAdd(d, loc, function() { // don't need to do anything extra + return _drawNode; }); }; @@ -346,6 +358,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { actionAddMidpoint({ loc: loc, edge: edge }, _drawNode), _annotation ); + return _drawNode; }); }; @@ -366,6 +379,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { }, _annotation ); + return node; }); }; @@ -377,7 +391,7 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { checkGeometry(false /* includeDrawNode */); if (context.surface().classed('nope')) { dispatch.call('rejectedSelfIntersection', this); - return; // can't click here + return false; // can't click here } context.pauseChangeDispatch(); @@ -388,35 +402,28 @@ export function behaviorDrawWay(context, wayID, index, mode, startGraph) { var way = context.hasEntity(wayID); if (!way || way.isDegenerate()) { - drawWay.cancel(); - return; + drawWay.revert(); + return true; } - window.setTimeout(function() { - context.map().dblclickZoomEnable(true); - }, 1000); + dispatch.call('finish', this); - var isNewFeature = !mode.isContinuing; - context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature)); + return true; }; - // Cancel the draw operation, delete everything, and return to browse mode. - drawWay.cancel = function() { + // Cancel drawing and reset everything from this draw session + drawWay.revert = function() { context.pauseChangeDispatch(); resetToStartGraph(); context.resumeChangeDispatch(); - window.setTimeout(function() { - context.map().dblclickZoomEnable(true); - }, 1000); - context.surface() .classed('nope', false) .classed('nope-disabled', false) .classed('nope-suppressed', false); - context.enter(modeBrowse(context)); + dispatch.call('revert', this); }; diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js index b20e78e2c0..56870e8c65 100644 --- a/modules/modes/add_area.js +++ b/modules/modes/add_area.js @@ -4,22 +4,52 @@ import { actionAddMidpoint } from '../actions/add_midpoint'; import { actionAddVertex } from '../actions/add_vertex'; import { behaviorAddWay } from '../behavior/add_way'; +import { modeBrowse } from './browse'; import { modeDrawArea } from './draw_area'; +import { modeSelect } from './select'; import { osmNode, osmWay } from '../osm'; export function modeAddArea(context, mode) { mode.id = 'add-area'; - var behavior = behaviorAddWay(context) + var _baselineGraph = context.graph(); + + var _behavior = behaviorAddWay(context) .tail(t('modes.add_area.tail')) .on('start', start) .on('startFromWay', startFromWay) - .on('startFromNode', startFromNode); + .on('startFromNode', startFromNode) + .on('cancel', function() { + context.enter(modeBrowse(context)); + }) + .on('finish', function() { + mode.finish(); + }); + + mode.defaultTags = { area: 'yes' }; + if (mode.preset) mode.defaultTags = mode.preset.setTags(mode.defaultTags, 'area'); + + var _repeatAddedFeature = false; + var _allAddedEntityIDs = []; + + mode.repeatAddedFeature = function(val) { + if (!arguments.length || val === undefined) return _repeatAddedFeature; + _repeatAddedFeature = val; + return mode; + }; - var defaultTags = { area: 'yes' }; - if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area'); + mode.addedEntityIDs = function() { + return _allAddedEntityIDs.filter(function(id) { + return context.hasEntity(id); + }); + }; + mode.addAddedEntityID = function(entityID) { + if (_allAddedEntityIDs.indexOf(entityID) === -1) { + _allAddedEntityIDs.push(entityID); + } + }; function actionClose(wayId) { return function (graph) { @@ -27,11 +57,10 @@ export function modeAddArea(context, mode) { }; } - function start(loc) { var startGraph = context.graph(); var node = osmNode({ loc: loc }); - var way = osmWay({ tags: defaultTags }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(node), @@ -40,14 +69,13 @@ export function modeAddArea(context, mode) { actionClose(way.id) ); - context.enter(modeDrawArea(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } - function startFromWay(loc, edge) { var startGraph = context.graph(); var node = osmNode({ loc: loc }); - var way = osmWay({ tags: defaultTags }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(node), @@ -57,13 +85,12 @@ export function modeAddArea(context, mode) { actionAddMidpoint({ loc: loc, edge: edge }, node) ); - context.enter(modeDrawArea(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } - function startFromNode(node) { var startGraph = context.graph(); - var way = osmWay({ tags: defaultTags }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(way), @@ -71,19 +98,45 @@ export function modeAddArea(context, mode) { actionClose(way.id) ); - context.enter(modeDrawArea(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } + function enterDrawMode(way, startGraph) { + _allAddedEntityIDs.push(way.id); + var drawMode = modeDrawArea(context, way.id, startGraph, mode.button, mode); + context.enter(drawMode); + } - mode.enter = function() { - context.install(behavior); + mode.finish = function() { + if (mode.addedEntityIDs().length) { + context.enter( + modeSelect(context, mode.addedEntityIDs()) + .newFeature(true) + ); + } else { + context.enter( + modeBrowse(context) + ); + } }; + function undone() { + if (context.graph() === _baselineGraph || mode.addedEntityIDs().length === 0) { + context.enter(modeBrowse(context)); + } + } - mode.exit = function() { - context.uninstall(behavior); + mode.enter = function() { + context.install(_behavior); + context.history() + .on('undone.add_area', undone); }; + mode.exit = function() { + context.history() + .on('undone.add_area', null); + context.uninstall(_behavior); + }; return mode; } diff --git a/modules/modes/add_line.js b/modules/modes/add_line.js index d0dc748d2e..c873978909 100644 --- a/modules/modes/add_line.js +++ b/modules/modes/add_line.js @@ -4,27 +4,59 @@ import { actionAddMidpoint } from '../actions/add_midpoint'; import { actionAddVertex } from '../actions/add_vertex'; import { behaviorAddWay } from '../behavior/add_way'; +import { modeBrowse } from './browse'; import { modeDrawLine } from './draw_line'; +import { modeSelect } from './select'; import { osmNode, osmWay } from '../osm'; export function modeAddLine(context, mode) { mode.id = 'add-line'; - var behavior = behaviorAddWay(context) + var _baselineGraph = context.graph(); + + var _behavior = behaviorAddWay(context) .tail(t('modes.add_line.tail')) .on('start', start) .on('startFromWay', startFromWay) - .on('startFromNode', startFromNode); + .on('startFromNode', startFromNode) + .on('cancel', function() { + context.enter(modeBrowse(context)); + }) + .on('finish', function() { + mode.finish(); + }); + + mode.defaultTags = {}; + if (mode.preset) mode.defaultTags = mode.preset.setTags(mode.defaultTags, 'line'); + + var _repeatAddedFeature = false; + var _allAddedEntityIDs = []; + + mode.repeatAddedFeature = function(val) { + if (!arguments.length || val === undefined) return _repeatAddedFeature; + _repeatAddedFeature = val; + return mode; + }; - var defaultTags = {}; - if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line'); + mode.addedEntityIDs = function() { + return _allAddedEntityIDs.filter(function(id) { + return context.hasEntity(id); + }); + }; + mode.addAddedEntityID = function(entityID) { + if (_allAddedEntityIDs.indexOf(entityID) === -1) { + _allAddedEntityIDs.push(entityID); + } + }; + + mode.defaultNodeTags = null; function start(loc) { var startGraph = context.graph(); - var node = osmNode({ loc: loc }); - var way = osmWay({ tags: defaultTags }); + var node = osmNode({ loc: loc, tags: mode.defaultNodeTags || {} }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(node), @@ -32,14 +64,13 @@ export function modeAddLine(context, mode) { actionAddVertex(way.id, node.id) ); - context.enter(modeDrawLine(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } - function startFromWay(loc, edge) { var startGraph = context.graph(); - var node = osmNode({ loc: loc }); - var way = osmWay({ tags: defaultTags }); + var node = osmNode({ loc: loc, tags: mode.defaultNodeTags || {} }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(node), @@ -48,30 +79,57 @@ export function modeAddLine(context, mode) { actionAddMidpoint({ loc: loc, edge: edge }, node) ); - context.enter(modeDrawLine(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } - function startFromNode(node) { var startGraph = context.graph(); - var way = osmWay({ tags: defaultTags }); + var way = osmWay({ tags: mode.defaultTags }); context.perform( actionAddEntity(way), actionAddVertex(way.id, node.id) ); - context.enter(modeDrawLine(context, way.id, startGraph, mode.button)); + enterDrawMode(way, startGraph); } + function enterDrawMode(way, startGraph) { + _allAddedEntityIDs.push(way.id); + var drawMode = modeDrawLine(context, way.id, startGraph, mode.button, null, mode); + drawMode.defaultNodeTags = mode.defaultNodeTags; + context.enter(drawMode); + } - mode.enter = function() { - context.install(behavior); + mode.finish = function() { + if (mode.addedEntityIDs().length) { + context.enter( + modeSelect(context, mode.addedEntityIDs()) + .newFeature(true) + ); + } else { + context.enter( + modeBrowse(context) + ); + } }; + function undone() { + if (context.graph() === _baselineGraph || mode.addedEntityIDs().length === 0) { + context.enter(modeBrowse(context)); + } + } + + mode.enter = function() { + context.install(_behavior); + context.history() + .on('undone.add_line', undone); + }; mode.exit = function() { - context.uninstall(behavior); + context.history() + .on('undone.add_line', null); + context.uninstall(_behavior); }; return mode; diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index d2eb990612..75118190f9 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -12,57 +12,67 @@ export function modeAddPoint(context, mode) { mode.id = 'add-point'; - var behavior = behaviorDraw(context) + var _baselineGraph = context.graph(); + + var _behavior = behaviorDraw(context) .tail(t('modes.add_point.tail')) .on('click', add) .on('clickWay', addWay) .on('clickNode', addNode) .on('cancel', cancel) - .on('finish', cancel); + .on('finish', function finish() { + mode.finish(); + }); + + mode.defaultTags = {}; + if (mode.preset) mode.defaultTags = mode.preset.setTags(mode.defaultTags, 'point'); - var defaultTags = {}; - if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point'); + var _repeatAddedFeature = false; + var _allAddedEntityIDs = []; + mode.repeatAddedFeature = function(val) { + if (!arguments.length) return _repeatAddedFeature; + _repeatAddedFeature = val; + return mode; + }; + + mode.addedEntityIDs = function() { + return _allAddedEntityIDs.filter(function(id) { + return context.hasEntity(id); + }); + }; function add(loc) { - var node = osmNode({ loc: loc, tags: defaultTags }); + var node = osmNode({ loc: loc, tags: mode.defaultTags }); context.perform( actionAddEntity(node), t('operations.add.annotation.point') ); - enterSelectMode(node); + didFinishAdding(node); } - function addWay(loc, edge) { - var node = osmNode({ tags: defaultTags }); + var node = osmNode({ tags: mode.defaultTags }); context.perform( actionAddMidpoint({loc: loc, edge: edge}, node), t('operations.add.annotation.vertex') ); - enterSelectMode(node); + didFinishAdding(node); } - function enterSelectMode(node) { - context.enter( - modeSelect(context, [node.id]).newFeature(true) - ); - } - - function addNode(node) { - if (Object.keys(defaultTags).length === 0) { - enterSelectMode(node); + if (Object.keys(mode.defaultTags).length === 0) { + didFinishAdding(node); return; } var tags = Object.assign({}, node.tags); // shallow copy - for (var key in defaultTags) { - tags[key] = defaultTags[key]; + for (var key in mode.defaultTags) { + tags[key] = mode.defaultTags[key]; } context.perform( @@ -70,24 +80,49 @@ export function modeAddPoint(context, mode) { t('operations.add.annotation.point') ); - enterSelectMode(node); + didFinishAdding(node); } + function didFinishAdding(node) { + _allAddedEntityIDs.push(node.id); + if (!mode.repeatAddedFeature()) { + mode.finish(); + } + } + + mode.finish = function() { + if (mode.addedEntityIDs().length) { + context.enter( + modeSelect(context, mode.addedEntityIDs()).newFeature(true) + ); + } else { + context.enter( + modeBrowse(context) + ); + } + }; function cancel() { context.enter(modeBrowse(context)); } + function undone() { + if (context.graph() === _baselineGraph || mode.addedEntityIDs().length === 0) { + context.enter(modeBrowse(context)); + } + } mode.enter = function() { - context.install(behavior); + context.install(_behavior); + context.history() + .on('undone.add_point', undone); }; - mode.exit = function() { - context.uninstall(behavior); + context.history() + .on('undone.add_point', null); + context.uninstall(_behavior); }; - return mode; } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 15bf9cb9f1..882d8ac901 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -1,59 +1,84 @@ import { t } from '../util/locale'; import { behaviorDrawWay } from '../behavior/draw_way'; +import { modeSelect } from './select'; import { uiFlash } from '../ui/flash'; -export function modeDrawArea(context, wayID, startGraph, button) { +export function modeDrawArea(context, wayID, startGraph, button, addMode) { var mode = { button: button, - id: 'draw-area' + id: 'draw-area', + addMode: addMode }; - var behavior; + var _behavior; mode.wayID = wayID; + mode.repeatAddedFeature = function(val) { + if (addMode) return addMode.repeatAddedFeature(val); + }; + + mode.addedEntityIDs = function() { + if (addMode) return addMode.addedEntityIDs(); + }; + mode.enter = function() { + if (addMode) { + // Add in case this draw mode was entered from somewhere besides modeAddArea. + // Duplicates are resolved later. + addMode.addAddedEntityID(wayID); + } + var way = context.entity(wayID); - behavior = behaviorDrawWay(context, wayID, undefined, mode, startGraph) + _behavior = behaviorDrawWay(context, wayID, undefined, startGraph) .tail(t('modes.draw_area.tail')) + .on('doneSegment.modeDrawArea', function() { + // re-enter this mode to start the next segment + context.enter(mode); + }) + .on('finish.modeDrawArea revert.modeDrawArea', function() { + if (mode.repeatAddedFeature()) { + context.enter(addMode); + } else { + var newMode = modeSelect(context, mode.addedEntityIDs() || [wayID]) + .newFeature(true); + context.enter(newMode); + } + }) .on('rejectedSelfIntersection.modeDrawArea', function() { uiFlash() .text(t('self_intersection.error.areas'))(); }); - var addNode = behavior.addNode; + var addNode = _behavior.addNode; - behavior.addNode = function(node, d) { + _behavior.addNode = function(node, d) { var length = way.nodes.length; var penultimate = length > 2 ? way.nodes[length - 2] : null; if (node.id === way.first() || node.id === penultimate) { - behavior.finish(); + _behavior.finish(); } else { addNode(node, d); } }; - context.install(behavior); + context.install(_behavior); }; - mode.exit = function() { - context.uninstall(behavior); + context.uninstall(_behavior); }; - mode.selectedIDs = function() { return [wayID]; }; - mode.activeID = function() { - return (behavior && behavior.activeID()) || []; + return _behavior && _behavior.activeID(); }; - return mode; } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 40d5840ff6..67ccef5167 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -1,57 +1,103 @@ import { t } from '../util/locale'; +import { actionChangeTags } from '../actions/change_tags'; import { behaviorDrawWay } from '../behavior/draw_way'; +import { modeSelect } from './select'; import { uiFlash } from '../ui/flash'; -export function modeDrawLine(context, wayID, startGraph, button, affix, continuing) { +export function modeDrawLine(context, wayID, startGraph, button, affix, addMode) { var mode = { button: button, - id: 'draw-line' + id: 'draw-line', + addMode: addMode, + affix: affix }; - var behavior; + var _behavior; mode.wayID = wayID; - mode.isContinuing = continuing; + mode.isContinuing = !!affix; + + mode.repeatAddedFeature = function(val) { + if (addMode) return addMode.repeatAddedFeature(val); + }; + + mode.addedEntityIDs = function() { + if (addMode) return addMode.addedEntityIDs(); + }; mode.enter = function() { + mode.skipEnter = false; + + if (addMode) { + // Add in case this draw mode was entered from somewhere besides modeAddLine. + // Duplicates are resolved later. + addMode.addAddedEntityID(wayID); + } + var way = context.entity(wayID); var index = (affix === 'prefix') ? 0 : undefined; var headID = (affix === 'prefix') ? way.first() : way.last(); - behavior = behaviorDrawWay(context, wayID, index, mode, startGraph) + _behavior = behaviorDrawWay(context, wayID, index, startGraph) .tail(t('modes.draw_line.tail')) + .on('doneSegment.modeDrawLine', function(node) { + if (mode.defaultNodeTags && node && !Object.keys(node.tags).length) { + // add the default tags to the node, if any + context.replace(actionChangeTags(node.id, mode.defaultNodeTags), context.history().undoAnnotation()); + } + if (mode.skipEnter) return; + + // re-enter this mode to start the next segment + context.enter(mode); + }) + .on('finish.modeDrawLine revert.modeDrawArea', function() { + if (mode.skipEnter) return; + + if (mode.repeatAddedFeature()) { + context.enter(addMode); + } else { + var newMode = modeSelect(context, mode.addedEntityIDs() || [wayID]) + .newFeature(!mode.isContinuing); + context.enter(newMode); + } + }) .on('rejectedSelfIntersection.modeDrawLine', function() { uiFlash() .text(t('self_intersection.error.lines'))(); }); - var addNode = behavior.addNode; - behavior.addNode = function(node, d) { + var addNode = _behavior.addNode; + _behavior.addNode = function(node, d) { if (node.id === headID) { - behavior.finish(); + _behavior.finish(); } else { addNode(node, d); } }; - context.install(behavior); + context.install(_behavior); }; - mode.exit = function() { - context.uninstall(behavior); + context.uninstall(_behavior); }; + // complete drawing, if possible + mode.finish = function(skipEnter) { + if (skipEnter) { + mode.skipEnter = true; + } + return _behavior.finish(); + }; mode.selectedIDs = function() { return [wayID]; }; - mode.activeID = function() { - return (behavior && behavior.activeID()) || []; + return _behavior && _behavior.activeID(); }; return mode; diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 04ce585299..64662dcdad 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -27,7 +27,7 @@ export function operationContinue(selectedIDs, context) { var operation = function() { var candidate = candidateWays()[0]; context.enter( - modeDrawLine(context, candidate.id, context.graph(), 'line', candidate.affix(vertex.id), true) + modeDrawLine(context, candidate.id, context.graph(), 'line', candidate.affix(vertex.id)) ); }; diff --git a/modules/osm/tags.js b/modules/osm/tags.js index a1381c721e..609315d2cf 100644 --- a/modules/osm/tags.js +++ b/modules/osm/tags.js @@ -114,6 +114,38 @@ export var osmRightSideIsInsideTags = { } }; +export var osmTagsAllowingBridges = { + highway: { + motorway: true, trunk: true, primary: true, secondary: true, tertiary: true, residential: true, + motorway_link: true, trunk_link: true, primary_link: true, secondary_link: true, tertiary_link: true, + unclassified: true, road: true, service: true, track: true, living_street: true, bus_guideway: true, + path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, + raceway: true + }, + railway: { + rail: true, light_rail: true, tram: true, subway: true, + monorail: true, funicular: true, miniature: true, narrow_gauge: true, + disused: true, preserved: true, abandoned: true + } +}; +export var osmTagsAllowingTunnels = { + highway: { + motorway: true, trunk: true, primary: true, secondary: true, tertiary: true, residential: true, + motorway_link: true, trunk_link: true, primary_link: true, secondary_link: true, tertiary_link: true, + unclassified: true, road: true, service: true, track: true, living_street: true, bus_guideway: true, + path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, + raceway: true + }, + railway: { + rail: true, light_rail: true, tram: true, subway: true, + monorail: true, funicular: true, miniature: true, narrow_gauge: true, + disused: true, preserved: true, abandoned: true + }, + waterway: { + canal: true, ditch: true, drain: true, fish_pass: true, river: true, stream: true, tidal_channel: true + } +}; + // "highway" tag values for pedestrian or vehicle right-of-ways that make up the routable network // (does not include `raceway`) export var osmRoutableHighwayTagValues = { diff --git a/modules/presets/index.js b/modules/presets/index.js index 28ae87e825..d7842cae44 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -18,7 +18,7 @@ export { presetPreset }; // with methods for loading new data and returning defaults // export function presetIndex(context) { - const dispatch = d3_dispatch('recentsChange'); + const dispatch = d3_dispatch('favoritePreset', 'recentsChange'); const MAXRECENTS = 30; let _presetData; @@ -44,6 +44,7 @@ export function presetIndex(context) { let _universal = []; let _addablePresetIDs = null; // Set of preset IDs that the user can add let _recents; + let _favorites; // Index of presets by (geometry, tag key). let _geometryIndex = { point: {}, vertex: {}, line: {}, area: {}, relation: {} }; @@ -321,7 +322,7 @@ export function presetIndex(context) { }; // pass a Set of addable preset ids - _this.addablePresetIDs = function(val) { + _this.addablePresetIDs = (val) => { if (!arguments.length) return _addablePresetIDs; _addablePresetIDs = val; @@ -342,39 +343,47 @@ export function presetIndex(context) { }; - function RibbonItem(preset, geometry, source) { + function RibbonItem(preset, source) { let item = {}; item.preset = preset; - item.geometry = geometry; item.source = source; + item.isFavorite = () => item.source === 'favorite'; item.isRecent = () => item.source === 'recent'; - item.matches = (preset, geometry) => item.preset.id === preset.id && item.geometry === geometry; - item.minified = () => ({ pID: item.preset.id, geom: item.geometry }); + item.matches = (preset) => item.preset.id === preset.id; + item.minified = () => ({ pID: item.preset.id }); return item; } function ribbonItemForMinified(d, source) { - if (d && d.pID && d.geom) { + if (d && d.pID) { const preset = _this.item(d.pID); if (!preset) return null; - - let geom = d.geom; - // treat point and vertex features as one geometry - if (geom === 'vertex') geom = 'point'; - - // iD's presets could have changed since this was saved, - // so make sure it's still valid. - if (preset.matchGeometry(geom) || (geom === 'point' && preset.matchGeometry('vertex'))) { - return RibbonItem(preset, geom, source); - } + return RibbonItem(preset, source); } return null; } + _this.getGenericRibbonItems = () => { + return ['point', 'line', 'area'].map(id => RibbonItem(_this.item(id), 'generic')); + }; + + + _this.getAddable = () => { + if (!_addablePresetIDs) return []; + + return _addablePresetIDs.map((id) => { + const preset = _this.item(id); + if (preset) { + return RibbonItem(preset, 'addable'); + } + }).filter(Boolean); + }; + + function setRecents(items) { _recents = items; const minifiedItems = items.map(d => d.minified()); @@ -397,8 +406,21 @@ export function presetIndex(context) { }; - _this.removeRecent = (preset, geometry) => { - const item = _this.recentMatching(preset, geometry); + _this.addRecent = (preset, besidePreset, after) => { + const recents = _this.getRecents(); + + const beforeItem = _this.recentMatching(besidePreset); + let toIndex = recents.indexOf(beforeItem); + if (after) toIndex += 1; + + const newItem = RibbonItem(preset, 'recent'); + recents.splice(toIndex, 0, newItem); + setRecents(recents); + }; + + + _this.removeRecent = (preset) => { + const item = _this.recentMatching(preset); if (item) { let items = _this.getRecents(); items.splice(items.indexOf(item), 1); @@ -407,11 +429,10 @@ export function presetIndex(context) { }; - _this.recentMatching = (preset, geometry) => { - geometry = _this.fallback(geometry).id; + _this.recentMatching = (preset) => { const items = _this.getRecents(); for (let i in items) { - if (items[i].matches(preset, geometry)) { + if (items[i].matches(preset)) { return items[i]; } } @@ -439,18 +460,16 @@ export function presetIndex(context) { }; - _this.setMostRecent = (preset, geometry) => { + _this.setMostRecent = (preset) => { if (context.inIntro()) return; if (preset.searchable === false) return; - geometry = _this.fallback(geometry).id; - let items = _this.getRecents(); - let item = _this.recentMatching(preset, geometry); + let item = _this.recentMatching(preset); if (item) { items.splice(items.indexOf(item), 1); } else { - item = RibbonItem(preset, geometry, 'recent'); + item = RibbonItem(preset, 'recent'); } // remove the last recent (first in, first out) @@ -463,6 +482,86 @@ export function presetIndex(context) { setRecents(items); }; + function setFavorites(items) { + _favorites = items; + const minifiedItems = items.map(d => d.minified()); + context.storage('preset_favorites', JSON.stringify(minifiedItems)); + + // call update + dispatch.call('favoritePreset'); + } + + _this.addFavorite = (preset, besidePreset, after) => { + const favorites = _this.getFavorites(); + + const beforeItem = _this.favoriteMatching(besidePreset); + let toIndex = favorites.indexOf(beforeItem); + if (after) toIndex += 1; + + const newItem = RibbonItem(preset, 'favorite'); + favorites.splice(toIndex, 0, newItem); + setFavorites(favorites); + }; + + _this.toggleFavorite = (preset) => { + const favs = _this.getFavorites(); + const favorite = _this.favoriteMatching(preset); + if (favorite) { + favs.splice(favs.indexOf(favorite), 1); + } else { + // only allow 10 favorites + if (favs.length === 10) { + // remove the last favorite (last in, first out) + favs.pop(); + } + // append array + favs.push(RibbonItem(preset, 'favorite')); + } + setFavorites(favs); + }; + + + _this.removeFavorite = (preset) => { + const item = _this.favoriteMatching(preset); + if (item) { + const items = _this.getFavorites(); + items.splice(items.indexOf(item), 1); + setFavorites(items); + } + }; + + + _this.getFavorites = () => { + if (!_favorites) { + + // fetch from local storage + let rawFavorites = JSON.parse(context.storage('preset_favorites')); + + if (!rawFavorites) { + rawFavorites = []; + context.storage('preset_favorites', JSON.stringify(rawFavorites)); + } + + _favorites = rawFavorites.reduce((output, d) => { + const item = ribbonItemForMinified(d, 'favorite'); + if (item && item.preset.addable()) output.push(item); + return output; + }, []); + } + return _favorites; + }; + + + _this.favoriteMatching = (preset) => { + const favs = _this.getFavorites(); + for (let index in favs) { + if (favs[index].matches(preset)) { + return favs[index]; + } + } + return null; + }; + return utilRebind(_this, dispatch, 'on'); } diff --git a/modules/presets/preset.js b/modules/presets/preset.js index e56cb85bfd..5063677fa6 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -261,6 +261,37 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { } } + // The geometry type to use when adding a new feature of this preset + _this.defaultAddGeometry = (context, allowedGeometries)=> { + var geometry = _this.geometry.slice().filter((geom) => { + if (allowedGeometries && allowedGeometries.indexOf(geom) === -1) return false; + if (context.features().isHiddenPreset(_this, geom)) return false; + return true; + }); + + var mostRecentAddGeom = context.storage('preset.' + preset.id + '.addGeom'); + if (mostRecentAddGeom === 'vertex') mostRecentAddGeom = 'point'; + if (mostRecentAddGeom && geometry.indexOf(mostRecentAddGeom) !== -1) { + return mostRecentAddGeom; + } + var vertexIndex = geometry.indexOf('vertex'); + if (vertexIndex !== -1 && geometry.indexOf('point') !== -1) { + // both point and vertex allowed, just use point + geometry.splice(vertexIndex, 1); + } + if (geometry.length) { + return geometry[0]; + } + return null; + }; + + _this.setMostRecentAddGeometry = (context, geometry) => { + if (_this.geometry.length > 1 && + _this.geometry.indexOf(geometry) !== -1) { + context.storage('preset.' + _this.id + '.addGeom', geometry); + } + }; + return _this; } diff --git a/modules/ui/index.js b/modules/ui/index.js index b1731aab94..44ce05ce41 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -42,6 +42,8 @@ export { uiNoteComments } from './note_comments'; export { uiNoteEditor } from './note_editor'; export { uiNoteHeader } from './note_header'; export { uiNoteReport } from './note_report'; +export { uiPresetBrowser } from './preset_browser'; +export { uiPresetFavoriteButton } from './preset_favorite_button'; export { uiPresetIcon } from './preset_icon'; export { uiPresetList } from './preset_list'; export { uiRestore } from './restore'; diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js new file mode 100644 index 0000000000..f931656de0 --- /dev/null +++ b/modules/ui/preset_browser.js @@ -0,0 +1,684 @@ +import { + event as d3_event, + select as d3_select, + selectAll as d3_selectAll +} from 'd3-selection'; + +import { t, textDirection } from '../util/locale'; +import { services } from '../services'; +import { svgIcon } from '../svg/index'; +import { tooltip } from '../util/tooltip'; +import { popover } from '../util/popover'; +import { uiTagReference } from './tag_reference'; +import { uiPresetFavoriteButton } from './preset_favorite_button'; +import { uiPresetIcon } from './preset_icon'; +//import { groupManager } from '../entities/group_manager'; +import { utilKeybinding, utilNoAuto } from '../util'; + +export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { + + // multiple preset browsers could be instantiated at once, give each a unique ID + var uid = (new Date()).getTime().toString(); + + var presets; + + var shownGeometry = []; + updateShownGeometry(allowedGeometry); + + var search = d3_select(null), + poplistContent = d3_select(null), + poplistFooter = d3_select(null); + + var _countryCode; + + var browser = popover('poplist preset-browser fillL') + .placement('bottom') + .alignment('leading') + .hasArrow(false); + + browser.content(function() { + return function(selection) { + + var header = selection.selectAll('.poplist-header') + .data([0]) + .enter() + .append('div') + .attr('class', 'poplist-header'); + + header + .append('input') + .attr('class', 'search-input') + .attr('placeholder', t('modes.add_feature.search_placeholder')) + .attr('type', 'search') + .call(utilNoAuto) + .on('blur', function() { + browser.hide(); + }) + .on('keypress', keypress) + .on('keydown', keydown) + .on('input', updateResultsList); + + header + .call(svgIcon('#iD-icon-search', 'search-icon pre-text')); + + selection.selectAll('.poplist-content') + .data([0]) + .enter() + .append('div') + .attr('class', 'poplist-content') + .on('mousedown', function() { + // don't blur the search input (and thus close results) + d3_event.preventDefault(); + d3_event.stopPropagation(); + }) + .append('div') + .attr('class', 'list'); + + var footer = selection.selectAll('.poplist-footer') + .data([0]) + .enter() + .append('div') + .attr('class', 'poplist-footer') + .on('mousedown', function() { + // don't blur the search input (and thus close results) + d3_event.preventDefault(); + d3_event.stopPropagation(); + }); + + footer.append('div') + .attr('class', 'message'); + + footer.append('div') + .attr('class', 'filter-wrap'); + + search = selection.selectAll('.search-input'); + poplistContent = selection.selectAll('.poplist-content'); + poplistFooter = selection.selectAll('.poplist-footer'); + + renderFilterButtons(); + }; + }); + + var parentShow = browser.show; + browser.show = function() { + parentShow(); + search.node().focus(); + search.node().setSelectionRange(0, search.property('value').length); + + updateResultsList(); + + context.features() + .on('change.preset-browser.' + uid , updateForFeatureHiddenState); + + // reload in case the user moved countries + reloadCountryCode(); + }; + + var parentHide = browser.hide; + browser.hide = function() { + parentHide(); + if (onCancel) onCancel(); + }; + + function renderFilterButtons() { + var selection = poplistFooter.select('.filter-wrap'); + + var geomForButtons = allowedGeometry.slice(); + var vertexIndex = geomForButtons.indexOf('vertex'); + if (vertexIndex !== -1) geomForButtons.splice(vertexIndex, 1); + + if (geomForButtons.length === 1) { + // don't show filter buttons if only one geometry allowed + geomForButtons = []; + } + + var buttons = selection + .selectAll('button.filter') + .data(geomForButtons, function(d) { return d; }); + + buttons.exit() + .remove(); + + buttons + .enter() + .append('button') + .attr('class', 'filter active') + .attr('title', function(d) { + return t('modes.add_' + d + '.filter_tooltip'); + }) + .each(function(d) { + d3_select(this).call(svgIcon('#iD-icon-' + d)); + }) + .on('click', function(d) { + toggleShownGeometry(d); + if (shownGeometry.length === 0) { + updateShownGeometry(allowedGeometry); + toggleShownGeometry(d); + } + updateFilterButtonsStates(); + updateResultsList(); + }); + + updateFilterButtonsStates(); + } + + + browser.setAllowedGeometry = function(array) { + allowedGeometry = array; + updateShownGeometry(array); + renderFilterButtons(); + updateResultsList(); + }; + + + function updateShownGeometry(geom) { + shownGeometry = geom.slice().sort(); + presets = context.presets().matchAnyGeometry(shownGeometry); + } + + function toggleShownGeometry(d) { + var geom = shownGeometry; + var index = geom.indexOf(d); + if (index === -1) { + geom.push(d); + if (d === 'point') geom.push('vertex'); + } else { + geom.splice(index, 1); + if (d === 'point') geom.splice(geom.indexOf('vertex'), 1); + } + updateShownGeometry(geom); + } + + function updateFilterButtonsStates() { + poplistFooter.selectAll('button.filter') + .classed('active', function(d) { + return shownGeometry.indexOf(d) !== -1; + }); + } + + function keypress() { + if (d3_event.keyCode === utilKeybinding.keyCodes.enter) { + poplistContent.selectAll('.list .list-item.focused button.choose') + .each(function(d) { d.choose.call(this); }); + d3_event.preventDefault(); + d3_event.stopPropagation(); + } + } + + function keydown() { + + var nextFocus, + priorFocus, + parentSubsection; + if (d3_event.keyCode === utilKeybinding.keyCodes['↓'] || + d3_event.keyCode === utilKeybinding.keyCodes.tab && !d3_event.shiftKey) { + d3_event.preventDefault(); + d3_event.stopPropagation(); + + priorFocus = poplistContent.selectAll('.list .list-item.focused'); + if (priorFocus.empty()) { + nextFocus = poplistContent.selectAll('.list > .list-item:first-child'); + } else { + nextFocus = d3_select(priorFocus.nodes()[0].nextElementSibling); + if (!nextFocus.empty() && !nextFocus.classed('list-item')) { + nextFocus = nextFocus.selectAll('.list-item:first-child'); + } + if (nextFocus.empty()) { + parentSubsection = priorFocus.nodes()[0].closest('.list .subsection'); + if (parentSubsection && parentSubsection.nextElementSibling) { + nextFocus = d3_select(parentSubsection.nextElementSibling); + } + } + } + if (!nextFocus.empty()) { + focusListItem(nextFocus, true); + priorFocus.classed('focused', false); + } + + } else if (d3_event.keyCode === utilKeybinding.keyCodes['↑'] || + d3_event.keyCode === utilKeybinding.keyCodes.tab && d3_event.shiftKey) { + d3_event.preventDefault(); + d3_event.stopPropagation(); + + priorFocus = poplistContent.selectAll('.list .list-item.focused'); + if (!priorFocus.empty()) { + + nextFocus = d3_select(priorFocus.nodes()[0].previousElementSibling); + if (!nextFocus.empty() && !nextFocus.classed('list-item')) { + nextFocus = nextFocus.selectAll('.list-item:last-child'); + } + if (nextFocus.empty()) { + parentSubsection = priorFocus.nodes()[0].closest('.list .subsection'); + if (parentSubsection && parentSubsection.previousElementSibling) { + nextFocus = d3_select(parentSubsection.previousElementSibling); + } + } + if (!nextFocus.empty()) { + focusListItem(nextFocus, true); + priorFocus.classed('focused', false); + } + } + } else if (d3_event.keyCode === utilKeybinding.keyCodes.esc) { + search.node().blur(); + d3_event.preventDefault(); + d3_event.stopPropagation(); + } + } + + function getDefaultResults() { + + //var graph = context.graph(); + + //var superGroups = groupManager.groupsWithNearby; + //var scoredGroups = {}; + var scoredPresets = {}; + + context.presets().getRecents().slice(0, 15).forEach(function(item, index) { + var score = (15 - index) / 15; + + var id = item.preset.id; + if (!scoredPresets[id]) { + scoredPresets[id] = { + preset: item.preset, + score: score + }; + } + }); +/* + var queryExtent = context.map().extent(); + var nearbyEntities = context.history().tree().intersects(queryExtent, graph); + for (var i in nearbyEntities) { + var entity = nearbyEntities[i]; + // ignore boring features + if (!entity.hasInterestingTags()) continue; + + var geom = entity.geometry(graph); + + // evaluate preset + var preset = context.presets().match(entity, graph); + if (preset.searchable !== false && // don't recommend unsearchables + !preset.isFallback() && // don't recommend generics + !preset.suggestion) { // don't recommend brand suggestions again + if (!scoredPresets[preset.id]) { + scoredPresets[preset.id] = { + preset: preset, + score: 0 + }; + } + scoredPresets[preset.id].score += 1; + } + + // evaluate groups + for (var j in superGroups) { + var group = superGroups[j]; + if (group.matchesTags(entity.tags, geom)) { + var nearbyGroupID = group.nearby; + if (!scoredGroups[nearbyGroupID]) { + scoredGroups[nearbyGroupID] = { + group: groupManager.group(nearbyGroupID), + score: 0 + }; + } + var entityScore; + if (geom === 'area') { + // significantly prefer area features that dominate the viewport + // (e.g. editing within a park or school grounds) + var containedPercent = queryExtent.percentContainedIn(entity.extent(graph)); + entityScore = Math.max(1, containedPercent * 10); + } else { + entityScore = 1; + } + scoredGroups[nearbyGroupID].score += entityScore; + } + } + } + + Object.values(scoredGroups).forEach(function(scoredGroupItem) { + scoredGroupItem.group.scoredPresets().forEach(function(groupScoredPreset) { + var combinedScore = groupScoredPreset.score * scoredGroupItem.score; + if (!scoredPresets[groupScoredPreset.preset.id]) { + scoredPresets[groupScoredPreset.preset.id] = { + preset: groupScoredPreset.preset, + score: combinedScore + }; + } else { + scoredPresets[groupScoredPreset.preset.id].score += combinedScore; + } + }); + }); +*/ + return Object.values(scoredPresets).sort(function(item1, item2) { + return item2.score - item1.score; + }).map(function(item) { + return item.preset ? item.preset : item; + }).filter(function(d) { + var preset = d.preset || d; + // skip non-visible + if (preset.addable && !preset.addable()) return false; + + // skip presets not valid in this country + if (_countryCode && preset.countryCodes && preset.countryCodes.indexOf(_countryCode) === -1) return false; + + return preset.defaultAddGeometry(context, shownGeometry); + }).slice(0, 50); + } + + + function reloadCountryCode() { + if (!services.countryCoder) return; + + var center = context.map().center(); + var countryCode = services.countryCoder.iso1A2Code(center); + if (countryCode) countryCode = countryCode.toLowerCase(); + if (_countryCode !== countryCode) { + _countryCode = countryCode; + updateResultsList(); + } + } + + function getRawResults() { + if (search.empty()) return []; + + var value = search.property('value'); + var results; + if (value.length) { + results = presets.search(value, shownGeometry, _countryCode).collection + .filter(function(d) { + if (d.members) { + return d.members.collection.some(function(preset) { + return preset.addable(); + }); + } + return d.addable(); + }); + } else { + results = getDefaultResults(); + } + return results; + } + + function updateResultsList() { + + if (!browser.isShown()) return; + + var list = poplistContent.selectAll('.list'); + + if (search.empty() || list.empty()) return; + + var results = getRawResults(); + list.call(drawList, results); + + list.selectAll('.list-item.focused') + .classed('focused', false); + focusListItem(poplistContent.selectAll('.list > .list-item:first-child'), false); + + poplistContent.node().scrollTop = 0; + + var resultCount = results.length; + poplistFooter.selectAll('.message') + .text(t('modes.add_feature.' + (resultCount === 1 ? 'result' : 'results'), { count: resultCount })); + } + + function focusListItem(selection, scrollingToShow) { + if (!selection.empty()) { + selection.classed('focused', true); + if (scrollingToShow) { + // scroll to keep the focused item visible + scrollPoplistToShow(selection); + } + } + } + + function scrollPoplistToShow(selection) { + if (selection.empty()) return; + + var node = selection.nodes()[0]; + var scrollableNode = poplistContent.node(); + + if (node.offsetTop < scrollableNode.scrollTop) { + scrollableNode.scrollTop = node.offsetTop; + + } else if (node.offsetTop + node.offsetHeight > scrollableNode.scrollTop + scrollableNode.offsetHeight && + node.offsetHeight < scrollableNode.offsetHeight) { + scrollableNode.scrollTop = node.offsetTop + node.offsetHeight - scrollableNode.offsetHeight; + } + } + + function itemForPreset(d) { + if (d.members) { + return CategoryItem(d); + } + var preset = d.preset || d; + return AddablePresetItem(preset); + } + + function drawList(list, rawItems) { + + list.selectAll('.subsection.subitems').remove(); + + var dataItems = rawItems.map(function(rawItem) { + return itemForPreset(rawItem); + }); + + var items = list.selectAll('.list-item') + .data(dataItems, function(d) { return d.id(); }); + + items.order(); + + items.exit() + .remove(); + + drawItems(items.enter()); + + list.selectAll('.list-item.expanded') + .classed('expanded', false) + .selectAll('.label svg.icon use') + .attr('href', textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'); + + updateForFeatureHiddenState(); + } + + function drawItems(selection) { + + var item = selection + .append('div') + .attr('class', 'list-item') + .attr('id', function(d) { + return 'search-add-list-item-preset-' + d.id().replace(/[^a-zA-Z\d:]/g, '-'); + }) + .on('mouseover', function() { + poplistContent.selectAll('.list .list-item.focused') + .classed('focused', false); + d3_select(this) + .classed('focused', true); + }) + .on('mouseout', function() { + d3_select(this) + .classed('focused', false); + }); + + var row = item.append('div') + .attr('class', 'row'); + + row.append('button') + .attr('class', 'choose') + .on('click', function(d) { + d.choose.call(this); + }); + + row.each(function(d) { + var geometry = d.preset && d.preset.geometry[0]; + if ((d.preset && d.preset.geometry.length !== 1) || + (geometry !== 'area' && geometry !== 'line' && geometry !== 'vertex')) { + geometry = null; + } + d3_select(this).call( + uiPresetIcon(context) + .geometry(geometry) + .preset(d.preset || d.category) + .sizeClass('small') + ); + }); + var label = row.append('div') + .attr('class', 'label'); + + label.each(function(d) { + if (d.subitems) { + d3_select(this) + .call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline')); + } + }); + + label.each(function(d) { + // NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc) + d3_select(this) + .append('div') + .attr('class', 'label-inner') + .selectAll('.namepart') + .data(d.name().split(' – ')) + .enter() + .append('div') + .attr('class', 'namepart') + .text(function(d) { return d; }); + }); + + row.each(function(d) { + if (!d.preset) return; + + var presetFavorite = uiPresetFavoriteButton(d.preset, null, context, 'accessory'); + d3_select(this).call(presetFavorite.button); + }); + item.each(function(d) { + if (!d.preset) return; + + var reference = uiTagReference(d.preset.reference(d.preset.defaultAddGeometry(context, shownGeometry)), context); + + var thisItem = d3_select(this); + thisItem.selectAll('.row').call(reference.button, 'accessory', 'info'); + + var subsection = thisItem + .append('div') + .attr('class', 'subsection reference'); + subsection.call(reference.body); + }); + } + + function updateForFeatureHiddenState() { + + var listItem = d3_selectAll('.add-feature .poplist .list-item'); + + // remove existing tooltips + listItem.selectAll('button.choose').call(tooltip().destroyAny); + + listItem.each(function(item, index) { + + if (!item.preset) return; + + var hiddenPresetFeatures; + + for (var i in item.preset.geometry) { + if (shownGeometry.indexOf(item.preset.geometry[i]) !== -1) { + hiddenPresetFeatures = context.features().isHiddenPreset(item.preset, item.preset.geometry[i]); + if (!hiddenPresetFeatures) { + break; + } + } + } + + var button = d3_select(this).selectAll('button.choose'); + + d3_select(this).classed('disabled', !!hiddenPresetFeatures); + button.classed('disabled', !!hiddenPresetFeatures); + + if (!hiddenPresetFeatures) return; + + var isAutoHidden = context.features().autoHidden(hiddenPresetFeatures.key); + var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; + var tooltipObj = { features: hiddenPresetFeatures.title }; + button.call(tooltip('dark') + .html(true) + .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj)) + .placement(index < 2 ? 'bottom' : 'top') + ); + }); + } + + function chooseExpandable(item, itemSelection) { + + var shouldExpand = !itemSelection.classed('expanded'); + + itemSelection.classed('expanded', shouldExpand); + + var iconName = shouldExpand ? + '#iD-icon-down' : (textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'); + itemSelection.selectAll('.label svg.icon use') + .attr('href', iconName); + + if (shouldExpand) { + var subitems = item.subitems(); + var selector = '#' + itemSelection.node().id + ' + *'; + item.subsection = d3_select(itemSelection.node().parentNode).insert('div', selector) + .attr('class', 'subsection subitems'); + var subitemsEnter = item.subsection.selectAll('.list-item') + .data(subitems) + .enter(); + drawItems(subitemsEnter); + updateForFeatureHiddenState(); + scrollPoplistToShow(item.subsection); + } else { + item.subsection.remove(); + } + } + + function CategoryItem(category) { + var item = {}; + item.id = function() { + return category.id; + }; + item.name = function() { + return category.name(); + }; + item.subsection = d3_select(null); + item.category = category; + item.choose = function() { + var selection = d3_select(this); + if (selection.classed('disabled')) return; + chooseExpandable(item, d3_select(selection.node().closest('.list-item'))); + }; + item.subitems = function() { + return category.members.matchAnyGeometry(shownGeometry).collection + .filter(function(preset) { + return preset.addable(); + }) + .map(function(preset) { + return itemForPreset(preset); + }); + }; + return item; + } + + function AddablePresetItem(preset, isSubitem) { + var item = {}; + item.id = function() { + return preset.id + isSubitem; + }; + item.name = function() { + return preset.name(); + }; + item.isSubitem = isSubitem; + item.preset = preset; + item.choose = function() { + if (d3_select(this).classed('disabled')) return; + + if (onChoose) onChoose(preset, preset.defaultAddGeometry(context, shownGeometry)); + + search.node().blur(); + }; + return item; + } + + // load the initial country code + reloadCountryCode(); + + return browser; +} diff --git a/modules/ui/preset_favorite_button.js b/modules/ui/preset_favorite_button.js new file mode 100644 index 0000000000..97d4426d97 --- /dev/null +++ b/modules/ui/preset_favorite_button.js @@ -0,0 +1,54 @@ +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { t } from '../util/locale'; +import { svgIcon } from '../svg/icon'; + +export function uiPresetFavoriteButton(preset, geom, context, klass) { + + var presetFavorite = {}; + + var _button = d3_select(null); + + + presetFavorite.button = function(selection) { + + var canFavorite = geom !== 'relation' && preset.searchable !== false; + + _button = selection.selectAll('.preset-favorite-button') + .data(canFavorite ? [0] : []); + + _button.exit().remove(); + + _button = _button.enter() + .insert('button', '.tag-reference-button') + .attr('class', 'preset-favorite-button ' + klass) + .attr('title', t('icons.favorite')) + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-favorite')) + .merge(_button); + + _button + .on('click', function () { + d3_event.stopPropagation(); + d3_event.preventDefault(); + + context.presets().toggleFavorite(preset); + + update(); + }); + + update(); + }; + + function update() { + _button + .classed('active', context.presets().favoriteMatching(preset)); + } + + context.presets().on('favoritePreset.button-' + preset.safeid, update); + + return presetFavorite; +} diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index 2b42cc263b..73b5a611ea 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -8,6 +8,7 @@ export function uiPresetIcon(context) { let _preset; let _geometry; let _sizeClass = 'medium'; + let _pointMarker = true; function isSmall() { @@ -242,7 +243,7 @@ export function uiPresetIcon(context) { const isTnp = picon && /^tnp-/.test(picon); const isiDIcon = picon && !(isMaki || isTemaki || isFa || isTnp); const isCategory = !p.setTags; - const drawPoint = picon && geom === 'point' && isSmall() && !isFallback; + const drawPoint = picon && geom === 'point' && isSmall() && !isFallback && _pointMarker; const drawVertex = picon !== null && geom === 'vertex' && (!isSmall() || !isFallback); const drawLine = picon && geom === 'line' && !isFallback && !isCategory; const drawArea = picon && geom === 'area' && !isFallback; @@ -411,5 +412,12 @@ export function uiPresetIcon(context) { return presetIcon; }; + + presetIcon.pointMarker = function(val) { + if (!arguments.length) return _pointMarker; + _pointMarker = val; + return presetIcon; + }; + return presetIcon; } diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js new file mode 100644 index 0000000000..1baef457e8 --- /dev/null +++ b/modules/ui/tools/add_feature.js @@ -0,0 +1,163 @@ +import _debounce from 'lodash-es/debounce'; + +import { + event as d3_event, + select as d3_select, +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { svgIcon } from '../../svg/icon'; +import { tooltip } from '../../util/tooltip'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { uiPresetBrowser } from '../preset_browser'; +import { modeAddArea, modeAddLine, modeAddPoint } from '../../modes'; + +export function uiToolAddFeature(context) { + + var tool = { + id: 'add_feature', + label: t('toolbar.add_feature'), + itemClass: 'disclosing', + iconName: 'iD-presets-grid', + iconClass: 'icon-30' + }; + + var allowedGeometry = ['point', 'vertex', 'line', 'area']; + var presetBrowser = uiPresetBrowser(context, allowedGeometry, browserDidSelectPreset, browserDidClose) + .scrollContainer(d3_select('#bar')); + + var button = d3_select(null); + + var key = t('modes.add_feature.key'); + var keys = [key, '`', '²', '@']; // #5663, #6864 - common QWERTY, AZERTY + + tool.render = function(selection) { + + var buttonEnter = selection + .selectAll('.bar-button') + .data([0]) + .enter() + .append('button') + .attr('class', 'bar-button') + .attr('tabindex', -1) + .on('mousedown', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }) + .on('mouseup', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }) + .on('click', function() { + if (button.classed('disabled')) return; + + if (!presetBrowser.isShown()) { + button.classed('active', true); + presetBrowser.show(); + } else { + presetBrowser.hide(); + } + }) + .call(tooltip() + .placement('bottom') + .html(true) + .title(function() { + return uiTooltipHtml(t('modes.add_feature.description'), key); + }) + .scrollContainer(d3_select('#bar')) + ) + .call(svgIcon('#' + tool.iconName, tool.iconClass)); + + buttonEnter + .append('span') + .call(svgIcon('#iD-icon-down', 'disclosure-icon')); + + button = selection.select('.bar-button'); + + selection.call(presetBrowser); + + updateEnabledState(); + }; + + tool.allowed = function() { + var addableCount = context.presets().getAddable().length; + return addableCount === 0 || addableCount > 10; + }; + + tool.install = function() { + + context.keybinding().on(keys, function() { + button.classed('active', true); + + presetBrowser.show(); + d3_event.preventDefault(); + d3_event.stopPropagation(); + }); + + var debouncedUpdate = _debounce(updateEnabledState, 500, { leading: true, trailing: true }); + + context.map() + .on('move.add-feature-tool', debouncedUpdate) + .on('drawn.add-feature-tool', debouncedUpdate); + }; + + tool.uninstall = function() { + presetBrowser.hide(); + + context.keybinding().off(keys); + + context.features() + .on('change.add-feature-tool', null); + + context.map() + .on('move.add-feature-tool', null) + .on('drawn.add-feature-tool', null); + }; + + function browserDidSelectPreset(preset, geometry) { + + var markerClass = 'add-preset add-' + geometry + + ' add-preset-' + preset.name().replace(/\s+/g, '_') + '-' + geometry; + + var modeInfo = { + button: markerClass, + preset: preset, + geometry: geometry, + title: preset.name().split(' – ')[0] + }; + + var mode; + switch (geometry) { + case 'point': + case 'vertex': + mode = modeAddPoint(context, modeInfo); + break; + case 'line': + mode = modeAddLine(context, modeInfo); + break; + case 'area': + mode = modeAddArea(context, modeInfo); + break; + default: + return; + } + + context.presets().setMostRecent(preset); + + context.enter(mode); + } + + function browserDidClose() { + button.classed('active', false); + } + + function updateEnabledState() { + var isEnabled = context.editable(); + button.classed('disabled', !isEnabled); + if (!isEnabled) { + presetBrowser.hide(); + } + } + + return tool; +} diff --git a/modules/ui/tools/adding_geometry.js b/modules/ui/tools/adding_geometry.js new file mode 100644 index 0000000000..eb9c48000e --- /dev/null +++ b/modules/ui/tools/adding_geometry.js @@ -0,0 +1,95 @@ +import { uiToolSegemented } from './segmented'; +import { t } from '../../util/locale'; +import { modeAddPoint } from '../../modes/add_point'; +import { modeAddLine } from '../../modes/add_line'; +import { modeAddArea } from '../../modes/add_area'; + +export function uiToolAddingGeometry(context) { + + var tool = uiToolSegemented(context); + + tool.id = 'adding_geometry'; + tool.label = t('info_panels.measurement.geometry'); + tool.iconName = 'iD-logo-features'; + tool.iconClass = 'icon-30'; + tool.key = t('toolbar.geometry.key'); + + var items = { + point: { + id: 'point', + icon: 'iD-icon-point', + label: t('modes.add_point.title'), + mode: modeAddPoint + }, + vertex: { + id: 'vertex', + icon: 'iD-icon-vertex', + label: t('modes.add_point.title'), + mode: modeAddPoint + }, + line: { + id: 'line', + icon: 'iD-icon-line', + label: t('modes.add_line.title'), + mode: modeAddLine + }, + area: { + id: 'area', + icon: 'iD-icon-area', + label: t('modes.add_area.title'), + mode: modeAddArea + }, + building: { + id: 'building', + icon: 'maki-building-15', + label: t('presets.presets.building.name'), + mode: modeAddArea + } + }; + + tool.chooseItem = function(item) { + var oldMode = context.mode(); + + oldMode.preset.setMostRecentAddGeometry(context, item.id); + + var newMode = item.mode(context, { + button: oldMode.button, + preset: oldMode.preset, + geometry: item.id, + title: oldMode.title + }); + context.enter(newMode); + }; + + tool.activeItem = function() { + return items[context.mode().geometry]; + }; + + tool.loadItems = function() { + var mode = context.mode(); + + if (!mode.preset || + (mode.id !== 'add-point' && mode.id !== 'add-line' && mode.id !== 'add-area') || + mode.addedEntityIDs().length > 0) { + tool.items = []; + } else { + var geometries = context.mode().preset.geometry.slice().sort().reverse(); + var vertexIndex = geometries.indexOf('vertex'); + if (vertexIndex !== -1 && geometries.indexOf('point') !== -1) { + geometries.splice(vertexIndex, 1); + } + + var areaIndex = geometries.indexOf('area'); + if (areaIndex !== -1 && mode.preset.setTags(mode.defaultTags, 'area').building) { + geometries.splice(areaIndex, 1); + geometries.push('building'); + } + + tool.items = geometries.map(function(geom) { + return items[geom]; + }).filter(Boolean); + } + }; + + return tool; +} diff --git a/modules/ui/tools/index.js b/modules/ui/tools/index.js index 023bed85e0..7edc9b3195 100644 --- a/modules/ui/tools/index.js +++ b/modules/ui/tools/index.js @@ -1,5 +1,15 @@ -export * from './modes'; +export * from './add_feature'; +export * from './adding_geometry'; export * from './notes'; +export * from './power_support'; +export * from './quick_presets_addable'; +export * from './quick_presets_favorites'; +export * from './quick_presets_generic'; +export * from './quick_presets_recent'; +export * from './quick_presets'; export * from './save'; -export * from './sidebar_toggle'; +export * from './segmented'; +export * from './structure'; +export * from './toolbox'; export * from './undo_redo'; +export * from './way_segments'; diff --git a/modules/ui/tools/modes.js b/modules/ui/tools/modes.js deleted file mode 100644 index dabb0496ed..0000000000 --- a/modules/ui/tools/modes.js +++ /dev/null @@ -1,160 +0,0 @@ -import _debounce from 'lodash-es/debounce'; - -import { select as d3_select } from 'd3-selection'; - -import { - modeAddArea, - modeAddLine, - modeAddPoint, - modeBrowse -} from '../../modes'; - -import { t } from '../../util/locale'; -import { svgIcon } from '../../svg'; -import { tooltip } from '../../util/tooltip'; -import { uiTooltipHtml } from '../tooltipHtml'; - -export function uiToolOldDrawModes(context) { - - var tool = { - id: 'old_modes', - label: t('toolbar.add_feature') - }; - - var modes = [ - modeAddPoint(context, { - title: t('modes.add_point.title'), - button: 'point', - description: t('modes.add_point.description'), - preset: context.presets().item('point'), - key: '1' - }), - modeAddLine(context, { - title: t('modes.add_line.title'), - button: 'line', - description: t('modes.add_line.description'), - preset: context.presets().item('line'), - key: '2' - }), - modeAddArea(context, { - title: t('modes.add_area.title'), - button: 'area', - description: t('modes.add_area.description'), - preset: context.presets().item('area'), - key: '3' - }) - ]; - - - function enabled() { - return osmEditable(); - } - - function osmEditable() { - return context.editable(); - } - - modes.forEach(function(mode) { - context.keybinding().on(mode.key, function() { - if (!enabled(mode)) return; - - if (mode.id === context.mode().id) { - context.enter(modeBrowse(context)); - } else { - context.enter(mode); - } - }); - }); - - tool.render = function(selection) { - - var wrap = selection - .append('div') - .attr('class', 'joined') - .style('display', 'flex'); - - context - .on('enter.editor', function(entered) { - selection.selectAll('button.add-button') - .classed('active', function(mode) { return entered.button === mode.button; }); - context.container() - .classed('mode-' + entered.id, true); - }); - - context - .on('exit.editor', function(exited) { - context.container() - .classed('mode-' + exited.id, false); - }); - - - var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); - - context.map() - .on('move.modes', debouncedUpdate) - .on('drawn.modes', debouncedUpdate); - - context - .on('enter.modes', update); - - update(); - - - function update() { - - var buttons = wrap.selectAll('button.add-button') - .data(modes, function(d) { return d.id; }); - - // exit - buttons.exit() - .remove(); - - // enter - var buttonsEnter = buttons.enter() - .append('button') - .attr('class', function(d) { return d.id + ' add-button bar-button'; }) - .on('click.mode-buttons', function(d) { - if (!enabled(d)) return; - - // When drawing, ignore accidental clicks on mode buttons - #4042 - var currMode = context.mode().id; - if (/^draw/.test(currMode)) return; - - if (d.id === currMode) { - context.enter(modeBrowse(context)); - } else { - context.enter(d); - } - }) - .call(tooltip() - .placement('bottom') - .html(true) - .title(function(d) { return uiTooltipHtml(d.description, d.key); }) - .scrollContainer(d3_select('#bar')) - ); - - buttonsEnter - .each(function(d) { - d3_select(this) - .call(svgIcon('#iD-icon-' + d.button)); - }); - - buttonsEnter - .append('span') - .attr('class', 'label') - .text(function(mode) { return mode.title; }); - - // if we are adding/removing the buttons, check if toolbar has overflowed - if (buttons.enter().size() || buttons.exit().size()) { - context.ui().checkOverflow('#bar', true); - } - - // update - buttons = buttons - .merge(buttonsEnter) - .classed('disabled', function(d) { return !enabled(d); }); - } - }; - - return tool; -} diff --git a/modules/ui/tools/notes.js b/modules/ui/tools/notes.js index 93557eefc7..476143d5fe 100644 --- a/modules/ui/tools/notes.js +++ b/modules/ui/tools/notes.js @@ -16,7 +16,8 @@ export function uiToolNotes(context) { var tool = { id: 'notes', - label: t('modes.add_note.label') + label: t('modes.add_note.label'), + iconName: 'iD-icon-note' }; var mode = modeAddNote(context); @@ -45,23 +46,77 @@ export function uiToolNotes(context) { } }); - tool.render = function(selection) { + var selection; + tool.render = function(sel) { + selection = sel; + update(); + }; + + function update() { + var showNotes = notesEnabled(); + var data = showNotes ? [mode] : []; + + var buttons = selection.selectAll('button.add-button') + .data(data, function(d) { return d.id; }); + + // exit + buttons.exit() + .remove(); + + // enter + var buttonsEnter = buttons.enter() + .append('button') + .attr('tabindex', -1) + .attr('class', function(d) { return d.id + ' add-button bar-button'; }) + .on('click.notes', function(d) { + if (!enabled(d)) return; + + // When drawing, ignore accidental clicks on mode buttons - #4042 + var currMode = context.mode().id; + if (/^draw/.test(currMode)) return; + + if (d.id === currMode) { + context.enter(modeBrowse(context)); + } else { + context.enter(d); + } + }) + .call(tooltip() + .placement('bottom') + .html(true) + .title(function(d) { return uiTooltipHtml(d.description, d.key); }) + .scrollContainer(d3_select('#bar')) + ); + + buttonsEnter + .each(function() { + d3_select(this) + .call(svgIcon('#' + tool.iconName)); + }); + + // if we are adding/removing the buttons, check if toolbar has overflowed + if (buttons.enter().size() || buttons.exit().size()) { + context.ui().checkOverflow('#bar', true); + } + + // update + buttons = buttons + .merge(buttonsEnter) + .classed('disabled', function(d) { return !enabled(d); }); + } + + tool.allowed = function() { + return notesEnabled(); + }; + + tool.install = function() { context .on('enter.editor.notes', function(entered) { selection.selectAll('button.add-button') .classed('active', function(mode) { return entered.button === mode.button; }); - context.container() - .classed('mode-' + entered.id, true); }); - context - .on('exit.editor.notes', function(exited) { - context.container() - .classed('mode-' + exited.id, false); - }); - - var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); context.map() @@ -70,62 +125,6 @@ export function uiToolNotes(context) { context .on('enter.notes', update); - - update(); - - - function update() { - var showNotes = notesEnabled(); - var data = showNotes ? [mode] : []; - - var buttons = selection.selectAll('button.add-button') - .data(data, function(d) { return d.id; }); - - // exit - buttons.exit() - .remove(); - - // enter - var buttonsEnter = buttons.enter() - .append('button') - .attr('tabindex', -1) - .attr('class', function(d) { return d.id + ' add-button bar-button'; }) - .on('click.notes', function(d) { - if (!enabled(d)) return; - - // When drawing, ignore accidental clicks on mode buttons - #4042 - var currMode = context.mode().id; - if (/^draw/.test(currMode)) return; - - if (d.id === currMode) { - context.enter(modeBrowse(context)); - } else { - context.enter(d); - } - }) - .call(tooltip() - .placement('bottom') - .html(true) - .title(function(d) { return uiTooltipHtml(d.description, d.key); }) - .scrollContainer(d3_select('#bar')) - ); - - buttonsEnter - .each(function(d) { - d3_select(this) - .call(svgIcon(d.icon || '#iD-icon-' + d.button)); - }); - - // if we are adding/removing the buttons, check if toolbar has overflowed - if (buttons.enter().size() || buttons.exit().size()) { - context.ui().checkOverflow('#bar', true); - } - - // update - buttons = buttons - .merge(buttonsEnter) - .classed('disabled', function(d) { return !enabled(d); }); - } }; tool.uninstall = function() { diff --git a/modules/ui/tools/power_support.js b/modules/ui/tools/power_support.js new file mode 100644 index 0000000000..af79bc4073 --- /dev/null +++ b/modules/ui/tools/power_support.js @@ -0,0 +1,77 @@ +import { uiToolSegemented } from './segmented'; +import { t } from '../../util/locale'; + +export function uiToolPowerSupport(context) { + + var tool = uiToolSegemented(context); + + tool.id = 'power_support'; + tool.label = t('toolbar.support.title'); + tool.key = t('toolbar.support.key'); + tool.iconName = 'temaki-power_tower'; + + tool.items = [ + { + id: 'none', + icon: 'temaki-vertex', + label: t('toolbar.structure.none.title'), + tags: {} + }, + { + id: 'pole', + icon: 'temaki-utility_pole', + label: t('toolbar.support.pole.title'), + tags: { + power: 'pole' + } + }, + { + id: 'tower', + icon: 'temaki-power_tower', + label: t('toolbar.support.tower.title'), + tags: { + power: 'tower' + } + } + ]; + + tool.chooseItem = function(item) { + context.mode().defaultNodeTags = item.tags; + }; + + tool.activeItem = function() { + var nodeTags = context.mode().defaultNodeTags; + + return tool.items.find(function(d) { + return nodeTags === d.tags; + }); + }; + + function powerLineValue() { + var mode = context.mode(); + var way = context.hasEntity(mode.wayID); + var tags = (way && way.tags) || mode.defaultTags; + var powerValue = tags && tags.power; + if (powerValue === 'line' || powerValue === 'minor_line') { + return powerValue; + } + return null; + } + + tool.allowed = function() { + if (context.mode().id !== 'draw-line' && context.mode().id !== 'add-line') return false; + return !!powerLineValue(); + }; + + var parentInstall = tool.install; + + tool.install = function() { + parentInstall(); + if (!tool.activeItem()) { + var index = powerLineValue() === 'line' ? 2 : 1; + tool.chooseItem(tool.items[index]); + } + }; + + return tool; +} diff --git a/modules/ui/tools/quick_presets.js b/modules/ui/tools/quick_presets.js new file mode 100644 index 0000000000..147a7e839d --- /dev/null +++ b/modules/ui/tools/quick_presets.js @@ -0,0 +1,354 @@ +import _debounce from 'lodash-es/debounce'; + +import { drag as d3_drag } from 'd3-drag'; +import { event as d3_event, select as d3_select, selectAll as d3_selectAll } from 'd3-selection'; + +import { modeAddArea, modeAddLine, modeAddPoint, modeBrowse } from '../../modes'; +import { t, textDirection } from '../../util/locale'; +import { tooltip } from '../../util/tooltip'; +import { utilSafeClassName } from '../../util/util'; +import { uiPresetIcon } from '../preset_icon'; +import { uiTooltipHtml } from '../tooltipHtml'; + + +export function uiToolQuickPresets(context) { + + var selection = d3_select(null); + + var tool = { + itemClass: 'modes' + }; + + tool.itemsToDraw = function() { + // override in subclass + return []; + }; + + function enabled(d) { + return d.id && context.editable(); + } + + function toggleMode(d) { + if (!enabled(d)) return; + + if (context.mode().id.includes('draw') && context.mode().finish) { + // gracefully complete the feature currently being drawn + var didFinish = context.mode().finish(); + if (!didFinish) return; + } + + if (context.mode().id.includes('add') && d.button === context.mode().button) { + context.enter(modeBrowse(context)); + } else { + if (d.preset && + // don't set a recent as most recent to avoid reordering buttons + !d.isRecent()) { + context.presets().setMostRecent(d.preset); + } + context.enter(d); + } + } + + tool.render = function(sel) { + selection = sel; + update(); + }; + + tool.willUpdate = function() {}; + + function update() { + + tool.willUpdate(); + + var items = tool.itemsToDraw(); + + var modes = items.map(function(d) { + var presetName = d.preset.name().split(' – ')[0]; + var markerClass = 'add-preset add-preset-' + d.preset.safeid + + ' add-' + d.source; // replace spaces with underscores to avoid css interpretation + if (d.preset.isFallback()) { + markerClass += ' add-generic-preset'; + } + + var geometry = d.preset.defaultAddGeometry(context); + + var protoMode = Object.assign({}, d); // shallow copy + protoMode.geometry = geometry; + protoMode.button = markerClass; + protoMode.title = presetName; + + if (geometry) { + protoMode.description = t('modes.add_preset.title', { feature: '' + presetName + '' }); + } else { + var hiddenPresetFeatures = context.features().isHiddenPreset(d.preset, d.preset.geometry[0]); + var isAutoHidden = context.features().autoHidden(hiddenPresetFeatures.key); + var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; + protoMode.description = t('inspector.hidden_preset.' + tooltipIdSuffix, { features: hiddenPresetFeatures.title }); + protoMode.key = null; + } + + var mode; + switch (geometry) { + case 'point': + case 'vertex': + mode = modeAddPoint(context, protoMode); + break; + case 'line': + mode = modeAddLine(context, protoMode); + break; + case 'area': + mode = modeAddArea(context, protoMode); + } + + if (protoMode.key && mode) { + context.keybinding().off(protoMode.key); + context.keybinding().on(protoMode.key, function() { + toggleMode(mode); + }); + } + + return mode || protoMode; + }); + + var buttons = selection.selectAll('button.add-button') + .data(modes, function(d) { return d.button; }) + .order(); + + // exit + buttons.exit() + .remove(); + + // enter + var buttonsEnter = buttons.enter() + .append('button') + .attr('tabindex', -1) + .attr('class', function(d) { + return d.button + ' add-button bar-button'; + }) + .attr('id', function(d) { + return utilSafeClassName(d.button); + }) + .on('click.mode-buttons', function(d) { + if (d3_select(this).classed('disabled')) return; + toggleMode(d); + }) + .call(tooltip() + .placement('bottom') + .html(true) + .title(function(d) { + return d.key ? uiTooltipHtml(d.description, d.key) : d.description; + }) + .scrollContainer(d3_select('#bar')) + ); + + buttonsEnter + .each(function(d) { + + var geometry = d.preset.geometry[0]; + if (d.preset.geometry.length !== 1 || + (geometry !== 'area' && geometry !== 'line' && geometry !== 'vertex')) { + geometry = null; + } + + d3_select(this) + .call(uiPresetIcon(context) + .geometry(geometry) + .preset(d.preset) + .sizeClass('small') + .pointMarker(true) + ); + }); + + var scrollNode = d3_select('#bar').node(); + var dragOrigin, dragMoved, targetData; + var ltr = textDirection === 'ltr', + rtl = !ltr; + + buttonsEnter + .filter('.add-favorite, .add-recent') + .call(d3_drag() + .on('start', function() { + var node = d3_select(this).node(); + dragOrigin = { + x: d3_event.x, + y: d3_event.y, + nodeLeft: node.offsetLeft, + nodeTop: node.offsetTop, + }; + targetData = null; + dragMoved = false; + }) + .on('drag', function(d) { + dragMoved = true; + + var deltaX = d3_event.x - dragOrigin.x, + deltaY = d3_event.y - dragOrigin.y; + + var button = d3_select(this); + + if (!button.classed('dragging')) { + // haven't committed to dragging yet + + // don't display drag until dragging beyond a distance threshold + if (Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) <= 5) return; + + // setup dragging + + d3_select(this.parentNode) + .insert('div', '#' + button.attr('id')) + .attr('class', 'drag-placeholder'); + + button + .classed('dragging', true) + // must use absolute position so button will display if dragged out of the toolbar + .style('position', 'absolute'); + } + + var draggingNode = button.node(); + var eventX = d3_event.x + draggingNode.parentNode.offsetLeft; + var origLeft = dragOrigin.nodeLeft; + + button + .classed('removing', deltaY > 50) + .style('left', dragOrigin.nodeLeft + deltaX - scrollNode.scrollLeft + 'px') + .style('top', dragOrigin.nodeTop + deltaY + 'px'); + + targetData = null; + + d3_selectAll('#bar button.add-favorite, #bar button.add-recent') + .style('transform', function(d2) { + + if (d.button === d2.button) return null; + + // no need to reposition elements if dragging out of toolbar + if (deltaY > 50) return null; + + var node = d3_select(this).node(), + nodeLeft = node.offsetLeft, + nodeRight = nodeLeft + node.offsetWidth; + + if ((ltr && nodeLeft > origLeft && eventX > nodeLeft) || + (rtl && nodeLeft < origLeft && eventX < nodeRight)) { + + if ((ltr && eventX < nodeRight) || + (rtl && eventX > nodeLeft)) { + targetData = d2; + } + return 'translateX(' + (ltr ? '-' : '') + '100%)'; + + } else if ((ltr && nodeLeft < origLeft && eventX < nodeRight) || + (rtl && nodeLeft > origLeft && eventX > nodeLeft)) { + + if ((ltr && eventX > nodeLeft) || + (rtl && eventX < nodeRight)) { + targetData = d2; + } + return 'translateX(' + (ltr ? '' : '-') + '100%)'; + } + + return null; + }); + }) + .on('end', function(d) { + + if (dragMoved && !d3_select(this).classed('dragging')) { + // didn't move, interpret as a click + toggleMode(d); + return; + } + + d3_selectAll('#bar .drag-placeholder') + .remove(); + + d3_select(this) + .classed('dragging', false) + .classed('removing', false) + .style('position', null); + + d3_selectAll('#bar button.add-favorite, #bar button.add-recent') + .style('transform', null); + + var deltaY = d3_event.y - dragOrigin.y; + if (deltaY > 50) { + // dragged out of the top bar, remove + + if (d.isFavorite()) { + context.presets().removeFavorite(d.preset); + // also remove this as a recent so it doesn't still appear + context.presets().removeRecent(d.preset); + } else if (d.isRecent()) { + context.presets().removeRecent(d.preset); + } + } else if (targetData !== null) { + // dragged to a new position, reorder + + if (d.isFavorite()) { + context.presets().removeFavorite(d.preset); + if (targetData.isRecent()) { + // also remove this as a recent so it doesn't appear twice + context.presets().removeRecent(d.preset); + } + } else if (d.isRecent()) { + context.presets().removeRecent(d.preset); + } + + var draggingAfter = (ltr && d3_event.x > dragOrigin.x) || + (rtl && d3_event.x < dragOrigin.x); + + if (targetData.isFavorite()) { + context.presets().addFavorite(d.preset, targetData.preset, draggingAfter); + } else if (targetData.isRecent()) { + context.presets().addRecent(d.preset, targetData.preset, draggingAfter); + } + } + }) + ); + + // update + buttons = buttons + .merge(buttonsEnter) + .classed('disabled', function(d) { return !enabled(d); }); + } + + tool.allowed = function() { + return tool.itemsToDraw().length > 0; + }; + + tool.install = function() { + context + .on('enter.editor.' + tool.id, function(entered) { + selection.selectAll('button.add-button') + .classed('active', function(mode) { return entered.button === mode.button; }); + }); + + var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); + + context.map() + .on('move.' + tool.id, debouncedUpdate) + .on('drawn.' + tool.id, debouncedUpdate); + + context + .on('enter.' + tool.id, update) + .presets() + .on('favoritePreset.' + tool.id, update) + .on('recentsChange.' + tool.id, update); + }; + + tool.uninstall = function() { + + context + .on('enter.editor.' + tool.id, null) + .on('exit.editor.' + tool.id, null) + .on('enter.' + tool.id, null); + + context.presets() + .on('favoritePreset.' + tool.id, null) + .on('recentsChange.' + tool.id, null); + + context.map() + .on('move.' + tool.id, null) + .on('drawn.' + tool.id, null); + }; + + return tool; +} diff --git a/modules/ui/tools/quick_presets_addable.js b/modules/ui/tools/quick_presets_addable.js new file mode 100644 index 0000000000..159d48553f --- /dev/null +++ b/modules/ui/tools/quick_presets_addable.js @@ -0,0 +1,33 @@ +import { t } from '../../util/locale'; +import { uiToolQuickPresets } from './quick_presets'; + +export function uiToolAddAddablePresets(context) { + + var tool = uiToolQuickPresets(context); + tool.id = 'add_addable_preset'; + tool.label = t('toolbar.add_feature'); + tool.userToggleable = false; + + tool.itemsToDraw = function() { + + var items = context.presets().getAddable().slice(0, 10); + + items.forEach(function(item, index) { + var keyCode; + // use number row order: 1 2 3 4 5 6 7 8 9 0 + // use the same for RTL even though the layout is backward: #6107 + if (index === 9) { + keyCode = 0; + } else if (index < 10) { + keyCode = index + 1; + } + if (keyCode !== undefined) { + item.key = keyCode.toString(); + } + }); + + return items; + }; + + return tool; +} diff --git a/modules/ui/tools/quick_presets_favorites.js b/modules/ui/tools/quick_presets_favorites.js new file mode 100644 index 0000000000..5dc91f1d51 --- /dev/null +++ b/modules/ui/tools/quick_presets_favorites.js @@ -0,0 +1,37 @@ +import { t } from '../../util/locale'; +import { uiToolQuickPresets } from './quick_presets'; + +export function uiToolAddFavorite(context) { + + var tool = uiToolQuickPresets(context); + tool.id = 'add_favorite'; + tool.label = t('toolbar.favorites'); + tool.iconName = 'iD-icon-favorite'; + + tool.itemsToDraw = function() { + if (context.presets().getAddable().length) return []; + + var items = context.presets().getFavorites().slice(0, 10); + + var precedingCount = context.storage('tool.add_generic.toggledOn') === 'true' ? 3 : 0; + + items.forEach(function(item, index) { + var totalIndex = precedingCount + index; + var keyCode; + // use number row order: 1 2 3 4 5 6 7 8 9 0 + // use the same for RTL even though the layout is backward: #6107 + if (totalIndex === 9) { + keyCode = 0; + } else if (totalIndex < 10) { + keyCode = totalIndex + 1; + } + if (keyCode !== undefined) { + item.key = keyCode.toString(); + } + }); + + return items; + }; + + return tool; +} diff --git a/modules/ui/tools/quick_presets_generic.js b/modules/ui/tools/quick_presets_generic.js new file mode 100644 index 0000000000..b569bbc1ab --- /dev/null +++ b/modules/ui/tools/quick_presets_generic.js @@ -0,0 +1,32 @@ +import { t } from '../../util/locale'; +import { uiToolQuickPresets } from './quick_presets'; + +export function uiToolAddGeneric(context) { + + var tool = uiToolQuickPresets(context); + tool.id = 'add_generic'; + tool.label = t('toolbar.generic.title'); + tool.iconName = 'iD-logo-features'; + tool.iconClass = 'icon-30'; + + if (context.storage('tool.add_generic.toggledOn') === null) { + if (!context.isFirstSession) { + // assume existing user coming from iD 2, enable this item by default + tool.isToggledOn = true; + } else { + tool.isToggledOn = false; + } + context.storage('tool.add_generic.toggledOn', tool.isToggledOn); + } + + tool.itemsToDraw = function() { + if (context.presets().getAddable().length) return []; + var items = context.presets().getGenericRibbonItems(); + for (var i in items) { + items[i].key = (parseInt(i, 10) + 1).toString(); + } + return items; + }; + + return tool; +} diff --git a/modules/ui/tools/quick_presets_recent.js b/modules/ui/tools/quick_presets_recent.js new file mode 100644 index 0000000000..d139ed35dd --- /dev/null +++ b/modules/ui/tools/quick_presets_recent.js @@ -0,0 +1,74 @@ +import { t } from '../../util/locale'; +import { uiToolQuickPresets } from './quick_presets'; + +export function uiToolAddRecent(context) { + + var tool = uiToolQuickPresets(context); + tool.id = 'add_recent'; + tool.label = t('toolbar.recent'); + tool.iconName = 'fas-clock'; + + tool.itemsToDraw = function() { + if (context.presets().getAddable().length) return []; + + var maxShown = 10; + var maxRecents = 5; + var precedingCount = context.storage('tool.add_generic.toggledOn') === 'true' ? 3 : 0; + + var favorites = context.presets().getFavorites().slice(0, maxShown); + var generics = context.presets().getGenericRibbonItems(); + precedingCount += favorites.length; + + function isAFavorite(recent) { + return favorites.some(function(favorite) { + return favorite.matches(recent.preset); + }); + } + + function isGeneric(recent) { + return generics.some(function(generic) { + return generic.matches(recent.preset); + }); + } + + maxRecents = Math.min(maxRecents, maxShown - precedingCount); + var items = []; + if (maxRecents > 0) { + var recents = context.presets().getRecents().filter(function(recent) { + return recent.preset.geometry.length > 1 || recent.preset.geometry[0] !== 'relation'; + }); + for (var i in recents) { + var recent = recents[i]; + if (isAFavorite(recent)) { + continue; + } + if (isGeneric(recent) && context.storage('tool.add_generic.toggledOn') === 'true') { + continue; + } + items.push(recent); + if (items.length === maxRecents) { + break; + } + } + } + + items.forEach(function(item, index) { + var totalIndex = precedingCount + index; + var keyCode; + // use number row order: 1 2 3 4 5 6 7 8 9 0 + // use the same for RTL even though the layout is backward: #6107 + if (totalIndex === 9) { + keyCode = 0; + } else if (totalIndex < 10) { + keyCode = totalIndex + 1; + } + if (keyCode !== undefined) { + item.key = keyCode.toString(); + } + }); + + return items; + }; + + return tool; +} diff --git a/modules/ui/tools/repeat_add.js b/modules/ui/tools/repeat_add.js new file mode 100644 index 0000000000..2f92386a84 --- /dev/null +++ b/modules/ui/tools/repeat_add.js @@ -0,0 +1,74 @@ +import { select as d3_select } from 'd3-selection'; +import { t } from '../../util/locale'; +import { svgIcon } from '../../svg/icon'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { tooltip } from '../../util/tooltip'; + +export function uiToolRepeatAdd(context) { + + var key = t('toolbar.repeat.key'); + + var tool = { + id: 'repeat_add', + label: t('toolbar.repeat.title'), + iconName: 'iD-icon-repeat' + }; + + var button; + + var tooltipBehavior = tooltip() + .placement('bottom') + .html(true) + .scrollContainer(d3_select('#bar')); + + tool.render = function(selection) { + + var mode = context.mode(); + var geom = mode.id.indexOf('point') !== -1 ? 'point' : 'way'; + + tooltipBehavior.title(uiTooltipHtml(t('toolbar.repeat.tooltip.' + geom, { feature: '' + mode.title + '' }), key)); + + button = selection + .selectAll('.bar-button') + .data([0]); + + button = button + .enter() + .append('button') + .attr('class', 'bar-button wide') + .classed('active', mode.repeatAddedFeature()) + .attr('tabindex', -1) + .call(tooltipBehavior) + .on('click', function() { + toggleRepeat(); + }) + .call(svgIcon('#' + tool.iconName)) + .merge(button); + }; + + function toggleRepeat() { + var mode = context.mode(); + mode.repeatAddedFeature(!mode.repeatAddedFeature()); + button.classed('active', mode.repeatAddedFeature()); + } + + tool.allowed = function() { + var mode = context.mode(); + if (mode.id === 'add-point' || mode.id === 'add-line' || mode.id === 'add-area') return true; + return (mode.id === 'draw-line' || mode.id === 'draw-area') && !mode.isContinuing; + }; + + tool.install = function() { + context.keybinding() + .on(key, toggleRepeat, true); + }; + + tool.uninstall = function() { + context.keybinding() + .off(key, true); + + button = null; + }; + + return tool; +} diff --git a/modules/ui/tools/save.js b/modules/ui/tools/save.js index 1e0c4a57a5..b04309c8c9 100644 --- a/modules/ui/tools/save.js +++ b/modules/ui/tools/save.js @@ -1,6 +1,5 @@ import { interpolateRgb as d3_interpolateRgb } from 'd3-interpolate'; import { event as d3_event, select as d3_select } from 'd3-selection'; - import { t } from '../../util/locale'; import { modeSave } from '../../modes'; import { svgIcon } from '../../svg'; @@ -13,14 +12,19 @@ export function uiToolSave(context) { var tool = { id: 'save', - label: t('save.title') + label: t('save.title'), + userToggleable: false }; var button = null; - var tooltipBehavior = null; + var tooltipBehavior = tooltip() + .placement('bottom') + .html(true) + .title(uiTooltipHtml(t('save.no_changes'), key)) + .scrollContainer(d3_select('#bar')); var history = context.history(); var key = uiCmd('⌘S'); - var _numChanges = 0; + var _numChanges; function isSaving() { var mode = context.mode(); @@ -28,7 +32,7 @@ export function uiToolSave(context) { } function isDisabled() { - return _numChanges === 0 || isSaving(); + return !_numChanges || isSaving(); } function save() { @@ -38,15 +42,15 @@ export function uiToolSave(context) { } } - function bgColor() { + function bgColor(count) { var step; - if (_numChanges === 0) { + if (count === 0) { return null; - } else if (_numChanges <= 50) { - step = _numChanges / 50; + } else if (count <= 50) { + step = count / 50; return d3_interpolateRgb('#fff', '#ff8')(step); // white -> yellow } else { - step = Math.min((_numChanges - 50) / 50, 1.0); + step = Math.min((count - 50) / 50, 1.0); return d3_interpolateRgb('#ff8', '#f88')(step); // yellow -> red } } @@ -60,50 +64,65 @@ export function uiToolSave(context) { if (tooltipBehavior) { tooltipBehavior .title(uiTooltipHtml( - t(_numChanges > 0 ? 'save.help' : 'save.no_changes'), key) + t(val > 0 ? 'save.help' : 'save.no_changes'), key) ); } if (button) { button .classed('disabled', isDisabled()) - .style('background', bgColor(_numChanges)); + .style('background', bgColor(val)); button.select('span.count') - .text(_numChanges); + .text(val); } } tool.render = function(selection) { - tooltipBehavior = tooltip() - .placement('bottom') - .html(true) - .title(uiTooltipHtml(t('save.no_changes'), key)) - .scrollContainer(d3_select('#bar')); button = selection + .selectAll('.bar-button') + .data([0]); + + var buttonEnter = button + .enter() .append('button') .attr('class', 'save disabled bar-button') .on('click', save) .call(tooltipBehavior); - button + buttonEnter .call(svgIcon('#iD-icon-save')); - button + buttonEnter .append('span') .attr('class', 'count') .attr('aria-hidden', 'true') .text('0'); + button = buttonEnter.merge(button); + updateCount(); + }; + var disallowedModes = new Set([ + 'save', + 'add-point', + 'add-line', + 'add-area', + 'draw-line', + 'draw-area' + ]); + + tool.allowed = function() { + return !disallowedModes.has(context.mode().id); + }; + tool.install = function() { context.keybinding() .on(key, save, true); - context.history() .on('change.save', updateCount); @@ -122,6 +141,9 @@ export function uiToolSave(context) { tool.uninstall = function() { + + _numChanges = null; + context.keybinding() .off(key, true); @@ -132,7 +154,6 @@ export function uiToolSave(context) { .on('enter.save', null); button = null; - tooltipBehavior = null; }; return tool; diff --git a/modules/ui/tools/segmented.js b/modules/ui/tools/segmented.js new file mode 100644 index 0000000000..567bbc9b28 --- /dev/null +++ b/modules/ui/tools/segmented.js @@ -0,0 +1,115 @@ +import { + select as d3_select +} from 'd3-selection'; + +import { svgIcon } from '../../svg/icon'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { tooltip } from '../../util/tooltip'; + +export function uiToolSegemented(context) { + + var tool = { + contentClass: 'joined' + }; + + tool.items = []; + + // populate the items array + tool.loadItems = function() { + // override in subclass + }; + + // set the active item + tool.chooseItem = function(/* item */) { + // override in subclass + }; + + // return the chosen item + tool.activeItem = function() { + // override in subclass + }; + + var container = d3_select(null); + + tool.render = function(selection) { + container = selection; + var active = tool.activeItem(); + + var buttons = selection.selectAll('.bar-button') + .data(tool.items, function(d) { return d.id; }); + + buttons.exit() + .remove(); + + buttons + .enter() + .append('button') + .attr('class', function(d) { + return 'bar-button ' + d.id + ' ' + (d === active ? 'active' : ''); + }) + .attr('tabindex', -1) + .on('click', function(d) { + if (d3_select(this).classed('active')) return; + + setActiveItem(d); + }) + .each(function(d) { + var title = tool.key ? uiTooltipHtml(d.label, tool.key) : d.label; + var tooltipBehavior = tooltip() + .placement('bottom') + .html(true) + .title(title) + .scrollContainer(d3_select('#bar')); + d3_select(this) + .call(tooltipBehavior) + .call(svgIcon('#' + d.icon, d.iconClass)); + }); + }; + + function setActiveItem(d) { + tool.chooseItem(d); + setButtonStates(); + } + + function setButtonStates() { + container.selectAll('.bar-button.active') + .classed('active', false); + container.selectAll('.bar-button.' + tool.activeItem().id) + .classed('active', true); + } + + function toggleItem() { + if (tool.items.length === 0) return; + + var active = tool.activeItem(); + var index = tool.items.indexOf(active); + if (index === tool.items.length - 1) { + index = 0; + } else { + index += 1; + } + + setActiveItem(tool.items[index]); + } + + tool.allowed = function() { + if (tool.loadItems) tool.loadItems(); + return tool.items.length > 1; + }; + + tool.install = function() { + if (tool.key) { + context.keybinding() + .on(tool.key, toggleItem, true); + } + }; + + tool.uninstall = function() { + if (tool.key) { + context.keybinding() + .off(tool.key, true); + } + }; + + return tool; +} diff --git a/modules/ui/tools/sidebar_toggle.js b/modules/ui/tools/sidebar_toggle.js deleted file mode 100644 index 62561cdc8f..0000000000 --- a/modules/ui/tools/sidebar_toggle.js +++ /dev/null @@ -1,31 +0,0 @@ -import { select as d3_select } from 'd3-selection'; -import { t, textDirection } from '../../util/locale'; -import { svgIcon } from '../../svg'; -import { uiTooltipHtml } from '../tooltipHtml'; -import { tooltip } from '../../util/tooltip'; - -export function uiToolSidebarToggle(context) { - - var tool = { - id: 'sidebar_toggle', - label: t('toolbar.inspect') - }; - - tool.render = function(selection) { - selection - .append('button') - .attr('class', 'bar-button') - .on('click', function() { - context.ui().sidebar.toggle(); - }) - .call(tooltip() - .placement('bottom') - .html(true) - .title(uiTooltipHtml(t('sidebar.tooltip'), t('sidebar.key'))) - .scrollContainer(d3_select('#bar')) - ) - .call(svgIcon('#iD-icon-sidebar-' + (textDirection === 'rtl' ? 'right' : 'left'))); - }; - - return tool; -} diff --git a/modules/ui/tools/simple_button.js b/modules/ui/tools/simple_button.js new file mode 100644 index 0000000000..c73c95ef49 --- /dev/null +++ b/modules/ui/tools/simple_button.js @@ -0,0 +1,40 @@ +import { select as d3_select } from 'd3-selection'; +import { svgIcon } from '../../svg/icon'; +import { uiTooltipHtml } from '../tooltipHtml'; +import { tooltip } from '../../util/tooltip'; +import { utilFunctor } from '../../util/util'; + +export function uiToolSimpleButton(protoTool) { + + var tool = protoTool || {}; + + var tooltipBehavior = tooltip() + .placement('bottom') + .html(true) + .scrollContainer(d3_select('#bar')); + + tool.render = function(selection) { + + tooltipBehavior.title(uiTooltipHtml(utilFunctor(tool.tooltipText)(), utilFunctor(tool.tooltipKey)())); + + var button = selection + .selectAll('.bar-button') + .data([0]); + + var buttonEnter = button + .enter() + .append('button') + .attr('class', 'bar-button ' + (utilFunctor(tool.barButtonClass)() || '')) + .attr('tabindex', -1) + .call(tooltipBehavior) + .on('click', tool.onClick) + .call(svgIcon('#', utilFunctor(tool.iconClass)())); + + button = buttonEnter.merge(button); + + button.selectAll('.icon use') + .attr('href', '#' + utilFunctor(tool.iconName)()); + }; + + return tool; +} diff --git a/modules/ui/tools/stop_draw.js b/modules/ui/tools/stop_draw.js new file mode 100644 index 0000000000..120d3bde06 --- /dev/null +++ b/modules/ui/tools/stop_draw.js @@ -0,0 +1,65 @@ + +import { uiToolSimpleButton } from './simple_button'; +import { t } from '../../util/locale'; +import { modeBrowse } from '../../modes/browse'; + +export function uiToolStopDraw(context) { + + var cancelOrFinish = 'cancel'; + + var tool = uiToolSimpleButton({ + id: 'stop_draw', + label: function() { + if (cancelOrFinish === 'finish') { + return t('toolbar.finish'); + } + return t('confirm.cancel'); + }, + iconName: function() { + if (cancelOrFinish === 'finish') { + return 'iD-icon-apply'; + } + return 'iD-icon-close'; + }, + onClick: function() { + var mode = context.mode(); + if (cancelOrFinish === 'finish' && mode.finish) { + mode.finish(); + } else { + context.enter(modeBrowse(context)); + } + }, + tooltipKey: 'Esc', + barButtonClass: 'wide', + userToggleable: false + }); + + tool.allowed = function() { + var newCancelOrFinish = drawCancelOrFinish(); + if (newCancelOrFinish) { + cancelOrFinish = newCancelOrFinish; + } + return newCancelOrFinish; + }; + + + function drawCancelOrFinish() { + var mode = context.mode(); + if (mode.id === 'draw-line' || mode.id === 'draw-area') { + var way = context.hasEntity(mode.wayID); + var wayIsDegenerate = way && new Set(way.nodes).size - 1 < (way.isArea() ? 3 : 2); + if (wayIsDegenerate) { + return 'cancel'; + } + return 'finish'; + } else if (mode.id === 'add-point' || mode.id === 'add-line' || mode.id === 'add-area') { + if (mode.addedEntityIDs().length === 0) { + return 'cancel'; + } + return 'finish'; + } + return null; + } + + return tool; +} diff --git a/modules/ui/tools/structure.js b/modules/ui/tools/structure.js new file mode 100644 index 0000000000..a41f0a0a3d --- /dev/null +++ b/modules/ui/tools/structure.js @@ -0,0 +1,192 @@ +import { uiToolSegemented } from './segmented'; +import { t } from '../../util/locale'; +import { osmTagsAllowingBridges, osmTagsAllowingTunnels } from '../../osm/tags'; +import { actionChangeTags } from '../../actions/change_tags'; +import { actionAddEntity } from '../../actions/add_entity'; +import { actionAddVertex } from '../../actions/add_vertex'; +import { actionJoin } from '../../actions/join'; +import { modeDrawLine } from '../../modes/draw_line'; +import { osmWay } from '../../osm/way'; + +export function uiToolStructure(context) { + + var tool = uiToolSegemented(context); + + tool.id = 'structure'; + tool.label = t('presets.fields.structure.label'); + tool.key = t('toolbar.structure.key'); + tool.iconName = 'temaki-bridge'; + tool.iconClass = 'icon-30'; + + var structureNone = { + id: 'none', + icon: 'iD-structure-none', + label: t('toolbar.structure.none.title'), + iconClass: 'icon-30', + tags: {} + }; + var structureBridge = { + id: 'bridge', + icon: 'temaki-bridge', + label: t('presets.fields.structure.options.bridge'), + iconClass: 'icon-30', + tags: { + bridge: 'yes' + }, + addTags: { + bridge: 'yes', + layer: '1' + } + }; + var structureTunnel = { + id: 'tunnel', + icon: 'temaki-tunnel', + label: t('presets.fields.structure.options.tunnel'), + iconClass: 'icon-30', + tags: { + tunnel: 'yes' + }, + addTags: { + tunnel: 'yes', + layer: '-1' + } + }; + + var prevWayID; + + tool.loadItems = function() { + tool.items = [ + structureNone + ]; + + var tags = activeTags(); + + function allowsStructure(osmTags) { + for (var key in tags) { + if (osmTags[key] && osmTags[key][tags[key]]) return true; + } + return false; + } + + if (allowsStructure(osmTagsAllowingBridges)) tool.items.push(structureBridge); + if (allowsStructure(osmTagsAllowingTunnels)) tool.items.push(structureTunnel); + }; + + tool.chooseItem = function(d) { + var tags = Object.assign({}, activeTags()); + + var priorStructure = tool.activeItem(); + var tagsToRemove = priorStructure.addTags || priorStructure.tags; + for (var key in tagsToRemove) { + if (tags[key]) { + delete tags[key]; + } + } + // add tags for structure + Object.assign(tags, d.addTags || d.tags); + + var mode = context.mode(); + if (mode.id === 'add-line') { + mode.defaultTags = tags; + + } else if (mode.id === 'draw-line') { + + if (mode.addMode) mode.addMode.defaultTags = tags; + + var wayID = mode.wayID; + var way = context.hasEntity(wayID); + if (!way) return; + + var prevWay = context.hasEntity(prevWayID); + + if (way.nodes.length <= (mode.activeID() ? 2 : 1)) { + context.replace( + actionChangeTags(wayID, tags) + ); + + // Reload way with updated tags + way = context.hasEntity(wayID); + + if (prevWay && JSON.stringify(prevWay.tags) === JSON.stringify(way.tags)) { + + var action = actionJoin([prevWay.id, way.id]); + + if (!action.disabled(context.graph())) { + context.perform(action); + + context.enter( + modeDrawLine(context, + prevWay.id, + context.graph(), + mode.button, + mode.affix, + mode.addMode + ) + ); + } + } + } else { + var isLast = mode.affix !== 'prefix'; + var offset = mode.activeID() ? 1 : 0; + var splitNodeID = isLast ? way.nodes[way.nodes.length - 1 - offset] : way.nodes[offset]; + + mode.finish(true); + + var startGraph = context.graph(); + + var newWay = osmWay({ tags: tags }); + context.perform( + actionAddEntity(newWay), + actionAddVertex(newWay.id, splitNodeID) + ); + + prevWayID = way.id; + context.enter( + modeDrawLine(context, + newWay.id, + startGraph, + mode.button, + mode.affix, + mode.addMode + ) + ); + } + } + }; + + function activeTags() { + var mode = context.mode(); + if (mode.id === 'add-line') { + return mode.defaultTags; + } else if (mode.id === 'draw-line') { + var way = context.hasEntity(mode.wayID); + return way ? way.tags : {}; + } + return {}; + } + + tool.activeItem = function() { + + var tags = activeTags(); + + function tagsMatchStructure(structure) { + for (var key in structure.tags) { + if (!tags[key] || tags[key] === 'no') return false; + } + return Object.keys(structure.tags).length !== 0; + } + + for (var i in tool.items) { + if (tagsMatchStructure(tool.items[i])) return tool.items[i]; + } + return structureNone; + }; + + var parentAvailable = tool.allowed; + tool.allowed = function() { + var modeID = context.mode().id; + return parentAvailable() && (modeID === 'add-line' || modeID === 'draw-line'); + }; + + return tool; +} diff --git a/modules/ui/tools/toolbox.js b/modules/ui/tools/toolbox.js new file mode 100644 index 0000000000..1883c90ce0 --- /dev/null +++ b/modules/ui/tools/toolbox.js @@ -0,0 +1,172 @@ +import { + event as d3_event, + select as d3_select, +} from 'd3-selection'; + +import { t } from '../../util/locale'; +import { svgIcon } from '../../svg/icon'; +import { tooltip } from '../../util/tooltip'; +import { popover } from '../../util/popover'; +import { utilFunctor } from '../../util/util'; + +export function uiToolToolbox(context) { + + var tool = { + id: 'toolbox', + label: t('toolbar.toolbox.title'), + itemClass: 'disclosing', + userToggleable: false + }; + + var allowedTools = []; + + var button = d3_select(null), + list = d3_select(null), + poplist = popover('poplist fillL') + .displayType('clickFocus') + .placement('bottom') + .alignment('leading') + .hasArrow(false) + .scrollContainer(d3_select('#bar')); + + tool.render = function(selection) { + + button = selection.selectAll('.bar-button') + .data([0]); + + var buttonEnter = button + .enter() + .append('button') + .attr('class', 'bar-button') + .attr('tabindex', -1) + .call(poplist) + .call(tooltip() + .placement('bottom') + .html(true) + .title(t('toolbar.toolbox.tooltip')) + .scrollContainer(d3_select('#bar')) + ) + .call(svgIcon('#fas-toolbox')); + + buttonEnter + .append('span') + .call(svgIcon('#iD-icon-down', 'disclosure-icon')); + + button = buttonEnter.merge(button); + + updateToolList(); + }; + + poplist.content(function() { + return function(selection) { + + var poplistContent = selection.selectAll('.poplist-content') + .data([0]); + + var poplistEnter = poplistContent.enter() + .append('div') + .attr('class', 'poplist-content') + .on('mousedown', function() { + // don't blur the search input (and thus close results) + d3_event.preventDefault(); + d3_event.stopPropagation(); + }); + + poplistEnter + .append('div') + .attr('class', 'list'); + + poplistContent = poplistContent.merge(poplistEnter); + + list = poplistContent.select('.list'); + + updateToolList(); + }; + }); + + function updateToolList() { + + if (list.empty()) return; + + var items = list.selectAll('.list-item') + .data(allowedTools, function(d) { return d.id; }); + + items.order(); + + items.exit() + .remove(); + + var itemsEnter = items.enter() + .append('div') + .attr('class', 'list-item') + .on('mouseover', function() { + list.selectAll('.list .list-item.focused') + .classed('focused', false); + d3_select(this) + .classed('focused', true); + }) + .on('mouseout', function() { + d3_select(this) + .classed('focused', false); + }); + + var row = itemsEnter.append('div') + .attr('class', 'row'); + + row.append('button') + .attr('class', 'choose') + .on('click', function(d) { + d3_event.preventDefault(); + d3_event.stopPropagation(); + + d.isToggledOn = !(d.isToggledOn !== false); + context.storage('tool.' + d.id + '.toggledOn', d.isToggledOn); + updateToolList(); + if (tool.onChange) tool.onChange(); + }); + + row.each(function(d) { + if (d.iconName) { + d3_select(this).call( + svgIcon('#' + (utilFunctor(d.toolboxIconName)() || utilFunctor(d.iconName)()), 'item-icon ' + (d.iconClass || '')) + ); + } + }); + + row.append('div') + .attr('class', 'label') + .text(function(d) { + return utilFunctor(d.toolboxLabel)() || utilFunctor(d.label)(); + }); + + row.append('div') + .attr('class', 'accessory') + .each(function() { + d3_select(this).call( + svgIcon('#iD-icon-apply', 'checkmark') + ); + }); + + items = itemsEnter.merge(items); + + items.selectAll('.accessory') + .classed('hide', function(d) { + return d.isToggledOn === false; + }); + } + + tool.setAllowedTools = function(newItems) { + allowedTools = newItems.filter(function(item) { + return typeof item === 'object' && item.userToggleable !== false; + }); + + allowedTools.forEach(function(d) { + var isToggledOn = context.storage('tool.' + d.id + '.toggledOn'); + if (isToggledOn !== null) { + d.isToggledOn = isToggledOn === 'true'; + } + }); + }; + + return tool; +} diff --git a/modules/ui/tools/undo_redo.js b/modules/ui/tools/undo_redo.js index b39ff390d6..6a454e099e 100644 --- a/modules/ui/tools/undo_redo.js +++ b/modules/ui/tools/undo_redo.js @@ -16,7 +16,9 @@ export function uiToolUndoRedo(context) { var tool = { id: 'undo_redo', - label: t('toolbar.undo_redo') + label: t('toolbar.undo_redo'), + iconName: textDirection === 'rtl' ? 'iD-icon-redo' : 'iD-icon-undo', + userToggleable: false }; var commands = [{ @@ -36,44 +38,68 @@ export function uiToolUndoRedo(context) { return context.mode() && context.mode().id !== 'save' && context.map().editableDataEnabled(true /* ignore min zoom */); } + var tooltipBehavior = tooltip() + .placement('bottom') + .html(true) + .title(function (d) { + return uiTooltipHtml(d.annotation() ? + t(d.id + '.tooltip', {action: d.annotation()}) : + t(d.id + '.nothing'), d.cmd); + }) + .scrollContainer(d3_select('#bar')); + + var buttons; tool.render = function(selection) { - var tooltipBehavior = tooltip() - .placement('bottom') - .html(true) - .title(function (d) { - return uiTooltipHtml(d.annotation() ? - t(d.id + '.tooltip', {action: d.annotation()}) : - t(d.id + '.nothing'), d.cmd); - }) - .scrollContainer(d3_select('#bar')); - var buttons = selection.selectAll('button') - .data(commands) + buttons = selection.selectAll('button') + .data(commands); + + var buttonsEnter = buttons .enter() .append('button') .attr('class', function(d) { return 'disabled ' + d.id + '-button bar-button'; }) .on('click', function(d) { return d.action(); }) .call(tooltipBehavior); - buttons.each(function(d) { - var iconName = d.id; + buttonsEnter.each(function(d) { + var iconName; if (textDirection === 'rtl') { - if (iconName === 'undo') { - iconName = 'redo'; - } else if (iconName === 'redo') { - iconName = 'undo'; - } + // reverse the icons for right-to-left layout + iconName = d.id === 'undo' ? 'redo' : 'undo'; + } else { + iconName = d.id; } d3_select(this) .call(svgIcon('#iD-icon-' + iconName)); }); + buttons = buttonsEnter.merge(buttons); + }; + + function update() { + buttons + .property('disabled', !editable()) + .classed('disabled', function(d) { + return !editable() || !d.annotation(); + }) + .each(function() { + var selection = d3_select(this); + if (!selection.select('.tooltip.in').empty()) { + selection.call(tooltipBehavior.updateContent); + } + }); + } + + tool.allowed = function() { + return context.mode().id !== 'save'; + }; + + tool.install = function() { context.keybinding() .on(commands[0].cmd, function() { d3_event.preventDefault(); commands[0].action(); }) .on(commands[1].cmd, function() { d3_event.preventDefault(); commands[1].action(); }); - var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); context.map() @@ -87,21 +113,6 @@ export function uiToolUndoRedo(context) { context .on('enter.undo_redo', update); - - - function update() { - buttons - .property('disabled', !editable()) - .classed('disabled', function(d) { - return !editable() || !d.annotation(); - }) - .each(function() { - var selection = d3_select(this); - if (!selection.select('.tooltip.in').empty()) { - selection.call(tooltipBehavior.updateContent); - } - }); - } }; tool.uninstall = function() { diff --git a/modules/ui/tools/way_segments.js b/modules/ui/tools/way_segments.js new file mode 100644 index 0000000000..fd6189dc77 --- /dev/null +++ b/modules/ui/tools/way_segments.js @@ -0,0 +1,44 @@ +import { uiToolSegemented } from './segmented'; +import { t } from '../../util/locale'; + +export function uiToolWaySegments(context) { + + var tool = uiToolSegemented(context); + + tool.id = 'way_segments'; + tool.label = t('toolbar.segments.title'); + tool.key = t('toolbar.segments.key'); + tool.iconName = 'iD-segment-orthogonal'; + tool.iconClass = 'icon-30'; + + tool.items = [ + { + id: 'straight', + icon: 'iD-segment-straight', + label: t('toolbar.segments.straight.title'), + iconClass: 'icon-30' + }, + { + id: 'orthogonal', + icon: 'iD-segment-orthogonal', + label: t('toolbar.segments.orthogonal.title'), + iconClass: 'icon-30' + } + ]; + + tool.chooseItem = function(item) { + context.storage('line-segments', item.id); + }; + + tool.activeItem = function() { + var id = context.storage('line-segments') || 'straight'; + return tool.items.filter(function(d) { return d.id === id; })[0]; + }; + + tool.allowed = function() { + var mode = context.mode(); + return mode.id.indexOf('line') !== -1 || mode.id.indexOf('area') !== -1; + }; + + return tool; +} diff --git a/modules/ui/top_toolbar.js b/modules/ui/top_toolbar.js index 5b35fac22c..53a496836b 100644 --- a/modules/ui/top_toolbar.js +++ b/modules/ui/top_toolbar.js @@ -1,24 +1,87 @@ - import { event as d3_event, select as d3_select } from 'd3-selection'; - import _debounce from 'lodash-es/debounce'; -import { uiToolOldDrawModes, uiToolNotes, uiToolSave, uiToolSidebarToggle, uiToolUndoRedo } from './tools'; +import { utilFunctor } from '../util/util'; + +import { uiToolAddFavorite, uiToolAddFeature, uiToolAddRecent, uiToolNotes, uiToolSave, uiToolUndoRedo } from './tools'; +import { uiToolAddAddablePresets } from './tools/quick_presets_addable'; +import { uiToolAddGeneric } from './tools/quick_presets_generic'; +import { uiToolWaySegments } from './tools/way_segments'; +import { uiToolRepeatAdd } from './tools/repeat_add'; +import { uiToolStructure } from './tools/structure'; +import { uiToolStopDraw } from './tools/stop_draw'; +import { uiToolToolbox } from './tools/toolbox'; +import { uiToolAddingGeometry } from './tools/adding_geometry'; +import { uiToolPowerSupport } from './tools/power_support'; export function uiTopToolbar(context) { - var sidebarToggle = uiToolSidebarToggle(context), - modes = uiToolOldDrawModes(context), + var toolbox = uiToolToolbox(context), + addAddable = uiToolAddAddablePresets(context), + addFeature = uiToolAddFeature(context), + addGeneric = uiToolAddGeneric(context), + addFavorite = uiToolAddFavorite(context), + addRecent = uiToolAddRecent(context), notes = uiToolNotes(context), undoRedo = uiToolUndoRedo(context), - save = uiToolSave(context); + save = uiToolSave(context), + waySegments = uiToolWaySegments(context), + structure = uiToolStructure(context), + repeatAdd = uiToolRepeatAdd(context), + stopDraw = uiToolStopDraw(context), + addingGeometry = uiToolAddingGeometry(context), + powerSupport = uiToolPowerSupport(context); + + function allowedTools() { + + var mode = context.mode(); + if (!mode) return []; + + var tools; + + if (mode.id === 'add-point' || mode.id === 'add-line' || mode.id === 'add-area' || + mode.id === 'draw-line' || mode.id === 'draw-area') { + + tools = [ + toolbox, + addingGeometry, + 'spacer', + structure, + powerSupport, + 'spacer', + waySegments, + 'spacer', + repeatAdd, + undoRedo, + stopDraw + ]; + + } else { + + tools = [ + toolbox, + 'spacer', + addFeature, + addAddable, + addGeneric, + addFavorite, + addRecent, + 'spacer', + notes, + 'spacer', + undoRedo, + save + ]; + } + + tools = tools.filter(function(tool) { + return !tool.allowed || tool.allowed(); + }); - function notesEnabled() { - var noteLayer = context.layers().layer('notes'); - return noteLayer && noteLayer.enabled(); + return tools; } function topToolbar(bar) { @@ -31,27 +94,40 @@ export function uiTopToolbar(context) { } }); - var debouncedUpdate = _debounce(update, 500, { leading: true, trailing: true }); + var debouncedUpdate = _debounce(update, 250, { leading: true, trailing: true }); context.layers() .on('change.topToolbar', debouncedUpdate); + context.on('enter.topToolbar', update); + + context.presets() + .on('favoritePreset.topToolbar', update) + .on('recentsChange.topToolbar', update); + + toolbox.onChange = function() { + update(); + }; + update(); function update() { - var tools = [ - sidebarToggle, - 'spacer', - modes - ]; + var tools = allowedTools(); - tools.push('spacer'); + toolbox.setAllowedTools(tools); - if (notesEnabled()) { - tools = tools.concat([notes, 'spacer']); - } + tools = tools.filter(function(tool) { + return tool.userToggleable === false || tool.isToggledOn !== false; + }); - tools = tools.concat([undoRedo, save]); + var deduplicatedTools = []; + // remove adjacent duplicates (i.e. spacers) + tools.forEach(function(tool) { + if (!deduplicatedTools.length || deduplicatedTools[deduplicatedTools.length - 1] !== tool) { + deduplicatedTools.push(tool); + } + }); + tools = deduplicatedTools; var toolbarItems = bar.selectAll('.toolbar-item') .data(tools, function(d) { @@ -68,27 +144,40 @@ export function uiTopToolbar(context) { var itemsEnter = toolbarItems .enter() + .each(function(d) { + if (d.install) { + d.install(); + } + }) .append('div') .attr('class', function(d) { var classes = 'toolbar-item ' + (d.id || d).replace('_', '-'); - if (d.klass) classes += ' ' + d.klass; + if (d.itemClass) classes += ' ' + d.itemClass; return classes; }); - var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; }); + var actionableItems = itemsEnter.filter(function(d) { return typeof d !== 'string'; }); actionableItems .append('div') - .attr('class', 'item-content') - .each(function(d) { - d3_select(this).call(d.render, bar); + .attr('class', function(d) { + var classes = 'item-content'; + if (d.contentClass) classes += ' ' + d.contentClass; + return classes; }); actionableItems .append('div') - .attr('class', 'item-label') + .attr('class', 'item-label'); + + toolbarItems = toolbarItems.merge(itemsEnter) + .each(function(d){ + if (d.render) d3_select(this).select('.item-content').call(d.render, bar); + }); + + toolbarItems.selectAll('.item-label') .text(function(d) { - return d.label; + return utilFunctor(d.label)(); }); } diff --git a/modules/validations/disconnected_way.js b/modules/validations/disconnected_way.js index 1978b9c3ad..dc956841b0 100644 --- a/modules/validations/disconnected_way.js +++ b/modules/validations/disconnected_way.js @@ -199,7 +199,7 @@ export function validationDisconnectedWay() { } context.enter( - modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId), true) + modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId)) ); } }); diff --git a/modules/validations/impossible_oneway.js b/modules/validations/impossible_oneway.js index 243452c162..b4fdb91f8a 100644 --- a/modules/validations/impossible_oneway.js +++ b/modules/validations/impossible_oneway.js @@ -225,7 +225,7 @@ export function validationImpossibleOneway() { } context.enter( - modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true) + modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id)) ); } diff --git a/scripts/build_data.js b/scripts/build_data.js index 0985577c44..3e62b8a1a7 100644 --- a/scripts/build_data.js +++ b/scripts/build_data.js @@ -69,7 +69,8 @@ function buildData() { 'fas-i-cursor': {}, 'fas-lock': {}, 'fas-th-list': {}, - 'fas-user-cog': {} + 'fas-user-cog': {}, + 'fas-toolbox': {} }; // The Noun Project icons used diff --git a/svg/fontawesome/fas-toolbox.svg b/svg/fontawesome/fas-toolbox.svg new file mode 100644 index 0000000000..43b38bbf75 --- /dev/null +++ b/svg/fontawesome/fas-toolbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svg/iD-sprite/icons/icon-repeat.svg b/svg/iD-sprite/icons/icon-repeat.svg new file mode 100644 index 0000000000..b208955d80 --- /dev/null +++ b/svg/iD-sprite/icons/icon-repeat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/iD-sprite/tools/presets-grid.svg b/svg/iD-sprite/tools/presets-grid.svg new file mode 100644 index 0000000000..cd8cac3158 --- /dev/null +++ b/svg/iD-sprite/tools/presets-grid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/iD-sprite/tools/segment-orthogonal.svg b/svg/iD-sprite/tools/segment-orthogonal.svg new file mode 100644 index 0000000000..be09c724b3 --- /dev/null +++ b/svg/iD-sprite/tools/segment-orthogonal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/iD-sprite/tools/segment-straight.svg b/svg/iD-sprite/tools/segment-straight.svg new file mode 100644 index 0000000000..e63457b32d --- /dev/null +++ b/svg/iD-sprite/tools/segment-straight.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/iD-sprite/tools/structure-none.svg b/svg/iD-sprite/tools/structure-none.svg new file mode 100644 index 0000000000..917a621bc2 --- /dev/null +++ b/svg/iD-sprite/tools/structure-none.svg @@ -0,0 +1,4 @@ + + + + From 0966192fd8b5406e1b8d51bb1f2ed7f68f8f7b1f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 10:35:49 -0700 Subject: [PATCH 02/43] Don't show more than seven favorites if generic presets are shown --- modules/ui/tools/quick_presets_favorites.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/ui/tools/quick_presets_favorites.js b/modules/ui/tools/quick_presets_favorites.js index 5dc91f1d51..4260e796b2 100644 --- a/modules/ui/tools/quick_presets_favorites.js +++ b/modules/ui/tools/quick_presets_favorites.js @@ -11,10 +11,12 @@ export function uiToolAddFavorite(context) { tool.itemsToDraw = function() { if (context.presets().getAddable().length) return []; - var items = context.presets().getFavorites().slice(0, 10); - var precedingCount = context.storage('tool.add_generic.toggledOn') === 'true' ? 3 : 0; + var maxFavorites = 10 - precedingCount; + + var items = context.presets().getFavorites().slice(0, maxFavorites); + items.forEach(function(item, index) { var totalIndex = precedingCount + index; var keyCode; From 02b4632f30fa82f6846918ab9f4fa008d12777f8 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 10:52:07 -0700 Subject: [PATCH 03/43] Reduce variability in the preset browser width --- css/80_app.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/80_app.css b/css/80_app.css index 52c6474176..566b432a0a 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -5471,7 +5471,8 @@ li.hide + li.version .badge .tooltip .popover-arrow { /* Preset browser ------------------------------------------------------- */ .preset-browser.poplist { - min-width: 300px; + width: 100%; + max-width: 325px; } .assistant .preset-browser.poplist { top: 84px; From cb8448d2ecabb9961aef62117d33f59a1f669ec8 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 11:12:26 -0700 Subject: [PATCH 04/43] Make the cancel drawing toolbar button work with repeat drawing --- modules/modes/draw_area.js | 14 ++++++++++++++ modules/ui/tools/stop_draw.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 882d8ac901..5227ec43e4 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -24,6 +24,8 @@ export function modeDrawArea(context, wayID, startGraph, button, addMode) { }; mode.enter = function() { + mode.skipEnter = false; + if (addMode) { // Add in case this draw mode was entered from somewhere besides modeAddArea. // Duplicates are resolved later. @@ -35,10 +37,14 @@ export function modeDrawArea(context, wayID, startGraph, button, addMode) { _behavior = behaviorDrawWay(context, wayID, undefined, startGraph) .tail(t('modes.draw_area.tail')) .on('doneSegment.modeDrawArea', function() { + if (mode.skipEnter) return; + // re-enter this mode to start the next segment context.enter(mode); }) .on('finish.modeDrawArea revert.modeDrawArea', function() { + if (mode.skipEnter) return; + if (mode.repeatAddedFeature()) { context.enter(addMode); } else { @@ -72,6 +78,14 @@ export function modeDrawArea(context, wayID, startGraph, button, addMode) { context.uninstall(_behavior); }; + // complete drawing, if possible + mode.finish = function(skipEnter) { + if (skipEnter) { + mode.skipEnter = true; + } + return _behavior.finish(); + }; + mode.selectedIDs = function() { return [wayID]; }; diff --git a/modules/ui/tools/stop_draw.js b/modules/ui/tools/stop_draw.js index 120d3bde06..d0a1b7bb58 100644 --- a/modules/ui/tools/stop_draw.js +++ b/modules/ui/tools/stop_draw.js @@ -23,7 +23,7 @@ export function uiToolStopDraw(context) { }, onClick: function() { var mode = context.mode(); - if (cancelOrFinish === 'finish' && mode.finish) { + if (mode && mode.finish) { mode.finish(); } else { context.enter(modeBrowse(context)); From 0e431de838a4f72e3d48971162dcc72b7883096f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 11:38:22 -0700 Subject: [PATCH 05/43] Fix hidden preset tooltip --- modules/ui/preset_browser.js | 2 +- modules/ui/tools/quick_presets.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index f931656de0..17079e492c 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -594,7 +594,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { var isAutoHidden = context.features().autoHidden(hiddenPresetFeatures.key); var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; - var tooltipObj = { features: hiddenPresetFeatures.title }; + var tooltipObj = { features: t('feature.' + hiddenPresetFeatures + '.description') }; button.call(tooltip('dark') .html(true) .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj)) diff --git a/modules/ui/tools/quick_presets.js b/modules/ui/tools/quick_presets.js index 147a7e839d..eee98dc60f 100644 --- a/modules/ui/tools/quick_presets.js +++ b/modules/ui/tools/quick_presets.js @@ -83,7 +83,8 @@ export function uiToolQuickPresets(context) { var hiddenPresetFeatures = context.features().isHiddenPreset(d.preset, d.preset.geometry[0]); var isAutoHidden = context.features().autoHidden(hiddenPresetFeatures.key); var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; - protoMode.description = t('inspector.hidden_preset.' + tooltipIdSuffix, { features: hiddenPresetFeatures.title }); + var layerTitle = t('feature.' + hiddenPresetFeatures + '.description'); + protoMode.description = t('inspector.hidden_preset.' + tooltipIdSuffix, { features: layerTitle }); protoMode.key = null; } From ba7f9db46f373950298e1af43007756f787259de Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 12:03:40 -0700 Subject: [PATCH 06/43] Show error feedback when attempting to add a vertex as a standalone point --- data/core.yaml | 2 ++ dist/locales/en.json | 5 ++++- modules/behavior/draw.js | 10 ++++++++-- modules/modes/add_point.js | 6 +++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 12cf2a7454..336bb2343c 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -71,6 +71,8 @@ en: description: "Add restaurants, monuments, postal boxes or other points to the map." tail: Click on the map to add a point. filter_tooltip: points + warning: + vertex_placement: This must be placed along a line or area. add_note: title: Note label: Add Note diff --git a/dist/locales/en.json b/dist/locales/en.json index 1d8e1ddfff..5606e5271c 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -88,7 +88,10 @@ "title": "Point", "description": "Add restaurants, monuments, postal boxes or other points to the map.", "tail": "Click on the map to add a point.", - "filter_tooltip": "points" + "filter_tooltip": "points", + "warning": { + "vertex_placement": "This must be placed along a line or area." + } }, "add_note": { "title": "Note", diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 61c263a771..3578d587a5 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -19,7 +19,8 @@ var _lastSpace = null; export function behaviorDraw(context) { var dispatch = d3_dispatch( - 'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish' + 'move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish', + 'rejectedVertexAsPoint' ); var keybinding = utilKeybinding('draw'); @@ -140,7 +141,12 @@ export function behaviorDraw(context) { dispatch.call('clickWay', this, choice.loc, edge, d); return; } - } else if (mode.id !== 'add-point' || mode.preset.matchGeometry('point')) { + } else { + if (mode.id === 'add-point' && !mode.preset.matchGeometry('point')) { + // attempting to add a vertex as an independent point + dispatch.call('rejectedVertexAsPoint', this); + return; + } var locLatLng = context.projection.invert(loc); dispatch.call('click', this, locLatLng, d); } diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index 75118190f9..d9726ed861 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -6,7 +6,7 @@ import { osmNode } from '../osm/node'; import { actionAddEntity } from '../actions/add_entity'; import { actionChangeTags } from '../actions/change_tags'; import { actionAddMidpoint } from '../actions/add_midpoint'; - +import { uiFlash } from '../ui/flash'; export function modeAddPoint(context, mode) { @@ -22,6 +22,10 @@ export function modeAddPoint(context, mode) { .on('cancel', cancel) .on('finish', function finish() { mode.finish(); + }) + .on('rejectedVertexAsPoint', function finish() { + uiFlash() + .text(t('modes.add_point.warning.vertex_placement'))(); }); mode.defaultTags = {}; From 6d5d414fca82689173e15c251531f429fdcc212d Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 12:57:04 -0700 Subject: [PATCH 07/43] Fix small preset image size --- css/80_app.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/css/80_app.css b/css/80_app.css index 566b432a0a..1d55695b32 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1050,6 +1050,10 @@ a.hide-toggle { z-index: 2; visibility: hidden; } +.preset-icon-container.small img.image-icon { + width: 34px; + height: 34px; +} .preset-icon-container.showing-img img.image-icon { visibility: visible; } From 43c843c1fcc1d05d039a38f4f1b5e04f1a03917c Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 13:34:18 -0700 Subject: [PATCH 08/43] Fix issue where stored add geometry would not be used --- modules/presets/preset.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/presets/preset.js b/modules/presets/preset.js index 5063677fa6..cc68b341f2 100644 --- a/modules/presets/preset.js +++ b/modules/presets/preset.js @@ -262,14 +262,14 @@ export function presetPreset(presetID, preset, addable, allFields, allPresets) { } // The geometry type to use when adding a new feature of this preset - _this.defaultAddGeometry = (context, allowedGeometries)=> { + _this.defaultAddGeometry = (context, allowedGeometries) => { var geometry = _this.geometry.slice().filter((geom) => { if (allowedGeometries && allowedGeometries.indexOf(geom) === -1) return false; if (context.features().isHiddenPreset(_this, geom)) return false; return true; }); - var mostRecentAddGeom = context.storage('preset.' + preset.id + '.addGeom'); + var mostRecentAddGeom = context.storage('preset.' + _this.id + '.addGeom'); if (mostRecentAddGeom === 'vertex') mostRecentAddGeom = 'point'; if (mostRecentAddGeom && geometry.indexOf(mostRecentAddGeom) !== -1) { return mostRecentAddGeom; From 1eb3ad273ca7b5051c0a14ad5f2afe8ac1c5bd3f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 13:35:41 -0700 Subject: [PATCH 09/43] Show the add geometry toolbar item even when just one geometry is supported --- css/80_app.css | 38 +++++++++++++++++------------------ modules/ui/tools/segmented.js | 2 +- modules/ui/tools/structure.js | 13 +++++++----- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 1d55695b32..fdddda8bca 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -328,33 +328,33 @@ button.disabled { cursor: not-allowed; } -.joined > * { +.joined > button { border-radius: 0; - border-right: 1px solid rgba(0,0,0,.5); -} -[dir='rtl'] .joined > * { - border-left: 1px solid rgba(0,0,0,.5); - border-right: none; + border-width: 0; + border-style: solid; + border-color: rgba(0,0,0,.5); } - -.fillL .joined > * { - border-right: 1px solid #fff; +.fillL .joined > button { + border-color: #fff; } -.joined > *:first-child { - border-radius: 4px 0 0 4px; +[dir='ltr'] .joined > button:not(:last-child) { + border-right-width: 1px; } -[dir='rtl'] .joined > *:first-child { - border-radius: 0 4px 4px 0; +[dir='rtl'] .joined > button:not(:last-child) { + border-left-width: 1px; } -.joined > *:last-child { - border-right-width: 0; - border-radius: 0 4px 4px 0; + +[dir='ltr'] .joined > button:first-child, +[dir='rtl'] .joined > button:last-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; } -[dir='rtl'] .joined > *.bar-button:last-child { - border-radius: 4px 0 0 4px; +[dir='ltr'] .joined > button:last-child, +[dir='rtl'] .joined > button:first-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } - /* Action buttons */ button.action { background: #7092ff; diff --git a/modules/ui/tools/segmented.js b/modules/ui/tools/segmented.js index 567bbc9b28..127e82418f 100644 --- a/modules/ui/tools/segmented.js +++ b/modules/ui/tools/segmented.js @@ -94,7 +94,7 @@ export function uiToolSegemented(context) { tool.allowed = function() { if (tool.loadItems) tool.loadItems(); - return tool.items.length > 1; + return tool.items.length > 0; }; tool.install = function() { diff --git a/modules/ui/tools/structure.js b/modules/ui/tools/structure.js index a41f0a0a3d..50b4a8e7d0 100644 --- a/modules/ui/tools/structure.js +++ b/modules/ui/tools/structure.js @@ -55,9 +55,7 @@ export function uiToolStructure(context) { var prevWayID; tool.loadItems = function() { - tool.items = [ - structureNone - ]; + tool.items = []; var tags = activeTags(); @@ -70,6 +68,11 @@ export function uiToolStructure(context) { if (allowsStructure(osmTagsAllowingBridges)) tool.items.push(structureBridge); if (allowsStructure(osmTagsAllowingTunnels)) tool.items.push(structureTunnel); + + if (tool.items.length) { + // only show "none" option if other structures are supported + tool.items = [structureNone].concat(tool.items); + } }; tool.chooseItem = function(d) { @@ -182,10 +185,10 @@ export function uiToolStructure(context) { return structureNone; }; - var parentAvailable = tool.allowed; + var parentAllowed = tool.allowed; tool.allowed = function() { var modeID = context.mode().id; - return parentAvailable() && (modeID === 'add-line' || modeID === 'draw-line'); + return parentAllowed() && (modeID === 'add-line' || modeID === 'draw-line'); }; return tool; From 4ff984ac2e0e0b5c313aba69c68460825f3b67e3 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 13:42:38 -0700 Subject: [PATCH 10/43] Update recent presets tool icon --- modules/ui/tools/quick_presets_recent.js | 2 +- scripts/build_data.js | 3 ++- svg/fontawesome/fas-history.svg | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 svg/fontawesome/fas-history.svg diff --git a/modules/ui/tools/quick_presets_recent.js b/modules/ui/tools/quick_presets_recent.js index d139ed35dd..81e64877bb 100644 --- a/modules/ui/tools/quick_presets_recent.js +++ b/modules/ui/tools/quick_presets_recent.js @@ -6,7 +6,7 @@ export function uiToolAddRecent(context) { var tool = uiToolQuickPresets(context); tool.id = 'add_recent'; tool.label = t('toolbar.recent'); - tool.iconName = 'fas-clock'; + tool.iconName = 'fas-history'; tool.itemsToDraw = function() { if (context.presets().getAddable().length) return []; diff --git a/scripts/build_data.js b/scripts/build_data.js index 3e62b8a1a7..216cf685cc 100644 --- a/scripts/build_data.js +++ b/scripts/build_data.js @@ -70,7 +70,8 @@ function buildData() { 'fas-lock': {}, 'fas-th-list': {}, 'fas-user-cog': {}, - 'fas-toolbox': {} + 'fas-toolbox': {}, + 'fas-history': {} }; // The Noun Project icons used diff --git a/svg/fontawesome/fas-history.svg b/svg/fontawesome/fas-history.svg new file mode 100644 index 0000000000..ddcfda076e --- /dev/null +++ b/svg/fontawesome/fas-history.svg @@ -0,0 +1 @@ + \ No newline at end of file From 2ee5389d7b7b5d824a2ec6a48fc2ae023ca9eb07 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 16:05:07 -0700 Subject: [PATCH 11/43] Improve the "add feature" icon --- modules/ui/tools/add_feature.js | 2 +- svg/iD-sprite/tools/features-grid.svg | 4 ++++ svg/iD-sprite/tools/presets-grid.svg | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 svg/iD-sprite/tools/features-grid.svg delete mode 100644 svg/iD-sprite/tools/presets-grid.svg diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index 1baef457e8..25710848a6 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -18,7 +18,7 @@ export function uiToolAddFeature(context) { id: 'add_feature', label: t('toolbar.add_feature'), itemClass: 'disclosing', - iconName: 'iD-presets-grid', + iconName: 'iD-features-grid', iconClass: 'icon-30' }; diff --git a/svg/iD-sprite/tools/features-grid.svg b/svg/iD-sprite/tools/features-grid.svg new file mode 100644 index 0000000000..b32c5df558 --- /dev/null +++ b/svg/iD-sprite/tools/features-grid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svg/iD-sprite/tools/presets-grid.svg b/svg/iD-sprite/tools/presets-grid.svg deleted file mode 100644 index cd8cac3158..0000000000 --- a/svg/iD-sprite/tools/presets-grid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - From a9cdd0915b596bccc4860f125050534ba9acc73c Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 16:31:23 -0700 Subject: [PATCH 12/43] Improve small vertex icons --- css/80_app.css | 2 +- modules/ui/preset_icon.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index fdddda8bca..7ab1fbb1b4 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1114,7 +1114,7 @@ a.hide-toggle { .preset-icon-fill-vertex circle { stroke-width: 1.5px; stroke: #333; - fill: #efefef; + fill: none; backface-visibility: hidden; } diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index 73b5a611ea..41ae469f43 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -54,9 +54,10 @@ export function uiPresetIcon(context) { function renderCircleFill(fillEnter) { - const w = 60; - const h = 60; - const d = 40; + const w = isSmall() ? 40 : 60; + const h = w; + const d = w * 2 / 3; + const r = d / 2; fillEnter .append('svg') @@ -67,7 +68,7 @@ export function uiPresetIcon(context) { .append('circle') .attr('cx', w / 2) .attr('cy', h / 2) - .attr('r', d / 2); + .attr('r', r); } From 9731bafda969265e217ee00f92e93902cd5c9c7c Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Tue, 17 Mar 2020 16:45:06 -0700 Subject: [PATCH 13/43] Fix preset browser country coding --- modules/ui/preset_browser.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index 17079e492c..1c4e741269 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -3,6 +3,7 @@ import { select as d3_select, selectAll as d3_selectAll } from 'd3-selection'; +import * as countryCoder from '@ideditor/country-coder'; import { t, textDirection } from '../util/locale'; import { services } from '../services'; @@ -365,10 +366,8 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { function reloadCountryCode() { - if (!services.countryCoder) return; - var center = context.map().center(); - var countryCode = services.countryCoder.iso1A2Code(center); + var countryCode = countryCoder.iso1A2Code(center); if (countryCode) countryCode = countryCode.toLowerCase(); if (_countryCode !== countryCode) { _countryCode = countryCode; From 53438f5cc53d4ca1407b7ae9c2ae8a406b7432a6 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 09:54:48 -0700 Subject: [PATCH 14/43] Fix eslint warnings --- modules/behavior/add_way.js | 1 - modules/ui/preset_browser.js | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/behavior/add_way.js b/modules/behavior/add_way.js index 8ab1432d2b..2392fa2977 100644 --- a/modules/behavior/add_way.js +++ b/modules/behavior/add_way.js @@ -1,7 +1,6 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { behaviorDraw } from './draw'; -import { modeBrowse } from '../modes/browse'; import { utilRebind } from '../util/rebind'; diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index 1c4e741269..2376ac566c 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -6,7 +6,6 @@ import { import * as countryCoder from '@ideditor/country-coder'; import { t, textDirection } from '../util/locale'; -import { services } from '../services'; import { svgIcon } from '../svg/index'; import { tooltip } from '../util/tooltip'; import { popover } from '../util/popover'; From 2036456eed7c585cae3530a94b9c62adc73a5f2a Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 09:56:38 -0700 Subject: [PATCH 15/43] Use single object argument for draw modes instead of arguments list Maintain the repeat drawing state when switching add geometry (close #7154) --- modules/modes/add_area.js | 7 +++- modules/modes/add_line.js | 7 +++- modules/modes/draw_area.js | 35 +++++++++++--------- modules/modes/draw_line.js | 42 ++++++++++++------------ modules/operations/continue.js | 2 +- modules/ui/tools/adding_geometry.js | 11 ++++--- modules/ui/tools/repeat_add.js | 2 +- modules/ui/tools/structure.js | 27 ++++++++------- modules/validations/disconnected_way.js | 2 +- modules/validations/impossible_oneway.js | 2 +- 10 files changed, 75 insertions(+), 62 deletions(-) diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js index 56870e8c65..194832a695 100644 --- a/modules/modes/add_area.js +++ b/modules/modes/add_area.js @@ -103,7 +103,12 @@ export function modeAddArea(context, mode) { function enterDrawMode(way, startGraph) { _allAddedEntityIDs.push(way.id); - var drawMode = modeDrawArea(context, way.id, startGraph, mode.button, mode); + var drawMode = modeDrawArea(context, { + wayID: way.id, + startGraph: startGraph, + button: mode.button, + addMode: mode + }); context.enter(drawMode); } diff --git a/modules/modes/add_line.js b/modules/modes/add_line.js index c873978909..2e3b12eb69 100644 --- a/modules/modes/add_line.js +++ b/modules/modes/add_line.js @@ -96,7 +96,12 @@ export function modeAddLine(context, mode) { function enterDrawMode(way, startGraph) { _allAddedEntityIDs.push(way.id); - var drawMode = modeDrawLine(context, way.id, startGraph, mode.button, null, mode); + var drawMode = modeDrawLine(context, { + wayID: way.id, + startGraph: startGraph, + button: mode.button, + addMode: mode + }); drawMode.defaultNodeTags = mode.defaultNodeTags; context.enter(drawMode); } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 5227ec43e4..70d1feefd6 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -4,37 +4,40 @@ import { modeSelect } from './select'; import { uiFlash } from '../ui/flash'; -export function modeDrawArea(context, wayID, startGraph, button, addMode) { - var mode = { - button: button, - id: 'draw-area', - addMode: addMode - }; +export function modeDrawArea(context, mode) { var _behavior; - mode.wayID = wayID; + mode.id = 'draw-area'; + + mode.button = mode.button || 'area'; + mode.startGraph = mode.startGraph || context.graph(); + mode.preset = mode.addMode && mode.addMode.preset; + + mode.isContinuing = function() { + return false; + }; mode.repeatAddedFeature = function(val) { - if (addMode) return addMode.repeatAddedFeature(val); + if (mode.addMode) return mode.addMode.repeatAddedFeature(val); }; mode.addedEntityIDs = function() { - if (addMode) return addMode.addedEntityIDs(); + if (mode.addMode) return mode.addMode.addedEntityIDs(); }; mode.enter = function() { mode.skipEnter = false; - if (addMode) { + if (mode.addMode) { // Add in case this draw mode was entered from somewhere besides modeAddArea. // Duplicates are resolved later. - addMode.addAddedEntityID(wayID); + mode.addMode.addAddedEntityID(mode.wayID); } - var way = context.entity(wayID); + var way = context.entity(mode.wayID); - _behavior = behaviorDrawWay(context, wayID, undefined, startGraph) + _behavior = behaviorDrawWay(context, mode.wayID, undefined, mode.startGraph) .tail(t('modes.draw_area.tail')) .on('doneSegment.modeDrawArea', function() { if (mode.skipEnter) return; @@ -46,9 +49,9 @@ export function modeDrawArea(context, wayID, startGraph, button, addMode) { if (mode.skipEnter) return; if (mode.repeatAddedFeature()) { - context.enter(addMode); + context.enter(mode.addMode); } else { - var newMode = modeSelect(context, mode.addedEntityIDs() || [wayID]) + var newMode = modeSelect(context, mode.addedEntityIDs() || [mode.wayID]) .newFeature(true); context.enter(newMode); } @@ -87,7 +90,7 @@ export function modeDrawArea(context, wayID, startGraph, button, addMode) { }; mode.selectedIDs = function() { - return [wayID]; + return [mode.wayID]; }; mode.activeID = function() { diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 67ccef5167..8c7df9361f 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -5,42 +5,42 @@ import { modeSelect } from './select'; import { uiFlash } from '../ui/flash'; -export function modeDrawLine(context, wayID, startGraph, button, affix, addMode) { - var mode = { - button: button, - id: 'draw-line', - addMode: addMode, - affix: affix - }; +export function modeDrawLine(context, mode) { var _behavior; - mode.wayID = wayID; + mode.id = 'draw-line'; + + mode.button = mode.button || 'line'; + mode.startGraph = mode.startGraph || context.graph(); + mode.preset = mode.addMode && mode.addMode.preset; - mode.isContinuing = !!affix; + mode.isContinuing = function() { + return !!mode.affix; + }; mode.repeatAddedFeature = function(val) { - if (addMode) return addMode.repeatAddedFeature(val); + if (mode.addMode) return mode.addMode.repeatAddedFeature(val); }; mode.addedEntityIDs = function() { - if (addMode) return addMode.addedEntityIDs(); + if (mode.addMode) return mode.addMode.addedEntityIDs(); }; mode.enter = function() { mode.skipEnter = false; - if (addMode) { + if (mode.addMode) { // Add in case this draw mode was entered from somewhere besides modeAddLine. // Duplicates are resolved later. - addMode.addAddedEntityID(wayID); + mode.addMode.addAddedEntityID(mode.wayID); } - var way = context.entity(wayID); - var index = (affix === 'prefix') ? 0 : undefined; - var headID = (affix === 'prefix') ? way.first() : way.last(); + var way = context.entity(mode.wayID); + var index = (mode.affix === 'prefix') ? 0 : undefined; + var headID = (mode.affix === 'prefix') ? way.first() : way.last(); - _behavior = behaviorDrawWay(context, wayID, index, startGraph) + _behavior = behaviorDrawWay(context, mode.wayID, index, mode.startGraph) .tail(t('modes.draw_line.tail')) .on('doneSegment.modeDrawLine', function(node) { if (mode.defaultNodeTags && node && !Object.keys(node.tags).length) { @@ -56,10 +56,10 @@ export function modeDrawLine(context, wayID, startGraph, button, affix, addMode) if (mode.skipEnter) return; if (mode.repeatAddedFeature()) { - context.enter(addMode); + context.enter(mode.addMode); } else { - var newMode = modeSelect(context, mode.addedEntityIDs() || [wayID]) - .newFeature(!mode.isContinuing); + var newMode = modeSelect(context, mode.addedEntityIDs() || [mode.wayID]) + .newFeature(!mode.isContinuing()); context.enter(newMode); } }) @@ -93,7 +93,7 @@ export function modeDrawLine(context, wayID, startGraph, button, affix, addMode) }; mode.selectedIDs = function() { - return [wayID]; + return [mode.wayID]; }; mode.activeID = function() { diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 64662dcdad..28559d3294 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -27,7 +27,7 @@ export function operationContinue(selectedIDs, context) { var operation = function() { var candidate = candidateWays()[0]; context.enter( - modeDrawLine(context, candidate.id, context.graph(), 'line', candidate.affix(vertex.id)) + modeDrawLine(context, { wayID: candidate.id, affix: candidate.affix(vertex.id) }) ); }; diff --git a/modules/ui/tools/adding_geometry.js b/modules/ui/tools/adding_geometry.js index eb9c48000e..29f95f2810 100644 --- a/modules/ui/tools/adding_geometry.js +++ b/modules/ui/tools/adding_geometry.js @@ -53,11 +53,12 @@ export function uiToolAddingGeometry(context) { oldMode.preset.setMostRecentAddGeometry(context, item.id); var newMode = item.mode(context, { - button: oldMode.button, - preset: oldMode.preset, - geometry: item.id, - title: oldMode.title - }); + button: oldMode.button, + preset: oldMode.preset, + geometry: item.id, + title: oldMode.title + }) + .repeatAddedFeature(oldMode.repeatAddedFeature()); context.enter(newMode); }; diff --git a/modules/ui/tools/repeat_add.js b/modules/ui/tools/repeat_add.js index 2f92386a84..5402b8ca88 100644 --- a/modules/ui/tools/repeat_add.js +++ b/modules/ui/tools/repeat_add.js @@ -55,7 +55,7 @@ export function uiToolRepeatAdd(context) { tool.allowed = function() { var mode = context.mode(); if (mode.id === 'add-point' || mode.id === 'add-line' || mode.id === 'add-area') return true; - return (mode.id === 'draw-line' || mode.id === 'draw-area') && !mode.isContinuing; + return (mode.id === 'draw-line' || mode.id === 'draw-area') && !mode.isContinuing(); }; tool.install = function() { diff --git a/modules/ui/tools/structure.js b/modules/ui/tools/structure.js index 50b4a8e7d0..39a594db19 100644 --- a/modules/ui/tools/structure.js +++ b/modules/ui/tools/structure.js @@ -118,13 +118,12 @@ export function uiToolStructure(context) { context.perform(action); context.enter( - modeDrawLine(context, - prevWay.id, - context.graph(), - mode.button, - mode.affix, - mode.addMode - ) + modeDrawLine(context, { + wayID: prevWay.id, + button: mode.button, + affix: mode.affix, + addMode: mode.addMode + }) ); } } @@ -145,13 +144,13 @@ export function uiToolStructure(context) { prevWayID = way.id; context.enter( - modeDrawLine(context, - newWay.id, - startGraph, - mode.button, - mode.affix, - mode.addMode - ) + modeDrawLine(context, { + wayID: newWay.id, + startGraph: startGraph, + button: mode.button, + affix: mode.affix, + addMode: mode.addMode + }) ); } } diff --git a/modules/validations/disconnected_way.js b/modules/validations/disconnected_way.js index dc956841b0..e303233bd3 100644 --- a/modules/validations/disconnected_way.js +++ b/modules/validations/disconnected_way.js @@ -199,7 +199,7 @@ export function validationDisconnectedWay() { } context.enter( - modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId)) + modeDrawLine(context, { wayID: wayId, affix: way.affix(vertexId) }) ); } }); diff --git a/modules/validations/impossible_oneway.js b/modules/validations/impossible_oneway.js index b4fdb91f8a..02c5e773c0 100644 --- a/modules/validations/impossible_oneway.js +++ b/modules/validations/impossible_oneway.js @@ -225,7 +225,7 @@ export function validationImpossibleOneway() { } context.enter( - modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id)) + modeDrawLine(context, { wayID: way.id, affix: way.affix(vertex.id) }) ); } From b14a27cbfb9fc41fda13690d98d6b60e674b3667 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 10:42:17 -0700 Subject: [PATCH 16/43] Show disabled add geometry buttons during drawing --- css/80_app.css | 3 ++ modules/modes/add_point.js | 9 ++++-- modules/modes/draw_area.js | 4 +-- modules/modes/draw_line.js | 4 +-- modules/ui/tools/adding_geometry.js | 45 ++++++++++++++++------------- modules/ui/tools/segmented.js | 36 ++++++++++++++++++----- 6 files changed, 67 insertions(+), 34 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 7ab1fbb1b4..2d6bcf92da 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -327,6 +327,9 @@ button.disabled { color: rgba(0,0,0,.4); cursor: not-allowed; } +button.disabled.active { + background-color: rgba(112, 146, 255, 0.25); +} .joined > button { border-radius: 0; diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index d9726ed861..626787a963 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -88,8 +88,13 @@ export function modeAddPoint(context, mode) { } function didFinishAdding(node) { - _allAddedEntityIDs.push(node.id); - if (!mode.repeatAddedFeature()) { + if (_allAddedEntityIDs.indexOf(node.id) === -1) { + _allAddedEntityIDs.push(node.id); + } + if (mode.repeatAddedFeature()) { + // re-enter the mode to trigger UI updates + context.enter(mode); + } else { mode.finish(); } } diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 70d1feefd6..90d5ed4f72 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -13,6 +13,7 @@ export function modeDrawArea(context, mode) { mode.button = mode.button || 'area'; mode.startGraph = mode.startGraph || context.graph(); mode.preset = mode.addMode && mode.addMode.preset; + mode.geometry = mode.addMode ? mode.addMode.geometry : 'area'; mode.isContinuing = function() { return false; @@ -30,8 +31,7 @@ export function modeDrawArea(context, mode) { mode.skipEnter = false; if (mode.addMode) { - // Add in case this draw mode was entered from somewhere besides modeAddArea. - // Duplicates are resolved later. + // Add in case this draw mode was entered from somewhere besides modeAddArea mode.addMode.addAddedEntityID(mode.wayID); } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index 8c7df9361f..e03a50fd62 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -14,6 +14,7 @@ export function modeDrawLine(context, mode) { mode.button = mode.button || 'line'; mode.startGraph = mode.startGraph || context.graph(); mode.preset = mode.addMode && mode.addMode.preset; + mode.geometry = mode.addMode ? mode.addMode.geometry : 'line'; mode.isContinuing = function() { return !!mode.affix; @@ -31,8 +32,7 @@ export function modeDrawLine(context, mode) { mode.skipEnter = false; if (mode.addMode) { - // Add in case this draw mode was entered from somewhere besides modeAddLine. - // Duplicates are resolved later. + // Add in case this draw mode was entered from somewhere besides modeAddLine mode.addMode.addAddedEntityID(mode.wayID); } diff --git a/modules/ui/tools/adding_geometry.js b/modules/ui/tools/adding_geometry.js index 29f95f2810..6fbb930717 100644 --- a/modules/ui/tools/adding_geometry.js +++ b/modules/ui/tools/adding_geometry.js @@ -38,13 +38,13 @@ export function uiToolAddingGeometry(context) { icon: 'iD-icon-area', label: t('modes.add_area.title'), mode: modeAddArea - }, + }/*, building: { id: 'building', icon: 'maki-building-15', label: t('presets.presets.building.name'), mode: modeAddArea - } + }*/ }; tool.chooseItem = function(item) { @@ -66,30 +66,35 @@ export function uiToolAddingGeometry(context) { return items[context.mode().geometry]; }; + tool.isItemEnabled = function(item) { + return item === tool.activeItem() || context.mode().addedEntityIDs().length === 0; + }; + + var _validModeIDs = new Set(['add-point', 'add-line', 'add-area', 'draw-line', 'draw-area']); + tool.loadItems = function() { var mode = context.mode(); - if (!mode.preset || - (mode.id !== 'add-point' && mode.id !== 'add-line' && mode.id !== 'add-area') || - mode.addedEntityIDs().length > 0) { + if (!mode || !mode.preset || !_validModeIDs.has(mode.id)) { tool.items = []; - } else { - var geometries = context.mode().preset.geometry.slice().sort().reverse(); - var vertexIndex = geometries.indexOf('vertex'); - if (vertexIndex !== -1 && geometries.indexOf('point') !== -1) { - geometries.splice(vertexIndex, 1); - } - - var areaIndex = geometries.indexOf('area'); - if (areaIndex !== -1 && mode.preset.setTags(mode.defaultTags, 'area').building) { - geometries.splice(areaIndex, 1); - geometries.push('building'); - } + return; + } - tool.items = geometries.map(function(geom) { - return items[geom]; - }).filter(Boolean); + var geometries = context.mode().preset.geometry.slice().sort().reverse(); + var vertexIndex = geometries.indexOf('vertex'); + if (vertexIndex !== -1 && geometries.indexOf('point') !== -1) { + geometries.splice(vertexIndex, 1); + } +/* + var areaIndex = geometries.indexOf('area'); + if (areaIndex !== -1 && mode.preset.setTags(mode.defaultTags, 'area').building) { + geometries.splice(areaIndex, 1); + geometries.push('building'); } +*/ + tool.items = geometries.map(function(geom) { + return items[geom]; + }).filter(Boolean); }; return tool; diff --git a/modules/ui/tools/segmented.js b/modules/ui/tools/segmented.js index 127e82418f..b77e7082f5 100644 --- a/modules/ui/tools/segmented.js +++ b/modules/ui/tools/segmented.js @@ -33,7 +33,7 @@ export function uiToolSegemented(context) { tool.render = function(selection) { container = selection; - var active = tool.activeItem(); + var activeItem = tool.activeItem(); var buttons = selection.selectAll('.bar-button') .data(tool.items, function(d) { return d.id; }); @@ -41,15 +41,16 @@ export function uiToolSegemented(context) { buttons.exit() .remove(); - buttons + var buttonsEnter = buttons .enter() .append('button') .attr('class', function(d) { - return 'bar-button ' + d.id + ' ' + (d === active ? 'active' : ''); + return 'bar-button ' + d.id; }) .attr('tabindex', -1) .on('click', function(d) { - if (d3_select(this).classed('active')) return; + var button = d3_select(this); + if (button.classed('active') || button.classed('disabled')) return; setActiveItem(d); }) @@ -64,6 +65,19 @@ export function uiToolSegemented(context) { .call(tooltipBehavior) .call(svgIcon('#' + d.icon, d.iconClass)); }); + + buttons = buttonsEnter.merge(buttons); + + buttons + .classed('active', function(d) { + return d === activeItem; + }) + .classed('disabled', function(d) { + if (tool.isItemEnabled) { + return !tool.isItemEnabled(d); + } + return false; + }); }; function setActiveItem(d) { @@ -79,11 +93,17 @@ export function uiToolSegemented(context) { } function toggleItem() { - if (tool.items.length === 0) return; - var active = tool.activeItem(); - var index = tool.items.indexOf(active); - if (index === tool.items.length - 1) { + + var enabledItems = tool.items.slice().filter(function(item) { + return item === active || !tool.isItemEnabled || tool.isItemEnabled(item); + }); + + if (enabledItems === 0 || + (enabledItems.length === 1 && enabledItems[0] === active)) return; + + var index = enabledItems.indexOf(active); + if (index === enabledItems.length - 1) { index = 0; } else { index += 1; From b474936072511c5b9fe6170e2992de03e9a703ca Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 12:12:04 -0700 Subject: [PATCH 17/43] Fix map tooltips --- modules/core/context.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/core/context.js b/modules/core/context.js index 1dcf42e9a0..842f80690a 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -257,11 +257,13 @@ export function coreContext() { context.enter = (newMode) => { if (_mode) { _mode.exit(); + _container.classed(`mode-${_mode.id}`, false); dispatch.call('exit', this, _mode); } _mode = newMode; _mode.enter(); + _container.classed(`mode-${newMode.id}`, true); dispatch.call('enter', this, _mode); }; From e2b5da95e45c3d674d50cbb713303a926c1adbad Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 12:21:02 -0700 Subject: [PATCH 18/43] Show poplist labels on narrow windows --- css/80_app.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 2d6bcf92da..73f40fc84d 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -621,10 +621,6 @@ button.add-note svg.icon { -ms-filter: "FlipH"; } -#bar.narrow button.bar-button .label { - display: none; -} - [dir='ltr'] .undo-redo button:first-of-type { margin-right: 1px; } From f26feb111d628aff3443ed40cacc1224e8116a13 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 12:48:45 -0700 Subject: [PATCH 19/43] Enable adding "weak" presets like addresses with the v3 toolbar (close #6838) --- modules/modes/add_area.js | 1 + modules/modes/add_line.js | 1 + modules/modes/add_point.js | 4 +++- modules/modes/draw_area.js | 1 + modules/modes/draw_line.js | 1 + modules/modes/select.js | 11 ++++++++++- modules/ui/inspector.js | 14 ++++++++++++-- modules/ui/sidebar.js | 4 ++-- 8 files changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/modes/add_area.js b/modules/modes/add_area.js index 194832a695..d9be555332 100644 --- a/modules/modes/add_area.js +++ b/modules/modes/add_area.js @@ -116,6 +116,7 @@ export function modeAddArea(context, mode) { if (mode.addedEntityIDs().length) { context.enter( modeSelect(context, mode.addedEntityIDs()) + .presets(mode.preset ? [mode.preset] : null) .newFeature(true) ); } else { diff --git a/modules/modes/add_line.js b/modules/modes/add_line.js index 2e3b12eb69..0e45d717f0 100644 --- a/modules/modes/add_line.js +++ b/modules/modes/add_line.js @@ -110,6 +110,7 @@ export function modeAddLine(context, mode) { if (mode.addedEntityIDs().length) { context.enter( modeSelect(context, mode.addedEntityIDs()) + .presets(mode.preset ? [mode.preset] : null) .newFeature(true) ); } else { diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index 626787a963..6c6b9c34a5 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -102,7 +102,9 @@ export function modeAddPoint(context, mode) { mode.finish = function() { if (mode.addedEntityIDs().length) { context.enter( - modeSelect(context, mode.addedEntityIDs()).newFeature(true) + modeSelect(context, mode.addedEntityIDs()) + .presets(mode.preset ? [mode.preset] : null) + .newFeature(true) ); } else { context.enter( diff --git a/modules/modes/draw_area.js b/modules/modes/draw_area.js index 90d5ed4f72..adb03f9cc2 100644 --- a/modules/modes/draw_area.js +++ b/modules/modes/draw_area.js @@ -52,6 +52,7 @@ export function modeDrawArea(context, mode) { context.enter(mode.addMode); } else { var newMode = modeSelect(context, mode.addedEntityIDs() || [mode.wayID]) + .presets(mode.preset ? [mode.preset] : null) .newFeature(true); context.enter(newMode); } diff --git a/modules/modes/draw_line.js b/modules/modes/draw_line.js index e03a50fd62..ba056ddcee 100644 --- a/modules/modes/draw_line.js +++ b/modules/modes/draw_line.js @@ -59,6 +59,7 @@ export function modeDrawLine(context, mode) { context.enter(mode.addMode); } else { var newMode = modeSelect(context, mode.addedEntityIDs() || [mode.wayID]) + .presets(mode.preset ? [mode.preset] : null) .newFeature(!mode.isContinuing()); context.enter(newMode); } diff --git a/modules/modes/select.js b/modules/modes/select.js index f25d17ea19..e516225011 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -50,6 +50,8 @@ export function modeSelect(context, selectedIDs) { var inspector; // unused? var editMenu; var _timeout = null; + // the explicit presets for the features in this selection + var _presets; var _newFeature = false; var _suppressMenu = true; var _follow = false; @@ -225,6 +227,13 @@ export function modeSelect(context, selectedIDs) { }; + mode.presets = function(val) { + if (!arguments.length) return _presets; + _presets = val; + return mode; + }; + + mode.suppressMenu = function(val) { if (!arguments.length) return _suppressMenu; _suppressMenu = val; @@ -291,7 +300,7 @@ export function modeSelect(context, selectedIDs) { .call(keybinding); context.ui().sidebar - .select(selectedIDs, _newFeature); + .select(selectedIDs, _newFeature, _presets); context.history() .on('change.select', function() { diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index 1fff0aa40d..4bedd2e8e5 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -17,7 +17,7 @@ export function uiInspector(context) { var _newFeature = false; - function inspector(selection, newFeature) { + function inspector(selection, presets) { presetList .entityIDs(_entityIDs) .autofocus(_newFeature) @@ -32,6 +32,10 @@ export function uiInspector(context) { .entityIDs(_entityIDs) .on('choose', inspector.showList); + if (presets) { + entityEditor.presets(presets); + } + wrap = selection.selectAll('.panewrap') .data([0]); @@ -52,6 +56,12 @@ export function uiInspector(context) { editorPane = wrap.selectAll('.entity-editor-pane'); function shouldDefaultToPresetList() { + + // if an explicit preset is set then we're coming from a draw mode + if (presets && presets.filter(function(preset) { + return !preset.isFallback(); + }).length) return false; + // can only change preset on single selection if (_entityIDs.length !== 1) return false; @@ -63,7 +73,7 @@ export function uiInspector(context) { if (entity.hasNonGeometryTags()) return false; // prompt to select preset if feature is new and untagged - if (newFeature) return true; + if (_newFeature) return true; // all existing features except vertices should default to inspector if (entity.geometry(context.graph()) !== 'vertex') return false; diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index a730d5aead..05ebf97d23 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -215,7 +215,7 @@ export function uiSidebar(context) { }; - sidebar.select = function(ids, newFeature) { + sidebar.select = function(ids, newFeature, presets) { sidebar.hide(); if (ids && ids.length) { @@ -241,7 +241,7 @@ export function uiSidebar(context) { .newFeature(newFeature); inspectorWrap - .call(inspector, newFeature); + .call(inspector, presets); } } else { From 57286ab97267e8b1cd916a6a7925b692448a1c78 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 15:50:21 -0700 Subject: [PATCH 20/43] Fix preset browser scrolling issue with an overflowing toolbar --- modules/util/popover.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/util/popover.js b/modules/util/popover.js index 9685bef515..1d42ff82d8 100644 --- a/modules/util/popover.js +++ b/modules/util/popover.js @@ -129,7 +129,11 @@ export function popover(klass) { var enter = popoverSelection.enter() .append('div') .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : '')) - .classed('arrowed', _hasArrow.apply(this, arguments)); + .classed('arrowed', _hasArrow.apply(this, arguments)) + .on('wheel.popover mousewheel.popover', function() { + // don't pass wheel events to the anchor + d3_event.stopPropagation(); + }); enter .append('div') From 861a8358a4f8603f62e42f69e5209976831800cf Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 16:36:37 -0700 Subject: [PATCH 21/43] Don't redeclare preset button drag behavior on every update --- modules/ui/tools/quick_presets.js | 238 +++++++++++++++--------------- 1 file changed, 122 insertions(+), 116 deletions(-) diff --git a/modules/ui/tools/quick_presets.js b/modules/ui/tools/quick_presets.js index eee98dc60f..a0544f3528 100644 --- a/modules/ui/tools/quick_presets.js +++ b/modules/ui/tools/quick_presets.js @@ -160,156 +160,162 @@ export function uiToolQuickPresets(context) { ); }); - var scrollNode = d3_select('#bar').node(); - var dragOrigin, dragMoved, targetData; - var ltr = textDirection === 'ltr', - rtl = !ltr; - buttonsEnter .filter('.add-favorite, .add-recent') - .call(d3_drag() - .on('start', function() { - var node = d3_select(this).node(); - dragOrigin = { - x: d3_event.x, - y: d3_event.y, - nodeLeft: node.offsetLeft, - nodeTop: node.offsetTop, - }; - targetData = null; - dragMoved = false; - }) - .on('drag', function(d) { - dragMoved = true; + .call(_d3Dragger); + + // update + buttons = buttons + .merge(buttonsEnter) + .classed('disabled', function(d) { return !enabled(d); }); + } - var deltaX = d3_event.x - dragOrigin.x, - deltaY = d3_event.y - dragOrigin.y; + var _scrollNode, _dragOrigin, _dragMoved, _targetData; - var button = d3_select(this); + var _d3Dragger = d3_drag() + .on('start', function() { + _scrollNode = d3_select('#bar').node(); - if (!button.classed('dragging')) { - // haven't committed to dragging yet + var node = d3_select(this).node(); + _dragOrigin = { + x: d3_event.x, + y: d3_event.y, + nodeLeft: node.offsetLeft, + nodeTop: node.offsetTop, + }; + _targetData = null; + _dragMoved = false; + }) + .on('drag', function(d) { + _dragMoved = true; - // don't display drag until dragging beyond a distance threshold - if (Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) <= 5) return; + var ltr = textDirection === 'ltr', + rtl = !ltr; - // setup dragging + var deltaX = d3_event.x - _dragOrigin.x, + deltaY = d3_event.y - _dragOrigin.y; - d3_select(this.parentNode) - .insert('div', '#' + button.attr('id')) - .attr('class', 'drag-placeholder'); + var button = d3_select(this); - button - .classed('dragging', true) - // must use absolute position so button will display if dragged out of the toolbar - .style('position', 'absolute'); - } + if (!button.classed('dragging')) { + // haven't committed to dragging yet + + // don't display drag until dragging beyond a distance threshold + if (Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) <= 5) return; - var draggingNode = button.node(); - var eventX = d3_event.x + draggingNode.parentNode.offsetLeft; - var origLeft = dragOrigin.nodeLeft; + // setup dragging + + d3_select(this.parentNode) + .insert('div', '#' + button.attr('id')) + .attr('class', 'drag-placeholder'); button - .classed('removing', deltaY > 50) - .style('left', dragOrigin.nodeLeft + deltaX - scrollNode.scrollLeft + 'px') - .style('top', dragOrigin.nodeTop + deltaY + 'px'); + .classed('dragging', true) + // must use absolute position so button will display if dragged out of the toolbar + .style('position', 'absolute'); + } - targetData = null; + var draggingNode = button.node(); + var eventX = d3_event.x + draggingNode.parentNode.offsetLeft; + var origLeft = _dragOrigin.nodeLeft; - d3_selectAll('#bar button.add-favorite, #bar button.add-recent') - .style('transform', function(d2) { + button + .classed('removing', deltaY > 50) + .style('left', _dragOrigin.nodeLeft + deltaX - _scrollNode.scrollLeft + 'px') + .style('top', _dragOrigin.nodeTop + deltaY + 'px'); - if (d.button === d2.button) return null; + _targetData = null; - // no need to reposition elements if dragging out of toolbar - if (deltaY > 50) return null; + d3_selectAll('#bar button.add-favorite, #bar button.add-recent') + .style('transform', function(d2) { - var node = d3_select(this).node(), - nodeLeft = node.offsetLeft, - nodeRight = nodeLeft + node.offsetWidth; + if (d.button === d2.button) return null; - if ((ltr && nodeLeft > origLeft && eventX > nodeLeft) || - (rtl && nodeLeft < origLeft && eventX < nodeRight)) { + // no need to reposition elements if dragging out of toolbar + if (deltaY > 50) return null; - if ((ltr && eventX < nodeRight) || - (rtl && eventX > nodeLeft)) { - targetData = d2; - } - return 'translateX(' + (ltr ? '-' : '') + '100%)'; + var node = d3_select(this).node(), + nodeLeft = node.offsetLeft, + nodeRight = nodeLeft + node.offsetWidth; - } else if ((ltr && nodeLeft < origLeft && eventX < nodeRight) || - (rtl && nodeLeft > origLeft && eventX > nodeLeft)) { + if ((ltr && nodeLeft > origLeft && eventX > nodeLeft) || + (rtl && nodeLeft < origLeft && eventX < nodeRight)) { - if ((ltr && eventX > nodeLeft) || - (rtl && eventX < nodeRight)) { - targetData = d2; - } - return 'translateX(' + (ltr ? '' : '-') + '100%)'; + if ((ltr && eventX < nodeRight) || + (rtl && eventX > nodeLeft)) { + _targetData = d2; } + return 'translateX(' + (ltr ? '-' : '') + '100%)'; - return null; - }); - }) - .on('end', function(d) { + } else if ((ltr && nodeLeft < origLeft && eventX < nodeRight) || + (rtl && nodeLeft > origLeft && eventX > nodeLeft)) { - if (dragMoved && !d3_select(this).classed('dragging')) { - // didn't move, interpret as a click - toggleMode(d); - return; - } + if ((ltr && eventX > nodeLeft) || + (rtl && eventX < nodeRight)) { + _targetData = d2; + } + return 'translateX(' + (ltr ? '' : '-') + '100%)'; + } - d3_selectAll('#bar .drag-placeholder') - .remove(); + return null; + }); + }) + .on('end', function(d) { - d3_select(this) - .classed('dragging', false) - .classed('removing', false) - .style('position', null); + if (_dragMoved && !d3_select(this).classed('dragging')) { + // didn't move, interpret as a click + toggleMode(d); + return; + } - d3_selectAll('#bar button.add-favorite, #bar button.add-recent') - .style('transform', null); + var ltr = textDirection === 'ltr', + rtl = !ltr; - var deltaY = d3_event.y - dragOrigin.y; - if (deltaY > 50) { - // dragged out of the top bar, remove + d3_selectAll('#bar .drag-placeholder') + .remove(); - if (d.isFavorite()) { - context.presets().removeFavorite(d.preset); - // also remove this as a recent so it doesn't still appear - context.presets().removeRecent(d.preset); - } else if (d.isRecent()) { - context.presets().removeRecent(d.preset); - } - } else if (targetData !== null) { - // dragged to a new position, reorder - - if (d.isFavorite()) { - context.presets().removeFavorite(d.preset); - if (targetData.isRecent()) { - // also remove this as a recent so it doesn't appear twice - context.presets().removeRecent(d.preset); - } - } else if (d.isRecent()) { - context.presets().removeRecent(d.preset); - } + d3_select(this) + .classed('dragging', false) + .classed('removing', false) + .style('position', null); + + d3_selectAll('#bar button.add-favorite, #bar button.add-recent') + .style('transform', null); - var draggingAfter = (ltr && d3_event.x > dragOrigin.x) || - (rtl && d3_event.x < dragOrigin.x); + var deltaY = d3_event.y - _dragOrigin.y; + if (deltaY > 50) { + // dragged out of the top bar, remove - if (targetData.isFavorite()) { - context.presets().addFavorite(d.preset, targetData.preset, draggingAfter); - } else if (targetData.isRecent()) { - context.presets().addRecent(d.preset, targetData.preset, draggingAfter); + if (d.isFavorite()) { + context.presets().removeFavorite(d.preset); + // also remove this as a recent so it doesn't still appear + context.presets().removeRecent(d.preset); + } else if (d.isRecent()) { + context.presets().removeRecent(d.preset); + } + } else if (_targetData !== null) { + // dragged to a new position, reorder + + if (d.isFavorite()) { + context.presets().removeFavorite(d.preset); + if (_targetData.isRecent()) { + // also remove this as a recent so it doesn't appear twice + context.presets().removeRecent(d.preset); } + } else if (d.isRecent()) { + context.presets().removeRecent(d.preset); } - }) - ); - // update - buttons = buttons - .merge(buttonsEnter) - .classed('disabled', function(d) { return !enabled(d); }); - } + var draggingAfter = (ltr && d3_event.x > _dragOrigin.x) || + (rtl && d3_event.x < _dragOrigin.x); + + if (_targetData.isFavorite()) { + context.presets().addFavorite(d.preset, _targetData.preset, draggingAfter); + } else if (_targetData.isRecent()) { + context.presets().addRecent(d.preset, _targetData.preset, draggingAfter); + } + } + }); tool.allowed = function() { return tool.itemsToDraw().length > 0; From d52d1efefe33019cce32349458289b66e5013427 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 16:44:07 -0700 Subject: [PATCH 22/43] Make structure tool icons smaller --- modules/ui/tools/structure.js | 4 ---- svg/iD-sprite/tools/structure-none.svg | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/ui/tools/structure.js b/modules/ui/tools/structure.js index 39a594db19..5336771274 100644 --- a/modules/ui/tools/structure.js +++ b/modules/ui/tools/structure.js @@ -16,20 +16,17 @@ export function uiToolStructure(context) { tool.label = t('presets.fields.structure.label'); tool.key = t('toolbar.structure.key'); tool.iconName = 'temaki-bridge'; - tool.iconClass = 'icon-30'; var structureNone = { id: 'none', icon: 'iD-structure-none', label: t('toolbar.structure.none.title'), - iconClass: 'icon-30', tags: {} }; var structureBridge = { id: 'bridge', icon: 'temaki-bridge', label: t('presets.fields.structure.options.bridge'), - iconClass: 'icon-30', tags: { bridge: 'yes' }, @@ -42,7 +39,6 @@ export function uiToolStructure(context) { id: 'tunnel', icon: 'temaki-tunnel', label: t('presets.fields.structure.options.tunnel'), - iconClass: 'icon-30', tags: { tunnel: 'yes' }, diff --git a/svg/iD-sprite/tools/structure-none.svg b/svg/iD-sprite/tools/structure-none.svg index 917a621bc2..df85748e4e 100644 --- a/svg/iD-sprite/tools/structure-none.svg +++ b/svg/iD-sprite/tools/structure-none.svg @@ -1,4 +1,4 @@ - - + + From 4b37fcf5ac7a899cdeeb183a1a3eb4d7608c52f3 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 17:03:56 -0700 Subject: [PATCH 23/43] Remove unread argument --- modules/ui/preset_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui/preset_list.js b/modules/ui/preset_list.js index 7eb92a4362..dd903ea354 100644 --- a/modules/ui/preset_list.js +++ b/modules/ui/preset_list.js @@ -401,7 +401,7 @@ export function uiPresetList(context) { item.choose = function() { if (d3_select(this).classed('disabled')) return; - context.presets().setMostRecent(preset, entityGeometries()[0]); + context.presets().setMostRecent(preset); context.perform( function(graph) { for (var i in _entityIDs) { From 54f60fd8a0aed95c4b9fb9aa5a6d60bacde8f057 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 17:43:02 -0700 Subject: [PATCH 24/43] Remove sidebar toggle shortcut --- data/core.yaml | 3 --- dist/locales/en.json | 4 ---- modules/ui/init.js | 1 - 3 files changed, 8 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 336bb2343c..bff393c586 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -470,9 +470,6 @@ en: loading_auth: "Connecting to OpenStreetMap..." report_a_bug: Report a bug help_translate: Help translate - sidebar: - key: '`' - tooltip: Toggle the sidebar. feature_info: hidden_warning: "{count} hidden features" hidden_details: "These features are currently hidden: {details}" diff --git a/dist/locales/en.json b/dist/locales/en.json index 5606e5271c..9116aec811 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -613,10 +613,6 @@ "loading_auth": "Connecting to OpenStreetMap...", "report_a_bug": "Report a bug", "help_translate": "Help translate", - "sidebar": { - "key": "`", - "tooltip": "Toggle the sidebar." - }, "feature_info": { "hidden_warning": "{count} hidden features", "hidden_details": "These features are currently hidden: {details}" diff --git a/modules/ui/init.js b/modules/ui/init.js index 88ceebab86..48b258ca71 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -310,7 +310,6 @@ export function uiInit(context) { var panPixels = 80; context.keybinding() .on('⌫', function() { d3_event.preventDefault(); }) - .on([t('sidebar.key'), '`', '²', '@'], ui.sidebar.toggle) // #5663, #6864 - common QWERTY, AZERTY .on('←', pan([panPixels, 0])) .on('↑', pan([0, panPixels])) .on('→', pan([-panPixels, 0])) From a98920fd158437763f31ca92d47c203fdfaa1c9f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 18 Mar 2020 17:43:39 -0700 Subject: [PATCH 25/43] Add additional keys for toggling the preset browser (close #7258) --- modules/ui/tools/add_feature.js | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index 25710848a6..0dec67a6ad 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -29,7 +29,39 @@ export function uiToolAddFeature(context) { var button = d3_select(null); var key = t('modes.add_feature.key'); - var keys = [key, '`', '²', '@']; // #5663, #6864 - common QWERTY, AZERTY + + // We really want the key to the left of the number row to trigger the preset + // browser, but this varies across keyboard layouts. Since web apps can't detect + // layouts, and since the locale and keyboard may not match even if this shortcut + // is translated, we hardcode the keys for a number of common layouts if + // collisions with other shortcuts are unlikely. + // - #7258, #5663, #6864 + var keys = [ + key, + '`', // US English (and more--all these locale names are just examples) + '\'',// Brazilian + '„', // Georgian + '²', // French + '@', // Dutch + '|', // Norwegian + '\\',// Italian + '§', // Swedish + '#', // Finnish + 'º', // Spanish + '^', // German + '½', // Danish + '÷', // Farsi + 'ˇ', // Estonian + 'ذ', // Arabic + '¸', // Croatian + '˛', // Polish + 'ё', // Belarusian + 'ä', // Uighur Latin + 'ә', // Bashkir + 'ċ', // Maltese + 'ч', // Bulgarian Phonetic + 'ञ' // Nepali + ]; tool.render = function(selection) { From f901d70c26e9f9b32e26f676608a4ec810b69edc Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 19 Mar 2020 09:04:16 -0700 Subject: [PATCH 26/43] Use dispatch instead of passing handlers to preset browser --- modules/ui/preset_browser.js | 67 ++++++++++++++++++--------------- modules/ui/tools/add_feature.js | 6 ++- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index 2376ac566c..eda4895970 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -3,6 +3,7 @@ import { select as d3_select, selectAll as d3_selectAll } from 'd3-selection'; +import { dispatch as d3_dispatch } from 'd3-dispatch'; import * as countryCoder from '@ideditor/country-coder'; import { t, textDirection } from '../util/locale'; @@ -14,16 +15,21 @@ import { uiPresetFavoriteButton } from './preset_favorite_button'; import { uiPresetIcon } from './preset_icon'; //import { groupManager } from '../entities/group_manager'; import { utilKeybinding, utilNoAuto } from '../util'; +import { utilRebind } from '../util/rebind'; -export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { +export function uiPresetBrowser(context) { + + var dispatch = d3_dispatch('choose', 'hide'); // multiple preset browsers could be instantiated at once, give each a unique ID - var uid = (new Date()).getTime().toString(); + var _uid = (new Date()).getTime().toString(); - var presets; + var _presets; - var shownGeometry = []; - updateShownGeometry(allowedGeometry); + // all possible geometries + var _allowedGeometry = []; + // subset of `_allowedGeometry` toggled on by the user + var _shownGeometry = []; var search = d3_select(null), poplistContent = d3_select(null), @@ -36,6 +42,15 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { .alignment('leading') .hasArrow(false); + browser.allowedGeometry = function(val) { + if (!arguments.length) return _allowedGeometry; + _allowedGeometry = val; + updateShownGeometry(val); + renderFilterButtons(); + updateResultsList(); + return browser; + }; + browser.content(function() { return function(selection) { @@ -108,7 +123,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { updateResultsList(); context.features() - .on('change.preset-browser.' + uid , updateForFeatureHiddenState); + .on('change.preset-browser.' + _uid , updateForFeatureHiddenState); // reload in case the user moved countries reloadCountryCode(); @@ -117,13 +132,13 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { var parentHide = browser.hide; browser.hide = function() { parentHide(); - if (onCancel) onCancel(); + dispatch.call('hide', this); }; function renderFilterButtons() { var selection = poplistFooter.select('.filter-wrap'); - var geomForButtons = allowedGeometry.slice(); + var geomForButtons = _allowedGeometry.slice(); var vertexIndex = geomForButtons.indexOf('vertex'); if (vertexIndex !== -1) geomForButtons.splice(vertexIndex, 1); @@ -151,8 +166,8 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { }) .on('click', function(d) { toggleShownGeometry(d); - if (shownGeometry.length === 0) { - updateShownGeometry(allowedGeometry); + if (_shownGeometry.length === 0) { + updateShownGeometry(_allowedGeometry); toggleShownGeometry(d); } updateFilterButtonsStates(); @@ -162,22 +177,13 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { updateFilterButtonsStates(); } - - browser.setAllowedGeometry = function(array) { - allowedGeometry = array; - updateShownGeometry(array); - renderFilterButtons(); - updateResultsList(); - }; - - function updateShownGeometry(geom) { - shownGeometry = geom.slice().sort(); - presets = context.presets().matchAnyGeometry(shownGeometry); + _shownGeometry = geom.slice().sort(); + _presets = context.presets().matchAnyGeometry(_shownGeometry); } function toggleShownGeometry(d) { - var geom = shownGeometry; + var geom = _shownGeometry; var index = geom.indexOf(d); if (index === -1) { geom.push(d); @@ -192,7 +198,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { function updateFilterButtonsStates() { poplistFooter.selectAll('button.filter') .classed('active', function(d) { - return shownGeometry.indexOf(d) !== -1; + return _shownGeometry.indexOf(d) !== -1; }); } @@ -359,7 +365,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { // skip presets not valid in this country if (_countryCode && preset.countryCodes && preset.countryCodes.indexOf(_countryCode) === -1) return false; - return preset.defaultAddGeometry(context, shownGeometry); + return preset.defaultAddGeometry(context, _shownGeometry); }).slice(0, 50); } @@ -380,7 +386,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { var value = search.property('value'); var results; if (value.length) { - results = presets.search(value, shownGeometry, _countryCode).collection + results = _presets.search(value, _shownGeometry, _countryCode).collection .filter(function(d) { if (d.members) { return d.members.collection.some(function(preset) { @@ -549,7 +555,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { item.each(function(d) { if (!d.preset) return; - var reference = uiTagReference(d.preset.reference(d.preset.defaultAddGeometry(context, shownGeometry)), context); + var reference = uiTagReference(d.preset.reference(d.preset.defaultAddGeometry(context, _shownGeometry)), context); var thisItem = d3_select(this); thisItem.selectAll('.row').call(reference.button, 'accessory', 'info'); @@ -575,7 +581,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { var hiddenPresetFeatures; for (var i in item.preset.geometry) { - if (shownGeometry.indexOf(item.preset.geometry[i]) !== -1) { + if (_shownGeometry.indexOf(item.preset.geometry[i]) !== -1) { hiddenPresetFeatures = context.features().isHiddenPreset(item.preset, item.preset.geometry[i]); if (!hiddenPresetFeatures) { break; @@ -644,7 +650,7 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { chooseExpandable(item, d3_select(selection.node().closest('.list-item'))); }; item.subitems = function() { - return category.members.matchAnyGeometry(shownGeometry).collection + return category.members.matchAnyGeometry(_shownGeometry).collection .filter(function(preset) { return preset.addable(); }) @@ -668,7 +674,8 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { item.choose = function() { if (d3_select(this).classed('disabled')) return; - if (onChoose) onChoose(preset, preset.defaultAddGeometry(context, shownGeometry)); + var geometry = preset.defaultAddGeometry(context, _shownGeometry); + dispatch.call('choose', this, preset, geometry); search.node().blur(); }; @@ -678,5 +685,5 @@ export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { // load the initial country code reloadCountryCode(); - return browser; + return utilRebind(browser, dispatch, 'on'); } diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index 0dec67a6ad..cc02c5e48c 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -22,8 +22,10 @@ export function uiToolAddFeature(context) { iconClass: 'icon-30' }; - var allowedGeometry = ['point', 'vertex', 'line', 'area']; - var presetBrowser = uiPresetBrowser(context, allowedGeometry, browserDidSelectPreset, browserDidClose) + var presetBrowser = uiPresetBrowser(context) + .allowedGeometry(['point', 'vertex', 'line', 'area']) + .on('choose.addFeature', browserDidSelectPreset) + .on('hide.addFeature', browserDidClose) .scrollContainer(d3_select('#bar')); var button = d3_select(null); From 611478050ac9f7acd9844e51a5cab1a569c3ac8a Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 19 Mar 2020 09:32:44 -0700 Subject: [PATCH 27/43] Let the key used to open the feature browser to close it if pressed quickly enough --- modules/ui/preset_browser.js | 18 ++++++++++++++++++ modules/ui/tools/add_feature.js | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index eda4895970..c47f2675d7 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -26,6 +26,8 @@ export function uiPresetBrowser(context) { var _presets; + var _closeKey; + // all possible geometries var _allowedGeometry = []; // subset of `_allowedGeometry` toggled on by the user @@ -51,6 +53,12 @@ export function uiPresetBrowser(context) { return browser; }; + browser.closeKey = function(val) { + if (!arguments.length) return _closeKey && _closeKey.key; + _closeKey = { key: val, time: new Date().getTime() }; + return browser; + }; + browser.content(function() { return function(selection) { @@ -213,6 +221,16 @@ export function uiPresetBrowser(context) { function keydown() { + if (_closeKey && d3_event.key === _closeKey.key) { + if (new Date().getTime() - _closeKey.time < 750) { + // limit close timeframe since the key could be used in searching + search.node().blur(); + d3_event.preventDefault(); + d3_event.stopPropagation(); + } + _closeKey = null; + } + var nextFocus, priorFocus, parentSubsection; diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index cc02c5e48c..4bfe9b4767 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -123,7 +123,9 @@ export function uiToolAddFeature(context) { context.keybinding().on(keys, function() { button.classed('active', true); - presetBrowser.show(); + presetBrowser + .closeKey(d3_event.key) + .show(); d3_event.preventDefault(); d3_event.stopPropagation(); }); From e873596ec4dec0dae8c05f8d5e9aa6075b559730 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 19 Mar 2020 09:35:43 -0700 Subject: [PATCH 28/43] Prevent opening the preset browser via hotkey --- modules/ui/tools/add_feature.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index 4bfe9b4767..3c18e49dc0 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -121,6 +121,8 @@ export function uiToolAddFeature(context) { tool.install = function() { context.keybinding().on(keys, function() { + if (button.classed('disabled')) return; + button.classed('active', true); presetBrowser From f66486c320443b46ce9a904f5c0257998882d9ff Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 19 Mar 2020 12:07:22 -0700 Subject: [PATCH 29/43] Fix styling issues with nested popovers --- css/80_app.css | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 73f40fc84d..3aa26832fb 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -5181,35 +5181,35 @@ svg.mouseclick use.right { border-color: transparent; border-style: solid; } -.popover.top .popover-arrow { +.popover.top > .popover-arrow { bottom: -5px; left: 50%; margin-left: -5px; border-top-color: #fff; border-width: 5px 5px 0; } -.popover.right .popover-arrow { +.popover.right > .popover-arrow { top: 50%; left: -5px; margin-top: -5px; border-right-color: #fff; border-width: 5px 5px 5px 0; } -.popover.left .popover-arrow { +.popover.left > .popover-arrow { top: 50%; right: -5px; margin-top: -5px; border-left-color: #fff; border-width: 5px 0 5px 5px; } -.popover.bottom .popover-arrow { +.popover.bottom > .popover-arrow { top: -5px; left: 50%; margin-left: -5px; border-bottom-color: #fff; border-width: 0 5px 5px; } -.popover:not(.arrowed) .popover-arrow { +.popover:not(.arrowed) > .popover-arrow { display: none; } @@ -5239,24 +5239,24 @@ svg.mouseclick use.right { } /* dark tooltips for sidebar / panels */ -.tooltip.dark.top .popover-arrow, -.map-pane .tooltip.top .popover-arrow, -#sidebar .tooltip.top .popover-arrow { +.tooltip.dark.top > .popover-arrow, +.map-pane .tooltip.top > .popover-arrow, +#sidebar .tooltip.top > .popover-arrow { border-top-color: #000; } -.tooltip.dark.bottom .popover-arrow, -.map-pane .tooltip.bottom .popover-arrow, -#sidebar .tooltip.bottom .popover-arrow { +.tooltip.dark.bottom > .popover-arrow, +.map-pane .tooltip.bottom > .popover-arrow, +#sidebar .tooltip.bottom > .popover-arrow { border-bottom-color: #000; } -.tooltip.dark.left .popover-arrow, -.map-pane .tooltip.left .popover-arrow, -#sidebar .tooltip.left .popover-arrow { +.tooltip.dark.left > .popover-arrow, +.map-pane .tooltip.left > .popover-arrow, +#sidebar .tooltip.left > .popover-arrow { border-left-color: #000; } -.tooltip.dark.right .popover-arrow, -.map-pane .tooltip.right .popover-arrow, -#sidebar .tooltip.right .popover-arrow { +.tooltip.dark.right > .popover-arrow, +.map-pane .tooltip.right > .popover-arrow, +#sidebar .tooltip.right > .popover-arrow { border-right-color: #000; } .tooltip.dark .popover-inner, @@ -5298,13 +5298,13 @@ li.hide + li.version .badge .tooltip { left: 5px !important; right: auto !important; } -li:first-of-type .badge .tooltip .popover-arrow, -li.hide + li.version .badge .tooltip .popover-arrow { +li:first-of-type .badge .tooltip > .popover-arrow, +li.hide + li.version .badge .tooltip > .popover-arrow { right: 15px !important; left: auto !important; } -[dir='rtl'] li:first-of-type .badge .tooltip .popover-arrow, -[dir='rtl'] li.hide + li.version .badge .tooltip .popover-arrow { +[dir='rtl'] li:first-of-type .badge .tooltip > .popover-arrow, +[dir='rtl'] li.hide + li.version .badge .tooltip > .popover-arrow { left: 15px !important; right: auto !important; } From 7cd524a6fd0e9e489ae0fa5d58722265c30dace8 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 19 Mar 2020 13:54:49 -0700 Subject: [PATCH 30/43] Improve performance of the preset browser --- modules/ui/preset_browser.js | 87 +++++++++++++++++++-------------- modules/ui/tools/add_feature.js | 2 +- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index c47f2675d7..b726294b45 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -1,9 +1,9 @@ import { event as d3_event, - select as d3_select, - selectAll as d3_selectAll + select as d3_select } from 'd3-selection'; import { dispatch as d3_dispatch } from 'd3-dispatch'; +import deepEqual from 'fast-deep-equal'; import * as countryCoder from '@ideditor/country-coder'; import { t, textDirection } from '../util/locale'; @@ -26,7 +26,8 @@ export function uiPresetBrowser(context) { var _presets; - var _closeKey; + // the hotkey that was used to open the browser + var _openKey; // all possible geometries var _allowedGeometry = []; @@ -53,9 +54,9 @@ export function uiPresetBrowser(context) { return browser; }; - browser.closeKey = function(val) { - if (!arguments.length) return _closeKey && _closeKey.key; - _closeKey = { key: val, time: new Date().getTime() }; + browser.openKey = function(val) { + if (!arguments.length) return _openKey && _openKey.key; + _openKey = { key: val, time: new Date().getTime() }; return browser; }; @@ -128,13 +129,12 @@ export function uiPresetBrowser(context) { search.node().focus(); search.node().setSelectionRange(0, search.property('value').length); + reloadCountryCode(); + updateResultsList(); context.features() .on('change.preset-browser.' + _uid , updateForFeatureHiddenState); - - // reload in case the user moved countries - reloadCountryCode(); }; var parentHide = browser.hide; @@ -175,6 +175,7 @@ export function uiPresetBrowser(context) { .on('click', function(d) { toggleShownGeometry(d); if (_shownGeometry.length === 0) { + // invert the selection instead of toggling all types off updateShownGeometry(_allowedGeometry); toggleShownGeometry(d); } @@ -221,14 +222,15 @@ export function uiPresetBrowser(context) { function keydown() { - if (_closeKey && d3_event.key === _closeKey.key) { - if (new Date().getTime() - _closeKey.time < 750) { - // limit close timeframe since the key could be used in searching + if (_openKey && d3_event.key === _openKey.key) { + if (new Date().getTime() - _openKey.time < 750) { + // Close the browser if the open key is pressed again within a short + // timeframe, but not longer since the key could be used for search input search.node().blur(); d3_event.preventDefault(); d3_event.stopPropagation(); } - _closeKey = null; + _openKey = null; } var nextFocus, @@ -289,7 +291,7 @@ export function uiPresetBrowser(context) { } } - function getDefaultResults() { + function getDefaultResults(geometries, countryCode) { //var graph = context.graph(); @@ -381,9 +383,9 @@ export function uiPresetBrowser(context) { if (preset.addable && !preset.addable()) return false; // skip presets not valid in this country - if (_countryCode && preset.countryCodes && preset.countryCodes.indexOf(_countryCode) === -1) return false; + if (countryCode && preset.countryCodes && preset.countryCodes.indexOf(countryCode) === -1) return false; - return preset.defaultAddGeometry(context, _shownGeometry); + return preset.defaultAddGeometry(context, geometries); }).slice(0, 50); } @@ -394,17 +396,39 @@ export function uiPresetBrowser(context) { if (countryCode) countryCode = countryCode.toLowerCase(); if (_countryCode !== countryCode) { _countryCode = countryCode; - updateResultsList(); } } - function getRawResults() { - if (search.empty()) return []; + // the query that was run to + var _shownQuery = { + value: null, + geometry: null, + countryCode: null + }; + + function updateResultsList() { + + // update only if the browser is visible + if (!browser.isShown()) return; + + var list = poplistContent.selectAll('.list'); + if (search.empty() || list.empty()) return; + + var value = search.property('value').trim(); + + var query = { + value: value, + geometry: _shownGeometry.slice(), + countryCode: _countryCode + }; + + // the results will be the same if the parameters are the same, so don't reload + if (deepEqual(query, _shownQuery)) return; - var value = search.property('value'); var results; + if (value.length) { - results = _presets.search(value, _shownGeometry, _countryCode).collection + results = _presets.search(query.value, query.geometry, query.countryCode).collection .filter(function(d) { if (d.members) { return d.members.collection.some(function(preset) { @@ -414,20 +438,9 @@ export function uiPresetBrowser(context) { return d.addable(); }); } else { - results = getDefaultResults(); + results = getDefaultResults(query.geometry, query.countryCode); } - return results; - } - - function updateResultsList() { - - if (!browser.isShown()) return; - - var list = poplistContent.selectAll('.list'); - if (search.empty() || list.empty()) return; - - var results = getRawResults(); list.call(drawList, results); list.selectAll('.list-item.focused') @@ -439,6 +452,8 @@ export function uiPresetBrowser(context) { var resultCount = results.length; poplistFooter.selectAll('.message') .text(t('modes.add_feature.' + (resultCount === 1 ? 'result' : 'results'), { count: resultCount })); + + _shownQuery = query; } function focusListItem(selection, scrollingToShow) { @@ -587,7 +602,8 @@ export function uiPresetBrowser(context) { function updateForFeatureHiddenState() { - var listItem = d3_selectAll('.add-feature .poplist .list-item'); + var listItem = poplistContent.selectAll('.list-item'); + if (listItem.empty()) return; // remove existing tooltips listItem.selectAll('button.choose').call(tooltip().destroyAny); @@ -700,8 +716,5 @@ export function uiPresetBrowser(context) { return item; } - // load the initial country code - reloadCountryCode(); - return utilRebind(browser, dispatch, 'on'); } diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index 3c18e49dc0..c841f39204 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -126,7 +126,7 @@ export function uiToolAddFeature(context) { button.classed('active', true); presetBrowser - .closeKey(d3_event.key) + .openKey(d3_event.key) .show(); d3_event.preventDefault(); d3_event.stopPropagation(); From c26f1ad93723213c7da0782533488949fad51177 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Wed, 25 Mar 2020 13:20:01 -0700 Subject: [PATCH 31/43] Fix save tooltip keyhint --- modules/ui/tools/save.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui/tools/save.js b/modules/ui/tools/save.js index 9fae034145..d5e3e72230 100644 --- a/modules/ui/tools/save.js +++ b/modules/ui/tools/save.js @@ -16,13 +16,13 @@ export function uiToolSave(context) { }; var button = null; + var key = uiCmd('⌘S'); var tooltipBehavior = uiTooltip() .placement('bottom') .title(t('save.no_changes')) .keys([key]) .scrollContainer(context.container().select('.top-toolbar')); var history = context.history(); - var key = uiCmd('⌘S'); var _numChanges; function isSaving() { From e30388ce3fb315780e4d889399ac102d8166d180 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 29 Mar 2020 11:59:06 -0700 Subject: [PATCH 32/43] Replace global selectors in v3 toolbar (re: #7445) --- modules/ui/tools/add_feature.js | 4 ++-- modules/ui/tools/quick_presets.js | 4 ++-- modules/ui/tools/repeat_add.js | 3 +-- modules/ui/tools/segmented.js | 2 +- modules/ui/tools/simple_button.js | 5 ++--- modules/ui/tools/stop_draw.js | 2 +- modules/ui/tools/toolbox.js | 4 ++-- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/modules/ui/tools/add_feature.js b/modules/ui/tools/add_feature.js index c4d9de6080..2437eef465 100644 --- a/modules/ui/tools/add_feature.js +++ b/modules/ui/tools/add_feature.js @@ -25,7 +25,7 @@ export function uiToolAddFeature(context) { .allowedGeometry(['point', 'vertex', 'line', 'area']) .on('choose.addFeature', browserDidSelectPreset) .on('hide.addFeature', browserDidClose) - .scrollContainer(d3_select('.top-toolbar')); + .scrollContainer(context.container().select('.top-toolbar')); var button = d3_select(null); @@ -95,7 +95,7 @@ export function uiToolAddFeature(context) { .placement('bottom') .title(t('modes.add_feature.description')) .keys([key]) - .scrollContainer(d3_select('.top-toolbar')) + .scrollContainer(context.container().select('.top-toolbar')) ) .call(svgIcon('#' + tool.iconName, tool.iconClass)); diff --git a/modules/ui/tools/quick_presets.js b/modules/ui/tools/quick_presets.js index afbcba0e0b..b31a782491 100644 --- a/modules/ui/tools/quick_presets.js +++ b/modules/ui/tools/quick_presets.js @@ -140,7 +140,7 @@ export function uiToolQuickPresets(context) { .keys(function(d) { return d.key ? [d.key] : null; }) - .scrollContainer(d3_select('.top-toolbar')) + .scrollContainer(context.container().select('.top-toolbar')) ); buttonsEnter @@ -175,7 +175,7 @@ export function uiToolQuickPresets(context) { var _d3Dragger = d3_drag() .on('start', function() { - _scrollNode = d3_select('.top-toolbar').node(); + _scrollNode = context.container().select('.top-toolbar').node(); var node = d3_select(this).node(); _dragOrigin = { diff --git a/modules/ui/tools/repeat_add.js b/modules/ui/tools/repeat_add.js index 17335e6a6b..19779c49fa 100644 --- a/modules/ui/tools/repeat_add.js +++ b/modules/ui/tools/repeat_add.js @@ -1,4 +1,3 @@ -import { select as d3_select } from 'd3-selection'; import { t } from '../../util/locale'; import { svgIcon } from '../../svg/icon'; import { uiTooltip } from '../tooltip'; @@ -18,7 +17,7 @@ export function uiToolRepeatAdd(context) { var tooltipBehavior = uiTooltip() .placement('bottom') .keys([key]) - .scrollContainer(d3_select('.top-toolbar')); + .scrollContainer(context.container().select('.top-toolbar')); tool.render = function(selection) { diff --git a/modules/ui/tools/segmented.js b/modules/ui/tools/segmented.js index 63e3532e3f..3e6bd3aa8e 100644 --- a/modules/ui/tools/segmented.js +++ b/modules/ui/tools/segmented.js @@ -58,7 +58,7 @@ export function uiToolSegemented(context) { .placement('bottom') .title(d.label) .keys(tool.key ? [tool.key] : null) - .scrollContainer(d3_select('.top-toolbar')); + .scrollContainer(context.container().select('.top-toolbar')); d3_select(this) .call(tooltipBehavior) .call(svgIcon('#' + d.icon, d.iconClass)); diff --git a/modules/ui/tools/simple_button.js b/modules/ui/tools/simple_button.js index 4c00db689d..6ab30e6707 100644 --- a/modules/ui/tools/simple_button.js +++ b/modules/ui/tools/simple_button.js @@ -1,15 +1,14 @@ -import { select as d3_select } from 'd3-selection'; import { svgIcon } from '../../svg/icon'; import { uiTooltip } from '../tooltip'; import { utilFunctor } from '../../util/util'; -export function uiToolSimpleButton(protoTool) { +export function uiToolSimpleButton(context, protoTool) { var tool = protoTool || {}; var tooltipBehavior = uiTooltip() .placement('bottom') - .scrollContainer(d3_select('.top-toolbar')); + .scrollContainer(context.container().select('.top-toolbar')); tool.render = function(selection) { diff --git a/modules/ui/tools/stop_draw.js b/modules/ui/tools/stop_draw.js index d0a1b7bb58..1715a74dc7 100644 --- a/modules/ui/tools/stop_draw.js +++ b/modules/ui/tools/stop_draw.js @@ -7,7 +7,7 @@ export function uiToolStopDraw(context) { var cancelOrFinish = 'cancel'; - var tool = uiToolSimpleButton({ + var tool = uiToolSimpleButton(context, { id: 'stop_draw', label: function() { if (cancelOrFinish === 'finish') { diff --git a/modules/ui/tools/toolbox.js b/modules/ui/tools/toolbox.js index c573d372d8..8c43e88638 100644 --- a/modules/ui/tools/toolbox.js +++ b/modules/ui/tools/toolbox.js @@ -27,7 +27,7 @@ export function uiToolToolbox(context) { .placement('bottom') .alignment('leading') .hasArrow(false) - .scrollContainer(d3_select('.top-toolbar')); + .scrollContainer(context.container().select('.top-toolbar')); tool.render = function(selection) { @@ -43,7 +43,7 @@ export function uiToolToolbox(context) { .call(uiTooltip() .placement('bottom') .title(t('toolbar.toolbox.tooltip')) - .scrollContainer(d3_select('.top-toolbar')) + .scrollContainer(context.container().select('.top-toolbar')) ) .call(svgIcon('#fas-toolbox')); From 743e93a73d5d3263c9e6463fde4dbd34360c58f9 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 29 Mar 2020 12:08:23 -0700 Subject: [PATCH 33/43] Use namespaced DOM ids in v3 toolbar (re: #7445) --- modules/ui/preset_browser.js | 8 +++----- modules/ui/tools/quick_presets.js | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index 2e110fa4b4..9629b3d79c 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -14,7 +14,7 @@ import { uiTagReference } from './tag_reference'; import { uiPresetFavoriteButton } from './preset_favorite_button'; import { uiPresetIcon } from './preset_icon'; //import { groupManager } from '../entities/group_manager'; -import { utilKeybinding, utilNoAuto } from '../util'; +import { utilKeybinding, utilNoAuto, utilUniqueDomId } from '../util'; import { utilRebind } from '../util/rebind'; export function uiPresetBrowser(context) { @@ -520,9 +520,7 @@ export function uiPresetBrowser(context) { var item = selection .append('div') .attr('class', 'list-item') - .attr('id', function(d) { - return 'search-add-list-item-preset-' + d.id().replace(/[^a-zA-Z\d:]/g, '-'); - }) + .attr('id', function(d) { return utilUniqueDomId('preset-browser-list-item-' + d.id()); }) .on('mouseover', function() { poplistContent.selectAll('.list .list-item.focused') .classed('focused', false); @@ -653,7 +651,7 @@ export function uiPresetBrowser(context) { if (shouldExpand) { var subitems = item.subitems(); - var selector = '#' + itemSelection.node().id + ' + *'; + var selector = '#' + itemSelection.attr('id') + ' + *'; item.subsection = d3_select(itemSelection.node().parentNode).insert('div', selector) .attr('class', 'subsection subitems'); var subitemsEnter = item.subsection.selectAll('.list-item') diff --git a/modules/ui/tools/quick_presets.js b/modules/ui/tools/quick_presets.js index b31a782491..633479de54 100644 --- a/modules/ui/tools/quick_presets.js +++ b/modules/ui/tools/quick_presets.js @@ -6,7 +6,7 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; import { modeAddArea, modeAddLine, modeAddPoint, modeBrowse } from '../../modes'; import { t, textDirection } from '../../util/locale'; import { uiTooltip } from '../tooltip'; -import { utilSafeClassName } from '../../util/util'; +import { utilUniqueDomId } from '../../util/util'; import { uiPresetIcon } from '../preset_icon'; @@ -125,9 +125,7 @@ export function uiToolQuickPresets(context) { .attr('class', function(d) { return d.button + ' add-button bar-button'; }) - .attr('id', function(d) { - return utilSafeClassName(d.button); - }) + .attr('id', function(d) { return utilUniqueDomId('quick-preset-' + d.button); }) .on('click.mode-buttons', function(d) { if (d3_select(this).classed('disabled')) return; toggleMode(d); From a8dc5100ea9a9262b1ffac036c7cb50f41aa9316 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 4 Apr 2020 19:40:31 -0700 Subject: [PATCH 34/43] Fix invalid call to uiFlash --- modules/modes/add_point.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index c0878d4e3b..69a5e1c596 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -6,7 +6,6 @@ import { osmNode } from '../osm/node'; import { actionAddEntity } from '../actions/add_entity'; import { actionChangeTags } from '../actions/change_tags'; import { actionAddMidpoint } from '../actions/add_midpoint'; -import { uiFlash } from '../ui/flash'; export function modeAddPoint(context, mode) { @@ -24,7 +23,7 @@ export function modeAddPoint(context, mode) { mode.finish(); }) .on('rejectedVertexAsPoint', function finish() { - uiFlash() + context.ui().flash .text(t('modes.add_point.warning.vertex_placement'))(); }); From ee6da9438bad7f116ae904142ca8383fd1b4ae82 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 11:41:12 -0700 Subject: [PATCH 35/43] Replace preset list UI with preset browser for switching presets --- css/80_app.css | 184 +++------- modules/ui/entity_editor.js | 65 ++-- modules/ui/index.js | 1 - modules/ui/inspector.js | 105 +----- modules/ui/popover.js | 42 ++- modules/ui/preset_browser.js | 33 +- modules/ui/preset_list.js | 524 ---------------------------- modules/ui/sections/feature_type.js | 51 ++- modules/ui/sidebar.js | 2 +- modules/ui/tools/toolbox.js | 4 +- modules/ui/tooltip.js | 2 +- 11 files changed, 198 insertions(+), 815 deletions(-) delete mode 100644 modules/ui/preset_list.js diff --git a/css/80_app.css b/css/80_app.css index 41ce4e5dac..ab85089775 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -809,33 +809,13 @@ a.hide-toggle { position: absolute; } -.panewrap { - position: absolute; - width: 200%; - height: 100%; - right: -100%; -} - -.pane { - position: absolute; - width: 50%; - top: 0; - bottom: 30px; -} - -.pane:first-child { - left: 0; -} - -.pane:last-child { - right: 0; -} - .inspector-wrap { width: 100%; height: 100%; overflow: hidden; position: relative; + display: flex; + flex-direction: column; } .inspector-hidden { @@ -843,8 +823,7 @@ a.hide-toggle { } .inspector-body { - overflow-y: scroll; - overflow-x: hidden; + overflow-y: auto; position: absolute; right: 0; left: 0; @@ -989,12 +968,6 @@ a.hide-toggle { /* Preset List and Icons ------------------------------------------------------- */ -.preset-list { - width: 100%; - padding: 20px 20px 10px 20px; - border-bottom: 1px solid #ccc; -} - .preset-list-item { margin-bottom: 10px; position: static; @@ -1152,130 +1125,80 @@ a.hide-toggle { transform: scale(0.5) !important; } -.preset-list-button .label { +.preset-list-button > .label { display: flex; flex-flow: row wrap; align-items: center; - background-color: #f6f6f6; text-align: left; - padding: 5px 10px; - border-left: 1px solid rgba(0, 0, 0, .1); + padding: 5px 5px; flex: 1 1 100%; align-self: stretch; } -.ideditor[dir='rtl'] .preset-list-button .label { +.ideditor[dir='rtl'] .preset-list-button > .label { text-align: right; - border-left: none; - border-right: 1px solid rgba(0, 0, 0, .1); } -.ideditor[dir='ltr'] .preset-list-item.mixed-types .preset-list-button .label { +.ideditor[dir='ltr'] .preset-list-item.mixed-types .preset-list-button > .label { border-top-right-radius: 4px; border-bottom-right-radius: 4px; } -.ideditor[dir='rtl'] .preset-list-item.mixed-types .preset-list-button .label { +.ideditor[dir='rtl'] .preset-list-item.mixed-types .preset-list-button > .label { border-top-left-radius: 4px; border-bottom-left-radius: 4px; } -.ideditor[dir='ltr'] .category .preset-list-button .label { +.ideditor[dir='ltr'] .category .preset-list-button > .label { border-radius: 0px 4px 4px 0px; } -.ideditor[dir='rtl'] .category .preset-list-button .label { +.ideditor[dir='rtl'] .category .preset-list-button > .label { border-radius: 4px 0px 0px 4px; } -.preset-list-item.mixed-types .label { +.preset-list-item.mixed-types .preset-list-button > .label { font-style: italic; } -.preset-list-button .label-inner { +.preset-list-button > .label .label-inner { width: 100%; line-height: 1.35em; } -.preset-list-button .label-inner .namepart { +.preset-list-button > .label .label-inner .namepart { text-overflow: ellipsis; } -.preset-list-button .label-inner .namepart:nth-child(2) { +.preset-list-button > .label .label-inner .namepart:nth-child(2) { font-weight: normal; } -.preset-list-button:hover .label, -.preset-list-button:focus .label, +.preset-list-button:hover > .label, +.preset-list-button:focus > .label, .preset-list-button.disabled, -.preset-list-button.disabled .label { +.preset-list-button.disabled > .label { background-color: #ececec; } -.preset-list-item button.tag-reference-button { +.preset-list-button-wrap > .accessory-buttons > button.tag-reference-button { height: 100%; width: 32px; flex: 0 0 auto; - background: #f6f6f6; } -.ideditor[dir='ltr'] .preset-list-item button.tag-reference-button { +.ideditor[dir='ltr'] .preset-list-button-wrap > .accessory-buttons > button.tag-reference-button { border-left: 1px solid #ccc; } -.ideditor[dir='rtl'] .preset-list-item button.tag-reference-button { +.ideditor[dir='rtl'] .preset-list-button-wrap > .accessory-buttons > button.tag-reference-button { border-right: 1px solid #ccc; } -.ideditor[dir='ltr'] .preset-list-item button:last-child { +.ideditor[dir='ltr'] .preset-list-button-wrap > .accessory-buttons > button:last-child { border-radius: 0 4px 4px 0; } -.ideditor[dir='rtl'] .preset-list-item button:last-child { +.ideditor[dir='rtl'] .preset-list-button-wrap > .accessory-buttons > button:last-child { border-radius: 4px 0 0 4px; } -.preset-list-item button.tag-reference-button:hover { +.preset-list-button-wrap > .accessory-buttons > button.tag-reference-button:hover { background: #f1f1f1; } -.preset-list-item button.tag-reference-button .icon { +.preset-list-button-wrap > .accessory-buttons > button.tag-reference-button .icon { opacity: .5; } - -.current .preset-list-button, -.current .preset-list-button .label { - background-color: #e8ebff; -} - -.category .preset-list-button:after, -.category .preset-list-button:before { - content: ""; - position: absolute; - top: -5px; - left: -1px; right: -1px; - border: 1px solid #ccc; - border-bottom: none; - border-radius: 6px 6px 0 0; - height: 6px; -} - -.category .preset-list-button:before { - top: -3px; -} - -.subgrid .preset-list { - padding: 10px; - margin-top: 0; - border: 0; - border-radius: 8px; - width: -webkit-calc(100% + 20px); - margin-left: -10px; -} -.subgrid .preset-list > *:last-child { - margin-bottom: 0; -} - -.subgrid .arrow { - border: solid rgba(0, 0, 0, 0); - border-width: 10px; - border-bottom-color: #ececec; - width: 0; - height: 0; - margin-left: 50%; - margin-left: -webkit-calc(50% - 10px); -} - - /* Quick links ------------------------------------------------------- */ .quick-links { @@ -2091,7 +2014,7 @@ a.hide-toggle { div.combobox { z-index: 9999; display: none; - box-shadow: 0 4px 10px 1px rgba(0,0,0,.2); + box-shadow: 5px 5.5px 8px -3px rgba(0,0,0,.15), -5px 5.5px 8px -3px rgba(0,0,0,.15); margin-top: -1px; background: #fff; max-height: 245px; @@ -2451,6 +2374,7 @@ button.raw-tag-option svg.icon { } .tag-reference-body { + font-weight: normal; flex: 1 1 auto; width: 100%; overflow: hidden; @@ -2479,10 +2403,6 @@ img.tag-reference-wiki-image { margin: 0 5px 0 0; } -.preset-list .tag-reference-body { - position: relative; - width: 100%; -} .raw-tag-editor .tag-reference-body { width: 100%; } @@ -2555,8 +2475,6 @@ img.tag-reference-wiki-image { opacity: 0.75; z-index: 3000; /* - -webkit-box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.3); - -moz-box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.3); box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.3); */ } @@ -5056,20 +4974,25 @@ svg.mouseclick use.right { z-index: 5000; height: auto; display: block; + cursor: default; + text-align: initial; } .tooltip.in { opacity: 0.95; } -.popover.top { +.popover.flush { + box-shadow: 5px 5.5px 8px -3px rgba(0,0,0,.15), -5px 5.5px 8px -3px rgba(0,0,0,.15); +} +.popover.offset.top { margin-top: -4px; } -.popover.right { +.popover.offset.right { margin-left: 4px; } -.popover.bottom { +.popover.offset.bottom { margin-top: 4px; } -.popover.left { +.popover.offset.left { margin-left: -4px; } .popover.arrowed.top { @@ -5251,7 +5174,7 @@ svg.mouseclick use.right { .map-pane .popover-inner, .map-pane .tooltip-heading, .map-pane .keyhint-wrap, -.sidebar .popover-inner, +.sidebar .tooltip > .popover-inner, .sidebar .tooltip-heading, .sidebar .keyhint-wrap { background: #000; @@ -5305,9 +5228,11 @@ li.hide + li.version .badge .tooltip > .popover-arrow { min-width: 160px; max-width: 325px; flex-direction: column; - -webkit-box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); - -moz-box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); - box-shadow: 0px 0px 3px 0px rgba(0,0,0,0.29); + border: 1px solid #DCDCDC; + box-shadow: 0 4px 10px 1px rgba(0,0,0,.2); +} +.poplist.flush { + border-radius: 0 0 6px 6px; } .poplist.in { display: flex; @@ -5320,8 +5245,10 @@ li.hide + li.version .badge .tooltip > .popover-arrow { font-size: 14px; text-indent: 25px; padding: 5px 10px; - border-top-left-radius: 6px; - border-top-right-radius: 6px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: inherit; + border-top-right-radius: inherit; } .poplist input[type='search'], .poplist input[type='search']:focus { @@ -5343,11 +5270,11 @@ li.hide + li.version .badge .tooltip > .popover-arrow { overflow-y: auto; max-height: 60vh; } -.poplist-content:first-child { +.poplist .popover-inner > *:first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; } -.poplist-content:last-child { +.poplist .popover-inner > *:last-child { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; } @@ -5463,32 +5390,25 @@ li.hide + li.version .badge .tooltip > .popover-arrow { width: 100%; max-width: 325px; } -.assistant .preset-browser.poplist { - top: 84px; - max-height: 300px +.sidebar .preset-browser.poplist { + max-height: 300px; + max-width: 100%; } -.assistant .preset-browser.poplist .poplist-content { +.sidebar .preset-browser.poplist .poplist-content { max-height: 30vh; } -.ideditor[dir='ltr'] .assistant .preset-browser.poplist { - left: 20px; -} -.ideditor[dir='rtl'] .assistant .preset-browser.poplist { - right: 20px; -} .preset-browser .poplist-header { height: 40px; border-bottom: 2px solid #DCDCDC; flex: 0 0 auto; } .preset-browser .poplist-footer { + font-weight: normal; padding: 5px 10px 5px 10px; background: #f6f6f6; border-top: 1px solid #DCDCDC; flex: 0 0 auto; display: flex; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; } .preset-browser .poplist-footer .message { color: #666666; diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index e18387fda2..a4ff0fe808 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -1,14 +1,13 @@ -import { dispatch as d3_dispatch } from 'd3-dispatch'; import { event as d3_event } from 'd3-selection'; import deepEqual from 'fast-deep-equal'; import { presetManager } from '../presets'; -import { t, localizer } from '../core/localizer'; +import { t } from '../core/localizer'; import { actionChangeTags } from '../actions/change_tags'; import { modeBrowse } from '../modes/browse'; import { svgIcon } from '../svg/icon'; import { utilArrayIdentical } from '../util/array'; -import { utilCleanTags, utilCombinedTags, utilRebind } from '../util'; +import { utilCleanTags, utilCombinedTags } from '../util'; import { uiSectionEntityIssues } from './sections/entity_issues'; import { uiSectionFeatureType } from './sections/feature_type'; @@ -19,7 +18,6 @@ import { uiSectionRawTagEditor } from './sections/raw_tag_editor'; import { uiSectionSelectionList } from './sections/selection_list'; export function uiEntityEditor(context) { - var dispatch = d3_dispatch('choose'); var _state = 'select'; var _coalesceChanges = false; var _modified = false; @@ -43,11 +41,6 @@ export function uiEntityEditor(context) { .append('div') .attr('class', 'header fillL cf'); - headerEnter - .append('button') - .attr('class', 'fl preset-reset preset-choose') - .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward')); - headerEnter .append('button') .attr('class', 'fr preset-close') @@ -64,11 +57,6 @@ export function uiEntityEditor(context) { header.selectAll('h3') .text(_entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features')); - header.selectAll('.preset-reset') - .on('click', function() { - dispatch.call('choose', this, _activePresets); - }); - // Body var body = selection.selectAll('.inspector-body') .data([0]); @@ -85,12 +73,16 @@ export function uiEntityEditor(context) { if (!_sections) { _sections = [ uiSectionSelectionList(context), - uiSectionFeatureType(context).on('choose', function(presets) { - dispatch.call('choose', this, presets); - }), + uiSectionFeatureType(context) + .on('choose.entityEditor', function(presets) { + entityEditor.presets(presets); + }), uiSectionEntityIssues(context), - uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags), - uiSectionRawTagEditor('raw-tag-editor', context).on('change', changeTags), + uiSectionPresetFields(context) + .on('change.entityEditor', changeTags) + .on('revert.entityEditor', revertTags), + uiSectionRawTagEditor('raw-tag-editor', context) + .on('change.entityEditor', changeTags), uiSectionRawMemberEditor(context), uiSectionRawMembershipEditor(context) ]; @@ -349,6 +341,39 @@ export function uiEntityEditor(context) { } return entityEditor; }; +/* + function shouldDefaultToPresetList() { + + // if an explicit preset is set then we're coming from a draw mode + if (presets && presets.filter(function(preset) { + return !preset.isFallback(); + }).length) return false; + + // can only change preset on single selection + if (_entityIDs.length !== 1) return false; - return utilRebind(entityEditor, dispatch, 'on'); + var entityID = _entityIDs[0]; + var entity = context.hasEntity(entityID); + if (!entity) return false; + + // default to inspector if there are already tags + if (entity.hasNonGeometryTags()) return false; + + // prompt to select preset if feature is new and untagged + if (_newFeature) return true; + + // all existing features except vertices should default to inspector + if (entity.geometry(context.graph()) !== 'vertex') return false; + + // show vertex issues if there are any + if (context.validator().getEntityIssues(entityID).length) return false; + + // show turn retriction editor for junction vertices + if (entity.isHighwayIntersection(context.graph())) return false; + + // otherwise show preset list for uninteresting vertices + return true; + } +*/ + return entityEditor; } diff --git a/modules/ui/index.js b/modules/ui/index.js index 69e22a2921..673a23eb0c 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -46,7 +46,6 @@ export { uiPresetBrowser } from './preset_browser'; export { uiPresetFavoriteButton } from './preset_favorite_button'; export { uiPopover } from './popover'; export { uiPresetIcon } from './preset_icon'; -export { uiPresetList } from './preset_list'; export { uiRestore } from './restore'; export { uiScale } from './scale'; export { uiSidebar } from './sidebar'; diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index d68a2320d5..393058cb9b 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -1,100 +1,37 @@ -import { interpolate as d3_interpolate } from 'd3-interpolate'; import { select as d3_select } from 'd3-selection'; import { uiEntityEditor } from './entity_editor'; -import { uiPresetList } from './preset_list'; import { uiViewOnOSM } from './view_on_osm'; export function uiInspector(context) { - var presetList = uiPresetList(context); var entityEditor = uiEntityEditor(context); - var wrap = d3_select(null), - presetPane = d3_select(null), - editorPane = d3_select(null); + var editorPane = d3_select(null); var _state = 'select'; var _entityIDs; var _newFeature = false; function inspector(selection, presets) { - presetList - .entityIDs(_entityIDs) - .autofocus(_newFeature) - .on('choose', inspector.setPreset) - .on('cancel', function() { - wrap.transition() - .styleTween('right', function() { return d3_interpolate('-100%', '0%'); }); - }); entityEditor .state(_state) - .entityIDs(_entityIDs) - .on('choose', inspector.showList); + .entityIDs(_entityIDs); if (presets) { entityEditor.presets(presets); } - wrap = selection.selectAll('.panewrap') + editorPane = selection.selectAll('.entity-editor-pane') .data([0]); - var enter = wrap.enter() + var enter = editorPane.enter() .append('div') - .attr('class', 'panewrap'); + .attr('class', 'entity-editor-pane'); - enter - .append('div') - .attr('class', 'preset-list-pane pane'); - - enter - .append('div') - .attr('class', 'entity-editor-pane pane'); - - wrap = wrap.merge(enter); - presetPane = wrap.selectAll('.preset-list-pane'); - editorPane = wrap.selectAll('.entity-editor-pane'); - - function shouldDefaultToPresetList() { - - // if an explicit preset is set then we're coming from a draw mode - if (presets && presets.filter(function(preset) { - return !preset.isFallback(); - }).length) return false; - - // can only change preset on single selection - if (_entityIDs.length !== 1) return false; + editorPane = editorPane.merge(enter); - var entityID = _entityIDs[0]; - var entity = context.hasEntity(entityID); - if (!entity) return false; - - // default to inspector if there are already tags - if (entity.hasNonGeometryTags()) return false; - - // prompt to select preset if feature is new and untagged - if (_newFeature) return true; - - // all existing features except vertices should default to inspector - if (entity.geometry(context.graph()) !== 'vertex') return false; - - // show vertex issues if there are any - if (context.validator().getEntityIssues(entityID).length) return false; - - // show turn retriction editor for junction vertices - if (entity.isHighwayIntersection(context.graph())) return false; - - // otherwise show preset list for uninteresting vertices - return true; - } - - if (shouldDefaultToPresetList()) { - wrap.style('right', '-100%'); - presetPane.call(presetList); - } else { - wrap.style('right', '0%'); - editorPane.call(entityEditor); - } + editorPane.call(entityEditor); var footer = selection.selectAll('.footer') .data([0]); @@ -110,34 +47,10 @@ export function uiInspector(context) { ); } - inspector.showList = function(presets) { - - wrap.transition() - .styleTween('right', function() { return d3_interpolate('0%', '-100%'); }); - - if (presets) { - presetList.presets(presets); - } - - presetPane - .call(presetList.autofocus(true)); - }; - inspector.setPreset = function(preset) { - // upon setting multipolygon, go to the area preset list instead of the editor - if (preset.id === 'type/multipolygon') { - presetPane - .call(presetList.autofocus(true)); - - } else { - wrap.transition() - .styleTween('right', function() { return d3_interpolate('-100%', '0%'); }); - - editorPane - .call(entityEditor.presets([preset])); - } - + editorPane + .call(entityEditor.presets([preset])); }; inspector.state = function(val) { diff --git a/modules/ui/popover.js b/modules/ui/popover.js index 1219988b84..9724e701a0 100644 --- a/modules/ui/popover.js +++ b/modules/ui/popover.js @@ -6,36 +6,42 @@ var _popoverID = 0; export function uiPopover(klass) { var _id = _popoverID++; var _anchorSelection = d3_select(null); - var popover = function(selection) { - _anchorSelection = selection; - selection.each(setup); - }; + var _animation = utilFunctor(false); var _placement = utilFunctor('top'); // top, bottom, left, right var _alignment = utilFunctor('center'); // leading, center, trailing var _scrollContainer = utilFunctor(d3_select(null)); var _content; - var _displayType = utilFunctor(''); - var _hasArrow = utilFunctor(true); + var _displayBehavior = utilFunctor(''); + var _displayStyle = utilFunctor('arrowed'); // arrowed, offset, flush // use pointer events on supported platforms; fallback to mouse events var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse'; - popover.displayType = function(val) { + var popover = function(selection) { + _anchorSelection = selection; + selection.each(setup); + }; + + popover.id = function() { + return _id; + }; + + popover.displayBehavior = function(val) { if (arguments.length) { - _displayType = utilFunctor(val); + _displayBehavior = utilFunctor(val); return popover; } else { - return _displayType; + return _displayBehavior; } }; - popover.hasArrow = function(val) { + popover.displayStyle = function(val) { if (arguments.length) { - _hasArrow = utilFunctor(val); + _displayStyle = utilFunctor(val); return popover; } else { - return _hasArrow; + return _displayStyle; } }; @@ -129,7 +135,7 @@ export function uiPopover(klass) { var enter = popoverSelection.enter() .append('div') .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : '')) - .classed('arrowed', _hasArrow.apply(this, arguments)) + .classed(_displayStyle.apply(this, arguments), true) .on('wheel.popover mousewheel.popover', function() { // don't pass wheel events to the anchor d3_event.stopPropagation(); @@ -153,7 +159,7 @@ export function uiPopover(klass) { var placement = _placement.apply(this, arguments); popoverSelection.classed(placement, true); - var display = _displayType.apply(this, arguments); + var display = _displayBehavior.apply(this, arguments); if (display === 'hover') { anchor.on(_pointerPrefix + 'enter.popover', show); @@ -183,8 +189,8 @@ export function uiPopover(klass) { function show() { - var displayType = _displayType.apply(this, arguments); - if (displayType === 'hover' && d3_event.pointerType === 'touch') { + var displayBehavior = _displayBehavior.apply(this, arguments); + if (displayBehavior === 'hover' && d3_event.pointerType === 'touch') { // don't show hover popovers on touch devices return; } @@ -199,7 +205,7 @@ export function uiPopover(klass) { popoverSelection.classed('in', true); - if (displayType === 'clickFocus') { + if (displayBehavior === 'clickFocus') { anchor.classed('active', true); popoverSelection.node().focus(); } @@ -317,7 +323,7 @@ export function uiPopover(klass) { function hide() { var anchor = d3_select(this); - if (_displayType.apply(this, arguments) === 'clickFocus') { + if (_displayBehavior.apply(this, arguments) === 'clickFocus') { anchor.classed('active', false); } anchor.selectAll('.popover-' + _id).classed('in', false); diff --git a/modules/ui/preset_browser.js b/modules/ui/preset_browser.js index 0efdda784e..1fb7efbb83 100644 --- a/modules/ui/preset_browser.js +++ b/modules/ui/preset_browser.js @@ -22,13 +22,10 @@ export function uiPresetBrowser(context) { var dispatch = d3_dispatch('choose', 'hide'); - // multiple preset browsers could be instantiated at once, give each a unique ID - var _uid = (new Date()).getTime().toString(); - var _presets; - // the hotkey that was used to open the browser - var _openKey; + // object containing details about the hotkey that was used to open the browser + var _openKeyInfo; // all possible geometries var _allowedGeometry = []; @@ -44,7 +41,7 @@ export function uiPresetBrowser(context) { var browser = uiPopover('poplist preset-browser fillL') .placement('bottom') .alignment('leading') - .hasArrow(false); + .displayStyle('offset'); browser.allowedGeometry = function(val) { if (!arguments.length) return _allowedGeometry; @@ -56,8 +53,8 @@ export function uiPresetBrowser(context) { }; browser.openKey = function(val) { - if (!arguments.length) return _openKey && _openKey.key; - _openKey = { key: val, time: new Date().getTime() }; + if (!arguments.length) return _openKeyInfo && _openKeyInfo.key; + _openKeyInfo = { key: val, time: new Date().getTime() }; return browser; }; @@ -135,7 +132,7 @@ export function uiPresetBrowser(context) { updateResultsList(); context.features() - .on('change.preset-browser.' + _uid , updateForFeatureHiddenState); + .on('change.preset-browser.' + browser.id() , updateForFeatureHiddenState); }; var parentHide = browser.hide; @@ -174,6 +171,9 @@ export function uiPresetBrowser(context) { d3_select(this).call(svgIcon('#iD-icon-' + d)); }) .on('click', function(d) { + d3_event.stopPropagation(); + d3_event.preventDefault(); + toggleShownGeometry(d); if (_shownGeometry.length === 0) { // invert the selection instead of toggling all types off @@ -223,15 +223,15 @@ export function uiPresetBrowser(context) { function keydown() { - if (_openKey && d3_event.key === _openKey.key) { - if (new Date().getTime() - _openKey.time < 750) { + if (_openKeyInfo && d3_event.key === _openKeyInfo.key) { + if (new Date().getTime() - _openKeyInfo.time < 750) { // Close the browser if the open key is pressed again within a short // timeframe, but not longer since the key could be used for search input search.node().blur(); d3_event.preventDefault(); d3_event.stopPropagation(); } - _openKey = null; + _openKeyInfo = null; } var nextFocus, @@ -539,12 +539,17 @@ export function uiPresetBrowser(context) { row.append('button') .attr('class', 'choose') .on('click', function(d) { + d3_event.stopPropagation(); + d3_event.preventDefault(); d.choose.call(this); }); row.each(function(d) { - var geometry = d.preset && d.preset.geometry[0]; - if ((d.preset && d.preset.geometry.length !== 1) || + var effectiveGeometries = d.preset && d.preset.geometry.filter(function(geom) { + return _allowedGeometry.indexOf(geom) !== -1; + }); + var geometry = effectiveGeometries && effectiveGeometries[0]; + if ((effectiveGeometries && effectiveGeometries.length !== 1) || (geometry !== 'area' && geometry !== 'line' && geometry !== 'vertex')) { geometry = null; } diff --git a/modules/ui/preset_list.js b/modules/ui/preset_list.js deleted file mode 100644 index f2f7c21056..0000000000 --- a/modules/ui/preset_list.js +++ /dev/null @@ -1,524 +0,0 @@ -import { dispatch as d3_dispatch } from 'd3-dispatch'; -import * as countryCoder from '@ideditor/country-coder'; - -import { - event as d3_event, - select as d3_select -} from 'd3-selection'; - -import { presetManager } from '../presets'; -import { t, localizer } from '../core/localizer'; -import { actionChangePreset } from '../actions/change_preset'; -import { operationDelete } from '../operations/delete'; -import { svgIcon } from '../svg/index'; -import { uiTooltip } from './tooltip'; -import { geoExtent } from '../geo/extent'; -import { uiPresetIcon } from './preset_icon'; -import { uiTagReference } from './tag_reference'; -import { utilKeybinding, utilNoAuto, utilRebind } from '../util'; - - -export function uiPresetList(context) { - var dispatch = d3_dispatch('cancel', 'choose'); - var _entityIDs; - var _currentPresets; - var _autofocus = false; - - - function presetList(selection) { - if (!_entityIDs) return; - - var presets = presetManager.matchAllGeometry(entityGeometries()); - - selection.html(''); - - var messagewrap = selection - .append('div') - .attr('class', 'header fillL'); - - var message = messagewrap - .append('h3') - .text(t('inspector.choose')); - - messagewrap - .append('button') - .attr('class', 'preset-choose') - .on('click', function() { dispatch.call('cancel', this); }) - .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward')); - - function initialKeydown() { - // hack to let delete shortcut work when search is autofocused - if (search.property('value').length === 0 && - (d3_event.keyCode === utilKeybinding.keyCodes['⌫'] || - d3_event.keyCode === utilKeybinding.keyCodes['⌦'])) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - operationDelete(_entityIDs, context)(); - - // hack to let undo work when search is autofocused - } else if (search.property('value').length === 0 && - (d3_event.ctrlKey || d3_event.metaKey) && - d3_event.keyCode === utilKeybinding.keyCodes.z) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - context.undo(); - } else if (!d3_event.ctrlKey && !d3_event.metaKey) { - // don't check for delete/undo hack on future keydown events - d3_select(this).on('keydown', keydown); - keydown.call(this); - } - } - - function keydown() { - // down arrow - if (d3_event.keyCode === utilKeybinding.keyCodes['↓'] && - // if insertion point is at the end of the string - search.node().selectionStart === search.property('value').length) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // move focus to the first item in the preset list - var buttons = list.selectAll('.preset-list-button'); - if (!buttons.empty()) buttons.nodes()[0].focus(); - } - } - - function keypress() { - // enter - var value = search.property('value'); - if (d3_event.keyCode === 13 && value.length) { - list.selectAll('.preset-list-item:first-child') - .each(function(d) { d.choose.call(this); }); - } - } - - function inputevent() { - var value = search.property('value'); - list.classed('filtered', value.length); - var extent = combinedEntityExtent(); - var results, messageText; - if (value.length && extent) { - var center = extent.center(); - var countryCode = countryCoder.iso1A2Code(center); - - results = presets.search(value, entityGeometries()[0], countryCode && countryCode.toLowerCase()); - messageText = t('inspector.results', { - n: results.collection.length, - search: value - }); - } else { - results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro()); - messageText = t('inspector.choose'); - } - list.call(drawList, results); - message.text(messageText); - } - - var searchWrap = selection - .append('div') - .attr('class', 'search-header'); - - var search = searchWrap - .append('input') - .attr('class', 'preset-search-input') - .attr('placeholder', t('inspector.search')) - .attr('type', 'search') - .call(utilNoAuto) - .on('keydown', initialKeydown) - .on('keypress', keypress) - .on('input', inputevent); - - searchWrap - .call(svgIcon('#iD-icon-search', 'pre-text')); - - if (_autofocus) { - search.node().focus(); - } - - var listWrap = selection - .append('div') - .attr('class', 'inspector-body'); - - var list = listWrap - .append('div') - .attr('class', 'preset-list') - .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro())); - - context.features().on('change.preset-list', updateForFeatureHiddenState); - } - - - function drawList(list, presets) { - presets = presets.matchAllGeometry(entityGeometries()); - var collection = presets.collection.reduce(function(collection, preset) { - if (!preset) return collection; - - if (preset.members) { - if (preset.members.collection.filter(function(preset) { - return preset.addable(); - }).length > 1) { - collection.push(CategoryItem(preset)); - } - } else if (preset.addable()) { - collection.push(PresetItem(preset)); - } - return collection; - }, []); - - var items = list.selectAll('.preset-list-item') - .data(collection, function(d) { return d.preset.id; }); - - items.order(); - - items.exit() - .remove(); - - items.enter() - .append('div') - .attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replace('/', '-'); }) - .classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; }) - .each(function(item) { d3_select(this).call(item); }) - .style('opacity', 0) - .transition() - .style('opacity', 1); - - updateForFeatureHiddenState(); - } - - function itemKeydown(){ - // the actively focused item - var item = d3_select(this.closest('.preset-list-item')); - var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item')); - - // arrow down, move focus to the next, lower item - if (d3_event.keyCode === utilKeybinding.keyCodes['↓']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // the next item in the list at the same level - var nextItem = d3_select(item.node().nextElementSibling); - // if there is no next item in this list - if (nextItem.empty()) { - // if there is a parent item - if (!parentItem.empty()) { - // the item is the last item of a sublist, - // select the next item at the parent level - nextItem = d3_select(parentItem.node().nextElementSibling); - } - // if the focused item is expanded - } else if (d3_select(this).classed('expanded')) { - // select the first subitem instead - nextItem = item.select('.subgrid .preset-list-item:first-child'); - } - if (!nextItem.empty()) { - // focus on the next item - nextItem.select('.preset-list-button').node().focus(); - } - - // arrow up, move focus to the previous, higher item - } else if (d3_event.keyCode === utilKeybinding.keyCodes['↑']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // the previous item in the list at the same level - var previousItem = d3_select(item.node().previousElementSibling); - - // if there is no previous item in this list - if (previousItem.empty()) { - // if there is a parent item - if (!parentItem.empty()) { - // the item is the first subitem of a sublist select the parent item - previousItem = parentItem; - } - // if the previous item is expanded - } else if (previousItem.select('.preset-list-button').classed('expanded')) { - // select the last subitem of the sublist of the previous item - previousItem = previousItem.select('.subgrid .preset-list-item:last-child'); - } - - if (!previousItem.empty()) { - // focus on the previous item - previousItem.select('.preset-list-button').node().focus(); - } else { - // the focus is at the top of the list, move focus back to the search field - var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input'); - search.node().focus(); - } - - // arrow left, move focus to the parent item if there is one - } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '→' : '←']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // if there is a parent item, focus on the parent item - if (!parentItem.empty()) { - parentItem.select('.preset-list-button').node().focus(); - } - - // arrow right, choose this item - } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '←' : '→']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - item.datum().choose.call(d3_select(this).node()); - } - } - - - function CategoryItem(preset) { - var box, sublist, shown = false; - - function item(selection) { - var wrap = selection.append('div') - .attr('class', 'preset-list-button-wrap category'); - - function click() { - var isExpanded = d3_select(this).classed('expanded'); - var iconName = isExpanded ? - (localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down'; - d3_select(this) - .classed('expanded', !isExpanded); - d3_select(this).selectAll('div.label-inner svg.icon use') - .attr('href', iconName); - item.choose(); - } - - var geometries = entityGeometries(); - - var button = wrap - .append('button') - .attr('class', 'preset-list-button') - .classed('expanded', false) - .call(uiPresetIcon() - .geometry(geometries.length === 1 && geometries[0]) - .preset(preset)) - .on('click', click) - .on('keydown', function() { - // right arrow, expand the focused item - if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '←' : '→']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // if the item isn't expanded - if (!d3_select(this).classed('expanded')) { - // toggle expansion (expand the item) - click.call(this); - } - // left arrow, collapse the focused item - } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '→' : '←']) { - d3_event.preventDefault(); - d3_event.stopPropagation(); - // if the item is expanded - if (d3_select(this).classed('expanded')) { - // toggle expansion (collapse the item) - click.call(this); - } - } else { - itemKeydown.call(this); - } - }); - - var label = button - .append('div') - .attr('class', 'label') - .append('div') - .attr('class', 'label-inner'); - - label - .append('div') - .attr('class', 'namepart') - .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline')) - .append('span') - .html(function() { return preset.name() + '…'; }); - - box = selection.append('div') - .attr('class', 'subgrid') - .style('max-height', '0px') - .style('opacity', 0); - - box.append('div') - .attr('class', 'arrow'); - - sublist = box.append('div') - .attr('class', 'preset-list fillL3'); - } - - - item.choose = function() { - if (!box || !sublist) return; - - if (shown) { - shown = false; - box.transition() - .duration(200) - .style('opacity', '0') - .style('max-height', '0px') - .style('padding-bottom', '0px'); - } else { - shown = true; - var members = preset.members.matchAllGeometry(entityGeometries()); - sublist.call(drawList, members); - box.transition() - .duration(200) - .style('opacity', '1') - .style('max-height', 200 + members.collection.length * 190 + 'px') - .style('padding-bottom', '10px'); - } - }; - - item.preset = preset; - return item; - } - - - function PresetItem(preset) { - function item(selection) { - var wrap = selection.append('div') - .attr('class', 'preset-list-button-wrap'); - - var geometries = entityGeometries(); - - var button = wrap.append('button') - .attr('class', 'preset-list-button') - .call(uiPresetIcon() - .geometry(geometries.length === 1 && geometries[0]) - .preset(preset)) - .on('click', item.choose) - .on('keydown', itemKeydown); - - var label = button - .append('div') - .attr('class', 'label') - .append('div') - .attr('class', 'label-inner'); - - // NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc) - label.selectAll('.namepart') - .data(preset.name().split(' – ')) - .enter() - .append('div') - .attr('class', 'namepart') - .text(function(d) { return d; }); - - wrap.call(item.reference.button); - selection.call(item.reference.body); - } - - item.choose = function() { - if (d3_select(this).classed('disabled')) return; - - if (!context.inIntro()) { - presetManager.setMostRecent(preset); - } - context.perform( - function(graph) { - for (var i in _entityIDs) { - var entityID = _entityIDs[i]; - var oldPreset = presetManager.match(graph.entity(entityID), graph); - graph = actionChangePreset(entityID, oldPreset, preset)(graph); - } - return graph; - }, - t('operations.change_tags.annotation') - ); - - context.validator().validate(); // rerun validation - dispatch.call('choose', this, preset); - }; - - item.help = function() { - d3_event.stopPropagation(); - item.reference.toggle(); - }; - - item.preset = preset; - item.reference = uiTagReference(preset.reference(entityGeometries()[0]), context); - - return item; - } - - - function updateForFeatureHiddenState() { - if (!_entityIDs.every(context.hasEntity)) return; - - var geometries = entityGeometries(); - var button = context.container().selectAll('.preset-list .preset-list-button'); - - // remove existing tooltips - button.call(uiTooltip().destroyAny); - - button.each(function(item, index) { - var hiddenPresetFeaturesId; - for (var i in geometries) { - hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]); - if (hiddenPresetFeaturesId) break; - } - var isHiddenPreset = !context.inIntro() && - !!hiddenPresetFeaturesId && - (_currentPresets.length !== 1 || item.preset !== _currentPresets[0]); - - d3_select(this) - .classed('disabled', isHiddenPreset); - - if (isHiddenPreset) { - var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId); - var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; - var tooltipObj = { features: t('feature.' + hiddenPresetFeaturesId + '.description') }; - d3_select(this).call(uiTooltip() - .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj)) - .placement(index < 2 ? 'bottom' : 'top') - ); - } - }); - } - - presetList.autofocus = function(val) { - if (!arguments.length) return _autofocus; - _autofocus = val; - return presetList; - }; - - presetList.entityIDs = function(val) { - if (!arguments.length) return _entityIDs; - _entityIDs = val; - if (_entityIDs && _entityIDs.length) { - var presets = _entityIDs.map(function(entityID) { - return presetManager.match(context.entity(entityID), context.graph()); - }); - presetList.presets(presets); - } - return presetList; - }; - - presetList.presets = function(val) { - if (!arguments.length) return _currentPresets; - _currentPresets = val; - return presetList; - }; - - function entityGeometries() { - - var counts = {}; - - for (var i in _entityIDs) { - var entityID = _entityIDs[i]; - var entity = context.entity(entityID); - var geometry = entity.geometry(context.graph()); - - // Treat entities on addr:interpolation lines as points, not vertices (#3241) - if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) { - geometry = 'point'; - } - - if (!counts[geometry]) counts[geometry] = 0; - counts[geometry] += 1; - } - - return Object.keys(counts).sort(function(geom1, geom2) { - return counts[geom2] - counts[geom1]; - }); - } - - function combinedEntityExtent() { - return _entityIDs.reduce(function(extent, entityID) { - var entity = context.graph().entity(entityID); - return extent.extend(entity.extent(context.graph())); - }, geoExtent()); - } - - return utilRebind(presetList, dispatch, 'on'); -} diff --git a/modules/ui/sections/feature_type.js b/modules/ui/sections/feature_type.js index 3bd55a427e..31305e4c78 100644 --- a/modules/ui/sections/feature_type.js +++ b/modules/ui/sections/feature_type.js @@ -3,11 +3,13 @@ import { event as d3_event } from 'd3-selection'; +import { actionChangePreset } from '../../actions/change_preset'; import { presetManager } from '../../presets'; import { utilArrayIdentical } from '../../util/array'; import { t } from '../../core/localizer'; import { uiTooltip } from '../tooltip'; import { utilRebind } from '../../util'; +import { uiPresetBrowser } from '../preset_browser'; import { uiPresetIcon } from '../preset_icon'; import { uiSection } from '../section'; import { uiTagReference } from '../tag_reference'; @@ -26,6 +28,31 @@ export function uiSectionFeatureType(context) { .title(t('inspector.feature_type')) .disclosureContent(renderDisclosureContent); + var _presetBrowser = uiPresetBrowser(context) + .displayStyle('flush') + .on('choose.sectionFeatureType', function(preset) { + _presetBrowser.hide(); + + dispatch.call('choose', this, [preset]); + + if (!context.inIntro()) { + presetManager.setMostRecent(preset); + } + context.perform( + function(graph) { + for (var i in _entityIDs) { + var entityID = _entityIDs[i]; + var oldPreset = presetManager.match(graph.entity(entityID), graph); + graph = actionChangePreset(entityID, oldPreset, preset)(graph); + } + return graph; + }, + t('operations.change_tags.annotation') + ); + + context.validator().validate(); // rerun validation + }); + function renderDisclosureContent(selection) { selection.classed('preset-list-item', true); @@ -80,9 +107,6 @@ export function uiSectionFeatureType(context) { } selection.selectAll('.preset-reset') - .on('click', function() { - dispatch.call('choose', this, _presets); - }) .on('mousedown', function() { d3_event.preventDefault(); d3_event.stopPropagation(); @@ -90,7 +114,15 @@ export function uiSectionFeatureType(context) { .on('mouseup', function() { d3_event.preventDefault(); d3_event.stopPropagation(); - }); + }) + .on('click', function() { + if (!_presetBrowser.isShown()) { + _presetBrowser.show(); + } else { + _presetBrowser.hide(); + } + }) + .call(_presetBrowser); var geometries = entityGeometries(); selection.select('.preset-list-item button') @@ -126,7 +158,7 @@ export function uiSectionFeatureType(context) { if (!arguments.length) return _presets; // don't reload the same preset - if (!utilArrayIdentical(val, _presets)) { + if (!val || !_presets || !utilArrayIdentical(val, _presets)) { _presets = val; var geometries = entityGeometries(); @@ -134,6 +166,8 @@ export function uiSectionFeatureType(context) { _tagReference = uiTagReference(_presets[0].reference(geometries[0]), context) .showing(false); } + _presetBrowser + .allowedGeometry(geometries); } return section; @@ -144,7 +178,12 @@ export function uiSectionFeatureType(context) { var counts = {}; for (var i in _entityIDs) { - var geometry = context.graph().geometry(_entityIDs[i]); + var entity = context.graph().entity(_entityIDs[i]); + var geometry = entity.geometry(context.graph()); + // Treat entities on addr:interpolation lines as points, not vertices (#3241) + if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) { + geometry = 'point'; + } if (!counts[geometry]) counts[geometry] = 0; counts[geometry] += 1; } diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index ab69f77218..1016f52084 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -111,7 +111,7 @@ export function uiSidebar(context) { var inspectorWrap = selection .append('div') - .attr('class', 'inspector-hidden inspector-wrap fr'); + .attr('class', 'inspector-hidden inspector-wrap'); function hover(datum) { diff --git a/modules/ui/tools/toolbox.js b/modules/ui/tools/toolbox.js index e45ab3ddc9..c482ffbffe 100644 --- a/modules/ui/tools/toolbox.js +++ b/modules/ui/tools/toolbox.js @@ -24,10 +24,10 @@ export function uiToolToolbox(context) { var button = d3_select(null), list = d3_select(null), poplist = uiPopover('poplist fillL') - .displayType('clickFocus') + .displayBehavior('clickFocus') .placement('bottom') .alignment('leading') - .hasArrow(false) + .displayStyle('offset') .scrollContainer(context.container().select('.top-toolbar')); tool.render = function(selection) { diff --git a/modules/ui/tooltip.js b/modules/ui/tooltip.js index 3049951399..160858ec28 100644 --- a/modules/ui/tooltip.js +++ b/modules/ui/tooltip.js @@ -5,7 +5,7 @@ import { uiPopover } from './popover'; export function uiTooltip(klass) { var tooltip = uiPopover((klass || '') + ' tooltip') - .displayType('hover'); + .displayBehavior('hover'); var _title = function() { var title = this.getAttribute('data-original-title'); From 0a6192ba4c8acbdb5c6712b4622299ea286aea49 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 13:10:22 -0700 Subject: [PATCH 36/43] Fix code test --- modules/presets/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/presets/index.js b/modules/presets/index.js index ad4da51cee..9860918fc3 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -325,7 +325,7 @@ export function presetIndex() { }; // pass a Set of addable preset ids - _this.addablePresetIDs = (val) => { + _this.addablePresetIDs = function(val) { if (!arguments.length) return _addablePresetIDs; _addablePresetIDs = val; From 2ee8ddb1f656188663ab2ae9e19ddb3776cdb10c Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 14:40:53 -0700 Subject: [PATCH 37/43] Flexbox more of the sidebar Reduce some redundant classes --- css/80_app.css | 67 +++++++++------------------------ modules/modes/save.js | 2 +- modules/services/osmose.js | 2 +- modules/ui/data_editor.js | 2 +- modules/ui/entity_editor.js | 18 ++++++++- modules/ui/field_help.js | 2 +- modules/ui/improveOSM_editor.js | 2 +- modules/ui/inspector.js | 16 +------- modules/ui/keepRight_editor.js | 2 +- modules/ui/note_editor.js | 2 +- modules/ui/osmose_editor.js | 2 +- modules/ui/sidebar.js | 8 +++- 12 files changed, 48 insertions(+), 77 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index ab85089775..b22f2d38bc 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -651,41 +651,17 @@ button.add-note svg.icon { } .field-help-title button.close, -.sidebar-component .header button.data-editor-close, -.sidebar-component .header button.note-editor-close, -.sidebar-component .header button.qa-editor-close, -.entity-editor-pane .header button.preset-close, -.preset-list-pane .header button.preset-choose { +.sidebar .header button.close { position: absolute; right: 0; top: 0; } .ideditor[dir='rtl'] .field-help-title button.close, -.ideditor[dir='rtl'] .sidebar-component .header button.data-editor-close, -.ideditor[dir='rtl'] .sidebar-component .header button.note-editor-close, -.ideditor[dir='rtl'] .sidebar-component .header button.qa-editor-close, -.ideditor[dir='rtl'] .entity-editor-pane .header button.preset-close, -.ideditor[dir='rtl'] .preset-list-pane .header button.preset-choose { +.ideditor[dir='rtl'] .sidebar .header button.close { left: 0; right: auto; } -.entity-editor-pane .header button.preset-choose { - position: absolute; - left: 0; - top: 0; -} -.ideditor[dir='rtl'] .entity-editor-pane .header button.preset-choose { - left: auto; - right: 0; -} - -.preset-choose { - font-size: 16px; - line-height: 1.25; - font-weight: bold; -} - .modal > button { position: absolute; right: 0; @@ -699,8 +675,8 @@ button.add-note svg.icon { } .footer { - position: absolute; - bottom: 0; + position: relative; + flex: 0 0 auto; margin: 0; padding: 5px 20px 5px 20px; border-top: 1px solid #ccc; @@ -794,19 +770,19 @@ a.hide-toggle { } .sidebar-component { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; + position: relative; + display: flex; + flex-direction: column; + height: 100%; } .sidebar-component .body { + position: relative; width: 100%; + height: 100%; overflow: auto; - top: 60px; - bottom: 0; - position: absolute; + display: flex; + flex-direction: column; } .inspector-wrap { @@ -824,18 +800,10 @@ a.hide-toggle { .inspector-body { overflow-y: auto; - position: absolute; - right: 0; - left: 0; - bottom: 0; -} - -.feature-list-pane .inspector-body, -.preset-list-pane .inspector-body { - top: 120px; -} -.entity-editor-pane .inspector-body { - top: 60px; + position: relative; + display: flex; + flex-direction: column; + height: 100%; } .entity-editor { padding: 20px; @@ -3489,7 +3457,6 @@ li.issue-fix-item:not(.actionable) .fix-icon { } .map-pane { position: relative; - top: 0; width: 400px; height: 100%; padding-bottom: 60px; @@ -3755,7 +3722,7 @@ li.issue-fix-item:not(.actionable) .fix-icon { /* Unstyle button fields */ .inspector-hover .form-field-input-radio label.active, -.inspector-hover .entity-editor-pane a.hide-toggle { +.inspector-hover a.hide-toggle { opacity: 1; background-color: transparent; color: #666; diff --git a/modules/modes/save.js b/modules/modes/save.js index 9acc5ac0f7..0a13301a32 100644 --- a/modules/modes/save.js +++ b/modules/modes/save.js @@ -59,7 +59,7 @@ export function modeSave(context) { var selection = context.container() .select('.sidebar') .append('div') - .attr('class','sidebar-component'); + .attr('class','sidebar-component save-pane'); context.container().selectAll('.main-content') .classed('active', true) diff --git a/modules/services/osmose.js b/modules/services/osmose.js index 286853e7f2..b082bb9788 100644 --- a/modules/services/osmose.js +++ b/modules/services/osmose.js @@ -175,7 +175,7 @@ export default { issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id); // Some issues have instance specific detail in a subtitle - issue.detail = marked(data.subtitle.auto); + issue.detail = data.subtitle && marked(data.subtitle.auto); this.replaceItem(issue); }; diff --git a/modules/ui/data_editor.js b/modules/ui/data_editor.js index df5370c241..7048f04a21 100644 --- a/modules/ui/data_editor.js +++ b/modules/ui/data_editor.js @@ -25,7 +25,7 @@ export function uiDataEditor(context) { headerEnter .append('button') - .attr('class', 'fr data-editor-close') + .attr('class', 'close') .on('click', function() { context.enter(modeBrowse(context)); }) diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index a4ff0fe808..38aa65d05d 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -16,6 +16,7 @@ import { uiSectionRawMemberEditor } from './sections/raw_member_editor'; import { uiSectionRawMembershipEditor } from './sections/raw_membership_editor'; import { uiSectionRawTagEditor } from './sections/raw_tag_editor'; import { uiSectionSelectionList } from './sections/selection_list'; +import { uiViewOnOSM } from './view_on_osm'; export function uiEntityEditor(context) { var _state = 'select'; @@ -39,11 +40,11 @@ export function uiEntityEditor(context) { // Enter var headerEnter = header.enter() .append('div') - .attr('class', 'header fillL cf'); + .attr('class', 'header fillL'); headerEnter .append('button') - .attr('class', 'fr preset-close') + .attr('class', 'close') .on('click', function() { context.enter(modeBrowse(context)); }) .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close')); @@ -122,6 +123,19 @@ export function uiEntityEditor(context) { } }); + var footer = selection.selectAll('.footer') + .data([0]); + + footer = footer.enter() + .append('div') + .attr('class', 'footer') + .merge(footer); + + footer + .call(uiViewOnOSM(context) + .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0])) + ); + context.history() .on('change.entity-editor', historyChanged); diff --git a/modules/ui/field_help.js b/modules/ui/field_help.js index 4581d2b0f8..50dc52b1d0 100644 --- a/modules/ui/field_help.js +++ b/modules/ui/field_help.js @@ -181,7 +181,7 @@ export function uiFieldHelp(context, fieldName) { if (_wrap.empty()) return; // absolute position relative to the inspector, so it "floats" above the fields - _inspector = context.container().select('.sidebar .entity-editor-pane .inspector-body'); + _inspector = context.container().select('.sidebar .entity-editor.inspector-body'); if (_inspector.empty()) return; _body = _inspector.selectAll('.field-help-body') diff --git a/modules/ui/improveOSM_editor.js b/modules/ui/improveOSM_editor.js index 784a6b815d..932f62f746 100644 --- a/modules/ui/improveOSM_editor.js +++ b/modules/ui/improveOSM_editor.js @@ -30,7 +30,7 @@ export function uiImproveOsmEditor(context) { headerEnter .append('button') - .attr('class', 'fr qa-editor-close') + .attr('class', 'close') .on('click', () => context.enter(modeBrowse(context))) .call(svgIcon('#iD-icon-close')); diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index 393058cb9b..aa0a9ea4b5 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -1,7 +1,6 @@ import { select as d3_select } from 'd3-selection'; import { uiEntityEditor } from './entity_editor'; -import { uiViewOnOSM } from './view_on_osm'; export function uiInspector(context) { @@ -27,24 +26,11 @@ export function uiInspector(context) { var enter = editorPane.enter() .append('div') - .attr('class', 'entity-editor-pane'); + .attr('class', 'entity-editor-pane sidebar-component'); editorPane = editorPane.merge(enter); editorPane.call(entityEditor); - - var footer = selection.selectAll('.footer') - .data([0]); - - footer = footer.enter() - .append('div') - .attr('class', 'footer') - .merge(footer); - - footer - .call(uiViewOnOSM(context) - .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0])) - ); } inspector.setPreset = function(preset) { diff --git a/modules/ui/keepRight_editor.js b/modules/ui/keepRight_editor.js index 4145d98aa2..1cf31797ec 100644 --- a/modules/ui/keepRight_editor.js +++ b/modules/ui/keepRight_editor.js @@ -29,7 +29,7 @@ export function uiKeepRightEditor(context) { headerEnter .append('button') - .attr('class', 'fr qa-editor-close') + .attr('class', 'close') .on('click', () => context.enter(modeBrowse(context))) .call(svgIcon('#iD-icon-close')); diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js index f6d0adac9c..eae2b8ace6 100644 --- a/modules/ui/note_editor.js +++ b/modules/ui/note_editor.js @@ -45,7 +45,7 @@ export function uiNoteEditor(context) { headerEnter .append('button') - .attr('class', 'fr note-editor-close') + .attr('class', 'close') .on('click', function() { context.enter(modeBrowse(context)); }) diff --git a/modules/ui/osmose_editor.js b/modules/ui/osmose_editor.js index 84c36bec11..6423d23c17 100644 --- a/modules/ui/osmose_editor.js +++ b/modules/ui/osmose_editor.js @@ -29,7 +29,7 @@ export function uiOsmoseEditor(context) { headerEnter .append('button') - .attr('class', 'fr qa-editor-close') + .attr('class', 'close') .on('click', () => context.enter(modeBrowse(context))) .call(svgIcon('#iD-icon-close')); diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 1016f52084..451161f2af 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -171,7 +171,9 @@ export function uiSidebar(context) { .classed('inspector-hidden', true); inspectorWrap - .classed('inspector-hidden', false) + .classed('inspector-hidden', false); + + selection.selectAll('.sidebar-component') .classed('inspector-hover', true); if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') { @@ -229,7 +231,9 @@ export function uiSidebar(context) { .classed('inspector-hidden', true); inspectorWrap - .classed('inspector-hidden', false) + .classed('inspector-hidden', false); + + selection.selectAll('.sidebar-component') .classed('inspector-hover', false); if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), ids) || inspector.state() !== 'select') { From 85b58f509ba5ecb4124e3b71ceb0105fa10b20da Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 15:18:15 -0700 Subject: [PATCH 38/43] Remove unnecessary uiInspector --- css/80_app.css | 9 ---- modules/core/context.js | 2 +- modules/modes/select.js | 6 --- modules/ui/entity_editor.js | 9 +++- modules/ui/fields/lanes.js | 2 +- modules/ui/fields/restrictions.js | 2 +- modules/ui/index.js | 1 - modules/ui/inspector.js | 69 ------------------------------- modules/ui/intro/area.js | 22 +++++----- modules/ui/intro/building.js | 24 +++++------ modules/ui/intro/line.js | 12 +++--- modules/ui/intro/navigation.js | 18 ++++---- modules/ui/intro/point.js | 16 +++---- modules/ui/sidebar.js | 45 +++++++++++--------- test/spec/behavior/select.js | 4 +- test/spec/modes/add_note.js | 4 +- test/spec/modes/add_point.js | 4 +- 17 files changed, 84 insertions(+), 165 deletions(-) delete mode 100644 modules/ui/inspector.js diff --git a/css/80_app.css b/css/80_app.css index b22f2d38bc..179362f70b 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -785,15 +785,6 @@ a.hide-toggle { flex-direction: column; } -.inspector-wrap { - width: 100%; - height: 100%; - overflow: hidden; - position: relative; - display: flex; - flex-direction: column; -} - .inspector-hidden { display: none; } diff --git a/modules/core/context.js b/modules/core/context.js index 8d365a0886..773ef31226 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -404,7 +404,7 @@ export function coreContext() { _uploader.reset(); // don't leave stale state in the inspector - context.container().select('.inspector-wrap *').remove(); + context.container().select('.entity-editor-pane *').remove(); return context; }; diff --git a/modules/modes/select.js b/modules/modes/select.js index 56fd60a27b..ca511357e7 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -47,7 +47,6 @@ export function modeSelect(context, selectedIDs) { modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior, modeDragNote(context).behavior ]; - var inspector; // unused? // the explicit presets for the features in this selection var _presets; var _editMenu; // uiEditMenu @@ -55,10 +54,6 @@ export function modeSelect(context, selectedIDs) { var _follow = false; - var wrap = context.container() - .select('.inspector-wrap'); - - function singular() { if (selectedIDs && selectedIDs.length === 1) { return context.hasEntity(selectedIDs[0]); @@ -512,7 +507,6 @@ export function modeSelect(context, selectedIDs) { mode.exit = function() { - if (inspector) wrap.call(inspector.close); operations.forEach(function(operation) { if (operation.behavior) { diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 38aa65d05d..1e82d25874 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -65,7 +65,7 @@ export function uiEntityEditor(context) { // Enter var bodyEnter = body.enter() .append('div') - .attr('class', 'entity-editor inspector-body sep-top'); + .attr('class', 'entity-editor inspector-body'); // Update body = body @@ -289,6 +289,10 @@ export function uiEntityEditor(context) { entityEditor.state = function(val) { if (!arguments.length) return _state; _state = val; + + // remove any old field help overlay that might have gotten attached to the inspector + context.container().selectAll('.field-help-body').remove(); + return entityEditor; }; @@ -350,11 +354,12 @@ export function uiEntityEditor(context) { if (!arguments.length) return _activePresets; // don't reload the same preset - if (!utilArrayIdentical(val, _activePresets)) { + if (!val || !_activePresets || !utilArrayIdentical(val, _activePresets)) { _activePresets = val; } return entityEditor; }; + /* function shouldDefaultToPresetList() { diff --git a/modules/ui/fields/lanes.js b/modules/ui/fields/lanes.js index 61297df9cc..4b47a8f045 100644 --- a/modules/ui/fields/lanes.js +++ b/modules/ui/fields/lanes.js @@ -13,7 +13,7 @@ export function uiFieldLanes(field, context) { function lanes(selection) { var lanesData = context.entity(_entityIDs[0]).lanes(); - if (!context.container().select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) { + if (!context.container().select('.sidebar-component.inspector-hidden').empty() || !selection.node().parentNode) { selection.call(lanes.off); return; } diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index af562d76d5..8670540f94 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -63,7 +63,7 @@ export function uiFieldRestrictions(field, context) { // if form field is hidden or has detached from dom, clean up. if (!isOK || - !context.container().select('.inspector-wrap.inspector-hidden').empty() || + !context.container().select('.sidebar-component.inspector-hidden').empty() || !selection.node().parentNode || !selection.node().parentNode.parentNode) { selection.call(restrictions.off); diff --git a/modules/ui/index.js b/modules/ui/index.js index 673a23eb0c..835a0d00c2 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -28,7 +28,6 @@ export { uiImproveOsmDetails } from './improveOSM_details'; export { uiImproveOsmEditor } from './improveOSM_editor'; export { uiImproveOsmHeader } from './improveOSM_header'; export { uiInfo } from './info'; -export { uiInspector } from './inspector'; export { uiIssuesInfo } from './issues_info'; export { uiKeepRightDetails } from './keepRight_details'; export { uiKeepRightEditor } from './keepRight_editor'; diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js deleted file mode 100644 index aa0a9ea4b5..0000000000 --- a/modules/ui/inspector.js +++ /dev/null @@ -1,69 +0,0 @@ -import { select as d3_select } from 'd3-selection'; - -import { uiEntityEditor } from './entity_editor'; - - -export function uiInspector(context) { - var entityEditor = uiEntityEditor(context); - var editorPane = d3_select(null); - var _state = 'select'; - var _entityIDs; - var _newFeature = false; - - - function inspector(selection, presets) { - - entityEditor - .state(_state) - .entityIDs(_entityIDs); - - if (presets) { - entityEditor.presets(presets); - } - - editorPane = selection.selectAll('.entity-editor-pane') - .data([0]); - - var enter = editorPane.enter() - .append('div') - .attr('class', 'entity-editor-pane sidebar-component'); - - editorPane = editorPane.merge(enter); - - editorPane.call(entityEditor); - } - - inspector.setPreset = function(preset) { - - editorPane - .call(entityEditor.presets([preset])); - }; - - inspector.state = function(val) { - if (!arguments.length) return _state; - _state = val; - entityEditor.state(_state); - - // remove any old field help overlay that might have gotten attached to the inspector - context.container().selectAll('.field-help-body').remove(); - - return inspector; - }; - - - inspector.entityIDs = function(val) { - if (!arguments.length) return _entityIDs; - _entityIDs = val; - return inspector; - }; - - - inspector.newFeature = function(val) { - if (!arguments.length) return _newFeature; - _newFeature = val; - return inspector; - }; - - - return inspector; -} diff --git a/modules/ui/intro/area.js b/modules/ui/intro/area.js index 53b4db2c5f..636f249e0b 100644 --- a/modules/ui/intro/area.js +++ b/modules/ui/intro/area.js @@ -207,11 +207,11 @@ export function uiIntroArea(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -233,9 +233,9 @@ export function uiIntroArea(context, reveal) { context.enter(modeSelect(context, [_areaID])); // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -269,7 +269,7 @@ export function uiIntroArea(context, reveal) { } function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.on('enter.intro', null); context.history().on('change.intro', null); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); @@ -292,11 +292,11 @@ export function uiIntroArea(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); // It's possible for the user to add a description in a previous step.. // If they did this already, just continue to next step. @@ -350,7 +350,7 @@ export function uiIntroArea(context, reveal) { }); function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.more-fields .combobox-input').on('click.intro', null); context.on('exit.intro', null); nextStep(); @@ -417,7 +417,7 @@ export function uiIntroArea(context, reveal) { } // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); if (context.container().select('.form-field-description').empty()) { return continueTo(retryChooseDescription); @@ -449,7 +449,7 @@ export function uiIntroArea(context, reveal) { } // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); reveal('.entity-editor-pane', t('intro.areas.retry_add_field', { field: descriptionField.label() }), { @@ -490,7 +490,7 @@ export function uiIntroArea(context, reveal) { context.on('enter.intro exit.intro', null); context.map().on('move.intro drawn.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); context.container().select('.more-fields .combobox-input').on('click.intro', null); }; diff --git a/modules/ui/intro/building.js b/modules/ui/intro/building.js index 7ed73d0f1e..5e3ba3f42f 100644 --- a/modules/ui/intro/building.js +++ b/modules/ui/intro/building.js @@ -207,11 +207,11 @@ export function uiIntroBuilding(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); var button = context.container().select('.preset-category-building .preset-list-button'); @@ -238,7 +238,7 @@ export function uiIntroBuilding(context, reveal) { }); function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-list-button').on('click.intro', null); context.on('enter.intro', null); nextStep(); @@ -256,11 +256,11 @@ export function uiIntroBuilding(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); var button = context.container().select('.preset-building-house .preset-list-button'); @@ -287,7 +287,7 @@ export function uiIntroBuilding(context, reveal) { }); function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-list-button').on('click.intro', null); context.on('enter.intro', null); nextStep(); @@ -545,11 +545,11 @@ export function uiIntroBuilding(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -571,9 +571,9 @@ export function uiIntroBuilding(context, reveal) { context.enter(modeSelect(context, [_tankID])); // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -607,7 +607,7 @@ export function uiIntroBuilding(context, reveal) { } function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.on('enter.intro', null); context.history().on('change.intro', null); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); @@ -777,7 +777,7 @@ export function uiIntroBuilding(context, reveal) { context.on('enter.intro exit.intro', null); context.map().on('move.intro drawn.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); context.container().select('.more-fields .combobox-input').on('click.intro', null); }; diff --git a/modules/ui/intro/line.js b/modules/ui/intro/line.js index 9696c2bcf9..f9f6f35716 100644 --- a/modules/ui/intro/line.js +++ b/modules/ui/intro/line.js @@ -247,11 +247,11 @@ export function uiIntroLine(context, reveal) { if (button.empty()) return chapter.restart(); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { // reset pane, in case user somehow happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '-100%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '-100%'); reveal(button.node(), t('intro.lines.choose_category_road', { category: roadCategory.name() }) @@ -264,7 +264,7 @@ export function uiIntroLine(context, reveal) { }, 400); // after editor pane visible function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-list-button').on('click.intro', null); context.on('exit.intro', null); nextStep(); @@ -316,7 +316,7 @@ export function uiIntroLine(context, reveal) { }); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); timeout(function() { var button = context.container().select('.entity-editor-pane .preset-list-button'); @@ -332,7 +332,7 @@ export function uiIntroLine(context, reveal) { }, 500); function continueTo(nextStep) { - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-list-button').on('click.intro', null); context.on('exit.intro', null); nextStep(); @@ -1058,7 +1058,7 @@ export function uiIntroLine(context, reveal) { context.on('enter.intro exit.intro', null); context.map().on('move.intro drawn.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-list-button').on('click.intro', null); }; diff --git a/modules/ui/intro/navigation.js b/modules/ui/intro/navigation.js index 235ce98332..6aa14ac0d4 100644 --- a/modules/ui/intro/navigation.js +++ b/modules/ui/intro/navigation.js @@ -253,7 +253,7 @@ export function uiIntroNavigation(context, reveal) { if (!isTownHallSelected()) return clickTownHall(); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); var onClick = function() { continueTo(presetTownHall); }; @@ -275,7 +275,7 @@ export function uiIntroNavigation(context, reveal) { function continueTo(nextStep) { context.on('exit.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); nextStep(); } } @@ -285,9 +285,9 @@ export function uiIntroNavigation(context, reveal) { if (!isTownHallSelected()) return clickTownHall(); // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); // preset match, in case the user happened to change it. var entity = context.entity(context.selectedIDs()[0]); @@ -313,7 +313,7 @@ export function uiIntroNavigation(context, reveal) { function continueTo(nextStep) { context.on('exit.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); nextStep(); } } @@ -323,9 +323,9 @@ export function uiIntroNavigation(context, reveal) { if (!isTownHallSelected()) return clickTownHall(); // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); var onClick = function() { continueTo(closeTownHall); }; @@ -347,7 +347,7 @@ export function uiIntroNavigation(context, reveal) { function continueTo(nextStep) { context.on('exit.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); nextStep(); } } @@ -551,7 +551,7 @@ export function uiIntroNavigation(context, reveal) { context.on('enter.intro exit.intro', null); context.map().on('move.intro drawn.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.search-header input').on('keydown.intro keyup.intro', null); }; diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js index 5cf276787c..370ad9411d 100644 --- a/modules/ui/intro/point.js +++ b/modules/ui/intro/point.js @@ -122,7 +122,7 @@ export function uiIntroPoint(context, reveal) { } // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -143,7 +143,7 @@ export function uiIntroPoint(context, reveal) { context.enter(modeSelect(context, [_pointID])); // disallow scrolling - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); context.container().select('.preset-search-input') .on('keydown.intro', null) @@ -180,7 +180,7 @@ export function uiIntroPoint(context, reveal) { function continueTo(nextStep) { context.on('enter.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', null); + context.container().select('.entity-editor-pane').on('wheel.intro', null); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); nextStep(); } @@ -218,7 +218,7 @@ export function uiIntroPoint(context, reveal) { } // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); timeout(function() { // It's possible for the user to add a name in a previous step.. @@ -259,7 +259,7 @@ export function uiIntroPoint(context, reveal) { function addCloseEditor() { // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); var selector = '.entity-editor-pane button.preset-close svg use'; var href = d3_select(selector).attr('href') || '#iD-icon-close'; @@ -328,7 +328,7 @@ export function uiIntroPoint(context, reveal) { } // reset pane, in case user happened to untag the point.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); context.on('exit.intro', function() { continueTo(reselectPoint); @@ -358,7 +358,7 @@ export function uiIntroPoint(context, reveal) { } // reset pane, in case user happened to change it.. - context.container().select('.inspector-wrap .panewrap').style('right', '0%'); + context.container().select('.entity-editor-pane .panewrap').style('right', '0%'); context.on('exit.intro', function() { continueTo(rightClickPoint); @@ -497,7 +497,7 @@ export function uiIntroPoint(context, reveal) { context.on('enter.intro exit.intro', null); context.map().on('move.intro drawn.intro', null); context.history().on('change.intro', null); - context.container().select('.inspector-wrap').on('wheel.intro', eventCancel); + context.container().select('.entity-editor-pane').on('wheel.intro', eventCancel); context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null); }; diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 451161f2af..07a43fbc4e 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -10,8 +10,8 @@ import { utilArrayIdentical } from '../util/array'; import { osmEntity, osmNote, QAItem } from '../osm'; import { services } from '../services'; import { uiDataEditor } from './data_editor'; +import { uiEntityEditor } from './entity_editor'; import { uiFeatureList } from './feature_list'; -import { uiInspector } from './inspector'; import { uiImproveOsmEditor } from './improveOSM_editor'; import { uiKeepRightEditor } from './keepRight_editor'; import { uiOsmoseEditor } from './osmose_editor'; @@ -20,7 +20,7 @@ import { localizer } from '../core/localizer'; export function uiSidebar(context) { - var inspector = uiInspector(context); + var entityEditor = uiEntityEditor(context); var dataEditor = uiDataEditor(context); var noteEditor = uiNoteEditor(context); var improveOsmEditor = uiImproveOsmEditor(context); @@ -109,9 +109,9 @@ export function uiSidebar(context) { .attr('class', 'feature-list-pane') .call(uiFeatureList(context)); - var inspectorWrap = selection + var entityEditorWrap = selection .append('div') - .attr('class', 'inspector-hidden inspector-wrap'); + .attr('class', 'entity-editor-pane sidebar-component inspector-hidden'); function hover(datum) { @@ -170,27 +170,27 @@ export function uiSidebar(context) { featureListWrap .classed('inspector-hidden', true); - inspectorWrap + entityEditorWrap .classed('inspector-hidden', false); selection.selectAll('.sidebar-component') .classed('inspector-hover', true); - if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') { - inspector + if (!entityEditor.entityIDs() || !utilArrayIdentical(entityEditor.entityIDs(), [datum.id]) || entityEditor.state() !== 'hover') { + entityEditor .state('hover') .entityIDs([datum.id]); - inspectorWrap - .call(inspector); + entityEditorWrap + .call(entityEditor); } } else if (!_current) { featureListWrap .classed('inspector-hidden', false); - inspectorWrap + entityEditorWrap .classed('inspector-hidden', true); - inspector + entityEditor .state('hide'); } else if (_wasData || _wasNote || _wasQaItem) { @@ -230,38 +230,43 @@ export function uiSidebar(context) { featureListWrap .classed('inspector-hidden', true); - inspectorWrap + entityEditorWrap .classed('inspector-hidden', false); selection.selectAll('.sidebar-component') .classed('inspector-hover', false); - if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), ids) || inspector.state() !== 'select') { - inspector + if (!entityEditor.entityIDs() || !utilArrayIdentical(entityEditor.entityIDs(), ids) || entityEditor.state() !== 'select') { + entityEditor .state('select') .entityIDs(ids) .newFeature(newFeature); - inspectorWrap - .call(inspector, presets); + if (presets) { + entityEditor + .presets(presets); + } + + entityEditorWrap + .call(entityEditor); } } else { - inspector + entityEditor .state('hide'); } }; sidebar.showPresetList = function() { - inspector.showList(); + entityEditor.showList(); }; sidebar.show = function(component, element) { featureListWrap .classed('inspector-hidden', true); - inspectorWrap + entityEditorWrap .classed('inspector-hidden', true); if (_current) _current.remove(); @@ -275,7 +280,7 @@ export function uiSidebar(context) { sidebar.hide = function() { featureListWrap .classed('inspector-hidden', false); - inspectorWrap + entityEditorWrap .classed('inspector-hidden', true); if (_current) _current.remove(); diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index e3b3843b93..f543fbc66d 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -10,9 +10,7 @@ describe('iD.behaviorSelect', function() { context.perform(iD.actionAddEntity(a), iD.actionAddEntity(b)); - container.call(context.map()) - .append('div') - .attr('class', 'inspector-wrap'); + container.call(context.map()); context.surface().select('.data-layer.osm').selectAll('circle') .data([a, b]) diff --git a/test/spec/modes/add_note.js b/test/spec/modes/add_note.js index b4ddc91139..36357bc9e0 100644 --- a/test/spec/modes/add_note.js +++ b/test/spec/modes/add_note.js @@ -17,9 +17,7 @@ describe('iD.modeAddNote', function() { context.loadTiles = function () {}; - container.call(context.map()) - .append('div') - .attr('class', 'inspector-wrap'); + container.call(context.map()); context.map().centerZoom([-77.02271, 38.90085], 20); context.enter(iD.modeAddNote(context)); diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index cb00a7fa43..a8adb46f5e 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -10,9 +10,7 @@ describe.skip('iD.modeAddPoint', function() { context.loadTiles = function () {}; - container.call(context.map()) - .append('div') - .attr('class', 'inspector-wrap'); + container.call(context.map()); context.map().centerZoom([-77.02271, 38.90085], 20); context.enter(iD.modeAddPoint(context)); From fdd30b4f327993bd339db1c9014ad4de71723ea7 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 15:54:57 -0700 Subject: [PATCH 39/43] Open the preset browser by default when adding a new fallback feature --- modules/ui/entity_editor.js | 39 +++------------------ modules/ui/sections/feature_type.js | 53 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 1e82d25874..c3c41b3bbd 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -25,7 +25,7 @@ export function uiEntityEditor(context) { var _base; var _entityIDs; var _activePresets = []; - var _newFeature; + var _newFeature = false; var _sections; @@ -102,6 +102,9 @@ export function uiEntityEditor(context) { if (section.state) { section.state(_state); } + if (section.newFeature) { + section.newFeature(_newFeature); + } body.call(section.render); }); @@ -360,39 +363,5 @@ export function uiEntityEditor(context) { return entityEditor; }; -/* - function shouldDefaultToPresetList() { - - // if an explicit preset is set then we're coming from a draw mode - if (presets && presets.filter(function(preset) { - return !preset.isFallback(); - }).length) return false; - - // can only change preset on single selection - if (_entityIDs.length !== 1) return false; - - var entityID = _entityIDs[0]; - var entity = context.hasEntity(entityID); - if (!entity) return false; - - // default to inspector if there are already tags - if (entity.hasNonGeometryTags()) return false; - - // prompt to select preset if feature is new and untagged - if (_newFeature) return true; - - // all existing features except vertices should default to inspector - if (entity.geometry(context.graph()) !== 'vertex') return false; - - // show vertex issues if there are any - if (context.validator().getEntityIssues(entityID).length) return false; - - // show turn retriction editor for junction vertices - if (entity.isHighwayIntersection(context.graph())) return false; - - // otherwise show preset list for uninteresting vertices - return true; - } -*/ return entityEditor; } diff --git a/modules/ui/sections/feature_type.js b/modules/ui/sections/feature_type.js index 31305e4c78..8ea35f46d0 100644 --- a/modules/ui/sections/feature_type.js +++ b/modules/ui/sections/feature_type.js @@ -21,6 +21,7 @@ export function uiSectionFeatureType(context) { var _entityIDs = []; var _presets = []; + var _newFeature = false; var _tagReference; @@ -146,6 +147,11 @@ export function uiSectionFeatureType(context) { .append('div') .attr('class', 'namepart') .text(function(d) { return d; }); + + + if (shouldOpenPresetBrowserByDefault()) { + _presetBrowser.show(); + } } section.entityIDs = function(val) { @@ -173,6 +179,12 @@ export function uiSectionFeatureType(context) { return section; }; + section.newFeature = function(val) { + if (!arguments.length) return _newFeature; + _newFeature = val; + return section; + }; + function entityGeometries() { var counts = {}; @@ -193,5 +205,46 @@ export function uiSectionFeatureType(context) { }); } + function shouldOpenPresetBrowserByDefault() { + + // don't open if a non-geometry preset is specified (including addresses) + if (_presets && _presets.filter(function(preset) { + return !preset.isFallback(); + }).length) return false; + + var entities = _entityIDs.map(function(entityID) { + return context.hasEntity(entityID); + }).filter(Boolean); + + // ignore if entities aren't valid + if (!entities.length) return false; + + // don't open if there are already non-geometry tags + if (entities.some(function(entity) { + return entity.hasNonGeometryTags(); + })) return false; + + // open if feature is new and untagged + if (_newFeature) return true; + + return false; + + /* + // don't open for non-vertices for any other reason + if (entities.some(function(entity) { + return entity.geometry(context.graph()) !== 'vertex'; + })) return false; + + // don't open if there are vertex issues, we need to show the issues list + if (context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true }).length) return false; + + // don't open for junction vertices, we need to show the turn retriction editor + if (entities.length === 1 && entities[0].isHighwayIntersection(context.graph())) return false; + + // open for uninteresting vertices + return true; + */ + } + return utilRebind(section, dispatch, 'on'); } From a7e36ec6f2792456a70741c495d33af38603e484 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 17:08:05 -0700 Subject: [PATCH 40/43] Flexbox the feature search sidebar --- css/80_app.css | 8 +++++--- modules/ui/sidebar.js | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 179362f70b..7c4968e513 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -804,11 +804,15 @@ a.hide-toggle { margin-bottom: 150px; } +.sidebar .search-header { + position: relative; +} + .sidebar .search-header .icon { display: block; position: absolute; left: 10px; - top: 80px; + top: 20px; pointer-events: none; } .ideditor[dir='rtl'] .sidebar .search-header .icon { @@ -817,8 +821,6 @@ a.hide-toggle { } .sidebar .search-header input { - position: absolute; - top: 60px; height: 60px; width: 100%; padding: 5px 10px; diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 07a43fbc4e..cb75959735 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -106,7 +106,7 @@ export function uiSidebar(context) { var featureListWrap = selection .append('div') - .attr('class', 'feature-list-pane') + .attr('class', 'feature-list-pane sidebar-component') .call(uiFeatureList(context)); var entityEditorWrap = selection @@ -193,6 +193,9 @@ export function uiSidebar(context) { entityEditor .state('hide'); + selection.selectAll('.sidebar-component') + .classed('inspector-hover', false); + } else if (_wasData || _wasNote || _wasQaItem) { _wasNote = false; _wasData = false; @@ -283,6 +286,9 @@ export function uiSidebar(context) { entityEditorWrap .classed('inspector-hidden', true); + selection.selectAll('.sidebar-component') + .classed('inspector-hover', false); + if (_current) _current.remove(); _current = null; }; From c2bbe40e75f3ad2c88d4cb6ad3586bf6701c5c57 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sun, 5 Apr 2020 19:30:17 -0700 Subject: [PATCH 41/43] Make the sidebar header shorter --- css/80_app.css | 10 ++++++---- modules/ui/feature_list.js | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 7c4968e513..0924283003 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -623,9 +623,10 @@ button.add-note svg.icon { /* Header for modals / panes ------------------------------------------------------- */ .header { - border-bottom: 1px solid #ccc; - height: 60px; position: relative; + flex: 0 0 auto; + height: 41px; + border-bottom: 1px solid #ccc; } .header h3 { @@ -634,7 +635,8 @@ button.add-note svg.icon { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; - padding: 20px; + padding: 11.5px; + font-size: 14px; } .header button, @@ -648,6 +650,7 @@ button.add-note svg.icon { .header button { position: relative; height: 100%; + color: #666; } .field-help-title button.close, @@ -2532,7 +2535,6 @@ input.key-trap { background-color: #f6f6f6; padding: 0 15px; flex: 1 1 100%; - font-size: 14px; font-weight: bold; border-radius: 0 5px 5px 0; } diff --git a/modules/ui/feature_list.js b/modules/ui/feature_list.js index b91c456fb8..02500ade52 100644 --- a/modules/ui/feature_list.js +++ b/modules/ui/feature_list.js @@ -31,7 +31,7 @@ export function uiFeatureList(context) { function featureList(selection) { var header = selection .append('div') - .attr('class', 'header fillL cf'); + .attr('class', 'header fillL'); header .append('h3') @@ -59,7 +59,7 @@ export function uiFeatureList(context) { var list = listWrap .append('div') - .attr('class', 'feature-list cf'); + .attr('class', 'feature-list'); context .on('exit.feature-list', clearSearch); From 4a9ee0c8b92ad5e50db977985ea1c5fba402869a Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Mon, 6 Apr 2020 10:26:03 -0700 Subject: [PATCH 42/43] Add folder border to preset category icons (close #6085) --- css/80_app.css | 12 ++++-------- modules/ui/preset_icon.js | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 0924283003..4185767078 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -989,10 +989,13 @@ a.hide-toggle { visibility: hidden; } +.preset-icon-fill-vertex circle, +.preset-icon-category-border path, .preset-icon-point-border path { stroke: #333; - stroke-width: 1.2; + stroke-width: 1.5px; fill: transparent; + backface-visibility: hidden; } .preset-icon-line { @@ -1039,13 +1042,6 @@ a.hide-toggle { fill: transparent; } -.preset-icon-fill-vertex circle { - stroke-width: 1.5px; - stroke: #333; - fill: none; - backface-visibility: hidden; -} - .preset-icon { width: 100%; height:100%; diff --git a/modules/ui/preset_icon.js b/modules/ui/preset_icon.js index e2e8e8123a..138ea8948c 100644 --- a/modules/ui/preset_icon.js +++ b/modules/ui/preset_icon.js @@ -39,6 +39,22 @@ export function uiPresetIcon() { } + function renderCategoryBorder(enter) { + const w = 40; + const h = 40; + + enter + .append('svg') + .attr('class', 'preset-icon-fill preset-icon-category-border') + .attr('width', w) + .attr('height', h) + .attr('viewBox', `0 0 ${w} ${h}`) + .append('path') + .attr('transform', 'translate(4.5, 5)') + .attr('d', 'M2.40138782,0.75 L0.75,3.22708173 L0.75,24 C0.75,25.7949254 2.20507456,27.25 4,27.25 L27,27.25 C28.7949254,27.25 30.25,25.7949254 30.25,24 L30.25,7 C30.25,5.20507456 28.7949254,3.75 27,3.75 L13.5986122,3.75 L11.5986122,0.75 L2.40138782,0.75 Z'); + } + + function renderPointBorder(enter) { const w = 40; const h = 40; @@ -277,6 +293,17 @@ export function uiPresetIcon() { .classed('fallback', isFallback); + let categoryBorder = container.selectAll('.preset-icon-category-border') + .data(isCategory ? [0] : []); + + categoryBorder.exit() + .remove(); + + let categoryBorderEnter = categoryBorder.enter(); + renderCategoryBorder(categoryBorderEnter); + categoryBorder = categoryBorderEnter.merge(categoryBorder); + + let pointBorder = container.selectAll('.preset-icon-point-border') .data(drawPoint ? [0] : []); From 3305c13151d9167fd8f258e97c3408e482840dd6 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Mon, 6 Apr 2020 11:32:01 -0700 Subject: [PATCH 43/43] Fix issue where turn restriction editor wouldn't render --- modules/ui/fields/restrictions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index 8670540f94..296ff26014 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -63,7 +63,7 @@ export function uiFieldRestrictions(field, context) { // if form field is hidden or has detached from dom, clean up. if (!isOK || - !context.container().select('.sidebar-component.inspector-hidden').empty() || + !context.container().select('.entity-editor-pane.inspector-hidden').empty() || !selection.node().parentNode || !selection.node().parentNode.parentNode) { selection.call(restrictions.off);