diff --git a/css/80_app.css b/css/80_app.css index 74874394e..716c46fba 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -11,7 +11,7 @@ html, body { } body { - font: normal 12px/1.6667 -apple-system, BlinkMacSystemFont, + font: normal 12px/1.6667 "-apple-system", BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Arial", sans-serif; @@ -564,6 +564,7 @@ button.save.has-count .count::before { min-width: 768px; } + /* Header for modals / panes ------------------------------------------------------- */ @@ -2499,14 +2500,14 @@ img.tile-removing { bottom:0; border-radius: 0; pointer-events: none; + display: flex; + flex-direction: column; } #attrib { width: 100%; height: 20px; margin-bottom: 5px; - float: left; - clear: both; } #attrib * { pointer-events: all; } @@ -2531,24 +2532,80 @@ img.tile-removing { } .source-image { - height:20px; + height: 20px; vertical-align:top; } #footer { - width: 100%; - float: left; - clear: both; pointer-events: all; + display: block; + height: 30px; } + +#flash-wrap { + display: flex; + flex: 0 0 100%; + flex-flow: row nowrap; + justify-content: space-between; + max-height: 30px; + position: absolute; + right: 0; + left: 0; +} + +#flash-wrap .content { + display: flex; + flex: 1 0 auto; + flex-flow: row nowrap; + align-items: center; + padding: 2px; + height: 30px; +} + + +#flash-wrap svg.operation-icon { + flex: 0 0 auto; + width: 20px; + height: 20px; + margin: 0 8px; +} + +#flash-wrap div.operation-tip { + flex: 1 1 auto; +} + +#footer-wrap { + display: flex; + flex: 0 0 100%; + flex-flow: row nowrap; + justify-content: space-between; + max-height: 30px; + position: absolute; + right: 0; + left: 0; +} + +.footer-show { + bottom: 0px; + transition: bottom 75ms linear; + -moz-transition: bottom 75ms linear; + -webkit-transition: bottom 75ms linear; +} + +.footer-hide { + bottom: -35px; + transition: bottom 75ms linear; + -moz-transition: bottom 75ms linear; + -webkit-transition: bottom 75ms linear; +} + + #scale-block { - display: table-cell; vertical-align: bottom; width: 250px; max-height: 30px; - float: left; - clear: left; + flex: 0 0 250px; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; @@ -2557,13 +2614,16 @@ img.tile-removing { #info-block { max-height: 30px; - clear: right; + flex: 1 1 auto; } #scale { height: 30px; width: 100%; } +[dir='rtl'] #scale { + transform: scaleX(-1); +} #scale:hover { cursor: pointer; @@ -2575,6 +2635,9 @@ img.tile-removing { fill: #ccc; text-anchor: start; } +[dir='rtl'] #scale text { + transform: scaleX(-1); +} #scale path { fill: none; @@ -2583,12 +2646,19 @@ img.tile-removing { shape-rendering: crispEdges; } + #about-list { text-align: right; margin-right: 10px; clear: right; overflow: hidden; } +[dir='rtl'] #about-list { + text-align: left; + clear: left; + margin-left: 10px; + margin-right: 0; +} #about-list li { float: right; @@ -2596,12 +2666,24 @@ img.tile-removing { padding: 5px 0 5px 5px; margin-left: 5px; } +[dir='rtl'] #about-list li { + float: left; + border-left: none; + border-right: 1px solid rgba(255,255,255,.5); + margin-left: 0; + margin-right: 5px; + padding: 5px 5px 5px 0; +} + #about-list li:last-child { border-left: 0; margin-left: 0; padding-left: 0; } +[dir='rtl'] #about-list li:last-child { + border-right: none; +} .source-switch a { padding: 2px 4px 4px 4px; @@ -2624,12 +2706,13 @@ img.tile-removing { } .api-status { - float: right; - clear: both; text-align: right; - width: 100%; padding: 0px 10px; color: #eee; + flex: 1 1 auto; +} +[dir='rtl'] .api-status { + text-align: left; } .api-status.offline, @@ -2645,6 +2728,7 @@ img.tile-removing { color: #ccf; } + /* Modals ------------------------------------------------------- */ @@ -3095,15 +3179,25 @@ img.tile-removing { border-width: 0 5px 5px; } +.tooltip-heading { + font-weight: bold; + background: #F6F6F6; + padding: 10px; + margin: -10px -10px 10px -10px; + border-radius: 3px 3px 0 0; + font-size: 14px; +} + .keyhint-wrap { background: #F6F6F6; padding: 10px; - margin: 10px -10px -10px; + margin: 10px -10px -10px -10px; border-radius: 0 0 3px 3px; } .tooltip-inner .keyhint { font-weight: bold; + margin-left: 5px; } /* Exceptions for tooltip layouts */ @@ -3134,6 +3228,7 @@ img.tile-removing { } .map-overlay .tooltip-inner, +.map-overlay .tooltip-heading, .map-overlay .keyhint-wrap, .entity-editor-pane .tooltip-inner, .warning-section .tooltip-inner { @@ -3159,6 +3254,8 @@ img.tile-removing { left: 60px; } +/* radial menu (deprecated) */ + .radial-menu-tooltip { opacity: 0.8; display: none; @@ -3196,6 +3293,46 @@ img.tile-removing { color: rgba(40,40,40,.5); } +/* edit menu */ + +.edit-menu-tooltip { + display: none; + position: absolute; + width: 200px; +} + +.edit-menu-background { + fill: #eee; +} + +.edit-menu-item rect { + fill: #eee; +} + +.edit-menu-item rect:active, +.edit-menu-item rect:hover { + fill: #ccc; +} + +.edit-menu-item.disabled rect { + cursor: not-allowed; +} +.edit-menu-item.disabled rect:hover { + cursor: not-allowed; + fill: #eee; +} + +.edit-menu-item use { + fill: #222; + color: #79f; +} + +.edit-menu-item.disabled use { + fill: rgba(32,32,32,.2); + color: rgba(40,40,40,.2); +} + + .lasso-path { fill-opacity:0.3; stroke: #fff; @@ -3677,39 +3814,6 @@ img.tile-removing { -ms-filter: "FlipH"; } -/* footer */ -[dir='rtl'] #scale-block { - float: right; - clear: right; -} - -[dir='rtl'] #info-block { - clear: left; -} - -[dir='rtl'] #about-list { - text-align: left; - clear: left; - margin-left: 10px; - margin-right: 0; -} - -[dir='rtl'] #about-list li { - float: left; - border-left: none; - border-right: 1px solid rgba(255,255,255,.5); - margin-left: 0; - margin-right: 5px; - padding: 5px 5px 5px 0; -} - -[dir='rtl'] #about-list li:last-child { - border-right: none; -} - -[dir='rtl'] #scale text { - text-anchor: end; -} /* increment / decrement control - code by Naoufel Razouane */ diff --git a/data/core.yaml b/data/core.yaml index 0a85dbbb6..2de50cab3 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -146,7 +146,9 @@ en: single: This feature can't be moved because it is connected to a hidden feature. multiple: These features can't be moved because some are connected to hidden features. reflect: - title: reflect + title: + long: Reflect Long + short: Reflect Short description: long: single: Reflect this feature across its long axis. @@ -159,10 +161,10 @@ en: short: Y annotation: long: - single: Reflected an feature across its long axis. + single: Reflected a feature across its long axis. multiple: Reflected multiple features across their long axis. short: - single: Reflected an feature across its short axis. + single: Reflected a feature across its short axis. multiple: Reflected multiple features across their short axis. incomplete_relation: single: This feature can't be reflected because it hasn't been fully downloaded. @@ -812,8 +814,8 @@ en: close: "The feature editor will remember all of your changes automatically. When you change a feature, the close button will change to a checkmark. **Click the {button} button to close the feature editor**" reselect: "Often points will already exist, but have mistakes or be incomplete. We can edit existing points. **Click to select the point you just created.**" fixname: "**Change the name, then click the {button} button to close the feature editor.**" - reselect_delete: "All features on the map can be deleted. **Click to select the point you created.**" - delete: "The menu around the point contains operations that can be performed on it, including delete. **Click on the {button} button to delete the point.**" + rightclick: "You can right-click on features to see the list of operations that can be performed on them. **Right-click to select the point you created.**" + delete: "**Click on the {button} button to delete the point.**" areas: title: "Areas" add: "Areas are used to show the boundaries of features like lakes, buildings, and residential areas. They can be also be used for more detailed mapping of many features you might normally map as points. **Click the {button} Area button to add a new area.**" diff --git a/dist/locales/en.json b/dist/locales/en.json index 78b4a980e..ab7d6df1e 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -188,7 +188,10 @@ } }, "reflect": { - "title": "reflect", + "title": { + "long": "Reflect Long", + "short": "Reflect Short" + }, "description": { "long": { "single": "Reflect this feature across its long axis.", @@ -205,11 +208,11 @@ }, "annotation": { "long": { - "single": "Reflected an feature across its long axis.", + "single": "Reflected a feature across its long axis.", "multiple": "Reflected multiple features across their long axis." }, "short": { - "single": "Reflected an feature across its short axis.", + "single": "Reflected a feature across its short axis.", "multiple": "Reflected multiple features across their short axis." } }, @@ -666,8 +669,8 @@ "close": "The feature editor will remember all of your changes automatically. When you change a feature, the close button will change to a checkmark. **Click the {button} button to close the feature editor**", "reselect": "Often points will already exist, but have mistakes or be incomplete. We can edit existing points. **Click to select the point you just created.**", "fixname": "**Change the name, then click the {button} button to close the feature editor.**", - "reselect_delete": "All features on the map can be deleted. **Click to select the point you created.**", - "delete": "The menu around the point contains operations that can be performed on it, including delete. **Click on the {button} button to delete the point.**" + "rightclick": "You can right-click on features to see the list of operations that can be performed on them. **Right-click to select the point you created.**", + "delete": "**Click on the {button} button to delete the point.**" }, "areas": { "title": "Areas", diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 4e969058a..f8b2c1414 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -246,9 +246,7 @@ export function behaviorDrawWay(context, wayId, index, mode, baseGraph) { }, 1000); if (context.hasEntity(wayId)) { - context.enter( - modeSelect(context, [wayId]).suppressMenu(true).newFeature(true) - ); + context.enter(modeSelect(context, [wayId]).newFeature(true)); } else { context.enter(modeBrowse(context)); } diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 0e2741dd8..3c224656b 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -1,5 +1,6 @@ import * as d3 from 'd3'; import { d3keybinding } from '../lib/d3.keybinding.js'; +import { uiFlash } from '../ui'; /* Creates a keybinding behavior for an operation */ @@ -7,12 +8,53 @@ export function behaviorOperation(context) { var which, keybinding; + function drawIcon(selection) { + var button = selection + .append('svg') + .attr('class', 'operation-icon') + .append('g') + .attr('class', 'radial-menu-item radial-menu-item-' + which.id) + .attr('transform', 'translate(10,10)') + .classed('disabled', which.disabled()); + + button + .append('circle') + .attr('r', 9); + + button + .append('use') + .attr('transform', 'translate(-7,-7)') + .attr('width', '14') + .attr('height', '14') + .attr('xlink:href', '#operation-' + which.id); + + return selection; + } + + var behavior = function () { - if (which) { + if (which && which.available() && !context.inIntro()) { keybinding = d3keybinding('behavior.key.' + which.id); keybinding.on(which.keys, function() { d3.event.preventDefault(); - if (which.available() && !which.disabled() && !context.inIntro()) { + var disabled = which.disabled(); + + if (disabled) { + uiFlash(3000) + .html('') + .call(drawIcon) + .append('div') + .attr('class', 'operation-tip') + .text(which.tooltip); + + } else { + uiFlash(1500) + .html('') + .call(drawIcon) + .append('div') + .attr('class', 'operation-tip') + .text(which.annotation() || which.title); + which(); } }); diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 9c627701d..59273d144 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -1,10 +1,15 @@ import * as d3 from 'd3'; import _ from 'lodash'; -import { modeBrowse, modeSelect } from '../modes/index'; -import { osmEntity } from '../osm/index'; +import { geoEuclideanDistance } from '../geo'; +import { modeBrowse, modeSelect } from '../modes'; +import { osmEntity } from '../osm'; export function behaviorSelect(context) { + var suppressMenu = true, + tolerance = 4, + p1 = null; + function keydown() { if (d3.event && d3.event.shiftKey) { @@ -22,31 +27,96 @@ export function behaviorSelect(context) { } + function point() { + return d3.mouse(context.container().node()); + } + + + function contextmenu() { + if (!p1) p1 = point(); + d3.event.preventDefault(); + suppressMenu = false; + click(); + } + + + function mousedown() { + if (!p1) p1 = point(); + d3.select(window) + .on('mouseup.select', mouseup, true); + + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + suppressMenu = !isShowAlways; + } + + + function mouseup() { + click(); + } + + function click() { - var datum = d3.event.target.__data__, - lasso = d3.select('#surface .lasso').node(), + d3.select(window) + .on('mouseup.select', null, true); + + if (!p1) return; + var p2 = point(), + dist = geoEuclideanDistance(p1, p2); + + p1 = null; + if (dist > tolerance) { + return; + } + + var isMultiselect = d3.event.shiftKey || d3.select('#surface .lasso').node(), + isShowAlways = +context.storage('edit-menu-show-always') === 1, + datum = d3.event.target.__data__, mode = context.mode(); - if (datum.type === 'midpoint') { - // do nothing - } else if (!(datum instanceof osmEntity)) { - if (!d3.event.shiftKey && !lasso && mode.id !== 'browse') - context.enter(modeBrowse(context)); - } else if (!d3.event.shiftKey && !lasso) { - // Avoid re-entering Select mode with same entity. - if (context.selectedIDs().length !== 1 || context.selectedIDs()[0] !== datum.id) { - context.enter(modeSelect(context, [datum.id])); - } else { - mode.suppressMenu(false).reselect(); + if (datum.type === 'midpoint') { + // clicked midpoint, do nothing.. + + } else if (!(datum instanceof osmEntity)) { + // clicked nothing.. + if (!isMultiselect && mode.id !== 'browse') { + context.enter(modeBrowse(context)); } - } else if (context.selectedIDs().indexOf(datum.id) >= 0) { - var selectedIDs = _.without(context.selectedIDs(), datum.id); - context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); } else { - context.enter(modeSelect(context, context.selectedIDs().concat([datum.id]))); + // clicked an entity.. + var selectedIDs = context.selectedIDs(); + + if (!isMultiselect) { + if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) { + // multiple things already selected, just show the menu... + mode.suppressMenu(false).reselect(); + } else { + // select a single thing.. + context.enter(modeSelect(context, [datum.id]).suppressMenu(suppressMenu)); + } + + } else { + if (selectedIDs.indexOf(datum.id) !== -1) { + // clicked entity is already in the selectedIDs list.. + if (!suppressMenu && !isShowAlways) { + // don't deselect clicked entity, just show the menu. + mode.suppressMenu(false).reselect(); + } else { + // deselect clicked entity, then reenter select mode or return to browse mode.. + selectedIDs = _.without(selectedIDs, datum.id); + context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); + } + } else { + // clicked entity is not in the selected list, add it.. + selectedIDs = selectedIDs.concat([datum.id]); + context.enter(modeSelect(context, selectedIDs).suppressMenu(suppressMenu)); + } + } } + + // reset for next time.. + suppressMenu = true; } @@ -55,7 +125,9 @@ export function behaviorSelect(context) { .on('keydown.select', keydown) .on('keyup.select', keyup); - selection.on('click.select', click); + selection + .on('mousedown.select', mousedown) + .on('contextmenu.select', contextmenu); keydown(); }; @@ -64,9 +136,12 @@ export function behaviorSelect(context) { behavior.off = function(selection) { d3.select(window) .on('keydown.select', null) - .on('keyup.select', null); + .on('keyup.select', null) + .on('mouseup.select', null, true); - selection.on('click.select', null); + selection + .on('mousedown.select', null) + .on('contextmenu.select', null); keyup(); }; diff --git a/modules/modes/add_point.js b/modules/modes/add_point.js index 90d4046f2..1ea931527 100644 --- a/modules/modes/add_point.js +++ b/modules/modes/add_point.js @@ -32,7 +32,7 @@ export function modeAddPoint(context) { ); context.enter( - modeSelect(context, [node.id]).suppressMenu(true).newFeature(true) + modeSelect(context, [node.id]).newFeature(true) ); } diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 39f528942..782427218 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -222,7 +222,7 @@ export function modeDragNode(context) { }); if (reselection.length) { - context.enter(modeSelect(context, reselection).suppressMenu(true)); + context.enter(modeSelect(context, reselection)); } else { context.enter(modeBrowse(context)); } diff --git a/modules/modes/move.js b/modules/modes/move.js index 7ec0c3542..114063521 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -115,7 +115,7 @@ export function modeMove(context, entityIDs, baseGraph) { function finish() { d3.event.stopPropagation(); - context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + context.enter(modeSelect(context, entityIDs)); stopNudge(); } @@ -126,7 +126,7 @@ export function modeMove(context, entityIDs, baseGraph) { context.enter(modeBrowse(context)); } else { context.pop(); - context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + context.enter(modeSelect(context, entityIDs)); } stopNudge(); } diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index 51a8fff57..e1b3d6a4f 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -92,13 +92,13 @@ export function modeRotate(context, entityIDs) { function finish() { d3.event.stopPropagation(); - context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + context.enter(modeSelect(context, entityIDs)); } function cancel() { context.pop(); - context.enter(modeSelect(context, entityIDs).suppressMenu(true)); + context.enter(modeSelect(context, entityIDs)); } diff --git a/modules/modes/select.js b/modules/modes/select.js index 27032262e..f36f906fa 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -28,9 +28,12 @@ import { import { modeBrowse } from './browse'; import { modeDragNode } from './drag_node'; import * as Operations from '../operations/index'; -import { uiRadialMenu, uiSelectionList } from '../ui/index'; +import { uiEditMenu, uiSelectionList } from '../ui'; import { uiCmd } from '../ui/cmd'; -import { utilEntityOrMemberSelector, utilEntitySelector } from '../util/index'; +import { utilEntityOrMemberSelector, utilEntitySelector } from '../util'; + +// deprecation warning - Radial Menu to be removed in iD v3 +import { uiRadialMenu } from '../ui'; var relatedParent; @@ -54,9 +57,9 @@ export function modeSelect(context, selectedIDs) { modeDragNode(context).selectedIDs(selectedIDs).behavior ], inspector, - radialMenu, + editMenu, newFeature = false, - suppressMenu = false, + suppressMenu = true, follow = false; @@ -139,27 +142,24 @@ export function modeSelect(context, selectedIDs) { function closeMenu() { - if (radialMenu) { - context.surface().call(radialMenu.close); + if (editMenu) { + context.surface().call(editMenu.close); } } function positionMenu() { - if (suppressMenu || !radialMenu) { return; } + if (suppressMenu || !editMenu) { return; } var entity = singular(); if (entity && context.geometry(entity.id) === 'relation') { suppressMenu = true; - } else if (entity && entity.type === 'node') { - radialMenu.center(context.projection(entity.loc)); } else { var point = context.mouse(), viewport = geoExtent(context.projection.clipExtent()).polygon(); + if (geoPointInPolygon(point, viewport)) { - radialMenu.center(point); - } else { - suppressMenu = true; + editMenu.center(point); } } } @@ -167,14 +167,15 @@ export function modeSelect(context, selectedIDs) { function showMenu() { closeMenu(); - if (!suppressMenu && radialMenu) { - context.surface().call(radialMenu); + if (editMenu) { + context.surface().call(editMenu); } } function toggleMenu() { - if (d3.select('.radial-menu').empty()) { + // deprecation warning - Radial Menu to be removed in iD v3 + if (d3.select('.edit-menu, .radial-menu').empty()) { showMenu(); } else { closeMenu(); @@ -196,7 +197,9 @@ export function modeSelect(context, selectedIDs) { } positionMenu(); - showMenu(); + if (!suppressMenu) { + showMenu(); + } }; @@ -245,6 +248,7 @@ export function modeSelect(context, selectedIDs) { d3.event.preventDefault(); d3.event.stopPropagation(); + } else if (datum.type === 'midpoint') { context.perform( actionAddMidpoint({loc: datum.loc, edge: datum.edge}, osmNode()), @@ -306,7 +310,7 @@ export function modeSelect(context, selectedIDs) { if (parent) { var way = context.entity(parent); context.enter( - modeSelect(context, [way.first()]).follow(true).suppressMenu(true) + modeSelect(context, [way.first()]).follow(true) ); } } @@ -318,7 +322,7 @@ export function modeSelect(context, selectedIDs) { if (parent) { var way = context.entity(parent); context.enter( - modeSelect(context, [way.last()]).follow(true).suppressMenu(true) + modeSelect(context, [way.last()]).follow(true) ); } } @@ -342,7 +346,7 @@ export function modeSelect(context, selectedIDs) { if (index !== -1) { context.enter( - modeSelect(context, [way.nodes[index]]).follow(true).suppressMenu(true) + modeSelect(context, [way.nodes[index]]).follow(true) ); } } @@ -366,7 +370,7 @@ export function modeSelect(context, selectedIDs) { if (index !== -1) { context.enter( - modeSelect(context, [way.nodes[index]]).follow(true).suppressMenu(true) + modeSelect(context, [way.nodes[index]]).follow(true) ); } } @@ -401,7 +405,14 @@ export function modeSelect(context, selectedIDs) { .map(function(o) { return o(selectedIDs, context); }) .filter(function(o) { return o.available(); }); - operations.unshift(Operations.operationDelete(selectedIDs, context)); + // deprecation warning - Radial Menu to be removed in iD v3 + var isRadialMenu = context.storage('edit-menu-style') === 'radial'; + if (isRadialMenu) { + operations = operations.slice(0,7); + operations.unshift(Operations.operationDelete(selectedIDs, context)); + } else { + operations.push(Operations.operationDelete(selectedIDs, context)); + } operations.forEach(function(operation) { if (operation.behavior) { @@ -425,7 +436,11 @@ export function modeSelect(context, selectedIDs) { d3.select(document) .call(keybinding); - radialMenu = uiRadialMenu(context, operations); + + // deprecation warning - Radial Menu to be removed in iD v3 + editMenu = isRadialMenu + ? uiRadialMenu(context, operations) + : uiEditMenu(context, operations); context.ui().sidebar .select(singular() ? singular().id : null, newFeature); @@ -438,12 +453,15 @@ export function modeSelect(context, selectedIDs) { .on('move.select', closeMenu) .on('drawn.select', selectElements); + context.surface() + .on('dblclick.select', dblclick); + + selectElements(); - var show = d3.event && !suppressMenu; - - if (show) { - positionMenu(); + if (selectedIDs.length > 1) { + var entities = uiSelectionList(context, selectedIDs); + context.ui().sidebar.show(entities); } if (follow) { @@ -459,18 +477,12 @@ export function modeSelect(context, selectedIDs) { } timeout = window.setTimeout(function() { - if (show) { + positionMenu(); + if (!suppressMenu) { showMenu(); } + }, 270); /* after any centerEase completes */ - context.surface() - .on('dblclick.select', dblclick); - }, 200); - - if (selectedIDs.length > 1) { - var entities = uiSelectionList(context, selectedIDs); - context.ui().sidebar.show(entities); - } }; @@ -485,7 +497,7 @@ export function modeSelect(context, selectedIDs) { keybinding.off(); closeMenu(); - radialMenu = undefined; + editMenu = undefined; context.history() .on('undone.select', null) diff --git a/modules/operations/circularize.js b/modules/operations/circularize.js index 8aebef11e..a7b713bb0 100644 --- a/modules/operations/circularize.js +++ b/modules/operations/circularize.js @@ -13,7 +13,7 @@ export function operationCircularize(selectedIDs, context) { var operation = function() { - context.perform(action, t('operations.circularize.annotation.' + geometry)); + context.perform(action, operation.annotation()); }; @@ -43,6 +43,11 @@ export function operationCircularize(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.circularize.annotation.' + geometry); + }; + + operation.id = 'circularize'; operation.keys = [t('operations.circularize.key')]; operation.title = t('operations.circularize.title'); diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 5d24f463a..77e45905c 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -53,6 +53,11 @@ export function operationContinue(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.continue.annotation.line'); + }; + + operation.id = 'continue'; operation.keys = [t('operations.continue.key')]; operation.title = t('operations.continue.title'); diff --git a/modules/operations/delete.js b/modules/operations/delete.js index ead998bb2..6132f3685 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -13,21 +13,15 @@ export function operationDelete(selectedIDs, context) { var operation = function() { - var annotation, - nextSelectedID; + var nextSelectedID; - if (selectedIDs.length > 1) { - annotation = t('operations.delete.annotation.multiple', { n: selectedIDs.length }); - - } else { + if (selectedIDs.length === 1) { var id = selectedIDs[0], entity = context.entity(id), geometry = context.geometry(id), parents = context.graph().parentWays(entity), parent = parents[0]; - annotation = t('operations.delete.annotation.' + geometry); - // Select the next closest node in the way. if (geometry === 'vertex' && parent.nodes.length > 2) { var nodes = parent.nodes, @@ -47,12 +41,10 @@ export function operationDelete(selectedIDs, context) { } } - context.perform(action, annotation); + context.perform(action, operation.annotation()); if (nextSelectedID && context.hasEntity(nextSelectedID)) { - context.enter( - modeSelect(context, [nextSelectedID]).follow(true).suppressMenu(true) - ); + context.enter(modeSelect(context, [nextSelectedID]).follow(true)); } else { context.enter(modeBrowse(context)); } @@ -108,6 +100,13 @@ export function operationDelete(selectedIDs, context) { }; + operation.annotation = function() { + return selectedIDs.length === 1 ? + t('operations.delete.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.delete.annotation.multiple', { n: selectedIDs.length }); + }; + + operation.id = 'delete'; operation.keys = [uiCmd('⌘⌫'), uiCmd('⌘⌦'), uiCmd('⌦')]; operation.title = t('operations.delete.title'); diff --git a/modules/operations/disconnect.js b/modules/operations/disconnect.js index 420162ecd..2649f0e6f 100644 --- a/modules/operations/disconnect.js +++ b/modules/operations/disconnect.js @@ -18,7 +18,7 @@ export function operationDisconnect(selectedIDs, context) { var operation = function() { - context.perform(action, t('operations.disconnect.annotation')); + context.perform(action, operation.annotation()); }; @@ -44,6 +44,11 @@ export function operationDisconnect(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.disconnect.annotation'); + }; + + operation.id = 'disconnect'; operation.keys = [t('operations.disconnect.key')]; operation.title = t('operations.disconnect.title'); diff --git a/modules/operations/merge.js b/modules/operations/merge.js index c55e3c350..afba24c72 100644 --- a/modules/operations/merge.js +++ b/modules/operations/merge.js @@ -15,8 +15,7 @@ export function operationMerge(selectedIDs, context) { mergePolygon = actionMergePolygon(selectedIDs); var operation = function() { - var annotation = t('operations.merge.annotation', {n: selectedIDs.length}), - action; + var action; if (!join.disabled(context.graph())) { action = join; @@ -26,12 +25,12 @@ export function operationMerge(selectedIDs, context) { action = mergePolygon; } - context.perform(action, annotation); + context.perform(action, operation.annotation()); var ids = selectedIDs.filter(function(id) { var entity = context.hasEntity(id); return entity && entity.type !== 'node'; }); - context.enter(modeSelect(context, ids).suppressMenu(true)); + context.enter(modeSelect(context, ids)); }; @@ -69,6 +68,11 @@ export function operationMerge(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.merge.annotation', { n: selectedIDs.length }); + }; + + operation.id = 'merge'; operation.keys = [t('operations.merge.key')]; operation.title = t('operations.merge.title'); diff --git a/modules/operations/move.js b/modules/operations/move.js index 7c9a9277a..1205562a0 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -49,6 +49,13 @@ export function operationMove(selectedIDs, context) { }; + operation.annotation = function() { + return selectedIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.move.annotation.multiple'); + }; + + operation.id = 'move'; operation.keys = [t('operations.move.key')]; operation.title = t('operations.move.title'); diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index 8275e4f7a..50f1f685e 100644 --- a/modules/operations/orthogonalize.js +++ b/modules/operations/orthogonalize.js @@ -13,7 +13,7 @@ export function operationOrthogonalize(selectedIDs, context) { var operation = function() { - context.perform(action, t('operations.orthogonalize.annotation.' + geometry)); + context.perform(action, operation.annotation()); }; @@ -44,6 +44,11 @@ export function operationOrthogonalize(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.orthogonalize.annotation.' + geometry); + }; + + operation.id = 'orthogonalize'; operation.keys = [t('operations.orthogonalize.key')]; operation.title = t('operations.orthogonalize.title'); diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 9a3ffad97..2457e20b3 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -26,7 +26,7 @@ export function operationReflect(selectedIDs, context, axis) { var operation = function() { var action = actionReflect(selectedIDs, context.projection) .useLongAxis(Boolean(axis === 'long')); - context.perform(action, t('operations.reflect.annotation.' + axis + '.' + multi)); + context.perform(action, operation.annotation()); }; @@ -67,9 +67,14 @@ export function operationReflect(selectedIDs, context, axis) { }; + operation.annotation = function() { + return t('operations.reflect.annotation.' + axis + '.' + multi); + }; + + operation.id = 'reflect-' + axis; operation.keys = [t('operations.reflect.key.' + axis)]; - operation.title = t('operations.reflect.title'); + operation.title = t('operations.reflect.title.' + axis); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/reverse.js b/modules/operations/reverse.js index 2e0d39b40..3320c6902 100644 --- a/modules/operations/reverse.js +++ b/modules/operations/reverse.js @@ -7,7 +7,7 @@ export function operationReverse(selectedIDs, context) { var entityId = selectedIDs[0]; var operation = function() { - context.perform(actionReverse(entityId), t('operations.reverse.annotation')); + context.perform(actionReverse(entityId), operation.annotation()); }; @@ -26,6 +26,11 @@ export function operationReverse(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.reverse.annotation'); + }; + + operation.id = 'reverse'; operation.keys = [t('operations.reverse.key')]; operation.title = t('operations.reverse.title'); diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index aa7359f09..b0fe066f4 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -54,6 +54,13 @@ export function operationRotate(selectedIDs, context) { }; + operation.annotation = function() { + return selectedIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.rotate.annotation.multiple'); + }; + + operation.id = 'rotate'; operation.keys = [t('operations.rotate.key')]; operation.title = t('operations.rotate.title'); diff --git a/modules/operations/split.js b/modules/operations/split.js index cc81c90a3..0823d4879 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -11,24 +11,19 @@ export function operationSplit(selectedIDs, context) { }); var entityId = vertices[0], - action = actionSplit(entityId); + action = actionSplit(entityId), + ways = []; - if (selectedIDs.length > 1) { - action.limitWays(_.without(selectedIDs, entityId)); + if (vertices.length === 1) { + if (selectedIDs.length > 1) { + action.limitWays(_.without(selectedIDs, entityId)); + } + ways = action.ways(context.graph()); } var operation = function() { - var annotation; - - var ways = action.ways(context.graph()); - if (ways.length === 1) { - annotation = t('operations.split.annotation.' + context.geometry(ways[0].id)); - } else { - annotation = t('operations.split.annotation.multiple', {n: ways.length}); - } - - var difference = context.perform(action, annotation); + var difference = context.perform(action, operation.annotation()); context.enter(modeSelect(context, difference.extantIDs())); }; @@ -52,8 +47,6 @@ export function operationSplit(selectedIDs, context) { if (disable) { return t('operations.split.' + disable); } - - var ways = action.ways(context.graph()); if (ways.length === 1) { return t('operations.split.description.' + context.geometry(ways[0].id)); } else { @@ -62,6 +55,13 @@ export function operationSplit(selectedIDs, context) { }; + operation.annotation = function() { + return ways.length === 1 ? + t('operations.split.annotation.' + context.geometry(ways[0].id)) : + t('operations.split.annotation.multiple', { n: ways.length }); + }; + + operation.id = 'split'; operation.keys = [t('operations.split.key')]; operation.title = t('operations.split.title'); diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index 8a029caa0..5d4f8a835 100644 --- a/modules/operations/straighten.js +++ b/modules/operations/straighten.js @@ -10,7 +10,7 @@ export function operationStraighten(selectedIDs, context) { function operation() { - context.perform(action, t('operations.straighten.annotation')); + context.perform(action, operation.annotation()); } @@ -40,6 +40,11 @@ export function operationStraighten(selectedIDs, context) { }; + operation.annotation = function() { + return t('operations.straighten.annotation'); + }; + + operation.id = 'straighten'; operation.keys = [t('operations.straighten.key')]; operation.title = t('operations.straighten.title'); diff --git a/modules/renderer/map.js b/modules/renderer/map.js index ec10c9201..b51bdd475 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -36,6 +36,7 @@ export function rendererMap(context) { dblclickEnabled = true, redrawEnabled = true, transformStart = projection.transform(), + transformLast, transformed = false, minzoom = 0, drawLayers = svgLayers(projection, context), @@ -77,7 +78,7 @@ export function rendererMap(context) { if (Array.isArray(stack.selectedIDs)) { followSelected = (stack.selectedIDs.length === 1 && stack.selectedIDs[0][0] === 'n'); context.enter( - modeSelect(context, stack.selectedIDs).suppressMenu(true).follow(followSelected) + modeSelect(context, stack.selectedIDs).follow(followSelected) ); } if (!followSelected && stack.transform) { @@ -275,7 +276,9 @@ export function rendererMap(context) { function zoomPan(manualEvent) { - var eventTransform = (manualEvent || d3.event).transform; + var event = (manualEvent || d3.event), + source = event.sourceEvent, + eventTransform = event.transform; if (transformStart.x === eventTransform.x && transformStart.y === eventTransform.y && @@ -283,11 +286,31 @@ export function rendererMap(context) { return; // no change } + // Normalize mousewheel - #3029 + // If wheel delta is provided in LINE units, recalculate it in PIXEL units + // We are essentially redoing the calculations that occur here: + // https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203 + // See this for more info: + // https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js + if (source && source.type === 'wheel' && source.deltaMode === 1 /* LINE */) { + // pick sensible scroll amount if user scrolling fast or slow.. + var lines = Math.abs(source.deltaY), + scroll = lines > 2 ? 40 : lines * 10; + + var t0 = transformed ? transformLast : transformStart, + p0 = mouse(source), + p1 = t0.invert(p0), + k2 = t0.k * Math.pow(2, -source.deltaY * scroll / 500), + x2 = p0[0] - p1[0] * k2, + y2 = p0[1] - p1[1] * k2; + + eventTransform = d3.zoomIdentity.translate(x2,y2).scale(k2); + _selection.node().__zoom = eventTransform; + } + if (ktoz(eventTransform.k * 2 * Math.PI) < minzoom) { surface.interrupt(); - uiFlash(context.container()) - .select('.content') - .text(t('cannot_zoom')); + uiFlash().text(t('cannot_zoom')); setZoom(context.minEditableZoom(), true); queueRedraw(); dispatch.call('move', this, map); @@ -301,6 +324,7 @@ export function rendererMap(context) { tY = (eventTransform.y / scale - transformStart.y) * scale; transformed = true; + transformLast = eventTransform; utilSetTransform(supersurface, tX, tY, scale); queueRedraw(); @@ -311,7 +335,8 @@ export function rendererMap(context) { function resetTransform() { if (!transformed) return false; - surface.selectAll('.radial-menu').interrupt().remove(); + // deprecation warning - Radial Menu to be removed in iD v3 + surface.selectAll('.edit-menu, .radial-menu').interrupt().remove(); utilSetTransform(supersurface, 0, 0); transformed = false; return true; @@ -569,9 +594,7 @@ export function rendererMap(context) { if (z2 < minzoom) { surface.interrupt(); - uiFlash(context.container()) - .select('.content') - .text(t('cannot_zoom')); + uiFlash().text(t('cannot_zoom')); z2 = context.minEditableZoom(); } diff --git a/modules/ui/commit.js b/modules/ui/commit.js index 998b10d7f..210ff8eea 100644 --- a/modules/ui/commit.js +++ b/modules/ui/commit.js @@ -274,9 +274,7 @@ export function uiCommit(context) { function warningClick(d) { if (d.entity) { context.map().zoomTo(d.entity); - context.enter( - modeSelect(context, [d.entity.id]).suppressMenu(true) - ); + context.enter(modeSelect(context, [d.entity.id])); } } @@ -286,8 +284,7 @@ export function uiCommit(context) { if (change.changeType !== 'deleted' && context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') { context.map().zoomTo(entity); - context.surface().selectAll( - utilEntityOrMemberSelector([entity.id], context.graph())) + context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph())) .classed('hover', true); } } diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js new file mode 100644 index 000000000..490c4bf69 --- /dev/null +++ b/modules/ui/edit_menu.js @@ -0,0 +1,180 @@ +import * as d3 from 'd3'; +import { geoRoundCoords } from '../geo/index'; +import { textDirection } from '../util/locale'; +import { uiTooltipHtml } from './tooltipHtml'; + + +export function uiEditMenu(context, operations) { + var menu, + center = [0, 0], + offset = [0, 0], + tooltip; + + var p = 8, // top padding + m = 4, // top margin + h = 15, // height of icon + vpBottomMargin = 45, // viewport bottom margin + vpSideMargin = 35, // viewport side margin + buttonWidth = 44, + buttonHeight = (2 * p + h), + menuWidth = buttonWidth, + menuHeight = (2 * m) + operations.length * buttonHeight, + menuSideMargin = 10, + tooltipWidth = 200, + tooltipHeight = 200; // a reasonable guess, real height depends on tooltip contents + + + var editMenu = function (selection) { + if (!operations.length) return; + + selection.node().parentNode.focus(); + + var isRTL = textDirection === 'rtl', + viewport = context.surfaceRect(); + + if (!isRTL && (center[0] + menuSideMargin + menuWidth) > (viewport.width - vpSideMargin)) { + // menu is going left-to-right and near right viewport edge, go left instead + isRTL = true; + } else if (isRTL && (center[0] - menuSideMargin - menuWidth) < vpSideMargin) { + // menu is going right-to-left and near left viewport edge, go right instead + isRTL = false; + } + + offset[0] = (isRTL ? -1 * (menuSideMargin + menuWidth) : menuSideMargin); + + if (center[1] + menuHeight > (viewport.height - vpBottomMargin)) { + // menu is near bottom viewport edge, shift upwards + offset[1] = -1 * (center[1] + menuHeight - viewport.height + vpBottomMargin); + } + + var origin = [ center[0] + offset[0], center[1] + offset[1] ]; + + menu = selection + .append('g') + .attr('class', 'edit-menu') + .attr('transform', 'translate(' + origin + ')') + .attr('opacity', 0); + + menu + .transition() + .attr('opacity', 1); + + menu + .append('rect') + .attr('class', 'edit-menu-background') + .attr('x', 4) + .attr('rx', 4) + .attr('ry', 4) + .attr('width', menuWidth) + .attr('height', menuHeight) + .attr('stroke-linecap', 'round'); + + + var button = menu.selectAll('.edit-menu-item') + .data(operations) + .enter() + .append('g') + .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; }) + .classed('disabled', function (d) { return d.disabled(); }) + .attr('transform', function (d, i) { + return 'translate(' + geoRoundCoords([ + 0, + m + i * buttonHeight + ]).join(',') + ')'; + }); + + button + .append('rect') + .attr('x', 4) + .attr('width', buttonWidth) + .attr('height', buttonHeight) + .on('click', click) + .on('mousedown', mousedown) + .on('mouseover', mouseover) + .on('mouseout', mouseout); + + button + .append('use') + .attr('width', '20') + .attr('height', '20') + .attr('transform', function () { + return 'translate(' + [2 * p, 5] + ')'; + }) + .attr('xlink:href', function (d) { return '#operation-' + d.id; }); + + tooltip = d3.select(document.body) + .append('div') + .attr('class', 'tooltip-inner edit-menu-tooltip'); + + + function click(operation) { + d3.event.stopPropagation(); + if (operation.disabled()) return; + operation(); + editMenu.close(); + } + + function mousedown() { + d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869 + } + + function mouseover(d, i) { + var tipX, tipY; + + if (!isRTL) { + tipX = viewport.left + origin[0] + menuSideMargin + menuWidth; + } else { + tipX = viewport.left + origin[0] - 4 - tooltipWidth; + } + + if (tipX + tooltipWidth > viewport.right) { + // tip is going left-to-right and near right viewport edge, go left instead + tipX = viewport.left + origin[0] - 4 - tooltipWidth; + } else if (tipX < viewport.left) { + // tip is going right-to-left and near left viewport edge, go right instead + tipX = viewport.left + origin[0] + menuSideMargin + menuWidth; + } + + tipY = viewport.top + origin[1] + (i * buttonHeight); + if (tipY + tooltipHeight > viewport.bottom) { + // tip is near bottom viewport edge, shift upwards + tipY -= tipY + tooltipHeight - viewport.bottom; + } + + tooltip + .style('left', tipX + 'px') + .style('top', tipY + 'px') + .style('display', 'block') + .html(uiTooltipHtml(d.tooltip(), d.keys[0], d.title)); + } + + function mouseout() { + tooltip.style('display', 'none'); + } + }; + + + editMenu.close = function () { + if (menu) { + menu + .style('pointer-events', 'none') + .transition() + .attr('opacity', 0) + .remove(); + } + + if (tooltip) { + tooltip.remove(); + } + }; + + + editMenu.center = function (_) { + if (!arguments.length) return center; + center = _; + return editMenu; + }; + + + return editMenu; +} diff --git a/modules/ui/feature_list.js b/modules/ui/feature_list.js index a72abd48d..f04d98e9b 100644 --- a/modules/ui/feature_list.js +++ b/modules/ui/feature_list.js @@ -272,7 +272,7 @@ export function uiFeatureList(context) { edge = geoChooseEdge(context.childNodes(d.entity), center, context.projection); context.map().center(edge.loc); } - context.enter(modeSelect(context, [d.entity.id]).suppressMenu(true)); + context.enter(modeSelect(context, [d.entity.id])); } else { context.zoomToEntity(d.id); } diff --git a/modules/ui/flash.js b/modules/ui/flash.js index 6abcf4a98..45dcc0a09 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -1,26 +1,35 @@ -import { uiModal } from './modal'; +import * as d3 from 'd3'; +var timer; -export function uiFlash(selection) { - var modalSelection = uiModal(selection); +export function uiFlash(showDuration) { + showDuration = showDuration || 1500; - modalSelection.select('.modal') - .classed('modal-flash', true); + if (timer) { + timer.stop(); + } - modalSelection.select('.content') - .classed('modal-section', true) + d3.select('#footer-wrap') + .attr('class', 'footer-hide'); + d3.select('#flash-wrap') + .attr('class', 'footer-show'); + + var content = d3.select('#flash-wrap').selectAll('.content') + .data([0]); + + content = content.enter() .append('div') - .attr('class', 'description'); + .attr('class', 'content') + .merge(content); - modalSelection.on('click.flash', function() { - modalSelection.remove(); - }); - - setTimeout(function() { - modalSelection.remove(); - return true; - }, 1500); + timer = d3.timeout(function() { + timer = null; + d3.select('#footer-wrap') + .attr('class', 'footer-show'); + d3.select('#flash-wrap') + .attr('class', 'footer-hide'); + }, showDuration); - return modalSelection; + return content; } diff --git a/modules/ui/index.js b/modules/ui/index.js index 37bfc22cc..1c8f12898 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -47,3 +47,5 @@ export { uiTooltipHtml } from './tooltipHtml'; export { uiUndoRedo } from './undo_redo'; export { uiViewOnOSM } from './view_on_osm'; export { uiZoom } from './zoom'; + +export { uiEditMenu } from './edit_menu'; diff --git a/modules/ui/init.js b/modules/ui/init.js index d9114614a..2171c9b18 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -116,6 +116,7 @@ export function uiInit(context) { .attr('class', 'spinner') .call(uiSpinner(context)); + var controls = bar .append('div') .attr('class', 'map-controls'); @@ -145,6 +146,7 @@ export function uiInit(context) { .attr('class', 'map-control help-control') .call(uiHelp(context)); + var about = content .append('div') .attr('id', 'about'); @@ -155,6 +157,12 @@ export function uiInit(context) { .attr('dir', 'ltr') .call(uiAttribution(context)); + about + .append('div') + .attr('class', 'api-status') + .call(uiStatus(context)); + + var footer = about .append('div') .attr('id', 'footer') @@ -162,15 +170,20 @@ export function uiInit(context) { footer .append('div') - .attr('class', 'api-status') - .call(uiStatus(context)); + .attr('id', 'flash-wrap') + .attr('class', 'footer-hide'); - footer + var footerWrap = footer + .append('div') + .attr('id', 'footer-wrap') + .attr('class', 'footer-show'); + + footerWrap .append('div') .attr('id', 'scale-block') .call(uiScale(context)); - var aboutList = footer + var aboutList = footerWrap .append('div') .attr('id', 'info-block') .append('ul') diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js index 14f357c11..54bf08c70 100644 --- a/modules/ui/intro/point.js +++ b/modules/ui/intro/point.js @@ -31,7 +31,7 @@ export function uiIntroPoint(context, reveal) { t('intro.points.add', { button: icon('#icon-point', 'pre-text') }), { tooltipClass: 'intro-points-add' }); - var corner = [-85.632481,41.944094]; + var corner = [-85.632481, 41.944094]; context.on('enter.intro', addPoint); @@ -125,11 +125,11 @@ export function uiIntroPoint(context, reveal) { context.on('enter.intro', enterDelete); var pointBox = pad(corner, 150, context); - reveal(pointBox, t('intro.points.reselect_delete')); + reveal(pointBox, t('intro.points.rightclick')); context.map().on('move.intro', function() { pointBox = pad(corner, 150, context); - reveal(pointBox, t('intro.points.reselect_delete'), {duration: 0}); + reveal(pointBox, t('intro.points.rightclick'), {duration: 0}); }); } @@ -143,10 +143,15 @@ export function uiIntroPoint(context, reveal) { context.history().on('change.intro', deleted); setTimeout(function() { - var node = d3.select('.radial-menu-item-delete').node(); - var pointBox = pad(node.getBoundingClientRect(), 50, context); - reveal(pointBox, - t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); + // deprecation warning - Radial Menu to be removed in iD v3 + var node = d3.select('.edit-menu-item-delete, .radial-menu-item-delete').node(); + if (!node) { + deletePoint(); + } else { + var pointBox = pad(node.getBoundingClientRect(), 50, context); + reveal(pointBox, + t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); + } }, 300); } diff --git a/modules/ui/raw_member_editor.js b/modules/ui/raw_member_editor.js index ec0ec2068..d2a4afdeb 100644 --- a/modules/ui/raw_member_editor.js +++ b/modules/ui/raw_member_editor.js @@ -21,7 +21,7 @@ export function uiRawMemberEditor(context) { function selectMember(d) { d3.event.preventDefault(); - context.enter(modeSelect(context, [d.id]).suppressMenu(true)); + context.enter(modeSelect(context, [d.id])); } diff --git a/modules/ui/raw_membership_editor.js b/modules/ui/raw_membership_editor.js index c02ad7d17..b5cfaa9b3 100644 --- a/modules/ui/raw_membership_editor.js +++ b/modules/ui/raw_membership_editor.js @@ -25,7 +25,7 @@ export function uiRawMembershipEditor(context) { function selectRelation(d) { d3.event.preventDefault(); - context.enter(modeSelect(context, [d.relation.id]).suppressMenu(true)); + context.enter(modeSelect(context, [d.relation.id])); } @@ -55,7 +55,7 @@ export function uiRawMembershipEditor(context) { t('operations.add.annotation.relation') ); - context.enter(modeSelect(context, [relation.id]).suppressMenu(true)); + context.enter(modeSelect(context, [relation.id])); } } diff --git a/modules/ui/scale.js b/modules/ui/scale.js index c3df02c77..3a470e888 100644 --- a/modules/ui/scale.js +++ b/modules/ui/scale.js @@ -63,12 +63,13 @@ export function uiScale(context) { loc2 = projection.invert([maxLength, dims[1]]), scale = scaleDefs(loc1, loc2); - selection.select('#scalepath') + selection.select('#scale-path') .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight); - selection.select('#scaletext') - .attr('x', scale.px + 8) - .attr('y', tickHeight) + selection.select('#scale-textgroup') + .attr('transform', 'translate(' + (scale.px + 8) + ',' + tickHeight + ')'); + + selection.select('#scale-text') .text(scale.text); } @@ -79,14 +80,21 @@ export function uiScale(context) { selection.call(update); } - var g = selection.append('svg') + var scalegroup = selection.append('svg') .attr('id', 'scale') .on('click', switchUnits) .append('g') .attr('transform', 'translate(10,11)'); - g.append('path').attr('id', 'scalepath'); - g.append('text').attr('id', 'scaletext'); + scalegroup + .append('path') + .attr('id', 'scale-path'); + + scalegroup + .append('g') + .attr('id', 'scale-textgroup') + .append('text') + .attr('id', 'scale-text'); selection.call(update); diff --git a/modules/ui/selection_list.js b/modules/ui/selection_list.js index aaede3035..71ce89cb5 100644 --- a/modules/ui/selection_list.js +++ b/modules/ui/selection_list.js @@ -9,7 +9,7 @@ import { utilDisplayName } from '../util/index'; export function uiSelectionList(context, selectedIDs) { function selectEntity(entity) { - context.enter(modeSelect(context, [entity.id]).suppressMenu(true)); + context.enter(modeSelect(context, [entity.id])); } @@ -19,7 +19,7 @@ export function uiSelectionList(context, selectedIDs) { if (index > -1) { selectedIDs.splice(index, 1); } - context.enter(modeSelect(context, selectedIDs).suppressMenu(true)); + context.enter(modeSelect(context, selectedIDs)); } diff --git a/modules/ui/tooltipHtml.js b/modules/ui/tooltipHtml.js index 9553f6f02..3d57e6e6e 100644 --- a/modules/ui/tooltipHtml.js +++ b/modules/ui/tooltipHtml.js @@ -1,11 +1,18 @@ import { t } from '../util/locale'; -export function uiTooltipHtml(text, key) { - var s = '' + text + ''; - if (key) { - s += '
' + - ' ' + (t('tooltip_keyhint')) + ' ' + - ' ' + key + '
'; +export function uiTooltipHtml(text, key, heading) { + var s = ''; + + if (heading) { + s += '
' + heading + '
'; } + if (text) { + s += '
' + text + '
'; + } + if (key) { + s += '
' + t('tooltip_keyhint') + '' + + '' + key + '
'; + } + return s; } diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js index 2c7e6e68a..fda5c8da3 100644 --- a/test/spec/behavior/hash.js +++ b/test/spec/behavior/hash.js @@ -43,14 +43,14 @@ describe('iD.behaviorHash', function () { location.hash = 'map=20.00/38.87952/-77.02405'; }); - it('stores the current zoom and coordinates in location.hash on map move events', function () { + it('stores the current zoom and coordinates in location.hash on map move events', function (done) { location.hash = ''; hash(); - var clock = sinon.useFakeTimers(); context.map().center([-77.0, 38.9]); context.map().zoom(2.0); - clock.tick(500); - expect(location.hash).to.equal('#map=2.00/38.9/-77.0'); - clock.restore(); + window.setTimeout(function() { + expect(location.hash).to.equal('#map=2.00/38.9/-77.0'); + done(); + }, 300); }); }); diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 5deddc548..bcd837040 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -44,37 +44,49 @@ describe('iD.behaviorSelect', function() { }); specify('click on entity selects the entity', function() { - happen.click(context.surface().selectAll('.' + a.id).node()); + var el = context.surface().selectAll('.' + a.id).node(); + happen.mousedown(el); + happen.mouseup(el); expect(context.selectedIDs()).to.eql([a.id]); }); specify('click on empty space clears the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().node()); + var el = context.surface().node(); + happen.mousedown(el); + happen.mouseup(el); expect(context.mode().id).to.eql('browse'); }); specify('shift-click on unselected entity adds it to the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().selectAll('.' + b.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + b.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id, b.id]); }); specify('shift-click on selected entity removes it from the selection', function() { context.enter(iD.modeSelect(context, [a.id, b.id])); - happen.click(context.surface().selectAll('.' + b.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + b.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id]); }); specify('shift-click on last selected entity clears the selection', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().selectAll('.' + a.id).node(), {shiftKey: true}); + var el = context.surface().selectAll('.' + a.id).node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.mode().id).to.eql('browse'); }); specify('shift-click on empty space leaves the selection unchanged', function() { context.enter(iD.modeSelect(context, [a.id])); - happen.click(context.surface().node(), {shiftKey: true}); + var el = context.surface().node(); + happen.mousedown(el, { shiftKey: true }); + happen.mouseup(el, { shiftKey: true }); expect(context.selectedIDs()).to.eql([a.id]); }); }); diff --git a/test/spec/ui/flash.js b/test/spec/ui/flash.js index d264d6315..ce5916d57 100644 --- a/test/spec/ui/flash.js +++ b/test/spec/ui/flash.js @@ -1,25 +1,40 @@ describe('iD.uiFlash', function () { - var clock; - - var elem; beforeEach(function() { - elem = d3.select('body').append('div'); - }); - - afterEach(function() { elem.remove(); }); - - beforeEach(function () { - clock = sinon.useFakeTimers(); + d3.select('body') + .append('div') + .attr('id', 'flash-wrap') + .append('div') + .attr('id', 'footer-wrap'); }); afterEach(function () { - clock.restore(); + d3.select('body > div').remove(); }); - it('leaves after 1000 ms', function () { - var flash = iD.uiFlash(elem); - clock.tick(1610); - expect(flash.node().parentNode).to.be.null; + it('returns a selection', function () { + var content = iD.uiFlash(200); + expect(content.size()).to.eql(1); + expect(content.classed('content')).to.be.ok; }); + + it('flash is shown', function () { + iD.uiFlash(200); + var flashWrap = d3.selectAll('#flash-wrap'); + var footerWrap = d3.selectAll('#footer-wrap'); + expect(flashWrap.classed('footer-show')).to.be.ok; + expect(footerWrap.classed('footer-hide')).to.be.ok; + }); + + it('flash goes away', function (done) { + iD.uiFlash(200); + window.setTimeout(function() { + var flashWrap = d3.selectAll('#flash-wrap'); + var footerWrap = d3.selectAll('#footer-wrap'); + expect(flashWrap.classed('footer-hide')).to.be.ok; + expect(footerWrap.classed('footer-show')).to.be.ok; + done(); + }, 500); + }); + }); diff --git a/test/spec/util/session_mutex.js b/test/spec/util/session_mutex.js index 811694e63..f3f03f486 100644 --- a/test/spec/util/session_mutex.js +++ b/test/spec/util/session_mutex.js @@ -1,12 +1,7 @@ describe('iD.utilSessionMutex', function() { - var clock, a, b; - - beforeEach(function () { - clock = sinon.useFakeTimers(Date.now()); - }); + var a, b; afterEach(function() { - clock.restore(); if (a) a.unlock(); if (b) b.unlock(); });