From db9eed2434414cc918b098a7f81cfbc746f2e89d Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 14 May 2020 15:49:35 -0400 Subject: [PATCH] Move the edit menu logic to uiInit Make context the first argument of operation objects Add Paste operation to edit menu when opening the context menu on a blank area of the map (close #2508) --- data/core.yaml | 9 ++ dist/locales/en.json | 12 +++ modules/behavior/paste.js | 8 +- modules/behavior/select.js | 27 ++---- modules/modes/browse.js | 6 ++ modules/modes/move.js | 12 +-- modules/modes/rotate.js | 12 +-- modules/modes/select.js | 71 +++------------ modules/operations/circularize.js | 2 +- modules/operations/continue.js | 2 +- modules/operations/copy.js | 4 +- modules/operations/delete.js | 2 +- modules/operations/disconnect.js | 2 +- modules/operations/downgrade.js | 2 +- modules/operations/extract.js | 2 +- modules/operations/index.js | 1 + modules/operations/merge.js | 2 +- modules/operations/move.js | 2 +- modules/operations/orthogonalize.js | 2 +- modules/operations/paste.js | 95 ++++++++++++++++++++ modules/operations/reflect.js | 10 +-- modules/operations/reverse.js | 2 +- modules/operations/rotate.js | 2 +- modules/operations/split.js | 2 +- modules/operations/straighten.js | 2 +- modules/ui/init.js | 43 +++++++++ modules/ui/preset_list.js | 2 +- modules/validations/disconnected_way.js | 2 +- modules/validations/missing_tag.js | 4 +- svg/iD-sprite/operations/operation-paste.svg | 2 +- test/spec/operations/extract.js | 28 +++--- test/spec/operations/straighten.js | 30 +++---- 32 files changed, 256 insertions(+), 148 deletions(-) create mode 100644 modules/operations/paste.js diff --git a/data/core.yaml b/data/core.yaml index 327c8f78dd..bd98e79a48 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -88,6 +88,15 @@ en: annotation: single: Copied a feature. multiple: "Copied {n} features." + paste: + title: Paste + description: + single: "Add a duplicate {feature} here." + multiple: "Add {n} duplicate features here." + annotation: + single: Pasted a feature. + multiple: "Pasted {n} features." + nothing_copied: No features have been copied. circularize: title: Circularize description: diff --git a/dist/locales/en.json b/dist/locales/en.json index 5d48623946..e1aa33cb7f 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -114,6 +114,18 @@ "multiple": "Copied {n} features." } }, + "paste": { + "title": "Paste", + "description": { + "single": "Add a duplicate {feature} here.", + "multiple": "Add {n} duplicate features here." + }, + "annotation": { + "single": "Pasted a feature.", + "multiple": "Pasted {n} features." + }, + "nothing_copied": "No features have been copied." + }, "circularize": { "title": "Circularize", "description": { diff --git a/modules/behavior/paste.js b/modules/behavior/paste.js index 5976bd4f0a..f76cffee29 100644 --- a/modules/behavior/paste.js +++ b/modules/behavior/paste.js @@ -6,7 +6,7 @@ import { geoExtent, geoPointInPolygon, geoVecSubtract } from '../geo'; import { modeMove } from '../modes/move'; import { uiCmd } from '../ui/cmd'; - +// see also `operationPaste` export function behaviorPaste(context) { function doPaste() { @@ -22,13 +22,13 @@ export function behaviorPaste(context) { if (!geoPointInPolygon(mouse, viewport)) return; - var extent = geoExtent(); var oldIDs = context.copyIDs(); + if (!oldIDs.length) return; + + var extent = geoExtent(); var oldGraph = context.copyGraph(); var newIDs = []; - if (!oldIDs.length) return; - var action = actionCopyEntities(oldIDs, oldGraph); context.perform(action); diff --git a/modules/behavior/select.js b/modules/behavior/select.js index 2c36897413..f668f0c4af 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -1,7 +1,6 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; import { geoVecLength } from '../geo'; -import { prefs } from '../core/preferences'; import { modeBrowse } from '../modes/browse'; import { modeSelect } from '../modes/select'; import { modeSelectData } from '../modes/select_data'; @@ -12,8 +11,6 @@ import { utilFastMouse } from '../util/util'; export function behaviorSelect(context) { - // legacy option to show menu on every click - var _alwaysShowMenu = +prefs('edit-menu-show-always') === 1; var _tolerancePx = 4; var _lastPointerEvent = null; var _showMenu = false; @@ -35,7 +32,7 @@ export function behaviorSelect(context) { function keydown() { if (d3_event.keyCode === 32) { - // don't react to spacebar events during text input + // don't react to spacebar events during text input var activeNode = document.activeElement; if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return; } @@ -80,7 +77,6 @@ export function behaviorSelect(context) { } else if (d3_event.keyCode === 32) { // spacebar d3_event.preventDefault(); _lastInteractionType = 'spacebar'; - _showMenu = _alwaysShowMenu; click(); } } @@ -98,8 +94,6 @@ export function behaviorSelect(context) { d3_select(window) .on(_pointerPrefix + 'up.select', pointerup, true); - - _showMenu = _alwaysShowMenu; } @@ -176,6 +170,7 @@ export function behaviorSelect(context) { if (entity) datum = entity; if (datum && datum.type === 'midpoint') { + // treat targeting midpoints as if targeting the parent way datum = datum.parents[0]; } @@ -187,36 +182,27 @@ export function behaviorSelect(context) { context.selectedErrorID(null); if (!isMultiselect) { - if (selectedIDs.length > 1 && (_showMenu && !_alwaysShowMenu)) { - // multiple things already selected, just show the menu... - mode.reselect().showMenu(point, _lastInteractionType); - } else { + if (selectedIDs.length <= 1 || !_showMenu) { // always enter modeSelect even if the entity is already // selected since listeners may expect `context.enter` events, // e.g. in the walkthrough newMode = modeSelect(context, [datum.id]); context.enter(newMode); - if (_showMenu) newMode.showMenu(point, _lastInteractionType); } } else { if (selectedIDs.indexOf(datum.id) !== -1) { // clicked entity is already in the selectedIDs list.. - if (_showMenu && !_alwaysShowMenu) { - // don't deselect clicked entity, just show the menu. - mode.reselect().showMenu(point, _lastInteractionType); - } else { + if (!_showMenu) { // deselect clicked entity, then reenter select mode or return to browse mode.. selectedIDs = selectedIDs.filter(function(id) { return id !== 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]); newMode = modeSelect(context, selectedIDs); context.enter(newMode); - if (_showMenu) newMode.showMenu(point, _lastInteractionType); } } @@ -243,6 +229,11 @@ export function behaviorSelect(context) { } } + context.ui().closeEditMenu(); + + // always request to show the edit menu in case the mode needs it + if (_showMenu) context.ui().showEditMenu(point, _lastInteractionType); + resetProperties(); } diff --git a/modules/modes/browse.js b/modules/modes/browse.js index bae9dfec32..0f0af5100a 100644 --- a/modules/modes/browse.js +++ b/modules/modes/browse.js @@ -8,6 +8,7 @@ import { behaviorSelect } from '../behavior/select'; import { modeDragNode } from './drag_node'; import { modeDragNote } from './drag_note'; +import { operationPaste } from '../operations/paste'; export function modeBrowse(context) { var mode = { @@ -60,5 +61,10 @@ export function modeBrowse(context) { }; + mode.operations = function() { + return [operationPaste(context)]; + }; + + return mode; } diff --git a/modules/modes/move.js b/modules/modes/move.js index 83a94146fa..6a6a7e97ab 100644 --- a/modules/modes/move.js +++ b/modules/modes/move.js @@ -30,12 +30,12 @@ export function modeMove(context, entityIDs, baseGraph) { var keybinding = utilKeybinding('move'); var behaviors = [ behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior, - operationRotate(entityIDs, context).behavior + operationCircularize(context, entityIDs).behavior, + operationDelete(context, entityIDs).behavior, + operationOrthogonalize(context, entityIDs).behavior, + operationReflectLong(context, entityIDs).behavior, + operationReflectShort(context, entityIDs).behavior, + operationRotate(context, entityIDs).behavior ]; var annotation = entityIDs.length === 1 ? t('operations.move.annotation.' + context.graph().geometry(entityIDs[0])) : diff --git a/modules/modes/rotate.js b/modules/modes/rotate.js index cec58a65e8..d22bf8eeec 100644 --- a/modules/modes/rotate.js +++ b/modules/modes/rotate.js @@ -34,12 +34,12 @@ export function modeRotate(context, entityIDs) { var keybinding = utilKeybinding('rotate'); var behaviors = [ behaviorEdit(context), - operationCircularize(entityIDs, context).behavior, - operationDelete(entityIDs, context).behavior, - operationMove(entityIDs, context).behavior, - operationOrthogonalize(entityIDs, context).behavior, - operationReflectLong(entityIDs, context).behavior, - operationReflectShort(entityIDs, context).behavior + operationCircularize(context, entityIDs).behavior, + operationDelete(context, entityIDs).behavior, + operationMove(context, entityIDs).behavior, + operationOrthogonalize(context, entityIDs).behavior, + operationReflectLong(context, entityIDs).behavior, + operationReflectShort(context, entityIDs).behavior ]; var annotation = entityIDs.length === 1 ? t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) : diff --git a/modules/modes/select.js b/modules/modes/select.js index 94598f14be..11572c71c9 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -17,7 +17,6 @@ import { modeDragNode } from './drag_node'; import { modeDragNote } from './drag_note'; import { osmNode, osmWay } from '../osm'; import * as Operations from '../operations/index'; -import { uiEditMenu } from '../ui/edit_menu'; import { uiCmd } from '../ui/cmd'; import { utilArrayIntersection, utilDeepMemberSelector, utilEntityOrDeepMemberSelector, @@ -46,7 +45,6 @@ export function modeSelect(context, selectedIDs) { modeDragNote(context).behavior ]; var inspector; // unused? - var _editMenu; // uiEditMenu var _newFeature = false; var _follow = false; @@ -141,44 +139,6 @@ export function modeSelect(context, selectedIDs) { } - function closeMenu() { - // remove any existing menu no matter how it was added - context.map().supersurface - .select('.edit-menu').remove(); - } - - mode.showMenu = function(anchorPoint, triggerType) { - - // remove any displayed menu - closeMenu(); - - // disable menu if in wide selection, for example - if (!context.map().editableDataEnabled()) return; - - // don't show the menu for relations alone - if (selectedIDs.every(function(id) { - return context.graph().geometry(id) === 'relation'; - })) return; - - var surfaceNode = context.surface().node(); - if (surfaceNode.focus) { // FF doesn't support it - // focus the surface or else clicking off the menu may not trigger modeBrowse - surfaceNode.focus(); - } - - // don't load the menu until it's needed - if (!_editMenu) _editMenu = uiEditMenu(context); - - _editMenu - .anchorLoc(anchorPoint) - .triggerType(triggerType) - .operations(operations); - - // render the menu - context.map().supersurface.call(_editMenu); - }; - - mode.selectedIDs = function() { return selectedIDs; }; @@ -189,12 +149,6 @@ export function modeSelect(context, selectedIDs) { }; - mode.reselect = function() { - if (!checkSelectedIDs()) return; - return mode; - }; - - mode.newFeature = function(val) { if (!arguments.length) return _newFeature; _newFeature = val; @@ -219,12 +173,12 @@ export function modeSelect(context, selectedIDs) { }); operations = Object.values(Operations) - .map(function(o) { return o(selectedIDs, context); }) + .map(function(o) { return o(context, selectedIDs); }) .filter(function(o) { return o.available() && o.id !== 'delete' && o.id !== 'downgrade'; }); - var downgradeOperation = Operations.operationDowngrade(selectedIDs, context); + var downgradeOperation = Operations.operationDowngrade(context, selectedIDs); // don't allow delete if downgrade is available - var lastOperation = !context.inIntro() && downgradeOperation.available() ? downgradeOperation : Operations.operationDelete(selectedIDs, context); + var lastOperation = !context.inIntro() && downgradeOperation.available() ? downgradeOperation : Operations.operationDelete(context, selectedIDs); operations.push(lastOperation); @@ -235,9 +189,13 @@ export function modeSelect(context, selectedIDs) { }); // remove any displayed menu - closeMenu(); + context.ui().closeEditMenu(); } + mode.operations = function() { + return operations; + }; + mode.enter = function() { if (!checkSelectedIDs()) return; @@ -269,11 +227,10 @@ export function modeSelect(context, selectedIDs) { // reselect after change in case relation members were removed or added selectElements(); }) - .on('undone.select', update) - .on('redone.select', update); + .on('undone.select', checkSelectedIDs) + .on('redone.select', checkSelectedIDs); context.map() - .on('move.select', closeMenu) .on('drawn.select', selectElements) .on('crossEditableZoom.select', function() { selectElements(); @@ -301,12 +258,6 @@ export function modeSelect(context, selectedIDs) { } - function update() { - closeMenu(); - checkSelectedIDs(); - } - - function didDoubleUp(loc) { if (!context.map().withinEditableZoom()) return; @@ -500,7 +451,7 @@ export function modeSelect(context, selectedIDs) { d3_select(document) .call(keybinding.unbind); - closeMenu(); + context.ui().closeEditMenu(); context.history() .on('change.select', null) diff --git a/modules/operations/circularize.js b/modules/operations/circularize.js index 00aa183217..0ecbe3d1a2 100644 --- a/modules/operations/circularize.js +++ b/modules/operations/circularize.js @@ -4,7 +4,7 @@ import { behaviorOperation } from '../behavior/operation'; import { utilGetAllNodes } from '../util'; -export function operationCircularize(selectedIDs, context) { +export function operationCircularize(context, selectedIDs) { var _extent; var _actions = selectedIDs.map(getAction).filter(Boolean); var _amount = _actions.length === 1 ? 'single' : 'multiple'; diff --git a/modules/operations/continue.js b/modules/operations/continue.js index d685f9e338..90bc32fb19 100644 --- a/modules/operations/continue.js +++ b/modules/operations/continue.js @@ -4,7 +4,7 @@ import { behaviorOperation } from '../behavior/operation'; import { utilArrayGroupBy } from '../util'; -export function operationContinue(selectedIDs, context) { +export function operationContinue(context, selectedIDs) { var graph = context.graph(); var entities = selectedIDs.map(function(id) { return graph.entity(id); }); var geometries = Object.assign( diff --git a/modules/operations/copy.js b/modules/operations/copy.js index 95372463dc..270ddf7327 100644 --- a/modules/operations/copy.js +++ b/modules/operations/copy.js @@ -5,7 +5,7 @@ import { behaviorOperation } from '../behavior/operation'; import { uiCmd } from '../ui/cmd'; import { utilArrayGroupBy } from '../util'; -export function operationCopy(selectedIDs, context) { +export function operationCopy(context, selectedIDs) { function getFilteredIdsToCopy() { return selectedIDs.filter(function(selectedID) { @@ -112,7 +112,7 @@ export function operationCopy(selectedIDs, context) { operation.annotation = function() { return selectedIDs.length === 1 ? t('operations.copy.annotation.single') : - t('operations.copy.annotation.multiple', { n: selectedIDs.length }); + t('operations.copy.annotation.multiple', { n: selectedIDs.length.toString() }); }; diff --git a/modules/operations/delete.js b/modules/operations/delete.js index f9b79f1fc7..1ff66aa842 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -8,7 +8,7 @@ import { uiCmd } from '../ui/cmd'; import { utilGetAllNodes } from '../util'; -export function operationDelete(selectedIDs, context) { +export function operationDelete(context, selectedIDs) { var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'); var action = actionDeleteMultiple(selectedIDs); var nodes = utilGetAllNodes(selectedIDs, context.graph()); diff --git a/modules/operations/disconnect.js b/modules/operations/disconnect.js index 52e34aa64c..33288a29f3 100644 --- a/modules/operations/disconnect.js +++ b/modules/operations/disconnect.js @@ -4,7 +4,7 @@ import { behaviorOperation } from '../behavior/operation'; import { utilGetAllNodes } from '../util/index'; -export function operationDisconnect(selectedIDs, context) { +export function operationDisconnect(context, selectedIDs) { var vertexIDs = []; var wayIDs = []; var otherIDs = []; diff --git a/modules/operations/downgrade.js b/modules/operations/downgrade.js index 2b76a0f2b5..31c3745942 100644 --- a/modules/operations/downgrade.js +++ b/modules/operations/downgrade.js @@ -5,7 +5,7 @@ import { t } from '../core/localizer'; import { uiCmd } from '../ui/cmd'; import { presetManager } from '../presets'; -export function operationDowngrade(selectedIDs, context) { +export function operationDowngrade(context, selectedIDs) { var affectedFeatureCount = 0; var downgradeType; diff --git a/modules/operations/extract.js b/modules/operations/extract.js index c71c087f44..be0409d84d 100644 --- a/modules/operations/extract.js +++ b/modules/operations/extract.js @@ -4,7 +4,7 @@ import { modeSelect } from '../modes/select'; import { t } from '../core/localizer'; import { presetManager } from '../presets'; -export function operationExtract(selectedIDs, context) { +export function operationExtract(context, selectedIDs) { var entityID = selectedIDs.length && selectedIDs[0]; var action = actionExtract(entityID); diff --git a/modules/operations/index.js b/modules/operations/index.js index 6584fa2949..c76dd5a1f4 100644 --- a/modules/operations/index.js +++ b/modules/operations/index.js @@ -8,6 +8,7 @@ export { operationExtract } from './extract'; export { operationMerge } from './merge'; export { operationMove } from './move'; export { operationOrthogonalize } from './orthogonalize'; +export { operationPaste } from './paste'; export { operationReflectShort, operationReflectLong } from './reflect'; export { operationReverse } from './reverse'; export { operationRotate } from './rotate'; diff --git a/modules/operations/merge.js b/modules/operations/merge.js index da442715b4..de4ba276f1 100644 --- a/modules/operations/merge.js +++ b/modules/operations/merge.js @@ -9,7 +9,7 @@ import { behaviorOperation } from '../behavior/operation'; import { modeSelect } from '../modes/select'; import { presetManager } from '../presets'; -export function operationMerge(selectedIDs, context) { +export function operationMerge(context, selectedIDs) { var join = actionJoin(selectedIDs); var merge = actionMerge(selectedIDs); diff --git a/modules/operations/move.js b/modules/operations/move.js index 8f1e1f1ed9..b2ae570161 100644 --- a/modules/operations/move.js +++ b/modules/operations/move.js @@ -5,7 +5,7 @@ import { modeMove } from '../modes/move'; import { utilGetAllNodes } from '../util'; -export function operationMove(selectedIDs, context) { +export function operationMove(context, selectedIDs) { var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'); var nodes = utilGetAllNodes(selectedIDs, context.graph()); var coords = nodes.map(function(n) { return n.loc; }); diff --git a/modules/operations/orthogonalize.js b/modules/operations/orthogonalize.js index 68d362d43b..d5ad69d171 100644 --- a/modules/operations/orthogonalize.js +++ b/modules/operations/orthogonalize.js @@ -4,7 +4,7 @@ import { behaviorOperation } from '../behavior/operation'; import { utilGetAllNodes } from '../util'; -export function operationOrthogonalize(selectedIDs, context) { +export function operationOrthogonalize(context, selectedIDs) { var _extent; var _type; var _actions = selectedIDs.map(chooseAction).filter(Boolean); diff --git a/modules/operations/paste.js b/modules/operations/paste.js new file mode 100644 index 0000000000..8b36b3f61a --- /dev/null +++ b/modules/operations/paste.js @@ -0,0 +1,95 @@ + +import { actionCopyEntities } from '../actions/copy_entities'; +import { actionMove } from '../actions/move'; +import { modeSelect } from '../modes/select'; +import { geoExtent, geoVecSubtract } from '../geo'; +import { t } from '../core/localizer'; +import { uiCmd } from '../ui/cmd'; +import { utilDisplayLabel } from '../util/util'; + +// see also `behaviorPaste` +export function operationPaste(context) { + + var _point; + + var operation = function() { + + if (!_point) return; + + var oldIDs = context.copyIDs(); + if (!oldIDs.length) return; + + var projection = context.projection; + var extent = geoExtent(); + var oldGraph = context.copyGraph(); + var newIDs = []; + + var action = actionCopyEntities(oldIDs, oldGraph); + context.perform(action); + + var copies = action.copies(); + var originals = new Set(); + Object.values(copies).forEach(function(entity) { originals.add(entity.id); }); + + for (var id in copies) { + var oldEntity = oldGraph.entity(id); + var newEntity = copies[id]; + + extent._extend(oldEntity.extent(oldGraph)); + + // Exclude child nodes from newIDs if their parent way was also copied. + var parents = context.graph().parentWays(newEntity); + var parentCopied = parents.some(function(parent) { + return originals.has(parent.id); + }); + + if (!parentCopied) { + newIDs.push(newEntity.id); + } + } + + // Put pasted objects where mouse pointer is.. + var center = projection(extent.center()); + var delta = geoVecSubtract(_point, center); + + context.replace(actionMove(newIDs, delta, projection), operation.annotation()); + context.enter(modeSelect(context, newIDs)); + }; + + operation.point = function(val) { + _point = val; + return operation; + }; + + operation.available = function() { + return context.mode().id === 'browse'; + }; + + operation.disabled = function() { + return !context.copyIDs().length; + }; + + operation.tooltip = function() { + var oldGraph = context.copyGraph(); + var ids = context.copyIDs(); + if (!ids.length) { + return t('operations.paste.nothing_copied'); + } + return ids.length === 1 ? + t('operations.paste.description.single', { feature: utilDisplayLabel(oldGraph.entity(ids[0]), oldGraph) }) : + t('operations.paste.description.multiple', { n: ids.length.toString() }); + }; + + operation.annotation = function() { + var ids = context.copyIDs(); + return ids.length === 1 ? + t('operations.paste.annotation.single') : + t('operations.paste.annotation.multiple', { n: ids.length.toString() }); + }; + + operation.id = 'paste'; + operation.keys = [uiCmd('⌘V')]; + operation.title = t('operations.paste.title'); + + return operation; +} diff --git a/modules/operations/reflect.js b/modules/operations/reflect.js index 2aebcb1b6e..22b7534b66 100644 --- a/modules/operations/reflect.js +++ b/modules/operations/reflect.js @@ -5,17 +5,17 @@ import { geoExtent } from '../geo'; import { utilGetAllNodes } from '../util'; -export function operationReflectShort(selectedIDs, context) { - return operationReflect(selectedIDs, context, 'short'); +export function operationReflectShort(context, selectedIDs) { + return operationReflect(context, selectedIDs, 'short'); } -export function operationReflectLong(selectedIDs, context) { - return operationReflect(selectedIDs, context, 'long'); +export function operationReflectLong(context, selectedIDs) { + return operationReflect(context, selectedIDs, 'long'); } -export function operationReflect(selectedIDs, context, axis) { +export function operationReflect(context, selectedIDs, axis) { axis = axis || 'long'; var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'); var nodes = utilGetAllNodes(selectedIDs, context.graph()); diff --git a/modules/operations/reverse.js b/modules/operations/reverse.js index bcc0a93209..16e854826d 100644 --- a/modules/operations/reverse.js +++ b/modules/operations/reverse.js @@ -3,7 +3,7 @@ import { actionReverse } from '../actions/reverse'; import { behaviorOperation } from '../behavior/operation'; -export function operationReverse(selectedIDs, context) { +export function operationReverse(context, selectedIDs) { var operation = function() { context.perform(function combinedReverseAction(graph) { diff --git a/modules/operations/rotate.js b/modules/operations/rotate.js index 03aa25460b..61ee33a1f8 100644 --- a/modules/operations/rotate.js +++ b/modules/operations/rotate.js @@ -5,7 +5,7 @@ import { modeRotate } from '../modes/rotate'; import { utilGetAllNodes } from '../util'; -export function operationRotate(selectedIDs, context) { +export function operationRotate(context, selectedIDs) { var multi = (selectedIDs.length === 1 ? 'single' : 'multiple'); var nodes = utilGetAllNodes(selectedIDs, context.graph()); var coords = nodes.map(function(n) { return n.loc; }); diff --git a/modules/operations/split.js b/modules/operations/split.js index fb0444f0f7..14f7cf496b 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -4,7 +4,7 @@ import { behaviorOperation } from '../behavior/operation'; import { modeSelect } from '../modes/select'; -export function operationSplit(selectedIDs, context) { +export function operationSplit(context, selectedIDs) { var vertices = selectedIDs .filter(function(id) { return context.graph().geometry(id) === 'vertex'; }); var entityID = vertices[0]; diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index 09a5d63cf1..51951d5009 100644 --- a/modules/operations/straighten.js +++ b/modules/operations/straighten.js @@ -5,7 +5,7 @@ import { behaviorOperation } from '../behavior/operation'; import { utilArrayDifference, utilGetAllNodes } from '../util/index'; -export function operationStraighten(selectedIDs, context) { +export function operationStraighten(context, selectedIDs) { var wayIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'w'; }); var nodeIDs = selectedIDs.filter(function(id) { return id.charAt(0) === 'n'; }); diff --git a/modules/ui/init.js b/modules/ui/init.js index a5525eaead..0e9eaabb8f 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -14,6 +14,7 @@ import { utilGetDimensions } from '../util/dimensions'; import { uiAccount } from './account'; import { uiAttribution } from './attribution'; import { uiContributors } from './contributors'; +import { uiEditMenu } from './edit_menu'; import { uiFeatureInfo } from './feature_info'; import { uiFlash } from './flash'; import { uiFullScreen } from './full_screen'; @@ -548,6 +549,48 @@ export function uiInit(context) { }; + var _editMenu; // uiEditMenu + + ui.showEditMenu = function(anchorPoint, triggerType, operations) { + + // remove any displayed menu + ui.closeEditMenu(); + + if (!operations && context.mode().operations) operations = context.mode().operations(); + if (!operations || !operations.length) return; + + // disable menu if in wide selection, for example + if (!context.map().editableDataEnabled()) return; + + var surfaceNode = context.surface().node(); + if (surfaceNode.focus) { // FF doesn't support it + // focus the surface or else clicking off the menu may not trigger modeBrowse + surfaceNode.focus(); + } + + // don't load the menu until it's needed + if (!_editMenu) _editMenu = uiEditMenu(context); + + operations.forEach(function(operation) { + if (operation.point) operation.point(anchorPoint); + }); + + _editMenu + .anchorLoc(anchorPoint) + .triggerType(triggerType) + .operations(operations); + + // render the menu + context.map().supersurface.call(_editMenu); + }; + + ui.closeEditMenu = function() { + // remove any existing menu no matter how it was added + context.map().supersurface + .select('.edit-menu').remove(); + }; + + var _saveLoading = d3_select(null); context.uploader() diff --git a/modules/ui/preset_list.js b/modules/ui/preset_list.js index d3cca64b84..8e6aa8629c 100644 --- a/modules/ui/preset_list.js +++ b/modules/ui/preset_list.js @@ -53,7 +53,7 @@ export function uiPresetList(context) { d3_event.keyCode === utilKeybinding.keyCodes['⌦'])) { d3_event.preventDefault(); d3_event.stopPropagation(); - operationDelete(_entityIDs, context)(); + operationDelete(context, _entityIDs)(); // hack to let undo work when search is autofocused } else if (search.property('value').length === 0 && diff --git a/modules/validations/disconnected_way.js b/modules/validations/disconnected_way.js index b895ca5aed..4f9afda8ea 100644 --- a/modules/validations/disconnected_way.js +++ b/modules/validations/disconnected_way.js @@ -65,7 +65,7 @@ export function validationDisconnectedWay() { entityIds: [singleEntity.id], onClick: function(context) { var id = this.issue.entityIds[0]; - var operation = operationDelete([id], context); + var operation = operationDelete(context, [id]); if (!operation.disabled()) { operation(); } diff --git a/modules/validations/missing_tag.js b/modules/validations/missing_tag.js index efc7610b01..460cd352a5 100644 --- a/modules/validations/missing_tag.js +++ b/modules/validations/missing_tag.js @@ -107,12 +107,12 @@ export function validationMissingTag(context) { var deleteOnClick; var id = this.entityIds[0]; - var operation = operationDelete([id], context); + var operation = operationDelete(context, [id]); var disabledReasonID = operation.disabled(); if (!disabledReasonID) { deleteOnClick = function(context) { var id = this.issue.entityIds[0]; - var operation = operationDelete([id], context); + var operation = operationDelete(context, [id]); if (!operation.disabled()) { operation(); } diff --git a/svg/iD-sprite/operations/operation-paste.svg b/svg/iD-sprite/operations/operation-paste.svg index 68f1e37c49..ee898b03f0 100644 --- a/svg/iD-sprite/operations/operation-paste.svg +++ b/svg/iD-sprite/operations/operation-paste.svg @@ -2,6 +2,6 @@ - + diff --git a/test/spec/operations/extract.js b/test/spec/operations/extract.js index 3985e51d43..bc9727cdfd 100644 --- a/test/spec/operations/extract.js +++ b/test/spec/operations/extract.js @@ -37,52 +37,52 @@ describe('iD.operationExtract', function () { }); it('is not available for no selected ids', function () { - var result = iD.operationExtract([], fakeContext).available(); + var result = iD.operationExtract(fakeContext, []).available(); expect(result).to.be.not.ok; }); it('is not available for two selected ids', function () { - var result = iD.operationExtract(['a', 'b'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['a', 'b']).available(); expect(result).to.be.not.ok; }); it('is not available for unknown selected id', function () { - var result = iD.operationExtract(['z'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['z']).available(); expect(result).to.be.not.ok; }); it('is not available for selected way', function () { - var result = iD.operationExtract(['x'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['x']).available(); expect(result).to.be.not.ok; }); it('is not available for selected node with tags, no parent way', function () { - var result = iD.operationExtract(['e'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['e']).available(); expect(result).to.be.not.ok; }); it('is not available for selected node with no tags, no parent way', function () { - var result = iD.operationExtract(['f'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['f']).available(); expect(result).to.be.not.ok; }); it('is not available for selected node with no tags, parent way', function () { - var result = iD.operationExtract(['c'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['c']).available(); expect(result).to.be.not.ok; }); it('is not available for selected node with no tags, two parent ways', function () { - var result = iD.operationExtract(['d'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['d']).available(); expect(result).to.be.not.ok; }); it('is available for selected node with tags, parent way', function () { - var result = iD.operationExtract(['a'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['a']).available(); expect(result).to.be.ok; }); it('is available for selected node with tags, two parent ways', function () { - var result = iD.operationExtract(['b'], fakeContext).available(); + var result = iD.operationExtract(fakeContext, ['b']).available(); expect(result).to.be.ok; }); }); @@ -96,7 +96,7 @@ describe('iD.operationExtract', function () { iD.osmNode(createFakeNode('c', false)), iD.osmWay({ id: 'x', nodes: ['a', 'b', 'c'] }) ]); - var result = iD.operationExtract(['b'], fakeContext).disabled(); + var result = iD.operationExtract(fakeContext, ['b']).disabled(); expect(result).to.be.not.ok; }); @@ -108,7 +108,7 @@ describe('iD.operationExtract', function () { iD.osmWay({ id: 'x', nodes: ['a', 'b', 'c'] }), iD.osmRelation({ id: 'r', members: [{ id: 'b', role: 'label' }] }) ]); - var result = iD.operationExtract(['b'], fakeContext).disabled(); + var result = iD.operationExtract(fakeContext, ['b']).disabled(); expect(result).to.be.not.ok; }); @@ -133,7 +133,7 @@ describe('iD.operationExtract', function () { ] }) ]); - var result = iD.operationExtract(['d'], fakeContext).disabled(); + var result = iD.operationExtract(fakeContext, ['d']).disabled(); expect(result).to.eql('restriction'); }); @@ -159,7 +159,7 @@ describe('iD.operationExtract', function () { ] }) ]); - var result = iD.operationExtract(['d'], fakeContext).disabled(); + var result = iD.operationExtract(fakeContext, ['d']).disabled(); expect(result).to.eql('restriction'); }); }); diff --git a/test/spec/operations/straighten.js b/test/spec/operations/straighten.js index 5d62cf229d..f3ac89d858 100644 --- a/test/spec/operations/straighten.js +++ b/test/spec/operations/straighten.js @@ -42,77 +42,77 @@ describe('iD.operationStraighten', function () { }); it('is not available for no selected ids', function () { - var result = iD.operationStraighten([], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, []).available(); expect(result).to.be.not.ok; }); it('is not available for way with only 2 nodes', function () { - var result = iD.operationStraighten(['w1'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w1']).available(); expect(result).to.be.not.ok; }); it('is available for way with only 2 nodes connected to another 2-node way', function () { - var result = iD.operationStraighten(['w1', 'w1-2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w1', 'w1-2']).available(); expect(result).to.be.ok; }); it('is not available for non-continuous ways', function () { - var result = iD.operationStraighten(['w2', 'w4'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w2', 'w4']).available(); expect(result).to.be.not.ok; }); it('is available for selected way with more than 2 nodes', function () { - var result = iD.operationStraighten(['w2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w2']).available(); expect(result).to.be.ok; }); it('is available for selected, ordered, continuous ways', function () { - var result = iD.operationStraighten(['w1', 'w2', 'w3'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w1', 'w2', 'w3']).available(); expect(result).to.be.ok; }); it('is available for selected, un-ordered, continuous ways', function () { - var result = iD.operationStraighten(['w1', 'w3', 'w2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w1', 'w3', 'w2']).available(); expect(result).to.be.ok; }); it('is available for selected, continuous ways with different way-directions', function () { - var result = iD.operationStraighten(['w1', 'w3', 'w2-2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w1', 'w3', 'w2-2']).available(); expect(result).to.be.ok; }); it('is available for 2 selected nodes in the same way, more than one node apart', function () { - var result = iD.operationStraighten(['w5', 'n9', 'n11'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w5', 'n9', 'n11']).available(); expect(result).to.be.ok; }); it('is available for 2 selected nodes in adjacent ways, more than one node apart', function () { - var result = iD.operationStraighten(['w2', 'w3', 'n5', 'n3'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w2', 'w3', 'n5', 'n3']).available(); expect(result).to.be.ok; }); it('is available for 2 selected nodes in non-adjacent ways, providing inbetween ways are selected', function () { - var result = iD.operationStraighten(['n2', 'n7', 'w4', 'w1', 'w3', 'w2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['n2', 'n7', 'w4', 'w1', 'w3', 'w2']).available(); expect(result).to.be.ok; }); it('is available for 2 selected nodes in non-adjacent, non-same-directional ways, providing inbetween ways are selected', function () { - var result = iD.operationStraighten(['n2', 'n7', 'w4', 'w1', 'w3', 'w2-2'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['n2', 'n7', 'w4', 'w1', 'w3', 'w2-2']).available(); expect(result).to.be.ok; }); it('is not available for nodes not on selected ways', function () { - var result = iD.operationStraighten(['w5', 'n4', 'n11'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w5', 'n4', 'n11']).available(); expect(result).to.be.not.ok; }); it('is not available for one selected node', function () { - var result = iD.operationStraighten(['w5', 'n9'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w5', 'n9']).available(); expect(result).to.be.not.ok; }); it('is not available for more than two selected nodes', function () { - var result = iD.operationStraighten(['w5', 'n9', 'n11', 'n12'], fakeContext).available(); + var result = iD.operationStraighten(fakeContext, ['w5', 'n9', 'n11', 'n12']).available(); expect(result).to.be.not.ok; }); });