diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf5cdf59..bc465e7cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :sparkles: Usability & Accessibility * Render housenumbers (or housenames) of address points or buildings as dedicated labels on the map ([#10970]) * Simplify raw tag editor and make it easier to use with keyboard-only input ([#10889]) +* Show info message when a keyboard shortcut of an _operation_ is pressed, but the operation is not _available_ for the selected features ([#9896]) #### :scissors: Operations #### :camera: Street-Level #### :white_check_mark: Validation @@ -51,6 +52,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :mortar_board: Walkthrough / Help #### :hammer: Development +[#9896]: https://github.com/openstreetmap/iD/issues/9896 [#10889]: https://github.com/openstreetmap/iD/pull/10889 [#10970]: https://github.com/openstreetmap/iD/pull/10970 [#11027]: https://github.com/openstreetmap/iD/pull/11027 diff --git a/data/core.yaml b/data/core.yaml index 7b9a55961..873bda8d5 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -58,6 +58,8 @@ en: drag_node: connected_to_hidden: This can't be edited because it is connected to a hidden feature. operations: + _unavailable: + Cannot perform “{operation}” on currently selected features. add: annotation: point: Added a point. diff --git a/modules/behavior/operation.js b/modules/behavior/operation.js index d29a26e34..c0a8c8564 100644 --- a/modules/behavior/operation.js +++ b/modules/behavior/operation.js @@ -1,3 +1,5 @@ +import { t } from '../core'; + /* Creates a keybinding behavior for an operation */ export function behaviorOperation(context) { var _operation; @@ -7,19 +9,26 @@ export function behaviorOperation(context) { // prevent operations during low zoom selection if (!context.map().withinEditableZoom()) return; - if (_operation.availableForKeypress && !_operation.availableForKeypress()) return; + // ignore (temporarily) disabled operation keyboard shortcuts, + // e.g. Ctrl+C while text is selected + if (_operation.availableForKeypress?.() === false) return; d3_event.preventDefault(); - var disabled = _operation.disabled(); - - if (disabled) { + if (!_operation.available()) { + context.ui().flash + .duration(4000) + .iconName('#iD-operation-' + _operation.id) + .iconClass('operation disabled') + .label(t.append('operations._unavailable', { + operation: t(`operations.${_operation.id}.title`) || _operation.id + }))(); + } else if (_operation.disabled()) { context.ui().flash .duration(4000) .iconName('#iD-operation-' + _operation.id) .iconClass('operation disabled') .label(_operation.tooltip())(); - } else { context.ui().flash .duration(2000) @@ -35,14 +44,19 @@ export function behaviorOperation(context) { function behavior() { if (_operation && _operation.available()) { - context.keybinding() - .on(_operation.keys, keypress); + behavior.on(); } return behavior; } + behavior.on = function() { + context.keybinding() + .on(_operation.keys, keypress); + }; + + behavior.off = function() { context.keybinding() .off(_operation.keys); diff --git a/modules/modes/select.js b/modules/modes/select.js index d8c75802a..49ad38b8e 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -192,7 +192,6 @@ export function modeSelect(context, selectedIDs) { }; function loadOperations() { - _operations.forEach(function(operation) { if (operation.behavior) { context.uninstall(operation.behavior); @@ -200,29 +199,39 @@ export function modeSelect(context, selectedIDs) { }); _operations = Object.values(Operations) - .map(function(o) { return o(context, selectedIDs); }) - .filter(function(o) { return o.id !== 'delete' && o.id !== 'downgrade' && o.id !== 'copy'; }) + .map(o => o(context, selectedIDs)) + .filter(o => o.id !== 'delete' && o.id !== 'downgrade' && o.id !== 'copy') .concat([ // group copy/downgrade/delete operation together at the end of the list Operations.operationCopy(context, selectedIDs), Operations.operationDowngrade(context, selectedIDs), Operations.operationDelete(context, selectedIDs) - ]).filter(function(operation) { - return operation.available(); + ]); + + _operations + .filter(operation => operation.available()) + .forEach(operation => { + if (operation.behavior) { + context.install(operation.behavior); + } }); - _operations.forEach(function(operation) { - if (operation.behavior) { - context.install(operation.behavior); - } - }); + // unavailable operations: still install keybindings + // to show information message about the unavailability of the operation + _operations + .filter(operation => !operation.available()) + .forEach(operation => { + if (operation.behavior) { + operation.behavior.on(); + } + }); // remove any displayed menu context.ui().closeEditMenu(); } mode.operations = function() { - return _operations; + return _operations.filter(operation => operation.available()); }; @@ -638,7 +647,7 @@ export function modeSelect(context, selectedIDs) { _focusedVertexIds = null; - _operations.forEach(function(operation) { + _operations.forEach(operation => { if (operation.behavior) { context.uninstall(operation.behavior); } diff --git a/modules/operations/copy.js b/modules/operations/copy.js index 7bd1f86f0..19312b11f 100644 --- a/modules/operations/copy.js +++ b/modules/operations/copy.js @@ -104,9 +104,9 @@ export function operationCopy(context, selectedIDs) { operation.availableForKeypress = function() { - var selection = window.getSelection && window.getSelection(); + const selection = window.getSelection?.(); // if the user has text selected then let them copy that, not the selected feature - return !selection || !selection.toString(); + return !selection?.toString(); }; diff --git a/modules/util/keybinding.js b/modules/util/keybinding.js index f237f9b22..8099ecd82 100644 --- a/modules/util/keybinding.js +++ b/modules/util/keybinding.js @@ -12,7 +12,6 @@ export function utilKeybinding(namespace) { function testBindings(d3_event, isCapturing) { var didMatch = false; var bindings = Object.keys(_keybindings).map(function(id) { return _keybindings[id]; }); - var i, binding; // Most key shortcuts will accept either lower or uppercase ('h' or 'H'), // so we don't strictly match on the shift key, but we prioritize @@ -20,8 +19,7 @@ export function utilKeybinding(namespace) { // (This lets us differentiate between '←'/'⇧←' or '⌘Z'/'⌘⇧Z') // priority match shifted keybindings first - for (i = 0; i < bindings.length; i++) { - binding = bindings[i]; + for (const binding of bindings) { if (!binding.event.modifiers.shiftKey) continue; // no shift if (!!binding.capture !== isCapturing) continue; if (matches(d3_event, binding, true)) { @@ -36,8 +34,7 @@ export function utilKeybinding(namespace) { if (didMatch) return; // then unshifted keybindings - for (i = 0; i < bindings.length; i++) { - binding = bindings[i]; + for (const binding of bindings) { if (binding.event.modifiers.shiftKey) continue; // shift if (!!binding.capture !== isCapturing) continue; if (matches(d3_event, binding, false)) {