modules/ui/entity_editor.js (312 lines of code) (raw):
import { event as d3_event, selectAll as d3_selectAll } from 'd3-selection';
import deepEqual from 'fast-deep-equal';
import { t } from '../util/locale';
import { tooltip } from '../util/tooltip';
import { actionChangePreset } from '../actions/change_preset';
import { actionChangeTags } from '../actions/change_tags';
import { uiPresetFavoriteButton } from './preset_favorite_button';
import { uiPresetIcon } from './preset_icon';
import { uiRawMemberEditor } from './raw_member_editor';
import { uiRawMembershipEditor } from './raw_membership_editor';
import { uiRawTagEditor } from './raw_tag_editor';
import { uiTagReference } from './tag_reference';
import { uiPresetBrowser } from './preset_browser';
import { uiPresetEditor } from './preset_editor';
import { uiEntityIssues } from './entity_issues';
import { uiSelectionList } from './selection_list';
import { utilCleanTags } from '../util';
import { uiViewOnOSM } from './view_on_osm';
export function uiEntityEditor(context) {
var _state = 'select';
var _coalesceChanges = false;
var _modified = false;
var _base;
var _entityIDs;
var _activePreset;
var _tagReference;
var _presetFavorite;
var _newFeature;
var selectionList = uiSelectionList(context);
var entityIssues = uiEntityIssues(context);
var presetEditor = uiPresetEditor(context).on('change', changeTags);
var rawTagEditor = uiRawTagEditor(context).on('change', changeTags);
var rawMemberEditor = uiRawMemberEditor(context);
var rawMembershipEditor = uiRawMembershipEditor(context);
var presetBrowser = uiPresetBrowser(context, [], choosePreset);
function entityEditor(selection) {
var entityID = singularEntityID();
var entity = entityID && context.entity(entityID);
var tags = entity && Object.assign({}, entity.tags); // shallow copy
// 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);
function manageSection(klass, shouldHave, update, create) {
var section = body.selectAll('.' + klass.split(' ')[0])
.data(shouldHave ? [0] : []);
section.exit().remove();
var sectionEnter = section.enter()
.append('div')
.attr('class', klass);
if (create && !sectionEnter.empty()) {
create(sectionEnter);
}
section = sectionEnter
.merge(section);
if (update && !section.empty()) {
update(section);
}
}
manageSection('selection-list', _entityIDs.length > 1, function(section) {
section
.call(selectionList
.setSelectedIDs(_entityIDs)
);
});
manageSection('preset-list-item inspector-inner', entityID, function(section) {
if (_presetFavorite) {
section.selectAll('.preset-list-button-wrap .accessory-buttons')
.call(_presetFavorite.button);
}
// update header
if (_tagReference) {
section.selectAll('.preset-list-button-wrap .accessory-buttons')
.call(_tagReference.button);
section.selectAll('.preset-list-item')
.call(_tagReference.body);
}
section.selectAll('.preset-reset')
.on('click', function() {
if (presetBrowser.isShown()) {
presetBrowser.hide();
} else {
presetBrowser.setAllowedGeometry([context.geometry(entityID)]);
presetBrowser.show();
}
})
.on('mousedown', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
})
.on('mouseup', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
});
section.select('.preset-list-item button')
.call(uiPresetIcon(context)
.geometry(context.geometry(entityID))
.preset(_activePreset)
.pointMarker(false)
);
// NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names)
var label = section.select('.label-inner');
var nameparts = label.selectAll('.namepart')
.data(_activePreset.name().split(' – '), function(d) { return d; });
nameparts.exit()
.remove();
nameparts
.enter()
.append('div')
.attr('class', 'namepart')
.text(function(d) { return d; });
}, function(sectionEnter) {
var presetButtonWrap = sectionEnter
.append('div')
.attr('class', 'preset-list-button-wrap');
var presetButton = presetButtonWrap.append('button')
.attr('class', 'preset-list-button preset-reset')
.call(tooltip().title(t('inspector.back_tooltip')).placement('bottom'));
presetButton
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'label-inner');
presetButtonWrap.append('div')
.attr('class', 'accessory-buttons');
presetButtonWrap
.call(presetBrowser.scrollContainer(body));
// start with the preset browser open if the feature is new and untagged
if (_newFeature && !entity.hasNonGeometryTags()) {
presetBrowser.setAllowedGeometry([context.geometry(entityID)]);
presetBrowser.show();
}
});
manageSection('entity-issues', entityID, function(section) {
section
.call(entityIssues
.entityID(entityID)
);
});
manageSection('preset-editor', entityID, function(section) {
section
.call(presetEditor
.preset(_activePreset)
.entityID(entityID)
.tags(tags)
.state(_state)
);
});
manageSection('raw-tag-editor inspector-inner', true, function(section) {
section
.call(rawTagEditor
.preset(_activePreset)
.entityIDs(_entityIDs)
.state(_state)
);
});
manageSection('raw-member-editor inspector-inner', entity && entity.type === 'relation', function(section) {
section
.call(rawMemberEditor
.entityID(entityID)
);
});
manageSection('raw-membership-editor inspector-inner', entityID, function(section) {
section
.call(rawMembershipEditor
.entityID(entityID)
);
});
manageSection('key-trap-wrap', true, function(section) {
section.select('key-trap')
.on('keydown.key-trap', function() {
// On tabbing, send focus back to the first field on the inspector-body
// (probably the `name` field) #4159
if (d3_event.keyCode === 9 && !d3_event.shiftKey) {
d3_event.preventDefault();
body.select('input').node().focus();
}
});
}, function(sectionEnter) {
sectionEnter
.append('input')
.attr('type', 'text')
.attr('class', 'key-trap');
});
var footer = selection.selectAll('.inspector-footer')
.data([0]);
footer = footer.enter()
.append('div')
.attr('class', 'inspector-footer')
.merge(footer);
footer
.call(uiViewOnOSM(context)
.what(entityID && context.hasEntity(entityID))
);
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;
if (!_entityIDs.every(context.hasEntity)) return;
loadActivePreset();
var graph = context.graph();
entityEditor.modified(_base !== graph);
entityEditor(selection);
}
}
function choosePreset(preset) {
var entityID = singularEntityID();
if (!entityID) return;
context.perform(
actionChangePreset(entityID, _activePreset, preset),
t('operations.change_tags.annotation')
);
context.validator().validate(); // rerun validation
}
// 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 (_entityIDs === val) return entityEditor; // exit early if no change
_entityIDs = val;
_base = context.graph();
_coalesceChanges = false;
loadActivePreset();
return entityEditor
.modified(false);
};
entityEditor.newFeature = function(val) {
if (!arguments.length) return _newFeature;
_newFeature = val;
return entityEditor;
};
function singularEntityID() {
if (_entityIDs.length === 1) {
return _entityIDs[0];
}
return null;
}
function loadActivePreset() {
var entityID = singularEntityID();
var entity = entityID && context.hasEntity(entityID);
if (!entity) return;
var graph = context.graph();
var match = context.presets().match(entity, graph);
// A "weak" preset doesn't set any tags. (e.g. "Address")
var weakPreset = _activePreset &&
Object.keys(_activePreset.addTags || {}).length === 0;
// Don't replace a weak preset with a fallback preset (e.g. "Point")
if ((weakPreset && match.isFallback()) ||
// don't reload for same preset
match === _activePreset) return;
if (_activePreset && match.id !== _activePreset.id) {
// flash the button to indicate the preset changed
d3_selectAll('.entity-editor button.preset-reset .label')
.style('background-color', '#fff')
.transition()
.duration(500)
.style('background-color', null);
}
_activePreset = match;
_tagReference = uiTagReference(_activePreset.reference(context.geometry(entityID)), context)
.showing(false);
_presetFavorite = uiPresetFavoriteButton(_activePreset, context.geometry(entityID), context);
}
return entityEditor;
}