From eda51f6835bc7807fb68fad1bfa854bdd11d078f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Fri, 17 Jan 2020 17:49:26 -0500 Subject: [PATCH 01/11] Add initial multiselection raw tag editing in 2.x --- css/80_app.css | 8 +- data/core.yaml | 2 + dist/locales/en.json | 2 + modules/modes/select.js | 8 +- modules/ui/entity_editor.js | 508 +++++++++++++++++++++-------------- modules/ui/inspector.js | 55 ++-- modules/ui/preset_list.js | 9 +- modules/ui/raw_tag_editor.js | 131 +++++++-- modules/ui/selection_list.js | 31 +-- modules/ui/sidebar.js | 34 +-- 10 files changed, 500 insertions(+), 288 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 56c635af6..2c0e773e9 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -1245,18 +1245,12 @@ a.hide-toggle { display: flex; flex-flow: row wrap; justify-content: flex-end; - padding: 0 20px; + padding: 5px 0 0 0; } .quick-link { margin: 0 5px; } -.data-editor .quick-links, -.error-editor .quick-links, -.note-editor .quick-links { - padding: 5px 0 0 0; -} - /* Entity/Preset Editor ------------------------------------------------------- */ diff --git a/data/core.yaml b/data/core.yaml index 3fd1e0841..f1b7c7302 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -555,6 +555,7 @@ en: edit_reference: "edit/translate" wiki_reference: View documentation wiki_en_reference: View documentation in English + multiple_values: Multiple Values hidden_preset: manual: "{features} are hidden. Enable them in the Map Data pane." zoom: "{features} are hidden. Zoom in to enable them." @@ -566,6 +567,7 @@ en: incomplete: feature_list: Search features edit: Edit feature + edit_features: Edit features check: "yes": "Yes" "no": "No" diff --git a/dist/locales/en.json b/dist/locales/en.json index ca3c293e7..1289751ba 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -698,6 +698,7 @@ "edit_reference": "edit/translate", "wiki_reference": "View documentation", "wiki_en_reference": "View documentation in English", + "multiple_values": "Multiple Values", "hidden_preset": { "manual": "{features} are hidden. Enable them in the Map Data pane.", "zoom": "{features} are hidden. Zoom in to enable them." @@ -710,6 +711,7 @@ "incomplete": "", "feature_list": "Search features", "edit": "Edit feature", + "edit_features": "Edit features", "check": { "yes": "Yes", "no": "No", diff --git a/modules/modes/select.js b/modules/modes/select.js index 9f59eef6a..34b87d987 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -19,7 +19,6 @@ import { modeDragNote } from './drag_note'; import { osmNode, osmWay } from '../osm'; import * as Operations from '../operations/index'; import { uiEditMenu } from '../ui/edit_menu'; -import { uiSelectionList } from '../ui/selection_list'; import { uiCmd } from '../ui/cmd'; import { utilArrayIntersection, utilDeepMemberSelector, utilEntityOrDeepMemberSelector, @@ -307,7 +306,7 @@ export function modeSelect(context, selectedIDs) { .call(keybinding); context.ui().sidebar - .select(singular() ? singular().id : null, _newFeature); + .select(selectedIDs, _newFeature); context.history() .on('change.select', function() { @@ -332,11 +331,6 @@ export function modeSelect(context, selectedIDs) { selectElements(); - if (selectedIDs.length > 1) { - var entities = uiSelectionList(context, selectedIDs); - context.ui().sidebar.show(entities); - } - if (_follow) { var extent = geoExtent(); var graph = context.graph(); diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 18d70ce02..975c5035e 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -1,5 +1,5 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; -import {event as d3_event, selectAll as d3_selectAll } from 'd3-selection'; +import { event as d3_event, selectAll as d3_selectAll, select as d3_select } from 'd3-selection'; import deepEqual from 'fast-deep-equal'; import { t, textDirection } from '../util/locale'; @@ -15,6 +15,7 @@ import { uiRawTagEditor } from './raw_tag_editor'; import { uiTagReference } from './tag_reference'; import { uiPresetEditor } from './preset_editor'; import { uiEntityIssues } from './entity_issues'; +import { uiSelectionList } from './selection_list'; import { uiTooltipHtml } from './tooltipHtml'; import { utilCleanTags, utilRebind } from '../util'; @@ -24,12 +25,13 @@ export function uiEntityEditor(context) { var _state = 'select'; var _coalesceChanges = false; var _modified = false; - var _scrolled = false; var _base; - var _entityID; + var _entityIDs; var _activePreset; var _tagReference; + var _newFeature; + var selectionList = uiSelectionList(context); var entityIssues = uiEntityIssues(context); var quickLinks = uiQuickLinks(); var presetEditor = uiPresetEditor(context).on('change', changeTags); @@ -38,8 +40,9 @@ export function uiEntityEditor(context) { var rawMembershipEditor = uiRawMembershipEditor(context); function entityEditor(selection) { - var entity = context.entity(_entityID); - var tags = Object.assign({}, entity.tags); // shallow copy + var entityID = singularEntityID(); + var entity = entityID && context.entity(entityID); + var tags = entity && Object.assign({}, entity.tags); // shallow copy // Header var header = selection.selectAll('.header') @@ -62,19 +65,21 @@ export function uiEntityEditor(context) { .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close')); headerEnter - .append('h3') - .text(t('inspector.edit')); + .append('h3'); // Update header = header .merge(headerEnter); + header.selectAll('h3') + .text(entityID ? t('inspector.edit') : t('inspector.edit_features')); + header.selectAll('.preset-reset') + .style('display', entityID ? null : 'none') .on('click', function() { dispatch.call('choose', this, _activePreset); }); - // Body var body = selection.selectAll('.inspector-body') .data([0]); @@ -82,159 +87,215 @@ export function uiEntityEditor(context) { // Enter var bodyEnter = body.enter() .append('div') - .attr('class', 'inspector-body') - .on('scroll.entity-editor', function() { _scrolled = true; }); - - bodyEnter - .append('div') - .attr('class', 'preset-list-item inspector-inner') - .append('div') - .attr('class', 'preset-list-button-wrap') - .append('button') - .attr('class', 'preset-list-button preset-reset') - .call(tooltip().title(t('inspector.back_tooltip')).placement('bottom')) - .append('div') - .attr('class', 'label') - .append('div') - .attr('class', 'label-inner'); - - bodyEnter - .append('div') - .attr('class', 'preset-quick-links'); - - bodyEnter - .append('div') - .attr('class', 'entity-issues'); - - bodyEnter - .append('div') - .attr('class', 'preset-editor'); - - bodyEnter - .append('div') - .attr('class', 'raw-tag-editor inspector-inner'); - - bodyEnter - .append('div') - .attr('class', 'raw-member-editor inspector-inner'); - - bodyEnter - .append('div') - .attr('class', 'raw-membership-editor inspector-inner'); - - bodyEnter - .append('input') - .attr('type', 'text') - .attr('class', 'key-trap'); - + .attr('class', 'entity-editor inspector-body sep-top'); // Update body = body .merge(bodyEnter); - // update header - if (_tagReference) { - body.selectAll('.preset-list-button-wrap') - .call(_tagReference.button); - - body.selectAll('.preset-list-item') - .call(_tagReference.body); - } - - body.selectAll('.preset-reset') - .on('click', function() { - dispatch.call('choose', this, _activePreset); - }); - - body.select('.preset-list-item button') - .call(uiPresetIcon(context) - .geometry(context.geometry(_entityID)) - .preset(_activePreset) - ); - - // NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names) - var label = body.select('.label-inner'); - var nameparts = label.selectAll('.namepart') - .data(_activePreset.name().split(' – '), function(d) { return d; }); - - nameparts.exit() - .remove(); - - nameparts - .enter() - .append('div') - .attr('class', 'namepart') - .text(function(d) { return d; }); - - // update quick links - var choices = [{ - id: 'zoom_to', - label: 'inspector.zoom_to.title', - tooltip: function() { - return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key')); - }, - click: function zoomTo() { - context.mode().zoomToSelected(); - } - }]; - - body.select('.preset-quick-links') - .call(quickLinks.choices(choices)); - - - // update editor sections - body.select('.entity-issues') - .call(entityIssues - .entityID(_entityID) - ); - - body.select('.preset-editor') - .call(presetEditor - .preset(_activePreset) - .entityID(_entityID) - .tags(tags) - .state(_state) - ); - - body.select('.raw-tag-editor') - .call(rawTagEditor - .preset(_activePreset) - .entityID(_entityID) - .tags(tags) - .state(_state) - ); - - if (entity.type === 'relation') { - body.select('.raw-member-editor') - .style('display', 'block') - .call(rawMemberEditor - .entityID(_entityID) - ); - } else { - body.select('.raw-member-editor') - .style('display', 'none'); - } - - body.select('.raw-membership-editor') - .call(rawMembershipEditor - .entityID(_entityID) - ); - - body.select('.key-trap') - .on('keydown.key-trap', function() { - // On tabbing, send focus back to the first field on the inspector-body - // (probably the `name` field) #4159 - if (d3_event.keyCode === 9 && !d3_event.shiftKey) { - d3_event.preventDefault(); - body.select('input').node().focus(); + var sectionInfos = [ + { + klass: 'selection-list', + shouldHave: _entityIDs.length > 1, + update: function(section) { + section + .call(selectionList + .setSelectedIDs(_entityIDs) + ); } + }, + { + klass: 'preset-list-item inspector-inner', + shouldHave: entityID, + create: function(sectionEnter) { + + var presetButtonWrap = sectionEnter + .append('div') + .attr('class', 'preset-list-button-wrap'); + + var presetButton = presetButtonWrap.append('button') + .attr('class', 'preset-list-button preset-reset') + .call(tooltip().title(t('inspector.back_tooltip')).placement('bottom')); + + presetButton + .append('div') + .attr('class', 'label') + .append('div') + .attr('class', 'label-inner'); + + presetButtonWrap.append('div') + .attr('class', 'accessory-buttons'); + + // update quick links + var choices = [{ + id: 'zoom_to', + label: 'inspector.zoom_to.title', + tooltip: function() { + return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key')); + }, + click: function zoomTo() { + context.mode().zoomToSelected(); + } + }]; + + sectionEnter + .append('div') + .attr('class', 'preset-quick-links') + .call(quickLinks.choices(choices)); + }, + update: function(section) { + + // update header + if (_tagReference) { + section.selectAll('.preset-list-button-wrap .accessory-buttons') + .call(_tagReference.button); + + section.selectAll('.preset-list-item') + .call(_tagReference.body); + } + + section.selectAll('.preset-reset') + .on('click', function() { + dispatch.call('choose', this, _activePreset); + }) + .on('mousedown', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }) + .on('mouseup', function() { + d3_event.preventDefault(); + d3_event.stopPropagation(); + }); + + section.select('.preset-list-item button') + .call(uiPresetIcon(context) + .geometry(context.geometry(entityID)) + .preset(_activePreset) + ); + + // NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names) + var label = section.select('.label-inner'); + var nameparts = label.selectAll('.namepart') + .data(_activePreset.name().split(' – '), function(d) { return d; }); + + nameparts.exit() + .remove(); + + nameparts + .enter() + .append('div') + .attr('class', 'namepart') + .text(function(d) { return d; }); + + } + }, { + klass: 'entity-issues', + shouldHave: entityID, + update: function(section) { + section + .call(entityIssues + .entityID(entityID) + ); + } + }, { + klass: 'preset-editor', + shouldHave: entityID, + update: function(section) { + section + .call(presetEditor + .preset(_activePreset) + .entityID(entityID) + .tags(tags) + .state(_state) + ); + } + }, { + klass: 'raw-tag-editor inspector-inner', + shouldHave: true, + update: function(section) { + section + .call(rawTagEditor + .preset(_activePreset) + .entityIDs(_entityIDs) + .state(_state) + ); + } + }, { + klass: 'raw-member-editor inspector-inner', + shouldHave: entity && entity.type === 'relation', + update: function(section) { + section + .call(rawMemberEditor + .entityID(entityID) + ); + } + }, { + klass: 'raw-membership-editor inspector-inner', + shouldHave: entityID, + update: function(section) { + section + .call(rawMembershipEditor + .entityID(entityID) + ); + } + }, { + klass: 'key-trap-wrap', + shouldHave: true, + create: function(sectionEnter) { + sectionEnter + .append('input') + .attr('type', 'text') + .attr('class', 'key-trap'); + }, + update: function(section) { + section.select('key-trap') + .on('keydown.key-trap', function() { + // On tabbing, send focus back to the first field on the inspector-body + // (probably the `name` field) #4159 + if (d3_event.keyCode === 9 && !d3_event.shiftKey) { + d3_event.preventDefault(); + body.select('input').node().focus(); + } + }); + } + } + ]; + + sectionInfos = sectionInfos.filter(function(info) { + return info.shouldHave; + }); + + var sections = body.selectAll('.section') + .data(sectionInfos, function(d) { return d.klass; }); + + sections.exit().remove(); + + var sectionsEnter = sections.enter() + .append('div') + .attr('class', function(d) { + return 'section ' + d.klass; }); + sectionsEnter.each(function(d) { + if (d.create) { + d.create(d3_select(this)); + } + }); + + sections = sectionsEnter + .merge(sections); + + sections.each(function(d) { + if (d.update) { + d.update(d3_select(this)); + } + }); + context.history() .on('change.entity-editor', historyChanged); - function historyChanged(difference) { + if (selection.selectAll('.entity-editor').empty()) return; if (_state === 'hide') return; var significant = !difference || difference.didChange.properties || @@ -242,30 +303,12 @@ export function uiEntityEditor(context) { difference.didChange.deletion; if (!significant) return; - var entity = context.hasEntity(_entityID); + _entityIDs = _entityIDs.filter(context.hasEntity); + if (!_entityIDs.length) return; + + loadActivePreset(); + var graph = context.graph(); - if (!entity) return; - - var match = context.presets().match(entity, graph); - var activePreset = entityEditor.preset(); - var weakPreset = activePreset && - Object.keys(activePreset.addTags || {}).length === 0; - - // A "weak" preset doesn't set any tags. (e.g. "Address") - // Don't replace a weak preset with a fallback preset (e.g. "Point") - if (!(weakPreset && match.isFallback())) { - entityEditor.preset(match); - - if (match.id !== activePreset.id) { - // flash the button to indicate the preset changed - selection - .selectAll('button.preset-reset .label') - .style('background-color', '#fff') - .transition() - .duration(500) - .style('background-color', null); - } - } entityEditor.modified(_base !== graph); entityEditor(selection); } @@ -275,27 +318,45 @@ export function uiEntityEditor(context) { // Tag changes that fire on input can all get coalesced into a single // history operation when the user leaves the field. #2342 function changeTags(changed, onInput) { - var entity = context.entity(_entityID); - var annotation = t('operations.change_tags.annotation'); - var tags = Object.assign({}, entity.tags); // shallow copy - for (var k in changed) { - if (!k) continue; - var v = changed[k]; - if (v !== undefined || tags.hasOwnProperty(k)) { - tags[k] = v; + var actions = []; + for (var i in _entityIDs) { + var entityID = _entityIDs[i]; + var entity = context.entity(entityID); + + var tags = Object.assign({}, entity.tags); // shallow copy + + for (var k in changed) { + if (!k) continue; + var v = changed[k]; + if (v !== undefined || tags.hasOwnProperty(k)) { + tags[k] = v; + } + } + + if (!onInput) { + tags = utilCleanTags(tags); + } + + if (!deepEqual(entity.tags, tags)) { + actions.push(actionChangeTags(entityID, tags)); } } - if (!onInput) { - tags = utilCleanTags(tags); - } + if (actions.length) { + var combinedAction = function(graph) { + actions.forEach(function(action) { + graph = action(graph); + }); + return graph; + }; + + var annotation = t('operations.change_tags.annotation'); - if (!deepEqual(entity.tags, tags)) { if (_coalesceChanges) { - context.overwrite(actionChangeTags(_entityID, tags), annotation); + context.overwrite(combinedAction, annotation); } else { - context.perform(actionChangeTags(_entityID, tags), annotation); + context.perform(combinedAction, annotation); _coalesceChanges = !!onInput; } } @@ -310,8 +371,6 @@ export function uiEntityEditor(context) { entityEditor.modified = function(val) { if (!arguments.length) return _modified; _modified = val; - d3_selectAll('button.preset-close use') - .attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close')); return entityEditor; }; @@ -323,39 +382,74 @@ export function uiEntityEditor(context) { }; - entityEditor.entityID = function(val) { - if (!arguments.length) return _entityID; - if (_entityID === val) return entityEditor; // exit early if no change + entityEditor.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + if (_entityIDs === val) return entityEditor; // exit early if no change - _entityID = val; + _entityIDs = val; _base = context.graph(); _coalesceChanges = false; - // reset the scroll to the top of the inspector (warning: triggers reflow) - if (_scrolled) { - window.requestIdleCallback(function() { - var body = d3_selectAll('.entity-editor-pane .inspector-body'); - if (!body.empty()) { - _scrolled = false; - body.node().scrollTop = 0; - } - }); - } - - var presetMatch = context.presets().match(context.entity(_entityID), _base); + loadActivePreset(); return entityEditor - .preset(presetMatch) .modified(false); }; + entityEditor.newFeature = function(val) { + if (!arguments.length) return _newFeature; + _newFeature = val; + return entityEditor; + }; + + + function singularEntityID() { + if (_entityIDs.length === 1) { + return _entityIDs[0]; + } + return null; + } + + + function loadActivePreset() { + var entityID = singularEntityID(); + var entity = entityID && context.hasEntity(entityID); + if (!entity) return; + + var graph = context.graph(); + var match = context.presets().match(entity, graph); + + // A "weak" preset doesn't set any tags. (e.g. "Address") + var weakPreset = _activePreset && + Object.keys(_activePreset.addTags || {}).length === 0; + + // Don't replace a weak preset with a fallback preset (e.g. "Point") + if ((weakPreset && match.isFallback()) || + // don't reload for same preset + match === _activePreset) return; + + if (_activePreset && match.id !== _activePreset.id) { + // flash the button to indicate the preset changed + d3_selectAll('.entity-editor button.preset-reset .label') + .style('background-color', '#fff') + .transition() + .duration(500) + .style('background-color', null); + } + + entityEditor.preset(match); + } + entityEditor.preset = function(val) { if (!arguments.length) return _activePreset; if (val !== _activePreset) { _activePreset = val; - _tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context) - .showing(false); + var entityID = singularEntityID(); + if (entityID) { + _tagReference = uiTagReference(_activePreset.reference(context.geometry(entityID)), context) + .showing(false); + } } return entityEditor; }; diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index bce4f9479..6e58dac21 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -13,19 +13,19 @@ export function uiInspector(context) { presetPane = d3_select(null), editorPane = d3_select(null); var _state = 'select'; - var _entityID; + var _entityIDs; var _newFeature = false; function inspector(selection, newFeature) { presetList - .entityID(_entityID) + .entityID(_entityIDs.length === 1 && _entityIDs[0]) .autofocus(_newFeature) .on('choose', inspector.setPreset); entityEditor .state(_state) - .entityID(_entityID) + .entityIDs(_entityIDs) .on('choose', inspector.showList); wrap = selection.selectAll('.panewrap') @@ -47,16 +47,34 @@ export function uiInspector(context) { presetPane = wrap.selectAll('.preset-list-pane'); editorPane = wrap.selectAll('.entity-editor-pane'); - var entity = context.entity(_entityID); + function shouldDefaultToPresetList() { + // can only change preset on single selection + if (_entityIDs.length !== 1) return false; - var hasNonGeometryTags = entity.hasNonGeometryTags(); - var isTaglessOrIntersectionVertex = entity.geometry(context.graph()) === 'vertex' && - (!hasNonGeometryTags && !entity.isHighwayIntersection(context.graph())); - var issues = context.validator().getEntityIssues(_entityID); - // start with the preset list if the feature is new and untagged or is an uninteresting vertex - var showPresetList = (newFeature && !hasNonGeometryTags) || (isTaglessOrIntersectionVertex && !issues.length); + var entityID = _entityIDs[0]; + var entity = context.hasEntity(entityID); + if (!entity) return false; - if (showPresetList) { + // default to inspector if there are already tags + if (entity.hasNonGeometryTags()) return false; + + // prompt to select preset if feature is new and untagged + if (newFeature) return true; + + // all existing features except vertices should default to inspector + if (entity.geometry(context.graph()) !== 'vertex') return false; + + // show vertex issues if there are any + if (context.validator().getEntityIssues(entityID).length) return false; + + // show turn retriction editor for junction vertices + if (entity.isHighwayIntersection(context.graph())) return false; + + // otherwise show preset list for uninteresting vertices + return true; + } + + if (shouldDefaultToPresetList()) { wrap.style('right', '-100%'); presetPane.call(presetList); } else { @@ -74,11 +92,18 @@ export function uiInspector(context) { footer .call(uiViewOnOSM(context) - .what(context.hasEntity(_entityID)) + .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0])) ); } inspector.showList = function(preset) { + + if (!preset) { + if (_entityIDs.length !== 1) return; + + preset = context.presets().match(_entityIDs[0], context.graph()); + } + wrap.transition() .styleTween('right', function() { return d3_interpolate('0%', '-100%'); }); @@ -115,9 +140,9 @@ export function uiInspector(context) { }; - inspector.entityID = function(val) { - if (!arguments.length) return _entityID; - _entityID = val; + inspector.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return inspector; }; diff --git a/modules/ui/preset_list.js b/modules/ui/preset_list.js index 6fb7d146a..50ecddcb2 100644 --- a/modules/ui/preset_list.js +++ b/modules/ui/preset_list.js @@ -25,6 +25,11 @@ export function uiPresetList(context) { function presetList(selection) { + if (!_entityID) { + //selection.html(''); + return; + } + var entity = context.entity(_entityID); var geometry = context.geometry(_entityID); @@ -461,7 +466,9 @@ export function uiPresetList(context) { presetList.entityID = function(val) { if (!arguments.length) return _entityID; _entityID = val; - presetList.preset(context.presets().match(context.entity(_entityID), context.graph())); + if (_entityID) { + presetList.preset(context.presets().match(context.entity(_entityID), context.graph())); + } return presetList; }; diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index c24cd85ee..e7680e184 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -22,6 +22,7 @@ export function uiRawTagEditor(context) { var _readOnlyTags = []; // the keys in the order we want them to display var _orderedKeys = []; + var _keyValues = null; var _showBlank = false; var _updatePreference = true; var _expanded = false; @@ -29,7 +30,7 @@ export function uiRawTagEditor(context) { var _state; var _preset; var _tags; - var _entityID; + var _entityIDs; function rawTagEditor(selection) { @@ -89,7 +90,10 @@ export function uiRawTagEditor(context) { // View Options var options = wrap.selectAll('.raw-tag-options') - .data([0]); + .data((!_entityIDs || _entityIDs.length === 1) ? [0] : []); + + options.exit() + .remove(); var optionsEnter = options.enter() .append('div') @@ -237,17 +241,23 @@ export function uiRawTagEditor(context) { var key = row.select('input.key'); // propagate bound data var value = row.select('input.value'); // propagate bound data - if (_entityID && taginfo && _state !== 'hover') { + if (_entityIDs && taginfo && _state !== 'hover') { bindTypeahead(key, value); } - var isRelation = (_entityID && context.entity(_entityID).type === 'relation'); var reference; - if (isRelation && d.key === 'type') { - reference = uiTagReference({ rtype: d.value }, context); + if (typeof d.value !== 'string') { + reference = uiTagReference({ key: d.key }, context); } else { - reference = uiTagReference({ key: d.key, value: d.value }, context); + var isRelation = _entityIDs && _entityIDs.some(function(entityID) { + return context.entity(entityID).type === 'relation'; + }); + if (isRelation && d.key === 'type') { + reference = uiTagReference({ rtype: d.value }, context); + } else { + reference = uiTagReference({ key: d.key, value: d.value }, context); + } } if (_state === 'hover') { @@ -266,12 +276,19 @@ export function uiRawTagEditor(context) { .attr('title', function(d) { return d.key; }) .call(utilGetSetValue, function(d) { return d.key; }) .attr('readonly', function(d) { - return isReadOnly(d) || null; + return (isReadOnly(d) || (typeof d.value !== 'string')) || null; }); items.selectAll('input.value') - .attr('title', function(d) { return d.value; }) - .call(utilGetSetValue, function(d) { return d.value; }) + .attr('title', function(d) { + return typeof d.value === 'string' ? d.value : Array.from(_keyValues[d.key]).sort().join('; '); + }) + .attr('placeholder', function(d) { + return typeof d.value === 'string' ? null : t('inspector.multiple_values'); + }) + .call(utilGetSetValue, function(d) { + return typeof d.value === 'string' ? d.value : ''; + }) .attr('readonly', function(d) { return isReadOnly(d) || null; }); @@ -376,7 +393,24 @@ export function uiRawTagEditor(context) { function bindTypeahead(key, value) { if (isReadOnly(key.datum())) return; - var geometry = context.geometry(_entityID); + if (typeof value.datum().value !== 'string' && _keyValues) { + value.call(uiCombobox(context, 'tag-value') + .minItems(1) + .fetcher(function(value, callback) { + var keyString = utilGetSetValue(key); + if (!_keyValues[keyString]) return; + var data = Array.from(_keyValues[keyString]).map(function(tagValue) { + return { + value: tagValue, + title: tagValue + }; + }); + callback(data); + })); + return; + } + + var geometry = context.geometry(_entityIDs[0]); key.call(uiCombobox(context, 'tag-key') .fetcher(function(value, callback) { @@ -432,6 +466,8 @@ export function uiRawTagEditor(context) { function keyChange(d) { + if (d3_select(this).attr('readonly')) return; + var kOld = d.key; var kNew = this.value.trim(); var row = this.parentNode.parentNode; @@ -487,6 +523,9 @@ export function uiRawTagEditor(context) { function valueChange(d) { if (isReadOnly(d)) return; + // exit if this is a multiselection and no value was entered + if (typeof d.value !== 'string' && !this.value) return; + _pendingChange = _pendingChange || {}; // exit if we are currently about to delete this row anyway - #6366 @@ -550,7 +589,7 @@ export function uiRawTagEditor(context) { rawTagEditor.preset = function(val) { if (!arguments.length) return _preset; _preset = val; - if (_preset.isFallback()) { + if (_preset && _preset.isFallback()) { _expanded = true; _updatePreference = false; } else { @@ -568,12 +607,72 @@ export function uiRawTagEditor(context) { }; - rawTagEditor.entityID = function(val) { - if (!arguments.length) return _entityID; - if (_entityID !== val) { + rawTagEditor.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + if (_entityIDs !== val) { + _entityIDs = val; _orderedKeys = []; - _entityID = val; } + + if (_entityIDs.length > 1) { + // require the list editor when editing multiple entities + _tagView = 'list'; + } else { + _tagView = (context.storage('raw-tag-editor-view') || 'list'); + } + + var combinedTags = {}; + var sharedKeys = null; + _keyValues = {}; + + _entityIDs.forEach(function(entityID) { + var entity = context.entity(entityID); + var entityTags = entity.tags; + var entityKey; + + if (sharedKeys === null) { + sharedKeys = {}; + for (entityKey in entityTags) { + sharedKeys[entityKey] = true; + } + } else { + for (var sharedKey in sharedKeys) { + if (!entityTags.hasOwnProperty(sharedKey)) { + delete sharedKeys[sharedKey]; + } + } + } + + for (entityKey in entityTags) { + + var entityValue = entityTags[entityKey]; + + if (!_keyValues.hasOwnProperty(entityKey)) { + _keyValues[entityKey] = new Set(); + } + _keyValues[entityKey].add(entityValue); + + if (combinedTags.hasOwnProperty(entityKey)) { + var combinedValue = combinedTags[entityKey]; + if (combinedValue !== true && + combinedValue !== entityValue) { + + combinedTags[entityKey] = true; + } + } else { + combinedTags[entityKey] = entityValue; + } + } + }); + + for (var key in combinedTags) { + if (!sharedKeys.hasOwnProperty(key)) { + // treat tags that aren't shared by all entities the same as if there are multiple values + combinedTags[key] = true; + } + } + + rawTagEditor.tags(combinedTags); return rawTagEditor; }; diff --git a/modules/ui/selection_list.js b/modules/ui/selection_list.js index 07ce9357e..cbbf7b56e 100644 --- a/modules/ui/selection_list.js +++ b/modules/ui/selection_list.js @@ -1,13 +1,15 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; -import { t } from '../util/locale'; import { modeSelect } from '../modes/select'; import { osmEntity } from '../osm'; import { svgIcon } from '../svg/icon'; import { utilDisplayName, utilHighlightEntities } from '../util'; -export function uiSelectionList(context, selectedIDs) { +export function uiSelectionList(context) { + + var selectedIDs = []; + function selectEntity(entity) { context.enter(modeSelect(context, [entity.id])); @@ -25,24 +27,12 @@ export function uiSelectionList(context, selectedIDs) { function selectionList(selection) { - selection.classed('selection-list-pane', true); - var header = selection + var list = selection.selectAll('.feature-list') + .data([0]) + .enter() .append('div') - .attr('class', 'header fillL cf'); - - header - .append('h3') - .text(t('inspector.multiselect')); - - var listWrap = selection - .append('div') - .attr('class', 'inspector-body'); - - var list = listWrap - .append('div') - .attr('class', 'feature-list cf'); - + .attr('class', 'feature-list'); context.history() .on('change.selectionList', function(difference) { @@ -119,5 +109,10 @@ export function uiSelectionList(context, selectedIDs) { } } + selectionList.setSelectedIDs = function(val) { + selectedIDs = val; + return selectionList; + }; + return selectionList; } diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index d8f5d9a84..7ee9a8dd6 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -167,10 +167,10 @@ export function uiSidebar(context) { .classed('inspector-hidden', false) .classed('inspector-hover', true); - if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') { + if (inspector.entityIDs() !== [datum.id] || inspector.state() !== 'hover') { inspector .state('hover') - .entityID(datum.id); + .entityIDs([datum.id]); inspectorWrap .call(inspector); @@ -206,17 +206,16 @@ export function uiSidebar(context) { }; - sidebar.select = function(id, newFeature) { + sidebar.select = function(ids, newFeature) { sidebar.hide(); - if (id) { - var entity = context.entity(id); - // uncollapse the sidebar - if (selection.classed('collapsed')) { - if (newFeature) { - var extent = entity.extent(context.graph()); - sidebar.expand(sidebar.intersects(extent)); - } + if (ids && ids.length) { + + var entity = ids.length === 1 && context.entity(ids[0]); + if (entity && newFeature && selection.classed('collapsed')) { + // uncollapse the sidebar + var extent = entity.extent(context.graph()); + sidebar.expand(sidebar.intersects(extent)); } featureListWrap @@ -226,20 +225,16 @@ export function uiSidebar(context) { .classed('inspector-hidden', false) .classed('inspector-hover', false); - if (inspector.entityID() !== id || inspector.state() !== 'select') { + if (inspector.entityIDs() !== ids || inspector.state() !== 'select') { inspector .state('select') - .entityID(id) + .entityIDs(ids) .newFeature(newFeature); inspectorWrap .call(inspector, newFeature); } - sidebar.showPresetList = function() { - inspector.showList(context.presets().match(entity, context.graph())); - }; - } else { inspector .state('hide'); @@ -247,6 +242,11 @@ export function uiSidebar(context) { }; + sidebar.showPresetList = function() { + inspector.showList(); + }; + + sidebar.show = function(component, element) { featureListWrap .classed('inspector-hidden', true); From 73e64b2d7e4626f4ca6982cd2f10e59b05f1fb1f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 12:27:50 -0500 Subject: [PATCH 02/11] Update code test --- test/spec/ui/raw_tag_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ui/raw_tag_editor.js b/test/spec/ui/raw_tag_editor.js index ad699d506..f0f3bc526 100644 --- a/test/spec/ui/raw_tag_editor.js +++ b/test/spec/ui/raw_tag_editor.js @@ -3,7 +3,7 @@ describe('iD.uiRawTagEditor', function() { function render(tags) { taglist = iD.uiRawTagEditor(context) - .entityID(entity.id) + .entityIDs([entity.id]) .preset({isFallback: function() { return false; }}) .tags(tags) .expanded(true); From 721ee0e95f0c30419b1377a4f6cc65fce83c6bb5 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 12:30:35 -0500 Subject: [PATCH 03/11] Prevent unnecessary reloading of raw tag editor (close #7248) --- modules/ui/raw_tag_editor.js | 5 +++-- modules/ui/sidebar.js | 6 +++--- modules/util/array.js | 10 ++++++++++ modules/util/index.js | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index e7680e184..c9fca6983 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -7,7 +7,8 @@ import { svgIcon } from '../svg/icon'; import { uiCombobox } from './combobox'; import { uiDisclosure } from './disclosure'; import { uiTagReference } from './tag_reference'; -import { utilArrayDifference, utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../util'; +import { utilArrayDifference, utilArrayIdentical } from '../util/array'; +import { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../util'; export function uiRawTagEditor(context) { @@ -609,7 +610,7 @@ export function uiRawTagEditor(context) { rawTagEditor.entityIDs = function(val) { if (!arguments.length) return _entityIDs; - if (_entityIDs !== val) { + if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) { _entityIDs = val; _orderedKeys = []; } diff --git a/modules/ui/sidebar.js b/modules/ui/sidebar.js index 7ee9a8dd6..74ceeb52f 100644 --- a/modules/ui/sidebar.js +++ b/modules/ui/sidebar.js @@ -8,7 +8,7 @@ import { event as d3_event, selectAll as d3_selectAll } from 'd3-selection'; - +import { utilArrayIdentical } from '../util/array'; import { osmEntity, osmNote, qaError } from '../osm'; import { services } from '../services'; import { uiDataEditor } from './data_editor'; @@ -167,7 +167,7 @@ export function uiSidebar(context) { .classed('inspector-hidden', false) .classed('inspector-hover', true); - if (inspector.entityIDs() !== [datum.id] || inspector.state() !== 'hover') { + if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') { inspector .state('hover') .entityIDs([datum.id]); @@ -225,7 +225,7 @@ export function uiSidebar(context) { .classed('inspector-hidden', false) .classed('inspector-hover', false); - if (inspector.entityIDs() !== ids || inspector.state() !== 'select') { + if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), ids) || inspector.state() !== 'select') { inspector .state('select') .entityIDs(ids) diff --git a/modules/util/array.js b/modules/util/array.js index c06e66cde..aab92222d 100644 --- a/modules/util/array.js +++ b/modules/util/array.js @@ -1,4 +1,14 @@ +// Returns true if a and b have the same elements at the same indices. +export function utilArrayIdentical(a, b) { + var i = a.length; + if (i !== b.length) return false; + while (i--) { + if (a[i] !== b[i]) return false; + } + return true; +} + // http://2ality.com/2015/01/es6-set-operations.html // Difference (a \ b): create a set that contains those elements of set a that are not in set b. diff --git a/modules/util/index.js b/modules/util/index.js index ec202812b..9c83d6bd5 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -2,6 +2,7 @@ export { utilArrayChunk } from './array'; export { utilArrayDifference } from './array'; export { utilArrayFlatten } from './array'; export { utilArrayGroupBy } from './array'; +export { utilArrayIdentical } from './array'; export { utilArrayIntersection } from './array'; export { utilArrayUnion } from './array'; export { utilArrayUniq } from './array'; From 874acf8ccc6486d86432671a4962cbd1e04c16b8 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 13:35:31 -0500 Subject: [PATCH 04/11] Make raw tag editor display option tooltips translatable --- data/core.yaml | 2 ++ modules/ui/raw_tag_editor.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/data/core.yaml b/data/core.yaml index f1b7c7302..a6e33c698 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -8,6 +8,8 @@ en: copy: copy view_on: view on {domain} favorite: favorite + list: list + text: text toolbar: inspect: Inspect undo_redo: Undo / Redo diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index c9fca6983..664fe9038 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -109,7 +109,7 @@ export function uiRawTagEditor(context) { .attr('class', function(d) { return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : ''); }) - .attr('title', function(d) { return d.id; }) + .attr('title', function(d) { return t('icons.' + d.id); }) .on('click', function(d) { _tagView = d.id; context.storage('raw-tag-editor-view', d.id); From 6f2938b35e7a2013f06ee694c7cde39b0233c45f Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 13:36:36 -0500 Subject: [PATCH 05/11] Add "key=value" placeholder to text tag editor textarea --- data/core.yaml | 1 + modules/ui/raw_tag_editor.js | 1 + 2 files changed, 2 insertions(+) diff --git a/data/core.yaml b/data/core.yaml index a6e33c698..ef0c9fe28 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -557,6 +557,7 @@ en: edit_reference: "edit/translate" wiki_reference: View documentation wiki_en_reference: View documentation in English + key_value: "key=value" multiple_values: Multiple Values hidden_preset: manual: "{features} are hidden. Enable them in the Map Data pane." diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index 664fe9038..d7ae0a668 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -139,6 +139,7 @@ export function uiRawTagEditor(context) { .append('textarea') .attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : '')) .call(utilNoAuto) + .attr('placeholder', t('inspector.key_value')) .attr('spellcheck', 'false') .merge(textarea); From e62e3107cc498bcf7db4d04110e861f18d826cd1 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 13:37:04 -0500 Subject: [PATCH 06/11] Add derived data for prior two commits --- dist/locales/en.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dist/locales/en.json b/dist/locales/en.json index 1289751ba..f58bca9c0 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -8,7 +8,9 @@ "zoom_to": "zoom to", "copy": "copy", "view_on": "view on {domain}", - "favorite": "favorite" + "favorite": "favorite", + "list": "list", + "text": "text" }, "toolbar": { "inspect": "Inspect", @@ -698,6 +700,7 @@ "edit_reference": "edit/translate", "wiki_reference": "View documentation", "wiki_en_reference": "View documentation in English", + "key_value": "key=value", "multiple_values": "Multiple Values", "hidden_preset": { "manual": "{features} are hidden. Enable them in the Map Data pane.", From 8ca40ac0563f94db8e0bc5316848bdbf21d990b4 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 13:38:21 -0500 Subject: [PATCH 07/11] Support the raw tag text editor during multiselection --- modules/ui/raw_tag_editor.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index d7ae0a668..631240662 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -91,13 +91,13 @@ export function uiRawTagEditor(context) { // View Options var options = wrap.selectAll('.raw-tag-options') - .data((!_entityIDs || _entityIDs.length === 1) ? [0] : []); + .data([0]); options.exit() .remove(); var optionsEnter = options.enter() - .append('div') + .insert('div', ':first-child') .attr('class', 'raw-tag-options'); var optionEnter = optionsEnter.selectAll('.raw-tag-option') @@ -342,7 +342,9 @@ export function uiRawTagEditor(context) { var str = rows .filter(function(row) { return row.key && row.key.trim() !== ''; }) .map(function(row) { - var val = row.value ? stringify(row.value) : ''; + var rawVal = row.value; + if (rawVal === true) rawVal = '*'; + var val = rawVal ? stringify(rawVal) : ''; return stringify(row.key) + '=' + val; }) .join('\n'); @@ -371,6 +373,9 @@ export function uiRawTagEditor(context) { tagDiff.forEach(function(change) { if (isReadOnly({ key: change.key })) return; + // skip unchanged multiselection placeholders + if (change.newVal === '*' && change.oldVal === true) return; + if (change.type === '-') { _pendingChange[change.key] = undefined; } else if (change.type === '+') { @@ -378,6 +383,11 @@ export function uiRawTagEditor(context) { } }); + if (Object.keys(_pendingChange).length === 0) { + _pendingChange = null; + return; + } + scheduleChange(); } @@ -616,13 +626,6 @@ export function uiRawTagEditor(context) { _orderedKeys = []; } - if (_entityIDs.length > 1) { - // require the list editor when editing multiple entities - _tagView = 'list'; - } else { - _tagView = (context.storage('raw-tag-editor-view') || 'list'); - } - var combinedTags = {}; var sharedKeys = null; _keyValues = {}; From be8ccf01c4df59f372d8c1c2bc66890225e8f380 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 13:56:59 -0500 Subject: [PATCH 08/11] Fix issue with showing the preset list programmatically --- modules/ui/inspector.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index 6e58dac21..dcbf2c3bf 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -101,7 +101,10 @@ export function uiInspector(context) { if (!preset) { if (_entityIDs.length !== 1) return; - preset = context.presets().match(_entityIDs[0], context.graph()); + var entity = context.hasEntity(_entityIDs[0]); + if (!entity) return; + + preset = context.presets().match(entity, context.graph()); } wrap.transition() From 232375cfc01bfdf72223a31bb97bbd5222058dc4 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 14:30:14 -0500 Subject: [PATCH 09/11] Fix sidebar state issues with adding and removing features from multiselection --- modules/ui/entity_editor.js | 2 +- modules/ui/selection_list.js | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index 975c5035e..ed7c01bef 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -100,7 +100,7 @@ export function uiEntityEditor(context) { update: function(section) { section .call(selectionList - .setSelectedIDs(_entityIDs) + .selectedIDs(_entityIDs) ); } }, diff --git a/modules/ui/selection_list.js b/modules/ui/selection_list.js index cbbf7b56e..fe201f23f 100644 --- a/modules/ui/selection_list.js +++ b/modules/ui/selection_list.js @@ -8,7 +8,7 @@ import { utilDisplayName, utilHighlightEntities } from '../util'; export function uiSelectionList(context) { - var selectedIDs = []; + var _selectedIDs = []; function selectEntity(entity) { @@ -18,21 +18,25 @@ export function uiSelectionList(context) { function deselectEntity(entity) { d3_event.stopPropagation(); + + var selectedIDs = _selectedIDs.slice(); var index = selectedIDs.indexOf(entity.id); if (index > -1) { selectedIDs.splice(index, 1); + context.enter(modeSelect(context, selectedIDs)); } - context.enter(modeSelect(context, selectedIDs)); } function selectionList(selection) { var list = selection.selectAll('.feature-list') - .data([0]) - .enter() + .data([0]); + + list = list.enter() .append('div') - .attr('class', 'feature-list'); + .attr('class', 'feature-list') + .merge(list); context.history() .on('change.selectionList', function(difference) { @@ -41,11 +45,10 @@ export function uiSelectionList(context) { drawList(); - function drawList() { - var entities = selectedIDs + var entities = _selectedIDs .map(function(id) { return context.hasEntity(id); }) - .filter(function(entity) { return entity; }); + .filter(Boolean); var items = list.selectAll('.feature-list-item') .data(entities, osmEntity.key); @@ -109,10 +112,13 @@ export function uiSelectionList(context) { } } - selectionList.setSelectedIDs = function(val) { - selectedIDs = val; + + selectionList.selectedIDs = function(val) { + if (!arguments.length) return _selectedIDs; + _selectedIDs = val; return selectionList; }; + return selectionList; } From c5b2ad1aca2b904b6c3def32bcee7382b764ed68 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 14:39:15 -0500 Subject: [PATCH 10/11] Add fast path for array comparison if objects are equal --- modules/util/array.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/util/array.js b/modules/util/array.js index aab92222d..b653271da 100644 --- a/modules/util/array.js +++ b/modules/util/array.js @@ -1,6 +1,9 @@ // Returns true if a and b have the same elements at the same indices. export function utilArrayIdentical(a, b) { + // an array is always identical to itself + if (a === b) return true; + var i = a.length; if (i !== b.length) return false; while (i--) { @@ -146,4 +149,3 @@ export function utilArrayUniqBy(a, key) { return acc; }, []); } - From 08cfdc39874146e22789a1048ef3848313800fc2 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Sat, 18 Jan 2020 15:01:56 -0500 Subject: [PATCH 11/11] Remove unused string --- data/core.yaml | 1 - dist/locales/en.json | 1 - 2 files changed, 2 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index ef0c9fe28..a14a5092c 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -565,7 +565,6 @@ en: back_tooltip: Change feature remove: Remove search: Search - multiselect: Selected features unknown: Unknown incomplete: feature_list: Search features diff --git a/dist/locales/en.json b/dist/locales/en.json index f58bca9c0..f95a50d56 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -709,7 +709,6 @@ "back_tooltip": "Change feature", "remove": "Remove", "search": "Search", - "multiselect": "Selected features", "unknown": "Unknown", "incomplete": "", "feature_list": "Search features",