From 1b331bb678160b33fcb2862ae3696e1af0200df3 Mon Sep 17 00:00:00 2001 From: Quincy Morgan Date: Thu, 30 Jan 2020 13:53:29 -0500 Subject: [PATCH] Add mechanism for fields to support editing during multiselection (re: #7276) Add `utilCombinedTags` method and use it for the raw tag editor as well as fields Pass `entityIDs` array into fields instead of single `entity` object Give field revertion its own path separate from `change` Add multiselection editing to fields in files: access, address, check, combo, cycleway, input, maxspeed, textarea, and wikidata --- css/80_app.css | 16 +++- modules/presets/field.js | 6 ++ modules/ui/entity_editor.js | 69 ++++++++++++++-- modules/ui/field.js | 77 ++++++++++-------- modules/ui/fields/access.js | 70 +++++++++++----- modules/ui/fields/address.js | 88 +++++++++++++++------ modules/ui/fields/check.js | 46 ++++++++--- modules/ui/fields/combo.js | 127 ++++++++++++++++++++++-------- modules/ui/fields/cycleway.js | 78 +++++++++++++----- modules/ui/fields/input.js | 40 +++++++--- modules/ui/fields/lanes.js | 12 +-- modules/ui/fields/localized.js | 49 ++++++++---- modules/ui/fields/maxspeed.js | 53 +++++++++---- modules/ui/fields/radio.js | 19 +++-- modules/ui/fields/restrictions.js | 6 +- modules/ui/fields/textarea.js | 19 ++++- modules/ui/fields/wikidata.js | 72 +++++++++++++---- modules/ui/fields/wikipedia.js | 44 ++++++++--- modules/ui/form_fields.js | 2 +- modules/ui/preset_editor.js | 76 ++++++++++++------ modules/ui/preset_list.js | 14 +--- modules/ui/raw_tag_editor.js | 70 ++-------------- modules/util/index.js | 1 + modules/util/util.js | 87 ++++++++++++++++++++ test/spec/ui/fields/wikipedia.js | 6 +- 25 files changed, 808 insertions(+), 339 deletions(-) diff --git a/css/80_app.css b/css/80_app.css index 4050d7d6c..f681207f2 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -220,6 +220,11 @@ input[type="radio"] { margin-right: 0; } +input.mixed::placeholder, +textarea.mixed::placeholder { + font-style: italic; +} + /* tables */ table { background-color: #fff; @@ -1591,6 +1596,11 @@ a.hide-toggle { z-index: 3000; cursor: grabbing; } +.form-field-input-multicombo li.mixed { + border-color: #eff2f7; + color: #888; + font-style: italic; +} .form-field-input-multicombo li.chip span { display: block; @@ -1708,6 +1718,9 @@ a.hide-toggle { .form-field-input-check > span { flex: 1 1 auto; } +.form-field-input-check > span.mixed { + font-style: italic; +} .form-field-input-check > .reverser.button { flex: 0 1 auto; background-color: #eff2f7; @@ -2371,9 +2384,6 @@ button.raw-tag-option svg.icon { [dir='rtl'] .tag-row input.value { border-left: 1px solid #ccc; } -.tag-row input.value.conflicting::placeholder { - font-style: italic; -} .tag-row:first-child input.key { border-top: 1px solid #ccc; diff --git a/modules/presets/field.js b/modules/presets/field.js index 19282dd8b..b91937442 100644 --- a/modules/presets/field.js +++ b/modules/presets/field.js @@ -13,6 +13,12 @@ export function presetField(id, field) { return !field.geometry || field.geometry === geometry; }; + field.matchAllGeometry = function(geometries) { + return !field.geometry || geometries.every(function(geometry) { + return field.geometry.indexOf(geometry) !== -1; + }); + }; + field.t = function(scope, options) { return t('presets.fields.' + id + '.' + scope, options); diff --git a/modules/ui/entity_editor.js b/modules/ui/entity_editor.js index a74e2bcbd..48d83469e 100644 --- a/modules/ui/entity_editor.js +++ b/modules/ui/entity_editor.js @@ -19,7 +19,7 @@ import { uiEntityIssues } from './entity_issues'; import { uiSelectionList } from './selection_list'; import { uiTooltipHtml } from './tooltipHtml'; import { utilArrayIdentical } from '../util/array'; -import { utilCleanTags, utilRebind } from '../util'; +import { utilCleanTags, utilCombinedTags, utilRebind } from '../util'; export function uiEntityEditor(context) { @@ -36,7 +36,7 @@ export function uiEntityEditor(context) { var selectionList = uiSelectionList(context); var entityIssues = uiEntityIssues(context); var quickLinks = uiQuickLinks(); - var presetEditor = uiPresetEditor(context).on('change', changeTags); + var presetEditor = uiPresetEditor(context).on('change', changeTags).on('revert', revertTags); var rawTagEditor = uiRawTagEditor(context).on('change', changeTags); var rawMemberEditor = uiRawMemberEditor(context); var rawMembershipEditor = uiRawMembershipEditor(context); @@ -46,6 +46,8 @@ export function uiEntityEditor(context) { var singularEntityID = _entityIDs.length === 1 && _entityIDs[0]; var singularEntity = singularEntityID && context.entity(singularEntityID); + var combinedTags = utilCombinedTags(_entityIDs, context.graph()); + // Header var header = selection.selectAll('.header') .data([0]); @@ -234,13 +236,13 @@ export function uiEntityEditor(context) { } }, { klass: 'preset-editor', - shouldHave: singularEntityID, + shouldHave: true, update: function(section) { section .call(presetEditor - .preset(_activePresets[0]) - .entityID(singularEntityID) - .tags(Object.assign({}, singularEntity.tags)) + .presets(_activePresets) + .entityIDs(_entityIDs) + .tags(combinedTags) .state(_state) ); } @@ -252,6 +254,7 @@ export function uiEntityEditor(context) { .call(rawTagEditor .preset(_activePresets[0]) .entityIDs(_entityIDs) + .tags(combinedTags) .state(_state) ); } @@ -410,6 +413,60 @@ export function uiEntityEditor(context) { } } + function revertTags(keys) { + + var actions = []; + for (var i in _entityIDs) { + var entityID = _entityIDs[i]; + + var original = context.graph().base().entities[entityID]; + var changed = {}; + for (var j in keys) { + var key = keys[j]; + changed[key] = original ? original.tags[key] : undefined; + } + + 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; + } + } + + + tags = utilCleanTags(tags); + + if (!deepEqual(entity.tags, tags)) { + actions.push(actionChangeTags(entityID, 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 (_coalesceChanges) { + context.overwrite(combinedAction, annotation); + } else { + context.perform(combinedAction, annotation); + _coalesceChanges = false; + } + } + + context.validator().validate(); + } + entityEditor.modified = function(val) { if (!arguments.length) return _modified; diff --git a/modules/ui/field.js b/modules/ui/field.js index 11da3613e..e2eff5017 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -6,13 +6,14 @@ import { t } from '../util/locale'; import { textDirection } from '../util/locale'; import { svgIcon } from '../svg/icon'; import { tooltip } from '../util/tooltip'; +import { geoExtent } from '../geo/extent'; import { uiFieldHelp } from './field_help'; import { uiFields } from './fields'; import { uiTagReference } from './tag_reference'; import { utilRebind } from '../util'; -export function uiField(context, presetField, entity, options) { +export function uiField(context, presetField, entityIDs, options) { options = Object.assign({ show: true, wrap: true, @@ -21,7 +22,7 @@ export function uiField(context, presetField, entity, options) { info: true }, options); - var dispatch = d3_dispatch('change'); + var dispatch = d3_dispatch('change', 'revert'); var field = Object.assign({}, presetField); // shallow copy var _show = options.show; var _state = ''; @@ -48,21 +49,24 @@ export function uiField(context, presetField, entity, options) { dispatch.call('change', field, t, onInput); }); - if (entity) { - field.entityID = entity.id; - // if this field cares about the entity, pass it along - if (field.impl.entity) { - field.impl.entity(entity); + if (entityIDs) { + field.entityIDs = entityIDs; + // if this field cares about the entities, pass them along + if (field.impl.entityIDs) { + field.impl.entityIDs(entityIDs); } } } function isModified() { - if (!entity) return false; - var original = context.graph().base().entities[entity.id]; - return field.keys.some(function(key) { - return original ? _tags[key] !== original.tags[key] : _tags[key]; + if (!entityIDs || !entityIDs.length) return false; + return entityIDs.some(function(entityID) { + var original = context.graph().base().entities[entityID]; + var latest = context.graph().entity(entityID); + return field.keys.some(function(key) { + return original ? latest.tags[key] !== original.tags[key] : latest.tags[key]; + }); }); } @@ -85,15 +89,9 @@ export function uiField(context, presetField, entity, options) { function revert(d) { d3_event.stopPropagation(); d3_event.preventDefault(); - if (!entity || _locked) return; + if (!entityIDs || _locked) return; - var original = context.graph().base().entities[entity.id]; - var t = {}; - d.keys.forEach(function(key) { - t[key] = original ? original.tags[key] : undefined; - }); - - dispatch.call('change', d, t); + dispatch.call('revert', d, d.keys); } @@ -297,11 +295,13 @@ export function uiField(context, presetField, entity, options) { // A non-allowed field is hidden from the user altogether field.isAllowed = function() { - var latest = entity && context.hasEntity(entity.id); // check the most current copy of the entity - if (!latest) return true; + if (entityIDs.length > 1 && uiFields[field.type].supportsMultiselection === false) return; if (field.countryCodes || field.notCountryCodes) { - var center = latest.extent(context.graph()).center(); + var extent = combinedEntityExtent(); + if (!extent) return true; + + var center = extent.center(); var countryCode = countryCoder.iso1A2Code(center); if (!countryCode) return false; @@ -320,19 +320,22 @@ export function uiField(context, presetField, entity, options) { if (!tagsContainFieldKey() && // ignore tagging prerequisites if a value is already present prerequisiteTag) { - if (prerequisiteTag.key) { - var value = latest.tags[prerequisiteTag.key]; - if (!value) return false; + return entityIDs.some(function(entityID) { + var entity = context.graph().entity(entityID); + if (prerequisiteTag.key) { + var value = entity.tags[prerequisiteTag.key]; + if (!value) return false; - if (prerequisiteTag.valueNot) { - return prerequisiteTag.valueNot !== value; + if (prerequisiteTag.valueNot) { + return prerequisiteTag.valueNot !== value; + } + if (prerequisiteTag.value) { + return prerequisiteTag.value === value; + } + } else if (prerequisiteTag.keyNot) { + if (entity.tags[prerequisiteTag.keyNot]) return false; } - if (prerequisiteTag.value) { - return prerequisiteTag.value === value; - } - } else if (prerequisiteTag.keyNot) { - if (latest.tags[prerequisiteTag.keyNot]) return false; - } + }); } return true; @@ -346,5 +349,13 @@ export function uiField(context, presetField, entity, options) { }; + function combinedEntityExtent() { + return entityIDs && entityIDs.length && entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + + return utilRebind(field, dispatch, 'on'); } diff --git a/modules/ui/fields/access.js b/modules/ui/fields/access.js index 262f0a673..690e2b186 100644 --- a/modules/ui/fields/access.js +++ b/modules/ui/fields/access.js @@ -3,11 +3,12 @@ import { select as d3_select } from 'd3-selection'; import { uiCombobox } from '../combobox'; import { utilGetSetValue, utilNoAuto, utilRebind } from '../../util'; - +import { t } from '../../util/locale'; export function uiFieldAccess(field, context) { var dispatch = d3_dispatch('change'); var items = d3_select(null); + var _tags; function access(selection) { var wrap = selection.selectAll('.form-field-input-wrap') @@ -68,7 +69,12 @@ export function uiFieldAccess(field, context) { function change(d) { var tag = {}; - tag[d] = utilGetSetValue(d3_select(this)) || undefined; + var value = utilGetSetValue(d3_select(this)); + + // don't override multiple values with blank string + if (!value && typeof _tags[d] !== 'string') return; + + tag[d] = value || undefined; dispatch.call('change', this, tag); } @@ -94,7 +100,7 @@ export function uiFieldAccess(field, context) { }; - var placeholders = { + var placeholdersByHighway = { footway: { foot: 'designated', motor_vehicle: 'no' @@ -199,24 +205,48 @@ export function uiFieldAccess(field, context) { access.tags = function(tags) { - utilGetSetValue(items.selectAll('.preset-input-access'), - function(d) { return tags[d] || ''; }) - .attr('placeholder', function() { - return tags.access ? tags.access : field.placeholder(); + _tags = tags; + + utilGetSetValue(items.selectAll('.preset-input-access'), function(d) { + return typeof tags[d] === 'string' ? tags[d] : ''; + }) + .classed('mixed', function(d) { + return tags[d] && Array.isArray(tags[d]); + }) + .attr('title', function(d) { + return tags[d] && Array.isArray(tags[d]) && tags[d].filter(Boolean).join('; '); + }) + .attr('placeholder', function(d) { + if (tags[d] && Array.isArray(tags[d])) { + return t('inspector.multiple_values'); + } + if (d === 'access') { + return 'yes'; + } + if (tags.access && typeof tags.access === 'string') { + return tags.access; + } + if (tags.highway) { + if (typeof tags.highway === 'string') { + if (placeholdersByHighway[tags.highway] && + placeholdersByHighway[tags.highway][d]) { + + return placeholdersByHighway[tags.highway][d]; + } + } else { + var impliedAccesses = tags.highway.filter(Boolean).map(function(highwayVal) { + return placeholdersByHighway[highwayVal] && placeholdersByHighway[highwayVal][d]; + }).filter(Boolean); + + if (impliedAccesses.length === tags.highway.length && + new Set(impliedAccesses).size === 1) { + // if all the highway values have the same implied access for this type then use that + return impliedAccesses[0]; + } + } + } + return field.placeholder(); }); - - items.selectAll('.preset-input-access-access') - .attr('placeholder', 'yes'); - - var which = tags.highway; - if (!placeholders[which]) return; - - var keys = Object.keys(placeholders[which]); - keys.forEach(function(k) { - var v = placeholders[which][k]; - items.selectAll('.preset-input-access-' + k) - .attr('placeholder', tags.access || v); - }); }; diff --git a/modules/ui/fields/address.js b/modules/ui/fields/address.js index d1eaeb24f..8bb4cc10c 100644 --- a/modules/ui/fields/address.js +++ b/modules/ui/fields/address.js @@ -14,7 +14,9 @@ export function uiFieldAddress(field, context) { var addrField = context.presets().field('address'); // needed for placeholder strings var _isInitialized = false; - var _entity; + var _entityIDs = []; + var _tags; + var _countryCode; var _addressFormats = [{ format: [ ['housenumber', 'street'], @@ -28,7 +30,7 @@ export function uiFieldAddress(field, context) { function getNearStreets() { - var extent = _entity.extent(context.graph()); + var extent = combinedEntityExtent(); var l = extent.center(); var box = geoExtent(l).padByMeters(200); @@ -60,7 +62,7 @@ export function uiFieldAddress(field, context) { function getNearCities() { - var extent = _entity.extent(context.graph()); + var extent = combinedEntityExtent(); var l = extent.center(); var box = geoExtent(l).padByMeters(200); @@ -98,12 +100,12 @@ export function uiFieldAddress(field, context) { } function getNearValues(key) { - var extent = _entity.extent(context.graph()); + var extent = combinedEntityExtent(); var l = extent.center(); var box = geoExtent(l).padByMeters(200); var results = context.intersects(box) - .filter(function hasTag(d) { return d.id !== _entity.id && d.tags[key]; }) + .filter(function hasTag(d) { return _entityIDs.indexOf(d.id) === -1 && d.tags[key]; }) .map(function(d) { return { title: d.tags[key], @@ -119,15 +121,16 @@ export function uiFieldAddress(field, context) { } - function updateForCountryCode(countryCode) { - countryCode = countryCode.toLowerCase(); + function updateForCountryCode() { + + if (!_countryCode) return; var addressFormat; for (var i = 0; i < _addressFormats.length; i++) { var format = _addressFormats[i]; if (!format.countryCodes) { addressFormat = format; // choose the default format, keep going - } else if (format.countryCodes.indexOf(countryCode) !== -1) { + } else if (format.countryCodes.indexOf(_countryCode) !== -1) { addressFormat = format; // choose the country format, stop here break; } @@ -168,11 +171,7 @@ export function uiFieldAddress(field, context) { .enter() .append('input') .property('type', 'text') - .attr('placeholder', function (d) { - var localkey = d.id + '!' + countryCode; - var tkey = addrField.strings.placeholders[localkey] ? localkey : d.id; - return addrField.t('placeholders.' + tkey); - }) + .call(updatePlaceholder) .attr('maxlength', context.maxCharsForTagValue()) .attr('class', function (d) { return 'addr-' + d.id; }) .call(utilNoAuto) @@ -220,16 +219,21 @@ export function uiFieldAddress(field, context) { .attr('class', 'form-field-input-wrap form-field-input-' + field.type) .merge(wrap); - if (_entity) { + var extent = combinedEntityExtent(); + + if (extent) { var countryCode; if (context.inIntro()) { // localize the address format for the walkthrough countryCode = t('intro.graph.countrycode'); } else { - var center = _entity.extent(context.graph()).center(); + var center = extent.center(); countryCode = countryCoder.iso1A2Code(center); } - if (countryCode) updateForCountryCode(countryCode); + if (countryCode) { + _countryCode = countryCode.toLowerCase(); + updateForCountryCode(); + } } } @@ -240,29 +244,65 @@ export function uiFieldAddress(field, context) { wrap.selectAll('input') .each(function (subfield) { - tags[field.key + ':' + subfield.id] = this.value || undefined; + var key = field.key + ':' + subfield.id; + + // don't override multiple values with blank string + if (Array.isArray(_tags[key]) && !this.value) return; + + tags[key] = this.value || undefined; }); dispatch.call('change', this, tags, onInput); }; } - - function updateTags(tags) { - utilGetSetValue(wrap.selectAll('input'), function (subfield) { - return tags[field.key + ':' + subfield.id] || ''; + function updatePlaceholder(inputSelection) { + return inputSelection.attr('placeholder', function(subfield) { + if (_tags && Array.isArray(_tags[field.key + ':' + subfield.id])) { + return t('inspector.multiple_values'); + } + if (_countryCode) { + var localkey = subfield.id + '!' + _countryCode; + var tkey = addrField.strings.placeholders[localkey] ? localkey : subfield.id; + return addrField.t('placeholders.' + tkey); + } }); } - address.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + function updateTags(tags) { + utilGetSetValue(wrap.selectAll('input'), function (subfield) { + var val = tags[field.key + ':' + subfield.id]; + return typeof val === 'string' ? val : ''; + }) + .attr('title', function(subfield) { + var val = tags[field.key + ':' + subfield.id]; + return val && Array.isArray(val) && val.filter(Boolean).join('; '); + }) + .classed('mixed', function(subfield) { + return Array.isArray(tags[field.key + ':' + subfield.id]); + }) + .call(updatePlaceholder); + } + + + function combinedEntityExtent() { + return _entityIDs && _entityIDs.length && _entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + + + address.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return address; }; address.tags = function(tags) { + _tags = tags; if (_isInitialized) { updateTags(tags); } else { diff --git a/modules/ui/fields/check.js b/modules/ui/fields/check.js index 58ea76334..8f634b4aa 100644 --- a/modules/ui/fields/check.js +++ b/modules/ui/fields/check.js @@ -21,13 +21,15 @@ export function uiFieldCheck(field, context) { var values = []; var texts = []; + var _tags; + var input = d3_select(null); var text = d3_select(null); var label = d3_select(null); var reverser = d3_select(null); var _impliedYes; - var _entityID; + var _entityIDs = []; var _value; @@ -53,7 +55,7 @@ export function uiFieldCheck(field, context) { // hack: pretend `oneway` field is a `oneway_yes` field // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841 if (field.id === 'oneway') { - var entity = context.entity(_entityID); + var entity = context.entity(_entityIDs[0]); for (var key in entity.tags) { if (key in osmOneWayTags && (entity.tags[key] in osmOneWayTags[key])) { _impliedYes = true; @@ -72,7 +74,7 @@ export function uiFieldCheck(field, context) { function reverserSetText(selection) { - var entity = context.hasEntity(_entityID); + var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]); if (reverserHidden() || !entity) return selection; var first = entity.first(); @@ -127,7 +129,16 @@ export function uiFieldCheck(field, context) { .on('click', function() { d3_event.stopPropagation(); var t = {}; - t[field.key] = values[(values.indexOf(_value) + 1) % values.length]; + + if (Array.isArray(_tags[field.key])) { + if (values.indexOf('yes') !== -1) { + t[field.key] = 'yes'; + } else { + t[field.key] = values[0]; + } + } else { + t[field.key] = values[(values.indexOf(_value) + 1) % values.length]; + } // Don't cycle through `alternating` or `reversible` states - #4970 // (They are supported as translated strings, but should not toggle with clicks) @@ -147,10 +158,15 @@ export function uiFieldCheck(field, context) { d3_event.preventDefault(); d3_event.stopPropagation(); context.perform( - actionReverse(_entityID), + function(graph) { + for (var i in _entityIDs) { + graph = actionReverse(_entityIDs[i])(graph); + } + return graph; + }, t('operations.reverse.annotation') ); - + // must manually revalidate since no 'change' event was called context.validator().validate(); @@ -161,15 +177,17 @@ export function uiFieldCheck(field, context) { }; - check.entity = function(_) { - if (!arguments.length) return context.hasEntity(_entityID); - _entityID = _.id; + check.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return check; }; check.tags = function(tags) { + _tags = tags; + function isChecked(val) { return val !== 'no' && val !== '' && val !== undefined && val !== null; } @@ -181,18 +199,22 @@ export function uiFieldCheck(field, context) { } checkImpliedYes(); - _value = tags[field.key] && tags[field.key].toLowerCase(); + + var isMixed = Array.isArray(tags[field.key]); + + _value = !isMixed && tags[field.key] && tags[field.key].toLowerCase(); if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) { _value = 'yes'; } input - .property('indeterminate', field.type !== 'defaultCheck' && !_value) + .property('indeterminate', isMixed || (field.type !== 'defaultCheck' && !_value)) .property('checked', isChecked(_value)); text - .text(textFor(_value)); + .text(isMixed ? t('inspector.multiple_values') : textFor(_value)) + .classed('mixed', isMixed); label .classed('set', !!_value); diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index cc03fd19e..0c51d3f15 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -3,6 +3,7 @@ import { event as d3_event, select as d3_select } from 'd3-selection'; import { drag as d3_drag } from 'd3-drag'; import * as countryCoder from '@ideditor/country-coder'; +import { geoExtent } from '../../geo'; import { osmEntity } from '../../osm/entity'; import { t } from '../../util/locale'; import { services } from '../../services'; @@ -35,8 +36,10 @@ export function uiFieldCombo(field, context) { var input = d3_select(null); var _comboData = []; var _multiData = []; - var _entity; + var _entityIDs = []; + var _tags; var _countryCode; + var _staticPlaceholder; // initialize deprecated tags array var _dataDeprecated = []; @@ -117,7 +120,9 @@ export function uiFieldCombo(field, context) { // function objectDifference(a, b) { return a.filter(function(d1) { - return !b.some(function(d2) { return d1.value === d2.value; }); + return !b.some(function(d2) { + return !d2.isMixed && d1.value === d2.value; + }); }); } @@ -182,8 +187,8 @@ export function uiFieldCombo(field, context) { query: query }; - if (_entity) { - params.geometry = context.geometry(_entity.id); + if (_entityIDs.length) { + params.geometry = context.geometry(_entityIDs[0]); } taginfo[fn](params, function(err, data) { @@ -235,21 +240,27 @@ export function uiFieldCombo(field, context) { function setPlaceholder(values) { - var ph; if (isMulti || isSemi) { - ph = field.placeholder() || t('inspector.add'); + _staticPlaceholder = field.placeholder() || t('inspector.add'); } else { var vals = values .map(function(d) { return d.value; }) .filter(function(s) { return s.length < 20; }); var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; }); - ph = field.placeholder() || placeholders.slice(0, 3).join(', '); + _staticPlaceholder = field.placeholder() || placeholders.slice(0, 3).join(', '); } - if (!/(…|\.\.\.)$/.test(ph)) { - ph += '…'; + if (!/(…|\.\.\.)$/.test(_staticPlaceholder)) { + _staticPlaceholder += '…'; + } + + var ph; + if (!isMulti && !isSemi && _tags && Array.isArray(_tags[field.key])) { + ph = t('inspector.multiple_values'); + } else { + ph = _staticPlaceholder; } container.selectAll('input') @@ -272,11 +283,11 @@ export function uiFieldCombo(field, context) { if (isMulti) { utilArrayUniq(vals).forEach(function(v) { var key = field.key + v; - if (_entity) { + if (_tags) { // don't set a multicombo value to 'yes' if it already has a non-'no' value // e.g. `language:de=main` - var old = _entity.tags[key] || ''; - if (old && old.toLowerCase() !== 'no') return; + var old = _tags[key]; + if (typeof old === 'string' && old.toLowerCase() !== 'no') return; } field.keys.push(key); t[key] = 'yes'; @@ -291,7 +302,12 @@ export function uiFieldCombo(field, context) { window.setTimeout(function() { input.node().focus(); }, 10); } else { - val = tagValue(utilGetSetValue(input)); + var rawValue = utilGetSetValue(input); + + // don't override multiple values with blank string + if (!rawValue && Array.isArray(_tags[field.key])) return; + + val = tagValue(rawValue); t[field.key] = val; } @@ -371,9 +387,9 @@ export function uiFieldCombo(field, context) { .call(initCombo, selection) .merge(input); - if (isNetwork && _entity) { - var center = _entity.extent(context.graph()).center(); - var countryCode = countryCoder.iso1A2Code(center); + if (isNetwork) { + var extent = combinedEntityExtent(); + var countryCode = extent && countryCoder.iso1A2Code(extent.center()); _countryCode = countryCode && countryCode.toLowerCase(); } @@ -405,6 +421,8 @@ export function uiFieldCombo(field, context) { combo.tags = function(tags) { + _tags = tags; + if (isMulti || isSemi) { _multiData = []; @@ -414,13 +432,14 @@ export function uiFieldCombo(field, context) { // Build _multiData array containing keys already set.. for (var k in tags) { if (k.indexOf(field.key) !== 0) continue; - var v = (tags[k] || '').toLowerCase(); - if (v === '' || v === 'no') continue; + var v = tags[k]; + if (!v || (typeof v === 'string' && v.toLowerCase() === 'no')) continue; var suffix = k.substring(field.key.length); _multiData.push({ key: k, - value: displayValue(suffix) + value: displayValue(suffix), + isMixed: Array.isArray(v) }); } @@ -431,15 +450,36 @@ export function uiFieldCombo(field, context) { maxLength = context.maxCharsForTagKey() - field.key.length; } else if (isSemi) { - var arr = utilArrayUniq((tags[field.key] || '').split(';')).filter(Boolean); - _multiData = arr.map(function(k) { + + var allValues = []; + var commonValues; + if (Array.isArray(tags[field.key])) { + + tags[field.key].forEach(function(tagValue) { + var thisVals = utilArrayUniq((tagValue || '').split(';')).filter(Boolean); + allValues = allValues.concat(thisVals); + if (!commonValues) { + commonValues = thisVals; + } else { + commonValues = commonValues.filter(value => thisVals.includes(value)); + } + }); + allValues = utilArrayUniq(allValues).filter(Boolean); + + } else { + allValues = utilArrayUniq((tags[field.key] || '').split(';')).filter(Boolean); + commonValues = allValues; + } + + _multiData = allValues.map(function(v) { return { - key: k, - value: displayValue(k) + key: v, + value: displayValue(v), + isMixed: !commonValues.includes(v) }; }); - var currLength = arr.join(';').length; + var currLength = commonValues.join(';').length; // limit the input length to the remaining available characters maxLength = context.maxCharsForTagValue() - currLength; @@ -452,6 +492,9 @@ export function uiFieldCombo(field, context) { // a negative maxlength doesn't make sense maxLength = Math.max(0, maxLength); + var allowDragAndDrop = isSemi // only semiCombo values are ordered + && !Array.isArray(tags[field.key]); + // Exclude existing multikeys from combo options.. var available = objectDifference(_comboData, _multiData); combobox.data(available); @@ -473,16 +516,19 @@ export function uiFieldCombo(field, context) { var enter = chips.enter() .insert('li', '.input-wrap') - .attr('class', 'chip') - .classed('draggable', isSemi); + .attr('class', 'chip'); enter.append('span'); enter.append('a'); chips = chips.merge(enter) - .order(); + .order() + .classed('draggable', allowDragAndDrop) + .classed('mixed', function(d) { + return d.isMixed; + }); - if (isSemi) { // only semiCombo values are ordered + if (allowDragAndDrop) { registerDragAndDrop(chips); } @@ -498,7 +544,16 @@ export function uiFieldCombo(field, context) { .attr('maxlength', maxLength); } else { - utilGetSetValue(input, displayValue(tags[field.key])); + var isMixed = Array.isArray(tags[field.key]); + + var mixedValues = isMixed && tags[field.key].map(function(val) { + return displayValue(val); + }).filter(Boolean); + + utilGetSetValue(input, !isMixed ? displayValue(tags[field.key]) : '') + .attr('title', isMixed ? mixedValues.join('; ') : undefined) + .attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '') + .classed('mixed', isMixed); } }; @@ -627,12 +682,20 @@ export function uiFieldCombo(field, context) { }; - combo.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + combo.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return combo; }; + function combinedEntityExtent() { + return _entityIDs && _entityIDs.length && _entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + + return utilRebind(combo, dispatch, 'on'); } diff --git a/modules/ui/fields/cycleway.js b/modules/ui/fields/cycleway.js index 199192424..80f11f3ab 100644 --- a/modules/ui/fields/cycleway.js +++ b/modules/ui/fields/cycleway.js @@ -3,12 +3,14 @@ import { select as d3_select } from 'd3-selection'; import { uiCombobox } from '../combobox'; import { utilGetSetValue, utilNoAuto, utilRebind } from '../../util'; +import { t } from '../../util/locale'; export function uiFieldCycleway(field, context) { var dispatch = d3_dispatch('change'); var items = d3_select(null); var wrap = d3_select(null); + var _tags; function cycleway(selection) { @@ -73,29 +75,40 @@ export function uiFieldCycleway(field, context) { } - function change() { - var left = utilGetSetValue(d3_select('.preset-input-cyclewayleft')); - var right = utilGetSetValue(d3_select('.preset-input-cyclewayright')); + function change(key) { + + var newValue = utilGetSetValue(d3_select(this)); + + // don't override multiple values with blank string + if (!newValue && (Array.isArray(_tags.cycleway) || Array.isArray(_tags[key]))) return; + + if (newValue === 'none' || newValue === '') { newValue = undefined; } + + var otherKey = key === 'cycleway:left' ? 'cycleway:right' : 'cycleway:left'; + var otherValue = typeof _tags.cycleway === 'string' ? _tags.cycleway : _tags[otherKey]; + if (otherValue && Array.isArray(otherValue)) { + // we must always have an explicit value for comparison + otherValue = otherValue[0]; + } + if (otherValue === 'none' || otherValue === '') { otherValue = undefined; } + var tag = {}; - if (left === 'none' || left === '') { left = undefined; } - if (right === 'none' || right === '') { right = undefined; } - - // Always set both left and right as changing one can affect the other - tag = { - cycleway: undefined, - 'cycleway:left': left, - 'cycleway:right': right - }; - // If the left and right tags match, use the cycleway tag to tag both // sides the same way - if (left === right) { + if (newValue === otherValue) { tag = { - cycleway: left, + cycleway: newValue, 'cycleway:left': undefined, 'cycleway:right': undefined }; + } else { + // Always set both left and right as changing one can affect the other + tag = { + cycleway: undefined + }; + tag[key] = newValue; + tag[otherKey] = otherValue; } dispatch.call('change', this, tag); @@ -113,14 +126,37 @@ export function uiFieldCycleway(field, context) { cycleway.tags = function(tags) { + _tags = tags; + + // If cycleway is set, use that instead of individual values + var commonValue = typeof tags.cycleway === 'string' && tags.cycleway; + utilGetSetValue(items.selectAll('.preset-input-cycleway'), function(d) { - // If cycleway is set, always return that - if (tags.cycleway) { - return tags.cycleway; - } - return tags[d] || ''; + if (commonValue) return commonValue; + return !tags.cycleway && typeof tags[d] === 'string' ? tags[d] : ''; }) - .attr('placeholder', field.placeholder()); + .attr('title', function(d) { + if (Array.isArray(tags.cycleway) || Array.isArray(tags[d])) { + var vals = []; + if (Array.isArray(tags.cycleway)) { + vals = vals.concat(tags.cycleway); + } + if (Array.isArray(tags[d])) { + vals = vals.concat(tags[d]); + } + return vals.filter(Boolean).join('; '); + } + return null; + }) + .attr('placeholder', function(d) { + if (Array.isArray(tags.cycleway) || Array.isArray(tags[d])) { + return t('inspector.multiple_values'); + } + return field.placeholder(); + }) + .classed('mixed', function(d) { + return Array.isArray(tags.cycleway) || Array.isArray(tags[d]); + }); }; diff --git a/modules/ui/fields/input.js b/modules/ui/fields/input.js index 8ecec585d..a48400175 100644 --- a/modules/ui/fields/input.js +++ b/modules/ui/fields/input.js @@ -3,6 +3,7 @@ import { select as d3_select, event as d3_event } from 'd3-selection'; import * as countryCoder from '@ideditor/country-coder'; import { t, textDirection } from '../../util/locale'; +import { geoExtent } from '../../geo'; import { utilGetSetValue, utilNoAuto, utilRebind } from '../../util'; import { svgIcon } from '../../svg/icon'; @@ -19,7 +20,8 @@ export function uiFieldText(field, context) { var dispatch = d3_dispatch('change'); var input = d3_select(null); var outlinkButton = d3_select(null); - var _entity; + var _entityIDs = []; + var _tags; var _phoneFormats = {}; if (field.type === 'tel') { @@ -29,7 +31,8 @@ export function uiFieldText(field, context) { } function i(selection) { - var preset = _entity && context.presets().match(_entity, context.graph()); + var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]); + var preset = entity && context.presets().match(entity, context.graph()); var isLocked = preset && preset.suggestion && field.id === 'brand'; field.locked(isLocked); @@ -50,7 +53,6 @@ export function uiFieldText(field, context) { .append('input') .attr('type', field.type === 'identifier' ? 'text' : field.type) .attr('id', fieldID) - .attr('placeholder', field.placeholder() || t('inspector.unknown')) .attr('maxlength', context.maxCharsForTagValue()) .classed(field.type, true) .call(utilNoAuto) @@ -64,9 +66,9 @@ export function uiFieldText(field, context) { .on('change', change()); - if (field.type === 'tel' && _entity) { - var center = _entity.extent(context.graph()).center(); - var countryCode = countryCoder.iso1A2Code(center); + if (field.type === 'tel') { + var extent = combinedEntityExtent(); + var countryCode = extent && countryCoder.iso1A2Code(extent.center()); var format = countryCode && _phoneFormats[countryCode.toLowerCase()]; if (format) { wrap.selectAll('#' + fieldID) @@ -112,7 +114,6 @@ export function uiFieldText(field, context) { .attr('tabindex', -1) .call(svgIcon('#iD-icon-out-link')) .attr('class', 'form-field-button foreign-id-permalink') - .classed('disabled', !validIdentifierValueForLink()) .attr('title', function() { var domainResults = /^https?:\/\/(.{1,}?)\//.exec(field.urlFormat); if (domainResults.length >= 2 && domainResults[1]) { @@ -161,6 +162,9 @@ export function uiFieldText(field, context) { var t = {}; var val = utilGetSetValue(input).trim() || undefined; + // don't override multiple values with blank string + if (!val && Array.isArray(_tags[field.key])) return; + if (!onInput) { if (field.type === 'number' && val !== undefined) { var vals = val.split(';'); @@ -178,15 +182,22 @@ export function uiFieldText(field, context) { } - i.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + i.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return i; }; i.tags = function(tags) { - utilGetSetValue(input, tags[field.key] || ''); + _tags = tags; + + var isMixed = Array.isArray(tags[field.key]); + + utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '') + .attr('title', isMixed ? tags[field.key].filter(Boolean).join('; ') : undefined) + .attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown'))) + .classed('mixed', isMixed); if (outlinkButton && !outlinkButton.empty()) { var disabled = !validIdentifierValueForLink(); @@ -200,5 +211,12 @@ export function uiFieldText(field, context) { if (node) node.focus(); }; + function combinedEntityExtent() { + return _entityIDs && _entityIDs.length && _entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + return utilRebind(i, dispatch, 'on'); } diff --git a/modules/ui/fields/lanes.js b/modules/ui/fields/lanes.js index be9e460fc..db0462b72 100644 --- a/modules/ui/fields/lanes.js +++ b/modules/ui/fields/lanes.js @@ -9,10 +9,10 @@ export function uiFieldLanes(field, context) { var dispatch = d3_dispatch('change'); var LANE_WIDTH = 40; var LANE_HEIGHT = 200; - var _entityID; + var _entityIDs = []; function lanes(selection) { - var lanesData = context.entity(_entityID).lanes(); + var lanesData = context.entity(_entityIDs[0]).lanes(); if (!d3_select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) { selection.call(lanes.off); @@ -122,10 +122,8 @@ export function uiFieldLanes(field, context) { } - lanes.entity = function(val) { - if (!_entityID || _entityID !== val.id) { - _entityID = val.id; - } + lanes.entityIDs = function(val) { + _entityIDs = val; }; lanes.tags = function() {}; @@ -134,3 +132,5 @@ export function uiFieldLanes(field, context) { return utilRebind(lanes, dispatch, 'on'); } + +uiFieldLanes.supportsMultiselection = false; diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index 750f59047..d6fe2e6c1 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -3,6 +3,7 @@ import { select as d3_select, event as d3_event } from 'd3-selection'; import * as countryCoder from '@ideditor/country-coder'; import { currentLocale, t, languageName } from '../../util/locale'; +import { geoExtent } from '../../geo'; import { services } from '../../services'; import { svgIcon } from '../../svg'; import { tooltip } from '../../util/tooltip'; @@ -19,6 +20,7 @@ export function uiFieldLocalized(field, context) { var input = d3_select(null); var localizedInputs = d3_select(null); var _countryCode; + var _tags; context.data().get('languages') .then(loadLanguagesArray) @@ -49,7 +51,7 @@ export function uiFieldLocalized(field, context) { .title(t('translate.translate')) .placement('left'); var _wikiTitles; - var _entity; + var _entityIDs = []; function loadLanguagesArray(dataLanguages) { @@ -77,18 +79,20 @@ export function uiFieldLocalized(field, context) { function calcLocked() { - if (!_entity) { // the original entity + if (!_entityIDs || _entityIDs.length !== 1) { // the original entity field.locked(false); return; } - var latest = context.hasEntity(_entity.id); + var latest = context.hasEntity(_entityIDs[0]); if (!latest) { // get current entity, possibly edited field.locked(false); return; } - var hasOriginalName = (latest.tags.name && latest.tags.name === _entity.tags.name); + var original = context.graph().base().entities[_entityIDs[0]]; + + var hasOriginalName = original && latest.tags.name && latest.tags.name === original.tags.name; var hasWikidata = latest.tags.wikidata || latest.tags['name:etymology:wikidata']; var preset = context.presets().match(latest, context.graph()); var isSuggestion = preset && preset.suggestion; @@ -133,8 +137,8 @@ export function uiFieldLocalized(field, context) { _selection = selection; calcLocked(); var isLocked = field.locked(); - var entity = _entity && context.hasEntity(_entity.id); // get latest - var preset = entity && context.presets().match(entity, context.graph()); + var singularEntity = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]); + var preset = singularEntity && context.presets().match(singularEntity, context.graph()); var wrap = selection.selectAll('.form-field-input-wrap') .data([0]); @@ -216,8 +220,8 @@ export function uiFieldLocalized(field, context) { .on('click', addNew); - if (entity && !_multilingual.length) { - calcMultilingual(entity.tags); + if (_tags && !_multilingual.length) { + calcMultilingual(_tags); } localizedInputs = selection.selectAll('.localized-multilingual') @@ -241,7 +245,7 @@ export function uiFieldLocalized(field, context) { // (This can happen if the user actives the combo, arrows down, and then clicks off to blur) // So compare the current field value against the suggestions one last time. function checkBrandOnBlur() { - var latest = context.hasEntity(_entity.id); + var latest = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]); if (!latest) return; // deleting the entity blurred the field? var preset = context.presets().match(latest, context.graph()); @@ -260,12 +264,14 @@ export function uiFieldLocalized(field, context) { function acceptBrand(d) { - if (!d) { + + var entity = _entityIDs.length === 1 && context.hasEntity(_entityIDs[0]); + + if (!d || !entity) { cancelBrand(); return; } - var entity = context.entity(_entity.id); // get latest var tags = entity.tags; var geometry = entity.geometry(context.graph()); var removed = preset.unsetTags(tags, geometry); @@ -554,6 +560,8 @@ export function uiFieldLocalized(field, context) { localized.tags = function(tags) { + _tags = tags; + // Fetch translations from wikipedia if (tags.wikipedia && !_wikiTitles) { _wikiTitles = {}; @@ -580,19 +588,28 @@ export function uiFieldLocalized(field, context) { }; - localized.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + localized.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; _multilingual = []; loadCountryCode(); return localized; }; function loadCountryCode() { - var center = _entity.extent(context.graph()).center(); - var countryCode = countryCoder.iso1A2Code(center); + var extent = combinedEntityExtent(); + var countryCode = extent && countryCoder.iso1A2Code(extent.center()); _countryCode = countryCode && countryCode.toLowerCase(); } + function combinedEntityExtent() { + return _entityIDs && _entityIDs.length && _entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + return utilRebind(localized, dispatch, 'on'); } + +uiFieldLocalized.supportsMultiselection = false; diff --git a/modules/ui/fields/maxspeed.js b/modules/ui/fields/maxspeed.js index bfd2e171a..45c2c22b6 100644 --- a/modules/ui/fields/maxspeed.js +++ b/modules/ui/fields/maxspeed.js @@ -2,7 +2,9 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; import * as countryCoder from '@ideditor/country-coder'; +import { geoExtent } from '../../geo'; import { uiCombobox } from '../combobox'; +import { t } from '../../util/locale'; import { utilGetSetValue, utilNoAuto, utilRebind } from '../../util'; @@ -10,7 +12,8 @@ export function uiFieldMaxspeed(field, context) { var dispatch = d3_dispatch('change'); var unitInput = d3_select(null); var input = d3_select(null); - var _entity; + var _entityIDs = []; + var _tags; var _isImperial; var speedCombo = uiCombobox(context, 'maxspeed'); @@ -40,7 +43,6 @@ export function uiFieldMaxspeed(field, context) { .attr('type', 'text') .attr('id', 'preset-input-' + field.safeid) .attr('maxlength', context.maxCharsForTagValue() - 4) - .attr('placeholder', field.placeholder()) .call(utilNoAuto) .call(speedCombo) .merge(input); @@ -49,8 +51,7 @@ export function uiFieldMaxspeed(field, context) { .on('change', change) .on('blur', change); - var loc = _entity.extent(context.graph()).center(); - + var loc = combinedEntityExtent().center(); _isImperial = countryCoder.roadSpeedUnit(loc) === 'mph'; unitInput = wrap.selectAll('input.maxspeed-unit') @@ -71,13 +72,13 @@ export function uiFieldMaxspeed(field, context) { function changeUnits() { _isImperial = utilGetSetValue(unitInput) === 'mph'; utilGetSetValue(unitInput, _isImperial ? 'mph' : 'km/h'); - setSuggestions(); + setUnitSuggestions(); change(); } } - function setSuggestions() { + function setUnitSuggestions() { speedCombo.data((_isImperial ? imperialValues : metricValues).map(comboValues)); utilGetSetValue(unitInput, _isImperial ? 'mph' : 'km/h'); } @@ -95,6 +96,9 @@ export function uiFieldMaxspeed(field, context) { var tag = {}; var value = utilGetSetValue(input); + // don't override multiple values with blank string + if (!value && Array.isArray(_tags[field.key])) return; + if (!value) { tag[field.key] = undefined; } else if (isNaN(value) || !_isImperial) { @@ -108,17 +112,26 @@ export function uiFieldMaxspeed(field, context) { maxspeed.tags = function(tags) { - var value = tags[field.key]; + _tags = tags; - if (value && value.indexOf('mph') >= 0) { - value = parseInt(value, 10); - _isImperial = true; - } else if (value) { - _isImperial = false; + var value = tags[field.key]; + var isMixed = Array.isArray(value); + + if (!isMixed) { + if (value && value.indexOf('mph') >= 0) { + value = parseInt(value, 10).toString(); + _isImperial = true; + } else if (value) { + _isImperial = false; + } } - setSuggestions(); - utilGetSetValue(input, value || ''); + setUnitSuggestions(); + + utilGetSetValue(input, typeof value === 'string' ? value : '') + .attr('title', isMixed ? value.filter(Boolean).join('; ') : null) + .attr('placeholder', isMixed ? t('inspector.multiple_values') : field.placeholder()) + .classed('mixed', isMixed); }; @@ -127,10 +140,18 @@ export function uiFieldMaxspeed(field, context) { }; - maxspeed.entity = function(val) { - _entity = val; + maxspeed.entityIDs = function(val) { + _entityIDs = val; }; + function combinedEntityExtent() { + return _entityIDs && _entityIDs.length && _entityIDs.reduce(function(extent, entityID) { + var entity = context.graph().entity(entityID); + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); + } + + return utilRebind(maxspeed, dispatch, 'on'); } diff --git a/modules/ui/fields/radio.js b/modules/ui/fields/radio.js index 601be2470..e3015c1c7 100644 --- a/modules/ui/fields/radio.js +++ b/modules/ui/fields/radio.js @@ -19,7 +19,7 @@ export function uiFieldRadio(field, context) { var typeField; var layerField; var _oldType = {}; - var _entity; + var _entityIDs = []; function selectedKey() { @@ -104,7 +104,7 @@ export function uiFieldRadio(field, context) { // Type if (type) { if (!typeField || typeField.id !== selected) { - typeField = uiField(context, type, _entity, { wrap: false }) + typeField = uiField(context, type, _entityIDs, { wrap: false }) .on('change', changeType); } typeField.tags(tags); @@ -147,7 +147,7 @@ export function uiFieldRadio(field, context) { // Layer if (layer && showLayer) { if (!layerField) { - layerField = uiField(context, layer, _entity, { wrap: false }) + layerField = uiField(context, layer, _entityIDs, { wrap: false }) .on('change', changeLayer); } layerField.tags(tags); @@ -301,13 +301,20 @@ export function uiFieldRadio(field, context) { }; - radio.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + radio.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; _oldType = {}; return radio; }; + radio.isAllowed = function() { + return _entityIDs.length === 1; + }; + + return utilRebind(radio, dispatch, 'on'); } + +uiFieldRadio.supportsMultiselection = false; diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index 661973344..0ae5ab5e4 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -615,11 +615,11 @@ export function uiFieldRestrictions(field, context) { } - restrictions.entity = function(val) { + restrictions.entityIDs = function(val) { _intersection = null; _fromWayID = null; _oldTurns = null; - _vertexID = val.id; + _vertexID = val[0]; }; @@ -642,3 +642,5 @@ export function uiFieldRestrictions(field, context) { return utilRebind(restrictions, dispatch, 'on'); } + +uiFieldRestrictions.supportsMultiselection = false; diff --git a/modules/ui/fields/textarea.js b/modules/ui/fields/textarea.js index 66efba529..c637d96fe 100644 --- a/modules/ui/fields/textarea.js +++ b/modules/ui/fields/textarea.js @@ -12,6 +12,7 @@ import { export function uiFieldTextarea(field, context) { var dispatch = d3_dispatch('change'); var input = d3_select(null); + var _tags; function textarea(selection) { @@ -29,7 +30,6 @@ export function uiFieldTextarea(field, context) { input = input.enter() .append('textarea') .attr('id', 'preset-input-' + field.safeid) - .attr('placeholder', field.placeholder() || t('inspector.unknown')) .attr('maxlength', context.maxCharsForTagValue()) .call(utilNoAuto) .on('input', change(true)) @@ -41,15 +41,28 @@ export function uiFieldTextarea(field, context) { function change(onInput) { return function() { + + var val = utilGetSetValue(input) || undefined; + + // don't override multiple values with blank string + if (!val && Array.isArray(_tags[field.key])) return; + var t = {}; - t[field.key] = utilGetSetValue(input) || undefined; + t[field.key] = val; dispatch.call('change', this, t, onInput); }; } textarea.tags = function(tags) { - utilGetSetValue(input, tags[field.key] || ''); + _tags = tags; + + var isMixed = Array.isArray(tags[field.key]); + + utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '') + .attr('title', isMixed ? tags[field.key].filter(Boolean).join('; ') : undefined) + .attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown'))) + .classed('mixed', isMixed); }; diff --git a/modules/ui/fields/wikidata.js b/modules/ui/fields/wikidata.js index bcd4107da..0bd1004b6 100644 --- a/modules/ui/fields/wikidata.js +++ b/modules/ui/fields/wikidata.js @@ -27,7 +27,7 @@ export function uiFieldWikidata(field, context) { var _qid = null; var _wikidataEntity = null; var _wikiURL = ''; - var _entity; + var _entityIDs = []; var _wikipediaKey = field.keys && field.keys.find(function(key) { return key.includes('wikipedia'); @@ -141,8 +141,15 @@ export function uiFieldWikidata(field, context) { function fetchWikidataItems(q, callback) { - if (!q && _entity) { - q = (_hintKey && context.entity(_entity.id).tags[_hintKey]) || ''; + if (!q && _hintKey) { + // other tags may be good search terms + for (var i in _entityIDs) { + var entity = context.hasEntity(_entityIDs[i]); + if (entity.tags[_hintKey]) { + q = entity.tags[_hintKey]; + break; + } + } } wikidata.itemsForSearchQuery(q, function(err, data) { @@ -165,7 +172,7 @@ export function uiFieldWikidata(field, context) { // attempt asynchronous update of wikidata tag.. var initGraph = context.graph(); - var initEntityID = _entity.id; + var initEntityIDs = _entityIDs; wikidata.entityByQID(_qid, function(err, entity) { if (err) return; @@ -189,7 +196,7 @@ export function uiFieldWikidata(field, context) { } }); - var currTags = Object.assign({}, context.entity(initEntityID).tags); // shallow copy + var newWikipediaValue; if (_wikipediaKey) { var foundPreferred; @@ -198,7 +205,7 @@ export function uiFieldWikidata(field, context) { var siteID = lang.replace('-', '_') + 'wiki'; if (entity.sitelinks[siteID]) { foundPreferred = true; - currTags[_wikipediaKey] = (lang + ':' + entity.sitelinks[siteID].title).substr(0, context.maxCharsForTagValue()); + newWikipediaValue = lang + ':' + entity.sitelinks[siteID].title; // use the first match break; } @@ -214,26 +221,52 @@ export function uiFieldWikidata(field, context) { if (wikiSiteKeys.length === 0) { // if no wikipedia pages are linked to this wikidata entity, delete that tag - if (currTags[_wikipediaKey]) { - delete currTags[_wikipediaKey]; - } + newWikipediaValue = null; } else { var wikiLang = wikiSiteKeys[0].slice(0, -4).replace('_', '-'); var wikiTitle = entity.sitelinks[wikiSiteKeys[0]].title; - currTags[_wikipediaKey] = (wikiLang + ':' + wikiTitle).substr(0, context.maxCharsForTagValue()); + newWikipediaValue = wikiLang + ':' + wikiTitle; } } } + if (newWikipediaValue) { + newWikipediaValue = newWikipediaValue.substr(0, context.maxCharsForTagValue()); + } + + if (typeof newWikipediaValue === 'undefined') return; + + var actions = initEntityIDs.map(function(entityID) { + var entity = context.hasEntity(entityID); + if (!entity) return; + + var currTags = Object.assign({}, entity.tags); // shallow copy + if (newWikipediaValue === null) { + if (!currTags[_wikipediaKey]) return; + + delete currTags[_wikipediaKey]; + } else { + currTags[_wikipediaKey] = newWikipediaValue; + } + + return actionChangeTags(entityID, currTags); + }).filter(Boolean); + + if (!actions.length) return; + // Coalesce the update of wikidata tag into the previous tag change context.overwrite( - actionChangeTags(initEntityID, currTags), + function actionUpdateWikipediaTags(graph) { + actions.forEach(function(action) { + graph = action(graph); + }); + return graph; + }, context.history().undoAnnotation() ); // do not dispatch.call('change') here, because entity_editor // changeTags() is not intended to be called asynchronously - }); } @@ -250,7 +283,14 @@ export function uiFieldWikidata(field, context) { wiki.tags = function(tags) { - _qid = tags[field.key] || ''; + + var isMixed = Array.isArray(tags[field.key]); + d3_select('li.wikidata-search input') + .attr('title', isMixed ? tags[field.key].filter(Boolean).join('; ') : null) + .attr('placeholder', isMixed ? t('inspector.multiple_values') : '') + .classed('mixed', isMixed); + + _qid = typeof tags[field.key] === 'string' && tags[field.key] || ''; if (!/^Q[0-9]*$/.test(_qid)) { // not a proper QID unrecognized(); @@ -327,9 +367,9 @@ export function uiFieldWikidata(field, context) { } - wiki.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + wiki.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return wiki; }; diff --git a/modules/ui/fields/wikipedia.js b/modules/ui/fields/wikipedia.js index 4bf878fd6..94e810aaa 100644 --- a/modules/ui/fields/wikipedia.js +++ b/modules/ui/fields/wikipedia.js @@ -17,7 +17,7 @@ export function uiFieldWikipedia(field, context) { let _lang = d3_select(null); let _title = d3_select(null); let _wikiURL = ''; - let _entity; + let _entityIDs; // A concern here in switching to async data means that _dataWikipedia will not // be available the first time through, so things like the fetchers and @@ -43,8 +43,15 @@ export function uiFieldWikipedia(field, context) { const titleCombo = uiCombobox(context, 'wikipedia-title') .fetcher((value, callback) => { - if (!value && _entity) { - value = context.entity(_entity.id).tags.name || ''; + if (!value) { + value = ''; + for (let i in _entityIDs) { + let entity = context.hasEntity(_entityIDs[i]); + if (entity.tags.name) { + value = entity.tags.name; + break; + } + } } const searchfn = value.length > 7 ? wikipedia.search : wikipedia.suggestions; searchfn(language()[2], value, (query, data) => { @@ -196,7 +203,7 @@ export function uiFieldWikipedia(field, context) { // attempt asynchronous update of wikidata tag.. const initGraph = context.graph(); - const initEntityID = _entity.id; + const initEntityIDs = _entityIDs; wikidata.itemsByTitle(language()[2], value, (err, data) => { if (err || !data || !Object.keys(data).length) return; @@ -206,13 +213,26 @@ export function uiFieldWikipedia(field, context) { const qids = Object.keys(data); const value = qids && qids.find(id => id.match(/^Q\d+$/)); - let currTags = Object.assign({}, context.entity(initEntityID).tags); // shallow copy - currTags.wikidata = value; + let actions = initEntityIDs.map((entityID) => { + let entity = context.entity(entityID).tags; + let currTags = Object.assign({}, entity); // shallow copy + if (currTags.wikidata !== value) { + currTags.wikidata = value; + return actionChangeTags(entityID, currTags); + } + }).filter(Boolean); + + if (!actions.length) return; // Coalesce the update of wikidata tag into the previous tag change context.overwrite( - actionChangeTags(initEntityID, currTags), + function actionUpdateWikidataTags(graph) { + actions.forEach(function(action) { + graph = action(graph); + }); + return graph; + }, context.history().undoAnnotation() ); @@ -223,7 +243,7 @@ export function uiFieldWikipedia(field, context) { wiki.tags = (tags) => { - const value = tags[field.key] || ''; + const value = typeof tags[field.key] === 'string' ? tags[field.key] : ''; const m = value.match(/([^:]+):([^#]+)(?:#(.+))?/); const l = m && _dataWikipedia.find(d => m[1] === d[2]); let anchor = m && m[3]; @@ -256,9 +276,9 @@ export function uiFieldWikipedia(field, context) { }; - wiki.entity = function(val) { - if (!arguments.length) return _entity; - _entity = val; + wiki.entityIDs = (val) => { + if (!arguments.length) return _entityIDs; + _entityIDs = val; return wiki; }; @@ -270,3 +290,5 @@ export function uiFieldWikipedia(field, context) { return utilRebind(wiki, dispatch, 'on'); } + +uiFieldWikipedia.supportsMultiselection = false; diff --git a/modules/ui/form_fields.js b/modules/ui/form_fields.js index fee80193d..5b202a12a 100644 --- a/modules/ui/form_fields.js +++ b/modules/ui/form_fields.js @@ -28,7 +28,7 @@ export function uiFormFields(context) { var fields = container.selectAll('.wrap-form-field') - .data(shown, function(d) { return d.id + (d.entityID || ''); }); + .data(shown, function(d) { return d.id + (d.entityIDs ? d.entityIDs.join() : ''); }); fields.exit() .remove(); diff --git a/modules/ui/preset_editor.js b/modules/ui/preset_editor.js index 571fe83ec..45b34a903 100644 --- a/modules/ui/preset_editor.js +++ b/modules/ui/preset_editor.js @@ -10,17 +10,18 @@ import { modeBrowse } from '../modes/browse'; import { uiDisclosure } from './disclosure'; import { uiField } from './field'; import { uiFormFields } from './form_fields'; +import { utilArrayIdentical } from '../util/array'; import { utilArrayUnion, utilRebind } from '../util'; export function uiPresetEditor(context) { - var dispatch = d3_dispatch('change'); + var dispatch = d3_dispatch('change', 'revert'); var formFields = uiFormFields(context); var _state; var _fieldsArr; - var _preset; + var _presets = []; var _tags; - var _entityID; + var _entityIDs; function presetEditor(selection) { @@ -33,36 +34,56 @@ export function uiPresetEditor(context) { function render(selection) { if (!_fieldsArr) { - var entity = context.entity(_entityID); - var geometry = context.geometry(_entityID); - var presets = context.presets(); + + var graph = context.graph(); + + var geometries = Object.keys(_entityIDs.reduce(function(geoms, entityID) { + return geoms[graph.entity(entityID).geometry(graph)] = true; + }, {})); + + var presetsManager = context.presets(); + + var combinedFields = _presets.reduce(function(fields, preset) { + if (!fields.length) return preset.fields; + return fields.filter(function(field) { + return preset.fields.indexOf(field) !== -1 || preset.moreFields.indexOf(field) !== -1; + }); + }, []); + + var combinedMoreFields = _presets.reduce(function(fields, preset) { + if (!fields.length) return preset.moreFields; + return fields.filter(function(field) { + return preset.fields.indexOf(field) !== -1 || preset.moreFields.indexOf(field) !== -1; + }); + }, []); _fieldsArr = []; - _preset.fields.forEach(function(field) { - if (field.matchGeometry(geometry)) { + combinedFields.forEach(function(field) { + if (field.matchAllGeometry(geometries)) { _fieldsArr.push( - uiField(context, field, entity) + uiField(context, field, _entityIDs) ); } }); - if (entity.isHighwayIntersection(context.graph()) && presets.field('restrictions')) { + var singularEntity = _entityIDs.length === 1 && graph.hasEntity(_entityIDs[0]); + if (singularEntity && singularEntity.isHighwayIntersection(graph) && presetsManager.field('restrictions')) { _fieldsArr.push( - uiField(context, presets.field('restrictions'), entity) + uiField(context, presetsManager.field('restrictions'), _entityIDs) ); } - var additionalFields = utilArrayUnion(_preset.moreFields, presets.universal()); + var additionalFields = utilArrayUnion(combinedMoreFields, presetsManager.universal()); additionalFields.sort(function(field1, field2) { return field1.label().localeCompare(field2.label(), currentLocale); }); additionalFields.forEach(function(field) { - if (_preset.fields.indexOf(field) === -1 && - field.matchGeometry(geometry)) { + if (combinedFields.indexOf(field) === -1 && + field.matchAllGeometry(geometries)) { _fieldsArr.push( - uiField(context, field, entity, { show: false }) + uiField(context, field, _entityIDs, { show: false }) ); } }); @@ -71,6 +92,9 @@ export function uiPresetEditor(context) { field .on('change', function(t, onInput) { dispatch.call('change', field, t, onInput); + }) + .on('revert', function(keys) { + dispatch.call('revert', field, keys); }); }); } @@ -100,11 +124,12 @@ export function uiPresetEditor(context) { } - presetEditor.preset = function(val) { - if (!arguments.length) return _preset; - if (_preset && _preset.id === val.id) return presetEditor; - _preset = val; - _fieldsArr = null; + presetEditor.presets = function(val) { + if (!arguments.length) return _presets; + if (!_presets || !val || !utilArrayIdentical(_presets, val)) { + _presets = val; + _fieldsArr = null; + } return presetEditor; }; @@ -124,11 +149,12 @@ export function uiPresetEditor(context) { }; - presetEditor.entityID = function(val) { - if (!arguments.length) return _entityID; - if (_entityID === val) return presetEditor; - _entityID = val; - _fieldsArr = null; + presetEditor.entityIDs = function(val) { + if (!arguments.length) return _entityIDs; + if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) { + _entityIDs = val; + _fieldsArr = null; + } return presetEditor; }; diff --git a/modules/ui/preset_list.js b/modules/ui/preset_list.js index 3d0d55e72..ebb5f1d0b 100644 --- a/modules/ui/preset_list.js +++ b/modules/ui/preset_list.js @@ -12,6 +12,7 @@ import { actionChangePreset } from '../actions/change_preset'; import { operationDelete } from '../operations/delete'; import { svgIcon } from '../svg/index'; import { tooltip } from '../util/tooltip'; +import { geoExtent } from '../geo/extent'; import { uiPresetIcon } from './preset_icon'; import { uiTagReference } from './tag_reference'; import { utilKeybinding, utilNoAuto, utilRebind } from '../util'; @@ -511,17 +512,10 @@ export function uiPresetList(context) { } function combinedEntityExtent() { - var extent; - _entityIDs.forEach(function(entityID) { + return _entityIDs.reduce(function(extent, entityID) { var entity = context.graph().entity(entityID); - var entityExtent = entity.extent(context.graph()); - if (!extent) { - extent = entityExtent; - } else { - extent = extent.extend(entityExtent); - } - }); - return extent; + return extent.extend(entity.extent(context.graph())); + }, geoExtent()); } return utilRebind(presetList, dispatch, 'on'); diff --git a/modules/ui/raw_tag_editor.js b/modules/ui/raw_tag_editor.js index 39cd4eb40..4733e1309 100644 --- a/modules/ui/raw_tag_editor.js +++ b/modules/ui/raw_tag_editor.js @@ -23,7 +23,6 @@ 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; @@ -283,10 +282,10 @@ export function uiRawTagEditor(context) { items.selectAll('input.value') .attr('title', function(d) { - return typeof d.value === 'string' ? d.value : Array.from(_keyValues[d.key]).sort().join('; '); + return Array.isArray(d.value) ? d.value.filter(Boolean).join('; ') : d.value; }) - .classed('conflicting', function(d) { - return typeof d.value !== 'string'; + .classed('mixed', function(d) { + return Array.isArray(d.value); }) .attr('placeholder', function(d) { return typeof d.value === 'string' ? null : t('inspector.multiple_values'); @@ -346,7 +345,7 @@ export function uiRawTagEditor(context) { .filter(function(row) { return row.key && row.key.trim() !== ''; }) .map(function(row) { var rawVal = row.value; - if (rawVal === true) rawVal = '*'; + if (typeof rawVal !== 'string') rawVal = '*'; var val = rawVal ? stringify(rawVal) : ''; return stringify(row.key) + '=' + val; }) @@ -382,7 +381,7 @@ export function uiRawTagEditor(context) { if (isReadOnly({ key: change.key })) return; // skip unchanged multiselection placeholders - if (change.newVal === '*' && change.oldVal === true) return; + if (change.newVal === '*' && typeof change.oldVal !== 'string') return; if (change.type === '-') { _pendingChange[change.key] = undefined; @@ -413,13 +412,13 @@ export function uiRawTagEditor(context) { function bindTypeahead(key, value) { if (isReadOnly(key.datum())) return; - if (typeof value.datum().value !== 'string' && _keyValues) { + if (Array.isArray(value.datum().value)) { 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) { + if (!_tags[keyString]) return; + var data = _tags[keyString].filter(Boolean).map(function(tagValue) { return { value: tagValue, title: tagValue @@ -633,59 +632,6 @@ export function uiRawTagEditor(context) { _entityIDs = val; _orderedKeys = []; } - - 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/util/index.js b/modules/util/index.js index 9c83d6bd5..ce1e2ca16 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -10,6 +10,7 @@ export { utilArrayUniqBy } from './array'; export { utilAsyncMap } from './util'; export { utilCleanTags } from './clean_tags'; +export { utilCombinedTags } from './util'; export { utilDeepMemberSelector } from './util'; export { utilDetect } from './detect'; export { utilDisplayName } from './util'; diff --git a/modules/util/util.js b/modules/util/util.js index b5ab98e40..a7ea8675e 100644 --- a/modules/util/util.js +++ b/modules/util/util.js @@ -230,6 +230,93 @@ export function utilEntityRoot(entityType) { } +// Returns a single object containing the tags of all the given entities. +// Example: +// { +// highway: 'service', +// service: 'parking_aisle' +// } +// + +// { +// highway: 'service', +// service: 'driveway', +// width: '3' +// } +// = +// { +// highway: 'service', +// service: [ 'driveway', 'parking_aisle' ], +// width: [ '3', undefined ] +// } +export function utilCombinedTags(entityIDs, graph) { + + var tags = {}; + var tagCounts = {}; + var allKeys = new Set(); + + var entities = entityIDs.map(function(entityID) { + return graph.hasEntity(entityID); + }).filter(Boolean); + + // gather the aggregate keys + entities.forEach(function(entity) { + var keys = Object.keys(entity.tags).filter(Boolean); + keys.forEach(function(key) { + allKeys.add(key); + }); + }); + + entities.forEach(function(entity) { + + allKeys.forEach(function(key) { + + var value = entity.tags[key]; // purposely allow `undefined` + + if (!tags.hasOwnProperty(key)) { + // first value, set as raw + tags[key] = value; + } else { + if (!Array.isArray(tags[key])) { + if (tags[key] !== value) { + // first alternate value, replace single value with array + tags[key] = [tags[key], value]; + } + } else { // type is array + if (tags[key].indexOf(value) === -1) { + // subsequent alternate value, add to array + tags[key].push(value); + } + } + } + + var tagHash = key + '=' + value; + if (!tagCounts[tagHash]) tagCounts[tagHash] = 0; + tagCounts[tagHash] += 1; + }); + }); + + for (var key in tags) { + if (!Array.isArray(tags[key])) continue; + + // sort values by frequency then alphabetically + tags[key] = tags[key].sort(function(val1, val2) { + var key = key; // capture + var count2 = tagCounts[key + '=' + val2]; + var count1 = tagCounts[key + '=' + val1]; + if (count2 !== count1) { + return count2 - count1; + } + if (val2 && val1) { + return val1.localeCompare(val2); + } + return val1 ? 1 : -1; + }); + } + + return tags; +} + + export function utilStringQs(str) { return str.split('&').reduce(function(obj, pair){ var parts = pair.split('='); diff --git a/test/spec/ui/fields/wikipedia.js b/test/spec/ui/fields/wikipedia.js index e3aa6aa2f..f6258fc4d 100644 --- a/test/spec/ui/fields/wikipedia.js +++ b/test/spec/ui/fields/wikipedia.js @@ -79,7 +79,7 @@ describe('iD.uiFieldWikipedia', function() { }); it('sets language, value', function(done) { - var wikipedia = iD.uiFieldWikipedia(field, context).entity(entity); + var wikipedia = iD.uiFieldWikipedia(field, context).entityIDs([entity.id]); window.setTimeout(function() { // async, so data will be available wikipedia.on('change', changeTags); selection.call(wikipedia); @@ -105,7 +105,7 @@ describe('iD.uiFieldWikipedia', function() { }); it('recognizes pasted URLs', function(done) { - var wikipedia = iD.uiFieldWikipedia(field, context).entity(entity); + var wikipedia = iD.uiFieldWikipedia(field, context).entityIDs([entity.id]); window.setTimeout(function() { // async, so data will be available wikipedia.on('change', changeTags); selection.call(wikipedia); @@ -136,7 +136,7 @@ describe('iD.uiFieldWikipedia', function() { }); it.skip('does not set delayed wikidata tag if graph has changed', function(done) { - var wikipedia = iD.uiFieldWikipedia(field, context).entity(entity); + var wikipedia = iD.uiFieldWikipedia(field, context).entityIDs([entity.id]); wikipedia.on('change', changeTags); selection.call(wikipedia);