mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 09:12:52 +00:00
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
572 lines
20 KiB
JavaScript
572 lines
20 KiB
JavaScript
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||
import { event as d3_event, selectAll as d3_selectAll, select as d3_select } from 'd3-selection';
|
||
import deepEqual from 'fast-deep-equal';
|
||
|
||
import { t, textDirection } from '../util/locale';
|
||
import { tooltip } from '../util/tooltip';
|
||
import { actionChangeTags } from '../actions/change_tags';
|
||
import { modeBrowse } from '../modes/browse';
|
||
import { svgIcon } from '../svg/icon';
|
||
import { uiDisclosure } from './disclosure';
|
||
import { uiPresetIcon } from './preset_icon';
|
||
import { uiQuickLinks } from './quick_links';
|
||
import { uiRawMemberEditor } from './raw_member_editor';
|
||
import { uiRawMembershipEditor } from './raw_membership_editor';
|
||
import { uiRawTagEditor } from './raw_tag_editor';
|
||
import { uiTagReference } from './tag_reference';
|
||
import { uiPresetEditor } from './preset_editor';
|
||
import { uiEntityIssues } from './entity_issues';
|
||
import { uiSelectionList } from './selection_list';
|
||
import { uiTooltipHtml } from './tooltipHtml';
|
||
import { utilArrayIdentical } from '../util/array';
|
||
import { utilCleanTags, utilCombinedTags, utilRebind } from '../util';
|
||
|
||
|
||
export function uiEntityEditor(context) {
|
||
var dispatch = d3_dispatch('choose');
|
||
var _state = 'select';
|
||
var _coalesceChanges = false;
|
||
var _modified = false;
|
||
var _base;
|
||
var _entityIDs;
|
||
var _activePresets = [];
|
||
var _tagReference;
|
||
var _newFeature;
|
||
|
||
var selectionList = uiSelectionList(context);
|
||
var entityIssues = uiEntityIssues(context);
|
||
var quickLinks = uiQuickLinks();
|
||
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);
|
||
|
||
function entityEditor(selection) {
|
||
|
||
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]);
|
||
|
||
// Enter
|
||
var headerEnter = header.enter()
|
||
.append('div')
|
||
.attr('class', 'header fillL cf');
|
||
|
||
headerEnter
|
||
.append('button')
|
||
.attr('class', 'fl preset-reset preset-choose')
|
||
.call(svgIcon((textDirection === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward'));
|
||
|
||
headerEnter
|
||
.append('button')
|
||
.attr('class', 'fr preset-close')
|
||
.on('click', function() { context.enter(modeBrowse(context)); })
|
||
.call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));
|
||
|
||
headerEnter
|
||
.append('h3');
|
||
|
||
// Update
|
||
header = header
|
||
.merge(headerEnter);
|
||
|
||
header.selectAll('h3')
|
||
.text(singularEntityID ? t('inspector.edit') : t('inspector.edit_features'));
|
||
|
||
header.selectAll('.preset-reset')
|
||
.on('click', function() {
|
||
dispatch.call('choose', this, _activePresets);
|
||
});
|
||
|
||
// Body
|
||
var body = selection.selectAll('.inspector-body')
|
||
.data([0]);
|
||
|
||
// Enter
|
||
var bodyEnter = body.enter()
|
||
.append('div')
|
||
.attr('class', 'entity-editor inspector-body sep-top');
|
||
|
||
// Update
|
||
body = body
|
||
.merge(bodyEnter);
|
||
|
||
var sectionInfos = [
|
||
{
|
||
klass: 'selected-features inspector-inner',
|
||
shouldHave: _entityIDs.length > 1,
|
||
update: function(section) {
|
||
section
|
||
.call(selectionList
|
||
.selectedIDs(_entityIDs)
|
||
);
|
||
}
|
||
},
|
||
{
|
||
klass: 'preset-list-item inspector-inner',
|
||
shouldHave: true,
|
||
update: function(section) {
|
||
|
||
section.classed('mixed-types', _activePresets.length > 1);
|
||
|
||
section
|
||
.call(
|
||
uiDisclosure(context, 'feature_type', true)
|
||
.title(t('inspector.feature_type'))
|
||
.content(renderFeatureType)
|
||
);
|
||
|
||
function renderFeatureType(selection) {
|
||
var presetButtonWrap = selection
|
||
.selectAll('.preset-list-button-wrap')
|
||
.data([0])
|
||
.enter()
|
||
.append('div')
|
||
.attr('class', 'preset-list-button-wrap');
|
||
|
||
var presetButton = presetButtonWrap
|
||
.append('button')
|
||
.attr('class', 'preset-list-button preset-reset')
|
||
.call(tooltip()
|
||
.title(t('inspector.back_tooltip'))
|
||
.placement('bottom')
|
||
);
|
||
|
||
presetButton.append('div')
|
||
.attr('class', 'preset-icon-container');
|
||
|
||
presetButton
|
||
.append('div')
|
||
.attr('class', 'label')
|
||
.append('div')
|
||
.attr('class', 'label-inner');
|
||
|
||
presetButtonWrap.append('div')
|
||
.attr('class', 'accessory-buttons');
|
||
|
||
var tagReferenceBodyWrap = selection
|
||
.selectAll('.tag-reference-body-wrap')
|
||
.data([0]);
|
||
|
||
tagReferenceBodyWrap = tagReferenceBodyWrap
|
||
.enter()
|
||
.append('div')
|
||
.attr('class', 'tag-reference-body-wrap')
|
||
.merge(tagReferenceBodyWrap);
|
||
|
||
selection
|
||
.selectAll('.preset-quick-links')
|
||
.data([0])
|
||
.enter()
|
||
.append('div')
|
||
.attr('class', 'preset-quick-links')
|
||
.call(quickLinks.choices([{
|
||
id: 'zoom_to',
|
||
label: 'inspector.zoom_to.title',
|
||
tooltip: function() {
|
||
return uiTooltipHtml(t('inspector.zoom_to.tooltip_feature'), t('inspector.zoom_to.key'));
|
||
},
|
||
click: function zoomTo() {
|
||
context.mode().zoomToSelected();
|
||
}
|
||
}]));
|
||
|
||
// update header
|
||
if (_tagReference) {
|
||
selection.selectAll('.preset-list-button-wrap .accessory-buttons')
|
||
.style('display', _activePresets.length === 1 ? null : 'none')
|
||
.call(_tagReference.button);
|
||
|
||
tagReferenceBodyWrap
|
||
.style('display', _activePresets.length === 1 ? null : 'none')
|
||
.call(_tagReference.body);
|
||
}
|
||
|
||
selection.selectAll('.preset-reset')
|
||
.on('click', function() {
|
||
dispatch.call('choose', this, _activePresets);
|
||
})
|
||
.on('mousedown', function() {
|
||
d3_event.preventDefault();
|
||
d3_event.stopPropagation();
|
||
})
|
||
.on('mouseup', function() {
|
||
d3_event.preventDefault();
|
||
d3_event.stopPropagation();
|
||
});
|
||
|
||
var geometries = entityGeometries();
|
||
selection.select('.preset-list-item button')
|
||
.call(uiPresetIcon(context)
|
||
.geometry(_activePresets.length === 1 ? (geometries.length === 1 && geometries[0]) : null)
|
||
.preset(_activePresets.length === 1 ? _activePresets[0] : context.presets().item('point'))
|
||
);
|
||
|
||
// NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names)
|
||
var names = _activePresets.length === 1 ? _activePresets[0].name().split(' – ') : [t('inspector.multiple_types')];
|
||
|
||
var label = selection.select('.label-inner');
|
||
var nameparts = label.selectAll('.namepart')
|
||
.data(names, function(d) { return d; });
|
||
|
||
nameparts.exit()
|
||
.remove();
|
||
|
||
nameparts
|
||
.enter()
|
||
.append('div')
|
||
.attr('class', 'namepart')
|
||
.text(function(d) { return d; });
|
||
}
|
||
}
|
||
},
|
||
{
|
||
klass: 'entity-issues',
|
||
shouldHave: singularEntityID,
|
||
update: function(section) {
|
||
section
|
||
.call(entityIssues
|
||
.entityID(singularEntityID)
|
||
);
|
||
}
|
||
}, {
|
||
klass: 'preset-editor',
|
||
shouldHave: true,
|
||
update: function(section) {
|
||
section
|
||
.call(presetEditor
|
||
.presets(_activePresets)
|
||
.entityIDs(_entityIDs)
|
||
.tags(combinedTags)
|
||
.state(_state)
|
||
);
|
||
}
|
||
}, {
|
||
klass: 'raw-tag-editor inspector-inner',
|
||
shouldHave: true,
|
||
update: function(section) {
|
||
section
|
||
.call(rawTagEditor
|
||
.preset(_activePresets[0])
|
||
.entityIDs(_entityIDs)
|
||
.tags(combinedTags)
|
||
.state(_state)
|
||
);
|
||
}
|
||
}, {
|
||
klass: 'raw-member-editor inspector-inner',
|
||
shouldHave: singularEntity && singularEntity.type === 'relation',
|
||
update: function(section) {
|
||
section
|
||
.call(rawMemberEditor
|
||
.entityID(singularEntityID)
|
||
);
|
||
}
|
||
}, {
|
||
klass: 'raw-membership-editor inspector-inner',
|
||
shouldHave: singularEntityID,
|
||
update: function(section) {
|
||
section
|
||
.call(rawMembershipEditor
|
||
.entityID(singularEntityID)
|
||
);
|
||
}
|
||
}, {
|
||
klass: 'key-trap-wrap',
|
||
shouldHave: true,
|
||
create: function(sectionEnter) {
|
||
sectionEnter
|
||
.append('input')
|
||
.attr('type', 'text')
|
||
.attr('class', 'key-trap')
|
||
.on('keydown.key-trap', function() {
|
||
// On tabbing, send focus back to the first field on the inspector-body
|
||
// (probably the `name` field) #4159
|
||
if (d3_event.keyCode === 9 && !d3_event.shiftKey) {
|
||
d3_event.preventDefault();
|
||
body.select('input').node().focus();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
];
|
||
|
||
sectionInfos = sectionInfos.filter(function(info) {
|
||
return info.shouldHave;
|
||
});
|
||
|
||
var sections = body.selectAll('.section')
|
||
.data(sectionInfos, function(d) { return d.klass; });
|
||
|
||
sections.exit().remove();
|
||
|
||
var sectionsEnter = sections.enter()
|
||
.append('div')
|
||
.attr('class', function(d) {
|
||
return 'section ' + d.klass;
|
||
});
|
||
|
||
sectionsEnter.each(function(d) {
|
||
if (d.create) {
|
||
d.create(d3_select(this));
|
||
}
|
||
});
|
||
|
||
sections = sectionsEnter
|
||
.merge(sections);
|
||
|
||
sections.each(function(d) {
|
||
if (d.update) {
|
||
d.update(d3_select(this));
|
||
}
|
||
});
|
||
|
||
context.history()
|
||
.on('change.entity-editor', historyChanged);
|
||
|
||
function historyChanged(difference) {
|
||
if (selection.selectAll('.entity-editor').empty()) return;
|
||
if (_state === 'hide') return;
|
||
var significant = !difference ||
|
||
difference.didChange.properties ||
|
||
difference.didChange.addition ||
|
||
difference.didChange.deletion;
|
||
if (!significant) return;
|
||
|
||
_entityIDs = _entityIDs.filter(context.hasEntity);
|
||
if (!_entityIDs.length) return;
|
||
|
||
var priorActivePreset = _activePresets.length === 1 && _activePresets[0];
|
||
|
||
loadActivePresets();
|
||
|
||
var graph = context.graph();
|
||
entityEditor.modified(_base !== graph);
|
||
entityEditor(selection);
|
||
|
||
if (priorActivePreset && _activePresets.length === 1 && priorActivePreset !== _activePresets[0]) {
|
||
// flash the button to indicate the preset changed
|
||
d3_selectAll('.entity-editor button.preset-reset .label')
|
||
.style('background-color', '#fff')
|
||
.transition()
|
||
.duration(750)
|
||
.style('background-color', null);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Tag changes that fire on input can all get coalesced into a single
|
||
// history operation when the user leaves the field. #2342
|
||
function changeTags(changed, onInput) {
|
||
|
||
var actions = [];
|
||
for (var i in _entityIDs) {
|
||
var entityID = _entityIDs[i];
|
||
var entity = context.entity(entityID);
|
||
|
||
var tags = Object.assign({}, entity.tags); // shallow copy
|
||
|
||
for (var k in changed) {
|
||
if (!k) continue;
|
||
var v = changed[k];
|
||
if (v !== undefined || tags.hasOwnProperty(k)) {
|
||
tags[k] = v;
|
||
}
|
||
}
|
||
|
||
if (!onInput) {
|
||
tags = utilCleanTags(tags);
|
||
}
|
||
|
||
if (!deepEqual(entity.tags, tags)) {
|
||
actions.push(actionChangeTags(entityID, tags));
|
||
}
|
||
}
|
||
|
||
if (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 = !!onInput;
|
||
}
|
||
}
|
||
|
||
// if leaving field (blur event), rerun validation
|
||
if (!onInput) {
|
||
context.validator().validate();
|
||
}
|
||
}
|
||
|
||
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;
|
||
_modified = val;
|
||
return entityEditor;
|
||
};
|
||
|
||
|
||
entityEditor.state = function(val) {
|
||
if (!arguments.length) return _state;
|
||
_state = val;
|
||
return entityEditor;
|
||
};
|
||
|
||
|
||
entityEditor.entityIDs = function(val) {
|
||
if (!arguments.length) return _entityIDs;
|
||
if (val && _entityIDs && utilArrayIdentical(_entityIDs, val)) return entityEditor; // exit early if no change
|
||
|
||
_entityIDs = val;
|
||
_base = context.graph();
|
||
_coalesceChanges = false;
|
||
|
||
loadActivePresets();
|
||
|
||
return entityEditor
|
||
.modified(false);
|
||
};
|
||
|
||
|
||
entityEditor.newFeature = function(val) {
|
||
if (!arguments.length) return _newFeature;
|
||
_newFeature = val;
|
||
return entityEditor;
|
||
};
|
||
|
||
|
||
function loadActivePresets() {
|
||
|
||
var graph = context.graph();
|
||
|
||
var counts = {};
|
||
|
||
for (var i in _entityIDs) {
|
||
var entity = graph.hasEntity(_entityIDs[i]);
|
||
if (!entity) return;
|
||
|
||
var match = context.presets().match(entity, graph);
|
||
|
||
if (!counts[match.id]) counts[match.id] = 0;
|
||
counts[match.id] += 1;
|
||
}
|
||
|
||
var matches = Object.keys(counts).sort(function(p1, p2) {
|
||
return counts[p2] - counts[p1];
|
||
}).map(function(pID) {
|
||
return context.presets().item(pID);
|
||
});
|
||
|
||
// A "weak" preset doesn't set any tags. (e.g. "Address")
|
||
var weakPreset = _activePresets.length === 1 &&
|
||
Object.keys(_activePresets[0].addTags || {}).length === 0;
|
||
// Don't replace a weak preset with a fallback preset (e.g. "Point")
|
||
if (weakPreset && matches.length === 0 && matches[0].isFallback()) return;
|
||
|
||
entityEditor.presets(matches);
|
||
}
|
||
|
||
entityEditor.presets = function(val) {
|
||
if (!arguments.length) return _activePresets;
|
||
|
||
// don't reload the same preset
|
||
if (!utilArrayIdentical(val, _activePresets)) {
|
||
|
||
_activePresets = val;
|
||
|
||
var geometries = entityGeometries();
|
||
if (_activePresets.length === 1 && geometries.length) {
|
||
_tagReference = uiTagReference(_activePresets[0].reference(geometries[0]), context)
|
||
.showing(false);
|
||
}
|
||
}
|
||
return entityEditor;
|
||
};
|
||
|
||
function entityGeometries() {
|
||
|
||
var counts = {};
|
||
|
||
for (var i in _entityIDs) {
|
||
var geometry = context.geometry(_entityIDs[i]);
|
||
if (!counts[geometry]) counts[geometry] = 0;
|
||
counts[geometry] += 1;
|
||
}
|
||
|
||
return Object.keys(counts).sort(function(geom1, geom2) {
|
||
return counts[geom2] - counts[geom1];
|
||
});
|
||
}
|
||
|
||
return utilRebind(entityEditor, dispatch, 'on');
|
||
}
|