From 5b92b90ced6554555c3073915a98a895bc733042 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Tue, 7 Feb 2017 23:09:23 +0530 Subject: [PATCH 01/28] Add context menu --- css/app.css | 4 +- modules/behavior/select.js | 6 ++ modules/modes/select.js | 49 ++++++------ modules/ui/edit_menu.js | 150 +++++++++++++++++++++++++++++++++++++ modules/ui/index.js | 2 + 5 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 modules/ui/edit_menu.js diff --git a/css/app.css b/css/app.css index 74874394e..8dfce18b7 100644 --- a/css/app.css +++ b/css/app.css @@ -3167,9 +3167,7 @@ img.tile-removing { } .radial-menu-background { - fill: none; - stroke: black; - stroke-opacity: 0.5; + fill: black; } .radial-menu-item circle { diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 9c627701d..2b3628b4f 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -23,6 +23,11 @@ export function behaviorSelect(context) { function click() { + + if (d3.event.type === 'contextmenu') { + d3.event.preventDefault(); + } + var datum = d3.event.target.__data__, lasso = d3.select('#surface .lasso').node(), mode = context.mode(); @@ -56,6 +61,7 @@ export function behaviorSelect(context) { .on('keyup.select', keyup); selection.on('click.select', click); + selection.on('contextmenu.select', click); keydown(); }; diff --git a/modules/modes/select.js b/modules/modes/select.js index 27032262e..bb5c7e423 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -28,7 +28,7 @@ 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/index'; import { uiCmd } from '../ui/cmd'; import { utilEntityOrMemberSelector, utilEntitySelector } from '../util/index'; @@ -54,7 +54,7 @@ export function modeSelect(context, selectedIDs) { modeDragNode(context).selectedIDs(selectedIDs).behavior ], inspector, - radialMenu, + editMenu, newFeature = false, suppressMenu = false, follow = false; @@ -139,36 +139,30 @@ 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 (!editMenu) { return; } + var point = context.mouse(), + viewport = geoExtent(context.projection.clipExtent()).polygon(), + offset = (viewport[1][1] - 30) - point[1]; // 30 to account for the infoblock - 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; - } + if (geoPointInPolygon(point, viewport)) { + editMenu + .center(point) + .offset(offset); } } function showMenu() { closeMenu(); - if (!suppressMenu && radialMenu) { - context.surface().call(radialMenu); + if (editMenu) { + context.surface().call(editMenu); } } @@ -196,7 +190,9 @@ export function modeSelect(context, selectedIDs) { } positionMenu(); - showMenu(); + if (d3.event && d3.event.type === 'contextmenu') { + showMenu(); + } }; @@ -425,7 +421,7 @@ export function modeSelect(context, selectedIDs) { d3.select(document) .call(keybinding); - radialMenu = uiRadialMenu(context, operations); + editMenu = uiEditMenu(context, operations); context.ui().sidebar .select(singular() ? singular().id : null, newFeature); @@ -440,8 +436,9 @@ export function modeSelect(context, selectedIDs) { selectElements(); - var show = d3.event && !suppressMenu; - + var show = d3.event; + var rtClick = d3.event && d3.event.type === 'contextmenu'; + if (show) { positionMenu(); } @@ -459,7 +456,7 @@ export function modeSelect(context, selectedIDs) { } timeout = window.setTimeout(function() { - if (show) { + if (rtClick) { showMenu(); } @@ -485,7 +482,7 @@ export function modeSelect(context, selectedIDs) { keybinding.off(); closeMenu(); - radialMenu = undefined; + editMenu = undefined; context.history() .on('undone.select', null) diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js new file mode 100644 index 000000000..a0ef6edb6 --- /dev/null +++ b/modules/ui/edit_menu.js @@ -0,0 +1,150 @@ +import * as d3 from 'd3'; +import { geoRoundCoords } from '../geo/index'; +import { uiTooltipHtml } from './tooltipHtml'; + + +export function uiEditMenu(context, operations) { + var rect, + menu, + center = [0, 0], + offset = 0, + tooltip; + + var p = 5, + l = 10, // left padding + a = 30, + a1 = (operations.length) * (a + p) + p; + + var editMenu = function(selection) { + if (!operations.length) return; + + selection.node().parentNode.focus(); + + function click(operation) { + d3.event.stopPropagation(); + if (operation.disabled()) return; + operation(); + editMenu.close(); + } + + menu = selection + .append('g') + .attr('class', 'radial-menu') + .attr('transform', 'translate(' + [center[0] + l, center[1]] + ')') + .attr('opacity', 0); + + menu + .transition() + .attr('opacity', 1); + + rect = menu + .append('g') + .attr('class', 'radial-menu-rectangle') + .attr('transform', function() { + var pos = [0, 0]; + if (offset <= a1) { + pos = [0, offset - a1]; + } + return 'translate(' + pos + ')'; + }); + + menu + .append('path') + .attr('class', 'radial-menu-background') + .attr('transform', 'translate(1, 1)') + .attr('d', 'M0 8 L8 14 L8 8 L8 2 Z'); + + rect + .append('rect') + .attr('class', 'radial-menu-background') + .attr('x', 8) + .attr('rx', 4) + .attr('ry', 4) + .attr('width', 44) + .attr('height', a1) + .attr('stroke-linecap', 'round'); + + + var button = rect.selectAll() + .data(operations) + .enter() + .append('g') + .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) + .classed('disabled', function(d) { return d.disabled(); }) + .attr('transform', function(d, i) { + return 'translate(' +geoRoundCoords([ + a/2 + l + p, + a/2 + p * (i + 1) + i * a]).join(',') + ')'; + }); + + button + .append('circle') + .attr('r', 15) + .on('click', click) + .on('mousedown', mousedown) + .on('mouseover', mouseover) + .on('mouseout', mouseout); + + button + .append('use') + .attr('transform', 'translate(-10,-10)') + .attr('width', '20') + .attr('height', '20') + .attr('xlink:href', function(d) { return '#operation-' + d.id; }); + + tooltip = d3.select(document.body) + .append('div') + .attr('class', 'tooltip-inner radial-menu-tooltip'); + + function mousedown() { + d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869 + } + + function mouseover(d, i) { + var rect = context.surfaceRect(), + pos = [center[0], offset <= a1 ? center[1] - (a1 - offset) : center[1]], + top = rect.top + i * (p + a)+ pos[1] + 'px', + left = rect.left + (65) + pos[0] + 'px'; + + tooltip + .style('top', top) + .style('left', left) + .style('display', 'block') + .html(uiTooltipHtml(d.tooltip(), d.keys[0])); + } + + 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; + }; + + editMenu.offset = function(_) { + if (!arguments.length) return offset; + offset = _; + return editMenu; + }; + + return editMenu; +} 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'; From 11d7cc7b3412698a601f13af4fdbe9bd657f1d28 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Wed, 8 Feb 2017 12:01:43 +0530 Subject: [PATCH 02/28] white theme for edit_menu.js --- css/app.css | 29 ++++++++++++++----------- modules/modes/select.js | 2 +- modules/ui/edit_menu.js | 48 +++++++++++++++++++++++------------------ 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/css/app.css b/css/app.css index 8dfce18b7..d9e50291f 100644 --- a/css/app.css +++ b/css/app.css @@ -3160,38 +3160,41 @@ img.tile-removing { } .radial-menu-tooltip { - opacity: 0.8; display: none; position: absolute; width: 200px; } .radial-menu-background { - fill: black; -} - -.radial-menu-item circle { fill: #eee; } -.radial-menu-item circle:active, -.radial-menu-item circle:hover { - fill: #fff; +.radial-menu-item rect { + fill: #eee; } -.radial-menu-item.disabled circle { - cursor: auto; - fill: rgba(255,255,255,.5); +.radial-menu-item rect:active, +.radial-menu-item rect:hover { + fill: #ccc; } +.radial-menu-item.disabled rect { + cursor: not-allowed; +} +.radial-menu-item.disabled rect:hover { + cursor: not-allowed; + fill: #eee; +} + + .radial-menu-item use { fill: #222; color: #79f; } .radial-menu-item.disabled use { - fill: rgba(32,32,32,.5); - color: rgba(40,40,40,.5); + fill: rgba(32,32,32,.2); + color: rgba(40,40,40,.2); } .lasso-path { diff --git a/modules/modes/select.js b/modules/modes/select.js index bb5c7e423..cdba34aa6 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -149,7 +149,7 @@ export function modeSelect(context, selectedIDs) { if (!editMenu) { return; } var point = context.mouse(), viewport = geoExtent(context.projection.clipExtent()).polygon(), - offset = (viewport[1][1] - 30) - point[1]; // 30 to account for the infoblock + offset = [viewport[2][0] - point[0], (viewport[1][1] - 30) - point[1]]; // 30 to account for the infoblock if (geoPointInPolygon(point, viewport)) { editMenu diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index a0ef6edb6..0a8e454c9 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -7,13 +7,15 @@ export function uiEditMenu(context, operations) { var rect, menu, center = [0, 0], - offset = 0, + offset = [0, 0], tooltip; - var p = 5, + var p = 8, // top padding l = 10, // left padding - a = 30, - a1 = (operations.length) * (a + p) + p; + h = 15, // height of icon + m = 4, // top margin + a1 = 2 * m + operations.length * (2 * p + h); + var editMenu = function(selection) { if (!operations.length) return; @@ -42,22 +44,16 @@ export function uiEditMenu(context, operations) { .attr('class', 'radial-menu-rectangle') .attr('transform', function() { var pos = [0, 0]; - if (offset <= a1) { - pos = [0, offset - a1]; + if (offset[1] <= a1) { + pos = [0, offset[1] - a1]; } return 'translate(' + pos + ')'; }); - menu - .append('path') - .attr('class', 'radial-menu-background') - .attr('transform', 'translate(1, 1)') - .attr('d', 'M0 8 L8 14 L8 8 L8 2 Z'); - rect .append('rect') .attr('class', 'radial-menu-background') - .attr('x', 8) + .attr('x', 4) .attr('rx', 4) .attr('ry', 4) .attr('width', 44) @@ -73,13 +69,16 @@ export function uiEditMenu(context, operations) { .classed('disabled', function(d) { return d.disabled(); }) .attr('transform', function(d, i) { return 'translate(' +geoRoundCoords([ - a/2 + l + p, - a/2 + p * (i + 1) + i * a]).join(',') + ')'; + 0, + m + i*(2*p + h)]).join(',') + ')'; }); button - .append('circle') - .attr('r', 15) + .append('rect') + // .attr('r', 15) + .attr('x', 4) + .attr('width', 44) + .attr('height', 2*p + h) .on('click', click) .on('mousedown', mousedown) .on('mouseover', mouseover) @@ -87,9 +86,11 @@ export function uiEditMenu(context, operations) { button .append('use') - .attr('transform', 'translate(-10,-10)') .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) @@ -101,10 +102,14 @@ export function uiEditMenu(context, operations) { } function mouseover(d, i) { + var width = 260; var rect = context.surfaceRect(), - pos = [center[0], offset <= a1 ? center[1] - (a1 - offset) : center[1]], - top = rect.top + i * (p + a)+ pos[1] + 'px', - left = rect.left + (65) + pos[0] + 'px'; + pos = [ + offset[0] < width?center[0] - 255 :center[0], + offset[1] <= a1 ? center[1] - (a1 - offset[1]) : center[1] + ], + top = rect.top + m + i * (2 * p + h)+ pos[1] + 'px', + left = rect.left + (64) + pos[0] + 'px'; tooltip .style('top', top) @@ -142,6 +147,7 @@ export function uiEditMenu(context, operations) { editMenu.offset = function(_) { if (!arguments.length) return offset; + console.log(offset); offset = _; return editMenu; }; From 734f40c2a8b74ee1abb26b3d3fb1a314ac0a5d18 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Wed, 8 Feb 2017 14:11:41 +0530 Subject: [PATCH 03/28] fix tooltip overflow --- modules/ui/edit_menu.js | 52 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index 0a8e454c9..1855b8e26 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -15,9 +15,9 @@ export function uiEditMenu(context, operations) { h = 15, // height of icon m = 4, // top margin a1 = 2 * m + operations.length * (2 * p + h); - - var editMenu = function(selection) { + + var editMenu = function (selection) { if (!operations.length) return; selection.node().parentNode.focus(); @@ -34,7 +34,7 @@ export function uiEditMenu(context, operations) { .attr('class', 'radial-menu') .attr('transform', 'translate(' + [center[0] + l, center[1]] + ')') .attr('opacity', 0); - + menu .transition() .attr('opacity', 1); @@ -42,7 +42,7 @@ export function uiEditMenu(context, operations) { rect = menu .append('g') .attr('class', 'radial-menu-rectangle') - .attr('transform', function() { + .attr('transform', function () { var pos = [0, 0]; if (offset[1] <= a1) { pos = [0, offset[1] - a1]; @@ -59,18 +59,18 @@ export function uiEditMenu(context, operations) { .attr('width', 44) .attr('height', a1) .attr('stroke-linecap', 'round'); - + var button = rect.selectAll() .data(operations) .enter() .append('g') - .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) - .classed('disabled', function(d) { return d.disabled(); }) - .attr('transform', function(d, i) { - return 'translate(' +geoRoundCoords([ - 0, - m + i*(2*p + h)]).join(',') + ')'; + .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) + .classed('disabled', function (d) { return d.disabled(); }) + .attr('transform', function (d, i) { + return 'translate(' + geoRoundCoords([ + 0, + m + i * (2 * p + h)]).join(',') + ')'; }); button @@ -78,7 +78,7 @@ export function uiEditMenu(context, operations) { // .attr('r', 15) .attr('x', 4) .attr('width', 44) - .attr('height', 2*p + h) + .attr('height', 2 * p + h) .on('click', click) .on('mousedown', mousedown) .on('mouseover', mouseover) @@ -89,9 +89,9 @@ export function uiEditMenu(context, operations) { .attr('width', '20') .attr('height', '20') .attr('transform', function () { - return 'translate(' + [2*p, 5] + ')'; + return 'translate(' + [2 * p, 5] + ')'; }) - .attr('xlink:href', function(d) { return '#operation-' + d.id; }); + .attr('xlink:href', function (d) { return '#operation-' + d.id; }); tooltip = d3.select(document.body) .append('div') @@ -105,15 +105,19 @@ export function uiEditMenu(context, operations) { var width = 260; var rect = context.surfaceRect(), pos = [ - offset[0] < width?center[0] - 255 :center[0], - offset[1] <= a1 ? center[1] - (a1 - offset[1]) : center[1] + offset[0] < width ? center[0] - 255 : center[0], + offset[1] <= a1 ? m + center[1] - (a1 - offset[1]) : m + center[1] ], - top = rect.top + m + i * (2 * p + h)+ pos[1] + 'px', - left = rect.left + (64) + pos[0] + 'px'; - + top = rect.top + i * (2 * p + h) + pos[1], + left = rect.left + (64) + pos[0]; + var j = i; + // fix tooltip overflow on y axis + while (top - center[1] + 90 > offset[1] && j !== 0) { + top = rect.top + (--j) * (2 * p + h) + pos[1]; + } tooltip - .style('top', top) - .style('left', left) + .style('top', top + 'px') + .style('left', left+ 'px') .style('display', 'block') .html(uiTooltipHtml(d.tooltip(), d.keys[0])); } @@ -124,7 +128,7 @@ export function uiEditMenu(context, operations) { }; - editMenu.close = function() { + editMenu.close = function () { if (menu) { menu .style('pointer-events', 'none') @@ -139,13 +143,13 @@ export function uiEditMenu(context, operations) { }; - editMenu.center = function(_) { + editMenu.center = function (_) { if (!arguments.length) return center; center = _; return editMenu; }; - editMenu.offset = function(_) { + editMenu.offset = function (_) { if (!arguments.length) return offset; console.log(offset); offset = _; From ca25a34987373124650517fe608f81bdd68f2264 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Thu, 9 Feb 2017 13:03:12 +0530 Subject: [PATCH 04/28] semver compliance for edit_menu --- css/app.css | 68 +++++++++++++++++++++++++++++++-------- modules/modes/select.js | 29 +++++++++++------ modules/renderer/map.js | 2 +- modules/ui/edit_menu.js | 11 +++---- modules/ui/intro/point.js | 2 +- 5 files changed, 80 insertions(+), 32 deletions(-) diff --git a/css/app.css b/css/app.css index d9e50291f..f13e2c1b4 100644 --- a/css/app.css +++ b/css/app.css @@ -3160,43 +3160,83 @@ img.tile-removing { } .radial-menu-tooltip { + opacity: 0.8; display: none; position: absolute; width: 200px; } .radial-menu-background { + fill: none; + stroke: black; + stroke-opacity: 0.5; +} + +.radial-menu-item circle { fill: #eee; } -.radial-menu-item rect { - fill: #eee; +.radial-menu-item circle:active, +.radial-menu-item circle:hover { + fill: #fff; } -.radial-menu-item rect:active, -.radial-menu-item rect:hover { - fill: #ccc; +.radial-menu-item.disabled circle { + cursor: auto; + fill: rgba(255,255,255,.5); } -.radial-menu-item.disabled rect { - cursor: not-allowed; -} -.radial-menu-item.disabled rect:hover { - cursor: not-allowed; - fill: #eee; -} - - .radial-menu-item use { fill: #222; color: #79f; } .radial-menu-item.disabled use { + fill: rgba(32,32,32,.5); + 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; diff --git a/modules/modes/select.js b/modules/modes/select.js index cdba34aa6..681d39de5 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -146,15 +146,24 @@ export function modeSelect(context, selectedIDs) { function positionMenu() { - if (!editMenu) { return; } - var point = context.mouse(), - viewport = geoExtent(context.projection.clipExtent()).polygon(), - offset = [viewport[2][0] - point[0], (viewport[1][1] - 30) - point[1]]; // 30 to account for the infoblock + if (suppressMenu || !editMenu) { return; } - if (geoPointInPolygon(point, viewport)) { - editMenu - .center(point) - .offset(offset); + var entity = singular(); + if (entity && context.geometry(entity.id) === 'relation') { + suppressMenu = true; + } else { + var point = context.mouse(), + viewport = geoExtent(context.projection.clipExtent()).polygon(), + offset = [ + viewport[2][0] - point[0], + (viewport[1][1] - 30) - point[1] // 30 to account for the infoblock + ]; + + if (geoPointInPolygon(point, viewport)) { + editMenu + .center(point) + .offset(offset); + } } } @@ -168,7 +177,7 @@ export function modeSelect(context, selectedIDs) { function toggleMenu() { - if (d3.select('.radial-menu').empty()) { + if (d3.select('.edit-menu').empty()) { showMenu(); } else { closeMenu(); @@ -456,7 +465,7 @@ export function modeSelect(context, selectedIDs) { } timeout = window.setTimeout(function() { - if (rtClick) { + if (!suppressMenu && rtClick) { showMenu(); } diff --git a/modules/renderer/map.js b/modules/renderer/map.js index ec10c9201..5ee4fad16 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -311,7 +311,7 @@ export function rendererMap(context) { function resetTransform() { if (!transformed) return false; - surface.selectAll('.radial-menu').interrupt().remove(); + surface.selectAll('.edit-menu').interrupt().remove(); utilSetTransform(supersurface, 0, 0); transformed = false; return true; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index 1855b8e26..7150b4233 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -31,7 +31,7 @@ export function uiEditMenu(context, operations) { menu = selection .append('g') - .attr('class', 'radial-menu') + .attr('class', 'edit-menu') .attr('transform', 'translate(' + [center[0] + l, center[1]] + ')') .attr('opacity', 0); @@ -41,7 +41,7 @@ export function uiEditMenu(context, operations) { rect = menu .append('g') - .attr('class', 'radial-menu-rectangle') + .attr('class', 'edit-menu-rectangle') .attr('transform', function () { var pos = [0, 0]; if (offset[1] <= a1) { @@ -52,7 +52,7 @@ export function uiEditMenu(context, operations) { rect .append('rect') - .attr('class', 'radial-menu-background') + .attr('class', 'edit-menu-background') .attr('x', 4) .attr('rx', 4) .attr('ry', 4) @@ -65,7 +65,7 @@ export function uiEditMenu(context, operations) { .data(operations) .enter() .append('g') - .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) + .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([ @@ -95,7 +95,7 @@ export function uiEditMenu(context, operations) { tooltip = d3.select(document.body) .append('div') - .attr('class', 'tooltip-inner radial-menu-tooltip'); + .attr('class', 'tooltip-inner edit-menu-tooltip'); function mousedown() { d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869 @@ -151,7 +151,6 @@ export function uiEditMenu(context, operations) { editMenu.offset = function (_) { if (!arguments.length) return offset; - console.log(offset); offset = _; return editMenu; }; diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js index 14f357c11..eff7a5f3d 100644 --- a/modules/ui/intro/point.js +++ b/modules/ui/intro/point.js @@ -143,7 +143,7 @@ export function uiIntroPoint(context, reveal) { context.history().on('change.intro', deleted); setTimeout(function() { - var node = d3.select('.radial-menu-item-delete').node(); + var node = d3.select('.edit-menu-item-delete').node(); var pointBox = pad(node.getBoundingClientRect(), 50, context); reveal(pointBox, t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); From 07de6d975fc2e5084e82ecbf4de6eb171550d298 Mon Sep 17 00:00:00 2001 From: Kushan Joshi <0o3ko0@gmail.com> Date: Thu, 9 Feb 2017 15:28:11 +0530 Subject: [PATCH 05/28] Fix right clicking edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed - On reselecting multiple entities, right click doesn’t discard selection - On selecting new entity, right click discards previous selection - Preserved shift selection for both left & right click --- modules/behavior/select.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 2b3628b4f..52ab75d30 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -23,8 +23,9 @@ export function behaviorSelect(context) { function click() { - - if (d3.event.type === 'contextmenu') { + var rtClick = d3.event.type === 'contextmenu'; + + if (rtClick) { d3.event.preventDefault(); } @@ -39,16 +40,25 @@ export function behaviorSelect(context) { 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 { + // Reselect when 'rtClick on one of the selectedIDs' + // OR 'leftClick on the same singular selected entity' + // Explanation: leftClick should discard any multiple + // selection of entities and make the selection singlular. + // Whereas rtClick should preserve multiple selection of + // entities if and only if it clicks on one of the selectedIDs. + if (context.selectedIDs().indexOf(datum.id) >= 0 + && (rtClick || context.selectedIDs().length === 1)) { mode.suppressMenu(false).reselect(); + } else { + context.enter(modeSelect(context, [datum.id])); } } else if (context.selectedIDs().indexOf(datum.id) >= 0) { - var selectedIDs = _.without(context.selectedIDs(), datum.id); - context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); - + if (rtClick) { // To prevent datum.id from being removed when rtClick + mode.suppressMenu(false).reselect(); + } else { + 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]))); } From c5383c1f5533edb5458cb8e4d2381c8fa3fc8d23 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 10 Feb 2017 16:39:08 -0500 Subject: [PATCH 06/28] Darker unintrusive flash.. WIP flash operations on keypress --- css/app.css | 11 +++++++++++ modules/behavior/operation.js | 2 ++ modules/renderer/map.js | 8 ++------ modules/ui/flash.js | 36 ++++++++++++++++++++++------------- modules/ui/init.js | 5 +++++ 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/css/app.css b/css/app.css index f13e2c1b4..d329116b3 100644 --- a/css/app.css +++ b/css/app.css @@ -564,6 +564,17 @@ button.save.has-count .count::before { min-width: 768px; } +#flash { + position: absolute; + left: 300px; + top: 65px; + z-index: 1; +} + +#flash .content { + padding: 10px; +} + /* Header for modals / panes ------------------------------------------------------- */ diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 0e2741dd8..57c0e8d46 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/index'; /* Creates a keybinding behavior for an operation */ @@ -15,6 +16,7 @@ export function behaviorOperation(context) { if (which.available() && !which.disabled() && !context.inIntro()) { which(); } + uiFlash().text('you did ' + which.title); }); d3.select(document).call(keybinding); } diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 5ee4fad16..d9c93bd27 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -285,9 +285,7 @@ export function rendererMap(context) { 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); @@ -569,9 +567,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/flash.js b/modules/ui/flash.js index 6abcf4a98..37ae1c66b 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -1,26 +1,36 @@ +import * as d3 from 'd3'; import { uiModal } from './modal'; +var timeout; -export function uiFlash(selection) { - var modalSelection = uiModal(selection); - modalSelection.select('.modal') - .classed('modal-flash', true); +export function uiFlash() { + var content = d3.select('#flash').selectAll('.content') + .data([0]); - modalSelection.select('.content') - .classed('modal-section', true) + content = content.enter() .append('div') - .attr('class', 'description'); + .attr('class', 'content') + .merge(content); - modalSelection.on('click.flash', function() { - modalSelection.remove(); - }); + if (timeout) { + window.clearTimeout(timeout); + } + + timeout = window.setTimeout(function() { + content + .transition() + .duration(250) + .style('opacity', 0) + .style('transform', 'scaleY(.25)') + .on('end', function() { + content.remove(); + timeout = null; + }); - setTimeout(function() { - modalSelection.remove(); return true; }, 1500); - return modalSelection; + return content; } diff --git a/modules/ui/init.js b/modules/ui/init.js index d9114614a..0fa1b6fcb 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -71,6 +71,11 @@ export function uiInit(context) { .attr('id', 'bar') .attr('class', 'fillD'); + content + .append('div') + .attr('id', 'flash') + .attr('class', 'fillD'); + content .append('div') .attr('id', 'map') From 5aa519affb211d9677beb6db6df8f07f59d0286a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 10 Feb 2017 21:50:31 -0500 Subject: [PATCH 07/28] Flash style adjustments --- css/app.css | 7 ++++++- modules/ui/flash.js | 4 ++-- modules/ui/init.js | 3 +-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/css/app.css b/css/app.css index d329116b3..43ce8fd03 100644 --- a/css/app.css +++ b/css/app.css @@ -566,13 +566,18 @@ button.save.has-count .count::before { #flash { position: absolute; - left: 300px; top: 65px; z-index: 1; + width: 100%; + display: flex; + flex-flow: column wrap; } #flash .content { + margin: 0 auto; padding: 10px; + max-width: 50%; + border-radius: 3px; } /* Header for modals / panes diff --git a/modules/ui/flash.js b/modules/ui/flash.js index 37ae1c66b..86aee4727 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -10,7 +10,7 @@ export function uiFlash() { content = content.enter() .append('div') - .attr('class', 'content') + .attr('class', 'content fillD') .merge(content); if (timeout) { @@ -22,7 +22,7 @@ export function uiFlash() { .transition() .duration(250) .style('opacity', 0) - .style('transform', 'scaleY(.25)') + .style('transform', 'scaleY(.1)') .on('end', function() { content.remove(); timeout = null; diff --git a/modules/ui/init.js b/modules/ui/init.js index 0fa1b6fcb..db5d47678 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -73,8 +73,7 @@ export function uiInit(context) { content .append('div') - .attr('id', 'flash') - .attr('class', 'fillD'); + .attr('id', 'flash'); content .append('div') From c18cc7577d3b02e55c83915a0966ac54a803e403 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 11 Feb 2017 00:19:49 -0500 Subject: [PATCH 08/28] Add flash test, avoid using sinon.useFakeTimers in tests. sinon.useFakeTimers mocks setInterval, setTimeout, etc, but not requestAnimationFrame, which d3 transitions rely on. --- modules/ui/flash.js | 1 - test/spec/behavior/hash.js | 10 +++++----- test/spec/ui/flash.js | 31 +++++++++++++++++-------------- test/spec/util/session_mutex.js | 7 +------ 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/modules/ui/flash.js b/modules/ui/flash.js index 86aee4727..ee7f4d249 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -1,5 +1,4 @@ import * as d3 from 'd3'; -import { uiModal } from './modal'; var timeout; 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/ui/flash.js b/test/spec/ui/flash.js index d264d6315..f797f12fc 100644 --- a/test/spec/ui/flash.js +++ b/test/spec/ui/flash.js @@ -1,25 +1,28 @@ 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(); + elem = d3.select('body') + .append('div') + .attr('id', 'flash'); }); afterEach(function () { - clock.restore(); + elem.remove(); }); - it('leaves after 1000 ms', function () { - var flash = iD.uiFlash(elem); - clock.tick(1610); - expect(flash.node().parentNode).to.be.null; + it('creates a flash', function () { + iD.uiFlash(); + expect(elem.selectAll('#flash .content').size()).to.eql(1); }); + + it.skip('flash goes away', function (done) { + // test doesn't work on PhantomJS + iD.uiFlash(); + window.setTimeout(function() { + expect(elem.selectAll('#flash .content').size()).to.eql(0); + done(); + }, 1900); + }); + }); 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(); }); From 33227c2b539d1cd68b72c2904f950805deab40e3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 15 Feb 2017 16:37:37 -0500 Subject: [PATCH 09/28] Adjust text for Reflect operation messages --- data/core.yaml | 8 +++++--- dist/locales/en.json | 9 ++++++--- modules/operations/reflect.js | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 3aa09cd67..7373e5a16 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. diff --git a/dist/locales/en.json b/dist/locales/en.json index 52fe9bcc0..d4f072c16 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." } }, diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 9a3ffad97..ce224d9d2 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -69,7 +69,7 @@ export function operationReflect(selectedIDs, context, axis) { 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; From 028ef3de3f858390c9dbe91ad0c53b31b027edac Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 15 Feb 2017 22:01:30 -0500 Subject: [PATCH 10/28] Expose annotations for each operation --- modules/behavior/operation.js | 10 +++++++--- modules/operations/circularize.js | 3 ++- modules/operations/continue.js | 1 + modules/operations/delete.js | 15 ++++++--------- modules/operations/disconnect.js | 3 ++- modules/operations/merge.js | 6 +++--- modules/operations/move.js | 3 +++ modules/operations/orthogonalize.js | 3 ++- modules/operations/reflect.js | 3 ++- modules/operations/reverse.js | 3 ++- modules/operations/rotate.js | 3 +++ modules/operations/split.js | 26 +++++++++++--------------- modules/operations/straighten.js | 3 ++- 13 files changed, 46 insertions(+), 36 deletions(-) diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 57c0e8d46..804a5b7d9 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -9,14 +9,18 @@ export function behaviorOperation(context) { 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().text(which.tooltip); + } else { + var annotation = which.annotation || which.title; + uiFlash().text(annotation); which(); } - uiFlash().text('you did ' + which.title); }); d3.select(document).call(keybinding); } diff --git a/modules/operations/circularize.js b/modules/operations/circularize.js index 8aebef11e..895d33aef 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); }; @@ -46,6 +46,7 @@ export function operationCircularize(selectedIDs, context) { operation.id = 'circularize'; operation.keys = [t('operations.circularize.key')]; operation.title = t('operations.circularize.title'); + operation.annotation = t('operations.circularize.annotation.' + geometry); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 5d24f463a..2393ada58 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -56,6 +56,7 @@ export function operationContinue(selectedIDs, context) { operation.id = 'continue'; operation.keys = [t('operations.continue.key')]; operation.title = t('operations.continue.title'); + operation.annotation = t('operations.continue.annotation.line'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/delete.js b/modules/operations/delete.js index ead998bb2..b7d330482 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,7 +41,7 @@ export function operationDelete(selectedIDs, context) { } } - context.perform(action, annotation); + context.perform(action, operation.annotation); if (nextSelectedID && context.hasEntity(nextSelectedID)) { context.enter( @@ -111,6 +105,9 @@ export function operationDelete(selectedIDs, context) { operation.id = 'delete'; operation.keys = [uiCmd('⌘⌫'), uiCmd('⌘⌦'), uiCmd('⌦')]; operation.title = t('operations.delete.title'); + operation.annotation = selectedIDs.length === 1 ? + t('operations.delete.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.delete.annotation.multiple', { n: selectedIDs.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/disconnect.js b/modules/operations/disconnect.js index 420162ecd..815fe8c94 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); }; @@ -47,6 +47,7 @@ export function operationDisconnect(selectedIDs, context) { operation.id = 'disconnect'; operation.keys = [t('operations.disconnect.key')]; operation.title = t('operations.disconnect.title'); + operation.annotation = t('operations.disconnect.annotation'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/merge.js b/modules/operations/merge.js index c55e3c350..e986b4688 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,7 +25,7 @@ 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'; @@ -72,6 +71,7 @@ export function operationMerge(selectedIDs, context) { operation.id = 'merge'; operation.keys = [t('operations.merge.key')]; operation.title = t('operations.merge.title'); + operation.annotation = t('operations.merge.annotation', { n: selectedIDs.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/move.js b/modules/operations/move.js index 7c9a9277a..07af2d7cf 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -52,6 +52,9 @@ export function operationMove(selectedIDs, context) { operation.id = 'move'; operation.keys = [t('operations.move.key')]; operation.title = t('operations.move.title'); + operation.annotation = selectedIDs.length === 1 ? + t('operations.move.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.move.annotation.multiple'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index 8275e4f7a..f34f2facd 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); }; @@ -47,6 +47,7 @@ export function operationOrthogonalize(selectedIDs, context) { operation.id = 'orthogonalize'; operation.keys = [t('operations.orthogonalize.key')]; operation.title = t('operations.orthogonalize.title'); + operation.annotation = t('operations.orthogonalize.annotation.' + geometry); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index ce224d9d2..85fdcfdb9 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); }; @@ -70,6 +70,7 @@ export function operationReflect(selectedIDs, context, axis) { operation.id = 'reflect-' + axis; operation.keys = [t('operations.reflect.key.' + axis)]; operation.title = t('operations.reflect.title.' + axis); + operation.annotation = t('operations.reflect.annotation.' + axis + '.' + multi); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/reverse.js b/modules/operations/reverse.js index 2e0d39b40..a16904d3d 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); }; @@ -29,6 +29,7 @@ export function operationReverse(selectedIDs, context) { operation.id = 'reverse'; operation.keys = [t('operations.reverse.key')]; operation.title = t('operations.reverse.title'); + operation.annotation = t('operations.reverse.annotation'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index aa7359f09..9e702acc3 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -57,6 +57,9 @@ export function operationRotate(selectedIDs, context) { operation.id = 'rotate'; operation.keys = [t('operations.rotate.key')]; operation.title = t('operations.rotate.title'); + operation.annotation = selectedIDs.length === 1 ? + t('operations.rotate.annotation.' + context.geometry(selectedIDs[0])) : + t('operations.rotate.annotation.multiple'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/split.js b/modules/operations/split.js index cc81c90a3..229e83458 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 { @@ -65,6 +58,9 @@ export function operationSplit(selectedIDs, context) { operation.id = 'split'; operation.keys = [t('operations.split.key')]; operation.title = t('operations.split.title'); + operation.annotation = ways.length === 1 ? + t('operations.split.annotation.' + context.geometry(ways[0].id)) : + t('operations.split.annotation.multiple', { n: ways.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index 8a029caa0..afe87e4c8 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); } @@ -44,6 +44,7 @@ export function operationStraighten(selectedIDs, context) { operation.keys = [t('operations.straighten.key')]; operation.title = t('operations.straighten.title'); operation.behavior = behaviorOperation(context).which(operation); + operation.annotation = t('operations.straighten.annotation'); return operation; } From b7a81c6becf45c4c0fa645bc8ded56641b57396f Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 15 Feb 2017 22:28:19 -0500 Subject: [PATCH 11/28] Allow customizable flash showDuration and fadeDuration --- modules/behavior/operation.js | 4 ++-- modules/ui/flash.js | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 804a5b7d9..f397460d3 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -15,10 +15,10 @@ export function behaviorOperation(context) { d3.event.preventDefault(); var disabled = which.disabled(); if (disabled) { - uiFlash().text(which.tooltip); + uiFlash(2000, 500).text(which.tooltip); } else { var annotation = which.annotation || which.title; - uiFlash().text(annotation); + uiFlash(1500, 250).text(annotation); which(); } }); diff --git a/modules/ui/flash.js b/modules/ui/flash.js index ee7f4d249..d4dc40cfe 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -3,7 +3,10 @@ import * as d3 from 'd3'; var timeout; -export function uiFlash() { +export function uiFlash(showDuration, fadeDuration) { + showDuration = showDuration || 1500; + fadeDuration = fadeDuration || 250; + var content = d3.select('#flash').selectAll('.content') .data([0]); @@ -19,7 +22,7 @@ export function uiFlash() { timeout = window.setTimeout(function() { content .transition() - .duration(250) + .duration(fadeDuration) .style('opacity', 0) .style('transform', 'scaleY(.1)') .on('end', function() { @@ -28,7 +31,7 @@ export function uiFlash() { }); return true; - }, 1500); + }, showDuration); return content; From 2ce78d6c4399b954ee9dbfd058b3eeafac5bfab6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 15 Feb 2017 23:02:13 -0500 Subject: [PATCH 12/28] Call annotation as a function instead of a property This is because, like tooltip(), it doesn't always make sense to call it, and it should never get called if the operation is not available. --- modules/behavior/operation.js | 4 ++-- modules/operations/circularize.js | 8 ++++++-- modules/operations/continue.js | 6 +++++- modules/operations/delete.js | 12 ++++++++---- modules/operations/disconnect.js | 8 ++++++-- modules/operations/merge.js | 8 ++++++-- modules/operations/move.js | 10 +++++++--- modules/operations/orthogonalize.js | 8 ++++++-- modules/operations/reflect.js | 8 ++++++-- modules/operations/reverse.js | 8 ++++++-- modules/operations/rotate.js | 10 +++++++--- modules/operations/split.js | 12 ++++++++---- modules/operations/straighten.js | 8 ++++++-- 13 files changed, 79 insertions(+), 31 deletions(-) diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index f397460d3..997c42487 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -15,9 +15,9 @@ export function behaviorOperation(context) { d3.event.preventDefault(); var disabled = which.disabled(); if (disabled) { - uiFlash(2000, 500).text(which.tooltip); + uiFlash(2500, 500).text(which.tooltip); } else { - var annotation = which.annotation || which.title; + var annotation = which.annotation() || which.title; uiFlash(1500, 250).text(annotation); which(); } diff --git a/modules/operations/circularize.js b/modules/operations/circularize.js index 895d33aef..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, operation.annotation); + context.perform(action, operation.annotation()); }; @@ -43,10 +43,14 @@ 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'); - operation.annotation = t('operations.circularize.annotation.' + geometry); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/continue.js b/modules/operations/continue.js index 2393ada58..77e45905c 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -53,10 +53,14 @@ 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'); - operation.annotation = t('operations.continue.annotation.line'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/delete.js b/modules/operations/delete.js index b7d330482..ae8b28c29 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -41,7 +41,7 @@ export function operationDelete(selectedIDs, context) { } } - context.perform(action, operation.annotation); + context.perform(action, operation.annotation()); if (nextSelectedID && context.hasEntity(nextSelectedID)) { context.enter( @@ -102,12 +102,16 @@ 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'); - operation.annotation = selectedIDs.length === 1 ? - t('operations.delete.annotation.' + context.geometry(selectedIDs[0])) : - t('operations.delete.annotation.multiple', { n: selectedIDs.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/disconnect.js b/modules/operations/disconnect.js index 815fe8c94..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, operation.annotation); + context.perform(action, operation.annotation()); }; @@ -44,10 +44,14 @@ 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'); - operation.annotation = t('operations.disconnect.annotation'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/merge.js b/modules/operations/merge.js index e986b4688..b35d21973 100644 --- a/modules/operations/merge.js +++ b/modules/operations/merge.js @@ -25,7 +25,7 @@ export function operationMerge(selectedIDs, context) { action = mergePolygon; } - context.perform(action, operation.annotation); + context.perform(action, operation.annotation()); var ids = selectedIDs.filter(function(id) { var entity = context.hasEntity(id); return entity && entity.type !== 'node'; @@ -68,10 +68,14 @@ 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'); - operation.annotation = t('operations.merge.annotation', { n: selectedIDs.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/move.js b/modules/operations/move.js index 07af2d7cf..1205562a0 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -49,12 +49,16 @@ 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'); - operation.annotation = selectedIDs.length === 1 ? - t('operations.move.annotation.' + context.geometry(selectedIDs[0])) : - t('operations.move.annotation.multiple'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index f34f2facd..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, operation.annotation); + context.perform(action, operation.annotation()); }; @@ -44,10 +44,14 @@ 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'); - operation.annotation = t('operations.orthogonalize.annotation.' + geometry); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 85fdcfdb9..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, operation.annotation); + context.perform(action, operation.annotation()); }; @@ -67,10 +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.' + axis); - operation.annotation = t('operations.reflect.annotation.' + axis + '.' + multi); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/reverse.js b/modules/operations/reverse.js index a16904d3d..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), operation.annotation); + context.perform(actionReverse(entityId), operation.annotation()); }; @@ -26,10 +26,14 @@ 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'); - operation.annotation = t('operations.reverse.annotation'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 9e702acc3..b0fe066f4 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -54,12 +54,16 @@ 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'); - operation.annotation = selectedIDs.length === 1 ? - t('operations.rotate.annotation.' + context.geometry(selectedIDs[0])) : - t('operations.rotate.annotation.multiple'); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/split.js b/modules/operations/split.js index 229e83458..0823d4879 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -23,7 +23,7 @@ export function operationSplit(selectedIDs, context) { var operation = function() { - var difference = context.perform(action, operation.annotation); + var difference = context.perform(action, operation.annotation()); context.enter(modeSelect(context, difference.extantIDs())); }; @@ -55,12 +55,16 @@ 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'); - operation.annotation = ways.length === 1 ? - t('operations.split.annotation.' + context.geometry(ways[0].id)) : - t('operations.split.annotation.multiple', { n: ways.length }); operation.behavior = behaviorOperation(context).which(operation); return operation; diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index afe87e4c8..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, operation.annotation); + context.perform(action, operation.annotation()); } @@ -40,11 +40,15 @@ 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'); operation.behavior = behaviorOperation(context).which(operation); - operation.annotation = t('operations.straighten.annotation'); return operation; } From 3fc36b66a9191772ec1745edf13f858d2590a04b Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 16 Feb 2017 10:19:07 -0500 Subject: [PATCH 13/28] Let D3 handle the delay rather than using setTimeout This also fixes a race condition where it was possible to lose a flash message that was created while the previous one was transitioning away. --- modules/ui/flash.js | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/modules/ui/flash.js b/modules/ui/flash.js index d4dc40cfe..4c908d206 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -1,12 +1,13 @@ import * as d3 from 'd3'; -var timeout; - export function uiFlash(showDuration, fadeDuration) { showDuration = showDuration || 1500; fadeDuration = fadeDuration || 250; + d3.select('#flash').selectAll('.content') + .interrupt(); + var content = d3.select('#flash').selectAll('.content') .data([0]); @@ -15,24 +16,15 @@ export function uiFlash(showDuration, fadeDuration) { .attr('class', 'content fillD') .merge(content); - if (timeout) { - window.clearTimeout(timeout); - } - - timeout = window.setTimeout(function() { - content - .transition() - .duration(fadeDuration) - .style('opacity', 0) - .style('transform', 'scaleY(.1)') - .on('end', function() { - content.remove(); - timeout = null; - }); - - return true; - }, showDuration); - + content + .transition() + .delay(showDuration) + .duration(fadeDuration) + .style('opacity', 0) + .style('transform', 'scaleY(.1)') + .on('interrupt end', function() { + content.remove(); + }); return content; } From 1db4ea86f728323e8df7950a6841a52ea754491d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 16 Feb 2017 10:20:37 -0500 Subject: [PATCH 14/28] Add icons to operation flash messages --- css/app.css | 20 +++++++++++++++- modules/behavior/operation.js | 44 +++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/css/app.css b/css/app.css index 43ce8fd03..0472f1732 100644 --- a/css/app.css +++ b/css/app.css @@ -575,9 +575,22 @@ button.save.has-count .count::before { #flash .content { margin: 0 auto; - padding: 10px; + padding: 6px; max-width: 50%; border-radius: 3px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +#flash svg.operation-icon { + width: 36px; + height: 36px; +} + +#flash div.operation-tip { + margin: 0 10px; } /* Header for modals / panes @@ -3734,6 +3747,11 @@ img.tile-removing { -ms-filter: "FlipH"; } +[dir='rtl'] #flash .content { + display: flex; + flex-direction: row-reverse; +} + /* footer */ [dir='rtl'] #scale-block { float: right; diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 997c42487..19c5083cd 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -1,6 +1,6 @@ import * as d3 from 'd3'; import { d3keybinding } from '../lib/d3.keybinding.js'; -import { uiFlash } from '../ui/index'; +import { uiFlash } from '../ui'; /* Creates a keybinding behavior for an operation */ @@ -8,17 +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(18,18)') + .classed('disabled', which.disabled()); + + button + .append('circle') + .attr('r', 15); + + button + .append('use') + .attr('transform', 'translate(-10,-10)') + .attr('width', '20') + .attr('height', '20') + .attr('xlink:href', '#operation-' + which.id); + + return selection; + } + + var behavior = function () { if (which && which.available() && !context.inIntro()) { keybinding = d3keybinding('behavior.key.' + which.id); keybinding.on(which.keys, function() { d3.event.preventDefault(); var disabled = which.disabled(); + if (disabled) { - uiFlash(2500, 500).text(which.tooltip); + uiFlash(2500, 500) + .html('') + .call(drawIcon) + .append('div') + .attr('class', 'operation-tip') + .text(which.tooltip); + } else { - var annotation = which.annotation() || which.title; - uiFlash(1500, 250).text(annotation); + uiFlash(1500, 250) + .html('') + .call(drawIcon) + .append('div') + .attr('class', 'operation-tip') + .text(which.annotation() || which.title); + which(); } }); From 150b9fb7dc165a3dd8f8b594c09f78b69b5d35d3 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 16 Feb 2017 17:13:22 -0500 Subject: [PATCH 15/28] WIP: move flash to footer area, transition in/out Also took this opportunity to use flexbox for the footer layout --- css/app.css | 101 ++++++++++++++++++++++++++------------------ modules/ui/flash.js | 45 ++++++++++++++------ modules/ui/init.js | 25 +++++++---- 3 files changed, 111 insertions(+), 60 deletions(-) diff --git a/css/app.css b/css/app.css index 0472f1732..7b9f5ac00 100644 --- a/css/app.css +++ b/css/app.css @@ -564,34 +564,6 @@ button.save.has-count .count::before { min-width: 768px; } -#flash { - position: absolute; - top: 65px; - z-index: 1; - width: 100%; - display: flex; - flex-flow: column wrap; -} - -#flash .content { - margin: 0 auto; - padding: 6px; - max-width: 50%; - border-radius: 3px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -#flash svg.operation-icon { - width: 36px; - height: 36px; -} - -#flash div.operation-tip { - margin: 0 10px; -} /* Header for modals / panes ------------------------------------------------------- */ @@ -2528,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; } @@ -2565,19 +2537,70 @@ img.tile-removing { } #footer { - width: 100%; - float: left; - clear: both; pointer-events: all; + display: flex; + flex-flow: row nowrap; } +#flash { + display: none; + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + display: flex; + flex-flow: column wrap; +} + +#flash .content { + margin: 0 auto; + padding: 6px; + max-width: 50%; + border-radius: 3px; + display: flex; + flex-flow: row nowrap; + justify-content: center; + align-items: center; +} + +#flash svg.operation-icon { + width: 36px; + height: 36px; +} + +#flash div.operation-tip { + margin: 0 10px; +} + +#footer-wrap { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + flex: 1 1 auto; +} + +.footer-show { + margin-top: 0px; + margin-bottom: 0px; + transition: margin-bottom 100ms linear, margin-top 100ms linear; + -moz-transition: margin-bottom 100ms linear, margin-top 100ms linear; + -webkit-transition: margin-bottom 100ms linear, margin-top 100ms linear; +} +.footer-hide { + margin-top: 30px; + margin-bottom: -30px; + transition: margin-bottom 100ms linear, margin-top 100ms linear; + -moz-transition: margin-bottom 100ms linear, margin-top 100ms linear; + -webkit-transition: margin-bottom 100ms linear, margin-top 100ms 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; @@ -2586,7 +2609,7 @@ img.tile-removing { #info-block { max-height: 30px; - clear: right; + flex: 1 1 auto; } #scale { @@ -2653,12 +2676,10 @@ img.tile-removing { } .api-status { - float: right; - clear: both; text-align: right; - width: 100%; padding: 0px 10px; color: #eee; + flex: 1 0 auto; } .api-status.offline, diff --git a/modules/ui/flash.js b/modules/ui/flash.js index 4c908d206..0be1eb3b4 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -1,30 +1,51 @@ import * as d3 from 'd3'; +var timer; export function uiFlash(showDuration, fadeDuration) { showDuration = showDuration || 1500; fadeDuration = fadeDuration || 250; - d3.select('#flash').selectAll('.content') - .interrupt(); + // d3.select('#flash').selectAll('.content') + // .interrupt(); + + if (timer) { + timer.stop(); + } + + d3.select('#footer-wrap') + .attr('class', 'footer-hide'); + d3.select('#flash') + .attr('class', 'footer-show'); var content = d3.select('#flash').selectAll('.content') .data([0]); content = content.enter() .append('div') - .attr('class', 'content fillD') + .attr('class', 'content') .merge(content); - content - .transition() - .delay(showDuration) - .duration(fadeDuration) - .style('opacity', 0) - .style('transform', 'scaleY(.1)') - .on('interrupt end', function() { - content.remove(); - }); + timer = d3.timeout(function() { + timer = null; + d3.select('#footer-wrap') + .attr('class', 'footer-show'); + d3.select('#flash') + .attr('class', 'footer-hide'); + }, showDuration); + + + // content + // .transition() + // .delay(showDuration) + // .duration(fadeDuration) + // .style('opacity', 0) + // .style('transform', 'scaleY(.1)') + // .on('interrupt end', function() { + // content.remove(); + // d3.select('#footer-wrap') + // .attr('class', 'footer-show'); + // }); return content; } diff --git a/modules/ui/init.js b/modules/ui/init.js index db5d47678..bae73b3c8 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -71,10 +71,6 @@ export function uiInit(context) { .attr('id', 'bar') .attr('class', 'fillD'); - content - .append('div') - .attr('id', 'flash'); - content .append('div') .attr('id', 'map') @@ -120,6 +116,7 @@ export function uiInit(context) { .attr('class', 'spinner') .call(uiSpinner(context)); + var controls = bar .append('div') .attr('class', 'map-controls'); @@ -149,6 +146,7 @@ export function uiInit(context) { .attr('class', 'map-control help-control') .call(uiHelp(context)); + var about = content .append('div') .attr('id', 'about'); @@ -159,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') @@ -166,15 +170,20 @@ export function uiInit(context) { footer .append('div') - .attr('class', 'api-status') - .call(uiStatus(context)); + .attr('id', 'flash') + .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') From 3908da03cf748d1589715e74db7cddc607ab2f2a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 17 Feb 2017 00:23:24 -0500 Subject: [PATCH 16/28] Fix styling of flash and footer and use absolutely positioned divs Also fix flash tests --- css/app.css | 71 ++++++++++++++++++----------------- modules/behavior/operation.js | 14 +++---- modules/ui/flash.js | 24 ++---------- modules/ui/init.js | 2 +- test/spec/ui/flash.js | 36 ++++++++++++------ 5 files changed, 73 insertions(+), 74 deletions(-) diff --git a/css/app.css b/css/app.css index 7b9f5ac00..a308ccc8d 100644 --- a/css/app.css +++ b/css/app.css @@ -2532,67 +2532,70 @@ img.tile-removing { } .source-image { - height:20px; + height: 20px; vertical-align:top; } #footer { pointer-events: all; - display: flex; - flex-flow: row nowrap; + display: block; + height: 30px; } -#flash { - display: none; +#flash-wrap { + display: flex; + flex: 0 0 100%; + flex-flow: row nowrap; + justify-content: space-between; + max-height: 30px; position: absolute; - left: 0; right: 0; - bottom: 0; - z-index: 1; - display: flex; - flex-flow: column wrap; + left: 0; } -#flash .content { - margin: 0 auto; - padding: 6px; - max-width: 50%; - border-radius: 3px; +#flash-wrap .content { display: flex; + flex: 1 0 auto; flex-flow: row nowrap; - justify-content: center; align-items: center; + padding: 2px; + height: 30px; } -#flash svg.operation-icon { - width: 36px; - height: 36px; +#flash-wrap svg.operation-icon { + flex: 0 0 auto; + width: 20px; + height: 20px; + margin: 0 8px; } -#flash div.operation-tip { - margin: 0 10px; +#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; - flex: 1 1 auto; + max-height: 30px; + position: absolute; + right: 0; + left: 0; } .footer-show { - margin-top: 0px; - margin-bottom: 0px; - transition: margin-bottom 100ms linear, margin-top 100ms linear; - -moz-transition: margin-bottom 100ms linear, margin-top 100ms linear; - -webkit-transition: margin-bottom 100ms linear, margin-top 100ms linear; + bottom: 0px; + transition: bottom 75ms linear; + -moz-transition: bottom 75ms linear; + -webkit-transition: bottom 75ms linear; } + .footer-hide { - margin-top: 30px; - margin-bottom: -30px; - transition: margin-bottom 100ms linear, margin-top 100ms linear; - -moz-transition: margin-bottom 100ms linear, margin-top 100ms linear; - -webkit-transition: margin-bottom 100ms linear, margin-top 100ms linear; + bottom: -35px; + transition: bottom 75ms linear; + -moz-transition: bottom 75ms linear; + -webkit-transition: bottom 75ms linear; } @@ -2679,7 +2682,7 @@ img.tile-removing { text-align: right; padding: 0px 10px; color: #eee; - flex: 1 0 auto; + flex: 1 1 auto; } .api-status.offline, @@ -3768,7 +3771,7 @@ img.tile-removing { -ms-filter: "FlipH"; } -[dir='rtl'] #flash .content { +[dir='rtl'] #flash-wrap .content { display: flex; flex-direction: row-reverse; } diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index 19c5083cd..3c224656b 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -14,18 +14,18 @@ export function behaviorOperation(context) { .attr('class', 'operation-icon') .append('g') .attr('class', 'radial-menu-item radial-menu-item-' + which.id) - .attr('transform', 'translate(18,18)') + .attr('transform', 'translate(10,10)') .classed('disabled', which.disabled()); button .append('circle') - .attr('r', 15); + .attr('r', 9); button .append('use') - .attr('transform', 'translate(-10,-10)') - .attr('width', '20') - .attr('height', '20') + .attr('transform', 'translate(-7,-7)') + .attr('width', '14') + .attr('height', '14') .attr('xlink:href', '#operation-' + which.id); return selection; @@ -40,7 +40,7 @@ export function behaviorOperation(context) { var disabled = which.disabled(); if (disabled) { - uiFlash(2500, 500) + uiFlash(3000) .html('') .call(drawIcon) .append('div') @@ -48,7 +48,7 @@ export function behaviorOperation(context) { .text(which.tooltip); } else { - uiFlash(1500, 250) + uiFlash(1500) .html('') .call(drawIcon) .append('div') diff --git a/modules/ui/flash.js b/modules/ui/flash.js index 0be1eb3b4..45dcc0a09 100644 --- a/modules/ui/flash.js +++ b/modules/ui/flash.js @@ -2,12 +2,8 @@ import * as d3 from 'd3'; var timer; -export function uiFlash(showDuration, fadeDuration) { +export function uiFlash(showDuration) { showDuration = showDuration || 1500; - fadeDuration = fadeDuration || 250; - - // d3.select('#flash').selectAll('.content') - // .interrupt(); if (timer) { timer.stop(); @@ -15,10 +11,10 @@ export function uiFlash(showDuration, fadeDuration) { d3.select('#footer-wrap') .attr('class', 'footer-hide'); - d3.select('#flash') + d3.select('#flash-wrap') .attr('class', 'footer-show'); - var content = d3.select('#flash').selectAll('.content') + var content = d3.select('#flash-wrap').selectAll('.content') .data([0]); content = content.enter() @@ -30,22 +26,10 @@ export function uiFlash(showDuration, fadeDuration) { timer = null; d3.select('#footer-wrap') .attr('class', 'footer-show'); - d3.select('#flash') + d3.select('#flash-wrap') .attr('class', 'footer-hide'); }, showDuration); - // content - // .transition() - // .delay(showDuration) - // .duration(fadeDuration) - // .style('opacity', 0) - // .style('transform', 'scaleY(.1)') - // .on('interrupt end', function() { - // content.remove(); - // d3.select('#footer-wrap') - // .attr('class', 'footer-show'); - // }); - return content; } diff --git a/modules/ui/init.js b/modules/ui/init.js index bae73b3c8..2171c9b18 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -170,7 +170,7 @@ export function uiInit(context) { footer .append('div') - .attr('id', 'flash') + .attr('id', 'flash-wrap') .attr('class', 'footer-hide'); var footerWrap = footer diff --git a/test/spec/ui/flash.js b/test/spec/ui/flash.js index f797f12fc..ce5916d57 100644 --- a/test/spec/ui/flash.js +++ b/test/spec/ui/flash.js @@ -1,28 +1,40 @@ describe('iD.uiFlash', function () { - var elem; beforeEach(function() { - elem = d3.select('body') + d3.select('body') .append('div') - .attr('id', 'flash'); + .attr('id', 'flash-wrap') + .append('div') + .attr('id', 'footer-wrap'); }); afterEach(function () { - elem.remove(); + d3.select('body > div').remove(); }); - it('creates a flash', function () { - iD.uiFlash(); - expect(elem.selectAll('#flash .content').size()).to.eql(1); + it('returns a selection', function () { + var content = iD.uiFlash(200); + expect(content.size()).to.eql(1); + expect(content.classed('content')).to.be.ok; }); - it.skip('flash goes away', function (done) { - // test doesn't work on PhantomJS - iD.uiFlash(); + 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() { - expect(elem.selectAll('#flash .content').size()).to.eql(0); + 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(); - }, 1900); + }, 500); }); }); From 9d660076cad38c0d4586d0af3aab001dc029f677 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 17 Feb 2017 15:47:41 -0500 Subject: [PATCH 17/28] Add title to editmenu tooltip --- css/app.css | 18 +++++++++++++++--- modules/ui/edit_menu.js | 5 ++--- modules/ui/tooltipHtml.js | 19 +++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/css/app.css b/css/app.css index a308ccc8d..20d786bc0 100644 --- a/css/app.css +++ b/css/app.css @@ -3148,15 +3148,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 */ @@ -3187,6 +3197,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 { @@ -3212,6 +3223,8 @@ img.tile-removing { left: 60px; } +/* radial menu (deprecated) */ + .radial-menu-tooltip { opacity: 0.8; display: none; @@ -3249,7 +3262,7 @@ img.tile-removing { color: rgba(40,40,40,.5); } -/* edit menu*/ +/* edit menu */ .edit-menu-tooltip { display: none; @@ -3278,7 +3291,6 @@ img.tile-removing { fill: #eee; } - .edit-menu-item use { fill: #222; color: #79f; diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index 7150b4233..7777f4699 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -13,7 +13,7 @@ export function uiEditMenu(context, operations) { var p = 8, // top padding l = 10, // left padding h = 15, // height of icon - m = 4, // top margin + m = 4, // top margin a1 = 2 * m + operations.length * (2 * p + h); @@ -75,7 +75,6 @@ export function uiEditMenu(context, operations) { button .append('rect') - // .attr('r', 15) .attr('x', 4) .attr('width', 44) .attr('height', 2 * p + h) @@ -119,7 +118,7 @@ export function uiEditMenu(context, operations) { .style('top', top + 'px') .style('left', left+ 'px') .style('display', 'block') - .html(uiTooltipHtml(d.tooltip(), d.keys[0])); + .html(uiTooltipHtml(d.tooltip(), d.keys[0], d.title)); } function mouseout() { 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; } From d8237fa3eb674103b43177bd5f416b4955d7ceee Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Fri, 17 Feb 2017 22:51:29 -0500 Subject: [PATCH 18/28] Variable names, fix edge avoidance, tooltips placement, RTL --- modules/modes/select.js | 19 +++--- modules/ui/edit_menu.js | 126 +++++++++++++++++++++++----------------- 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/modules/modes/select.js b/modules/modes/select.js index 681d39de5..655113764 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -153,16 +153,15 @@ export function modeSelect(context, selectedIDs) { suppressMenu = true; } else { var point = context.mouse(), - viewport = geoExtent(context.projection.clipExtent()).polygon(), - offset = [ - viewport[2][0] - point[0], - (viewport[1][1] - 30) - point[1] // 30 to account for the infoblock - ]; + viewport = geoExtent(context.projection.clipExtent()).polygon(); + // offset = [ + // viewport[2][0] - point[0], + // (viewport[1][1] - 30) - point[1] // 30 to account for the infoblock + // ]; if (geoPointInPolygon(point, viewport)) { - editMenu - .center(point) - .offset(offset); + editMenu.center(point); + // .offset(offset); } } } @@ -446,8 +445,8 @@ export function modeSelect(context, selectedIDs) { selectElements(); var show = d3.event; - var rtClick = d3.event && d3.event.type === 'contextmenu'; - + var rtClick = d3.event && d3.event.type === 'contextmenu'; + if (show) { positionMenu(); } diff --git a/modules/ui/edit_menu.js b/modules/ui/edit_menu.js index 7777f4699..490c4bf69 100644 --- a/modules/ui/edit_menu.js +++ b/modules/ui/edit_menu.js @@ -1,20 +1,27 @@ 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 rect, - menu, + var menu, center = [0, 0], offset = [0, 0], tooltip; - var p = 8, // top padding - l = 10, // left padding - h = 15, // height of icon - m = 4, // top margin - a1 = 2 * m + operations.length * (2 * p + h); + 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) { @@ -22,46 +29,48 @@ export function uiEditMenu(context, operations) { selection.node().parentNode.focus(); - function click(operation) { - d3.event.stopPropagation(); - if (operation.disabled()) return; - operation(); - editMenu.close(); + 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(' + [center[0] + l, center[1]] + ')') + .attr('transform', 'translate(' + origin + ')') .attr('opacity', 0); menu .transition() .attr('opacity', 1); - rect = menu - .append('g') - .attr('class', 'edit-menu-rectangle') - .attr('transform', function () { - var pos = [0, 0]; - if (offset[1] <= a1) { - pos = [0, offset[1] - a1]; - } - return 'translate(' + pos + ')'; - }); - - rect + menu .append('rect') .attr('class', 'edit-menu-background') .attr('x', 4) .attr('rx', 4) .attr('ry', 4) - .attr('width', 44) - .attr('height', a1) + .attr('width', menuWidth) + .attr('height', menuHeight) .attr('stroke-linecap', 'round'); - var button = rect.selectAll() + var button = menu.selectAll('.edit-menu-item') .data(operations) .enter() .append('g') @@ -70,14 +79,15 @@ export function uiEditMenu(context, operations) { .attr('transform', function (d, i) { return 'translate(' + geoRoundCoords([ 0, - m + i * (2 * p + h)]).join(',') + ')'; + m + i * buttonHeight + ]).join(',') + ')'; }); button .append('rect') .attr('x', 4) - .attr('width', 44) - .attr('height', 2 * p + h) + .attr('width', buttonWidth) + .attr('height', buttonHeight) .on('click', click) .on('mousedown', mousedown) .on('mouseover', mouseover) @@ -96,27 +106,44 @@ export function uiEditMenu(context, operations) { .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 + d3.event.stopPropagation(); // https://github.com/openstreetmap/iD/issues/1869 } function mouseover(d, i) { - var width = 260; - var rect = context.surfaceRect(), - pos = [ - offset[0] < width ? center[0] - 255 : center[0], - offset[1] <= a1 ? m + center[1] - (a1 - offset[1]) : m + center[1] - ], - top = rect.top + i * (2 * p + h) + pos[1], - left = rect.left + (64) + pos[0]; - var j = i; - // fix tooltip overflow on y axis - while (top - center[1] + 90 > offset[1] && j !== 0) { - top = rect.top + (--j) * (2 * p + h) + pos[1]; - } + 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('top', top + 'px') - .style('left', left+ 'px') + .style('left', tipX + 'px') + .style('top', tipY + 'px') .style('display', 'block') .html(uiTooltipHtml(d.tooltip(), d.keys[0], d.title)); } @@ -148,11 +175,6 @@ export function uiEditMenu(context, operations) { return editMenu; }; - editMenu.offset = function (_) { - if (!arguments.length) return offset; - offset = _; - return editMenu; - }; return editMenu; } From e756520bd8ef06bfd1b42efc70ba66877836dad7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 21 Feb 2017 15:47:22 -0500 Subject: [PATCH 19/28] suppressMenu(true) is now the default --- modules/behavior/draw_way.js | 4 +--- modules/behavior/select.js | 14 +++++++------- modules/modes/add_point.js | 2 +- modules/modes/drag_node.js | 2 +- modules/modes/move.js | 4 ++-- modules/modes/rotate.js | 4 ++-- modules/modes/select.js | 16 ++++++---------- modules/operations/delete.js | 4 +--- modules/operations/merge.js | 2 +- modules/renderer/map.js | 2 +- modules/ui/commit.js | 7 ++----- modules/ui/feature_list.js | 2 +- modules/ui/raw_member_editor.js | 2 +- modules/ui/raw_membership_editor.js | 4 ++-- modules/ui/selection_list.js | 4 ++-- 15 files changed, 31 insertions(+), 42 deletions(-) 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/select.js b/modules/behavior/select.js index 52ab75d30..c3d9c2e78 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -23,8 +23,8 @@ export function behaviorSelect(context) { function click() { - var rtClick = d3.event.type === 'contextmenu'; - + var rtClick = d3.event.type === 'contextmenu'; + if (rtClick) { d3.event.preventDefault(); } @@ -40,21 +40,21 @@ export function behaviorSelect(context) { context.enter(modeBrowse(context)); } else if (!d3.event.shiftKey && !lasso) { - // Reselect when 'rtClick on one of the selectedIDs' + // Reselect when 'rtClick on one of the selectedIDs' // OR 'leftClick on the same singular selected entity' // Explanation: leftClick should discard any multiple // selection of entities and make the selection singlular. - // Whereas rtClick should preserve multiple selection of + // Whereas rtClick should preserve multiple selection of // entities if and only if it clicks on one of the selectedIDs. - if (context.selectedIDs().indexOf(datum.id) >= 0 - && (rtClick || context.selectedIDs().length === 1)) { + if (context.selectedIDs().indexOf(datum.id) >= 0 && + (rtClick || context.selectedIDs().length === 1)) { mode.suppressMenu(false).reselect(); } else { context.enter(modeSelect(context, [datum.id])); } } else if (context.selectedIDs().indexOf(datum.id) >= 0) { if (rtClick) { // To prevent datum.id from being removed when rtClick - mode.suppressMenu(false).reselect(); + mode.suppressMenu(false).reselect(); } else { var selectedIDs = _.without(context.selectedIDs(), datum.id); context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); 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 655113764..afe12def3 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -56,7 +56,7 @@ export function modeSelect(context, selectedIDs) { inspector, editMenu, newFeature = false, - suppressMenu = false, + suppressMenu = true, follow = false; @@ -154,14 +154,9 @@ export function modeSelect(context, selectedIDs) { } else { var point = context.mouse(), viewport = geoExtent(context.projection.clipExtent()).polygon(); - // offset = [ - // viewport[2][0] - point[0], - // (viewport[1][1] - 30) - point[1] // 30 to account for the infoblock - // ]; if (geoPointInPolygon(point, viewport)) { editMenu.center(point); - // .offset(offset); } } } @@ -249,6 +244,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()), @@ -310,7 +306,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) ); } } @@ -322,7 +318,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) ); } } @@ -346,7 +342,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) ); } } @@ -370,7 +366,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) ); } } diff --git a/modules/operations/delete.js b/modules/operations/delete.js index ae8b28c29..6132f3685 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -44,9 +44,7 @@ export function operationDelete(selectedIDs, context) { 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)); } diff --git a/modules/operations/merge.js b/modules/operations/merge.js index b35d21973..afba24c72 100644 --- a/modules/operations/merge.js +++ b/modules/operations/merge.js @@ -30,7 +30,7 @@ export function operationMerge(selectedIDs, context) { var entity = context.hasEntity(id); return entity && entity.type !== 'node'; }); - context.enter(modeSelect(context, ids).suppressMenu(true)); + context.enter(modeSelect(context, ids)); }; diff --git a/modules/renderer/map.js b/modules/renderer/map.js index d9c93bd27..845600805 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -77,7 +77,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) { 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/feature_list.js b/modules/ui/feature_list.js index f5e27572d..0a48a9466 100644 --- a/modules/ui/feature_list.js +++ b/modules/ui/feature_list.js @@ -265,7 +265,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/raw_member_editor.js b/modules/ui/raw_member_editor.js index 6a3f50eb6..1961a09ba 100644 --- a/modules/ui/raw_member_editor.js +++ b/modules/ui/raw_member_editor.js @@ -17,7 +17,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 0bb003897..49ece9206 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/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)); } From 4f8d772397a1be6c7d8fdaa63e67de5eca65845a Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 21 Feb 2017 22:16:17 -0500 Subject: [PATCH 20/28] More improvements to select behavior re contextmenu, shiftclick, etc. --- modules/behavior/select.js | 127 +++++++++++++++++++++++++---------- modules/modes/select.js | 26 +++---- test/spec/behavior/select.js | 24 +++++-- 3 files changed, 120 insertions(+), 57 deletions(-) diff --git a/modules/behavior/select.js b/modules/behavior/select.js index c3d9c2e78..72c28fcc0 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,46 +27,92 @@ export function behaviorSelect(context) { } - function click() { - var rtClick = d3.event.type === 'contextmenu'; + function point() { + return d3.mouse(context.container().node()); + } - if (rtClick) { - d3.event.preventDefault(); + + 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); + } + + + function mouseup() { + click(); + } + + + function click() { + 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 datum = d3.event.target.__data__, - lasso = d3.select('#surface .lasso').node(), + isMultiselect = d3.event.shiftKey || d3.select('#surface .lasso').node(), 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) { - // Reselect when 'rtClick on one of the selectedIDs' - // OR 'leftClick on the same singular selected entity' - // Explanation: leftClick should discard any multiple - // selection of entities and make the selection singlular. - // Whereas rtClick should preserve multiple selection of - // entities if and only if it clicks on one of the selectedIDs. - if (context.selectedIDs().indexOf(datum.id) >= 0 && - (rtClick || context.selectedIDs().length === 1)) { - mode.suppressMenu(false).reselect(); - } else { - context.enter(modeSelect(context, [datum.id])); - } - } else if (context.selectedIDs().indexOf(datum.id) >= 0) { - if (rtClick) { // To prevent datum.id from being removed when rtClick - mode.suppressMenu(false).reselect(); - } else { - var selectedIDs = _.without(context.selectedIDs(), datum.id); - context.enter(selectedIDs.length ? modeSelect(context, selectedIDs) : modeBrowse(context)); + 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 { - context.enter(modeSelect(context, context.selectedIDs().concat([datum.id]))); + // clicked an entity.. + var selectedIDs = context.selectedIDs(); + + if (!isMultiselect) { + if (selectedIDs.length > 1 && !suppressMenu) { + // 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) { + // 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; } @@ -70,8 +121,9 @@ export function behaviorSelect(context) { .on('keydown.select', keydown) .on('keyup.select', keyup); - selection.on('click.select', click); - selection.on('contextmenu.select', click); + selection + .on('mousedown.select', mousedown) + .on('contextmenu.select', contextmenu); keydown(); }; @@ -80,9 +132,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/select.js b/modules/modes/select.js index afe12def3..2aa34a793 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -193,7 +193,7 @@ export function modeSelect(context, selectedIDs) { } positionMenu(); - if (d3.event && d3.event.type === 'contextmenu') { + if (!suppressMenu) { showMenu(); } }; @@ -438,13 +438,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; - var rtClick = d3.event && d3.event.type === 'contextmenu'; - - if (show) { - positionMenu(); + if (selectedIDs.length > 1) { + var entities = uiSelectionList(context, selectedIDs); + context.ui().sidebar.show(entities); } if (follow) { @@ -460,18 +462,12 @@ export function modeSelect(context, selectedIDs) { } timeout = window.setTimeout(function() { - if (!suppressMenu && rtClick) { + 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); - } }; 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]); }); }); From f7c3de9545b422d700ed3ba3fbb51b14e79591a4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Feb 2017 16:50:42 -0500 Subject: [PATCH 21/28] Preserve backward compability with radial menu Old menu behavior can be restored with 2 cookies: - `edit-menu-style=radial` - Display menu as a radial menu, limited to 8 items - `edit-menu-show-always=1` - Show menu on all clicks, not just contextmenu/right --- modules/behavior/select.js | 12 ++++++++---- modules/modes/select.js | 25 ++++++++++++++++++++----- modules/renderer/map.js | 3 ++- modules/ui/intro/point.js | 5 +++-- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 72c28fcc0..59273d144 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -44,6 +44,9 @@ export function behaviorSelect(context) { if (!p1) p1 = point(); d3.select(window) .on('mouseup.select', mouseup, true); + + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + suppressMenu = !isShowAlways; } @@ -65,8 +68,9 @@ export function behaviorSelect(context) { return; } - var datum = d3.event.target.__data__, - isMultiselect = d3.event.shiftKey || d3.select('#surface .lasso').node(), + 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(); @@ -84,7 +88,7 @@ export function behaviorSelect(context) { var selectedIDs = context.selectedIDs(); if (!isMultiselect) { - if (selectedIDs.length > 1 && !suppressMenu) { + if (selectedIDs.length > 1 && (!suppressMenu && !isShowAlways)) { // multiple things already selected, just show the menu... mode.suppressMenu(false).reselect(); } else { @@ -95,7 +99,7 @@ export function behaviorSelect(context) { } else { if (selectedIDs.indexOf(datum.id) !== -1) { // clicked entity is already in the selectedIDs list.. - if (!suppressMenu) { + if (!suppressMenu && !isShowAlways) { // don't deselect clicked entity, just show the menu. mode.suppressMenu(false).reselect(); } else { diff --git a/modules/modes/select.js b/modules/modes/select.js index 2aa34a793..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 { uiEditMenu, 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; @@ -171,7 +174,8 @@ export function modeSelect(context, selectedIDs) { function toggleMenu() { - if (d3.select('.edit-menu').empty()) { + // deprecation warning - Radial Menu to be removed in iD v3 + if (d3.select('.edit-menu, .radial-menu').empty()) { showMenu(); } else { closeMenu(); @@ -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); - editMenu = uiEditMenu(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); diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 845600805..d26de2017 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -309,7 +309,8 @@ export function rendererMap(context) { function resetTransform() { if (!transformed) return false; - surface.selectAll('.edit-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; diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js index eff7a5f3d..14b8e235d 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); @@ -143,7 +143,8 @@ export function uiIntroPoint(context, reveal) { context.history().on('change.intro', deleted); setTimeout(function() { - var node = d3.select('.edit-menu-item-delete').node(); + // deprecation warning - Radial Menu to be removed in iD v3 + var node = d3.select('.edit-menu-item-delete, .radial-menu-item-delete').node(); var pointBox = pad(node.getBoundingClientRect(), 50, context); reveal(pointBox, t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); From dc2318ab634806f3d5c829152bd58230edf2514d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Feb 2017 17:31:08 -0500 Subject: [PATCH 22/28] Add mention of the right-click menu in the walkthrough --- data/core.yaml | 4 ++-- dist/locales/en.json | 4 ++-- modules/behavior/select.js | 1 + modules/ui/intro/point.js | 14 +++++++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 7373e5a16..5fb4a04de 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -814,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 d4f072c16..67c43b4b7 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -669,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/select.js b/modules/behavior/select.js index 59273d144..8d9727c33 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -35,6 +35,7 @@ export function behaviorSelect(context) { function contextmenu() { if (!p1) p1 = point(); d3.event.preventDefault(); + context.surface().node().focus(); suppressMenu = false; click(); } diff --git a/modules/ui/intro/point.js b/modules/ui/intro/point.js index 14b8e235d..54bf08c70 100644 --- a/modules/ui/intro/point.js +++ b/modules/ui/intro/point.js @@ -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}); }); } @@ -145,9 +145,13 @@ export function uiIntroPoint(context, reveal) { setTimeout(function() { // deprecation warning - Radial Menu to be removed in iD v3 var node = d3.select('.edit-menu-item-delete, .radial-menu-item-delete').node(); - var pointBox = pad(node.getBoundingClientRect(), 50, context); - reveal(pointBox, - t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); + if (!node) { + deletePoint(); + } else { + var pointBox = pad(node.getBoundingClientRect(), 50, context); + reveal(pointBox, + t('intro.points.delete', { button: icon('#operation-delete', 'pre-text') })); + } }, 300); } From 38f6d2d2bf52b665c7060b59739d268a75d1c86e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Feb 2017 22:02:35 -0500 Subject: [PATCH 23/28] Fix RTL for flash messages --- css/app.css | 66 +++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/css/app.css b/css/app.css index 20d786bc0..24c910914 100644 --- a/css/app.css +++ b/css/app.css @@ -2542,6 +2542,7 @@ img.tile-removing { height: 30px; } + #flash-wrap { display: flex; flex: 0 0 100%; @@ -2562,6 +2563,7 @@ img.tile-removing { height: 30px; } + #flash-wrap svg.operation-icon { flex: 0 0 auto; width: 20px; @@ -2630,6 +2632,9 @@ img.tile-removing { fill: #ccc; text-anchor: start; } +[dir='rtl'] #scale text { + text-anchor: end; +} #scale path { fill: none; @@ -2638,12 +2643,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; @@ -2651,12 +2663,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; @@ -2684,6 +2708,9 @@ img.tile-removing { color: #eee; flex: 1 1 auto; } +[dir='rtl'] .api-status { + text-align: left; +} .api-status.offline, .api-status.readonly, @@ -2698,6 +2725,7 @@ img.tile-removing { color: #ccf; } + /* Modals ------------------------------------------------------- */ @@ -3783,44 +3811,6 @@ img.tile-removing { -ms-filter: "FlipH"; } -[dir='rtl'] #flash-wrap .content { - display: flex; - flex-direction: row-reverse; -} - -/* 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 */ From 6ef85c4343f3c55b74b2c90465875e50250eb4a7 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Feb 2017 22:16:50 -0500 Subject: [PATCH 24/28] Fix RTL for scale bar --- css/app.css | 5 ++++- modules/ui/scale.js | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/css/app.css b/css/app.css index 24c910914..17b3f90ed 100644 --- a/css/app.css +++ b/css/app.css @@ -2621,6 +2621,9 @@ img.tile-removing { height: 30px; width: 100%; } +[dir='rtl'] #scale { + transform: scaleX(-1); +} #scale:hover { cursor: pointer; @@ -2633,7 +2636,7 @@ img.tile-removing { text-anchor: start; } [dir='rtl'] #scale text { - text-anchor: end; + transform: scaleX(-1); } #scale path { 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); From f291b0a1201b460bde327cb46dd581a48baa02ff Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 22 Feb 2017 22:32:19 -0500 Subject: [PATCH 25/28] Remove ineffective attempt to grab focus --- modules/behavior/select.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 8d9727c33..59273d144 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -35,7 +35,6 @@ export function behaviorSelect(context) { function contextmenu() { if (!p1) p1 = point(); d3.event.preventDefault(); - context.surface().node().focus(); suppressMenu = false; click(); } From 334188c6fefa79fe3ee37615322891c7f338a615 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Feb 2017 20:11:21 -0500 Subject: [PATCH 26/28] Normalize mousewheel zooming across browsers (closes #3029) --- modules/renderer/map.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index d26de2017..45ed64a48 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), @@ -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,6 +286,29 @@ 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 = [source.offsetX, source.offsetY], + 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 = transformLast = eventTransform; + } + if (ktoz(eventTransform.k * 2 * Math.PI) < minzoom) { surface.interrupt(); uiFlash().text(t('cannot_zoom')); From 5ba72292ac3ca0e3b03e2be0d256aa64656c4ba2 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 23 Feb 2017 21:45:36 -0500 Subject: [PATCH 27/28] Fix mouse xy calculation for mousewheel scroll normalization --- modules/renderer/map.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 45ed64a48..b51bdd475 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -293,20 +293,19 @@ export function rendererMap(context) { // 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 = [source.offsetX, source.offsetY], + 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 = transformLast = eventTransform; + _selection.node().__zoom = eventTransform; } if (ktoz(eventTransform.k * 2 * Math.PI) < minzoom) { @@ -325,6 +324,7 @@ export function rendererMap(context) { tY = (eventTransform.y / scale - transformStart.y) * scale; transformed = true; + transformLast = eventTransform; utilSetTransform(supersurface, tX, tY, scale); queueRedraw(); From bf71756b50d41c9dd6a21fd2ae1b675452c133f4 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sat, 25 Feb 2017 15:27:32 -0500 Subject: [PATCH 28/28] Quote "-apple-system" to make IE11 happy --- css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/app.css b/css/app.css index 17b3f90ed..716c46fba 100644 --- a/css/app.css +++ b/css/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;