Files
iD/modules/ui/entity_editor.js

515 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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);
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);
// 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: singularEntityID,
update: function(section) {
section
.call(presetEditor
.preset(_activePresets[0])
.entityID(singularEntityID)
.tags(Object.assign({}, singularEntity.tags))
.state(_state)
);
}
}, {
klass: 'raw-tag-editor inspector-inner',
shouldHave: true,
update: function(section) {
section
.call(rawTagEditor
.preset(_activePresets[0])
.entityIDs(_entityIDs)
.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();
}
}
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');
}