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
This commit is contained in:
Quincy Morgan
2020-01-30 13:53:29 -05:00
parent 1e21eea745
commit 1b331bb678
25 changed files with 808 additions and 339 deletions
+13 -3
View File
@@ -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;
+6
View File
@@ -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);
+63 -6
View File
@@ -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;
+44 -33
View File
@@ -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');
}
+50 -20
View File
@@ -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);
});
};
+64 -24
View File
@@ -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 {
+34 -12
View File
@@ -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);
+95 -32
View File
@@ -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');
}
+57 -21
View File
@@ -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]);
});
};
+29 -11
View File
@@ -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');
}
+6 -6
View File
@@ -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;
+33 -16
View File
@@ -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;
+37 -16
View File
@@ -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');
}
+13 -6
View File
@@ -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;
+4 -2
View File
@@ -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;
+16 -3
View File
@@ -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);
};
+56 -16
View File
@@ -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;
};
+33 -11
View File
@@ -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;
+1 -1
View File
@@ -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();
+51 -25
View File
@@ -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;
};
+4 -10
View File
@@ -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');
+8 -62
View File
@@ -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;
};
+1
View File
@@ -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';
+87
View File
@@ -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('=');
+3 -3
View File
@@ -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);