mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-24 00:54:03 +02:00
Add initial multiselection raw tag editing in 2.x
This commit is contained in:
+301
-207
@@ -1,5 +1,5 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import {event as d3_event, selectAll as d3_selectAll } from 'd3-selection';
|
||||
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';
|
||||
@@ -15,6 +15,7 @@ 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 { utilCleanTags, utilRebind } from '../util';
|
||||
|
||||
@@ -24,12 +25,13 @@ export function uiEntityEditor(context) {
|
||||
var _state = 'select';
|
||||
var _coalesceChanges = false;
|
||||
var _modified = false;
|
||||
var _scrolled = false;
|
||||
var _base;
|
||||
var _entityID;
|
||||
var _entityIDs;
|
||||
var _activePreset;
|
||||
var _tagReference;
|
||||
var _newFeature;
|
||||
|
||||
var selectionList = uiSelectionList(context);
|
||||
var entityIssues = uiEntityIssues(context);
|
||||
var quickLinks = uiQuickLinks();
|
||||
var presetEditor = uiPresetEditor(context).on('change', changeTags);
|
||||
@@ -38,8 +40,9 @@ export function uiEntityEditor(context) {
|
||||
var rawMembershipEditor = uiRawMembershipEditor(context);
|
||||
|
||||
function entityEditor(selection) {
|
||||
var entity = context.entity(_entityID);
|
||||
var tags = Object.assign({}, entity.tags); // shallow copy
|
||||
var entityID = singularEntityID();
|
||||
var entity = entityID && context.entity(entityID);
|
||||
var tags = entity && Object.assign({}, entity.tags); // shallow copy
|
||||
|
||||
// Header
|
||||
var header = selection.selectAll('.header')
|
||||
@@ -62,19 +65,21 @@ export function uiEntityEditor(context) {
|
||||
.call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));
|
||||
|
||||
headerEnter
|
||||
.append('h3')
|
||||
.text(t('inspector.edit'));
|
||||
.append('h3');
|
||||
|
||||
// Update
|
||||
header = header
|
||||
.merge(headerEnter);
|
||||
|
||||
header.selectAll('h3')
|
||||
.text(entityID ? t('inspector.edit') : t('inspector.edit_features'));
|
||||
|
||||
header.selectAll('.preset-reset')
|
||||
.style('display', entityID ? null : 'none')
|
||||
.on('click', function() {
|
||||
dispatch.call('choose', this, _activePreset);
|
||||
});
|
||||
|
||||
|
||||
// Body
|
||||
var body = selection.selectAll('.inspector-body')
|
||||
.data([0]);
|
||||
@@ -82,159 +87,215 @@ export function uiEntityEditor(context) {
|
||||
// Enter
|
||||
var bodyEnter = body.enter()
|
||||
.append('div')
|
||||
.attr('class', 'inspector-body')
|
||||
.on('scroll.entity-editor', function() { _scrolled = true; });
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-list-item inspector-inner')
|
||||
.append('div')
|
||||
.attr('class', 'preset-list-button-wrap')
|
||||
.append('button')
|
||||
.attr('class', 'preset-list-button preset-reset')
|
||||
.call(tooltip().title(t('inspector.back_tooltip')).placement('bottom'))
|
||||
.append('div')
|
||||
.attr('class', 'label')
|
||||
.append('div')
|
||||
.attr('class', 'label-inner');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-quick-links');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'entity-issues');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-editor');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'raw-tag-editor inspector-inner');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'raw-member-editor inspector-inner');
|
||||
|
||||
bodyEnter
|
||||
.append('div')
|
||||
.attr('class', 'raw-membership-editor inspector-inner');
|
||||
|
||||
bodyEnter
|
||||
.append('input')
|
||||
.attr('type', 'text')
|
||||
.attr('class', 'key-trap');
|
||||
|
||||
.attr('class', 'entity-editor inspector-body sep-top');
|
||||
|
||||
// Update
|
||||
body = body
|
||||
.merge(bodyEnter);
|
||||
|
||||
// update header
|
||||
if (_tagReference) {
|
||||
body.selectAll('.preset-list-button-wrap')
|
||||
.call(_tagReference.button);
|
||||
|
||||
body.selectAll('.preset-list-item')
|
||||
.call(_tagReference.body);
|
||||
}
|
||||
|
||||
body.selectAll('.preset-reset')
|
||||
.on('click', function() {
|
||||
dispatch.call('choose', this, _activePreset);
|
||||
});
|
||||
|
||||
body.select('.preset-list-item button')
|
||||
.call(uiPresetIcon(context)
|
||||
.geometry(context.geometry(_entityID))
|
||||
.preset(_activePreset)
|
||||
);
|
||||
|
||||
// NOTE: split on en-dash, not a hypen (to avoid conflict with hyphenated names)
|
||||
var label = body.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; });
|
||||
|
||||
// update quick links
|
||||
var 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();
|
||||
}
|
||||
}];
|
||||
|
||||
body.select('.preset-quick-links')
|
||||
.call(quickLinks.choices(choices));
|
||||
|
||||
|
||||
// update editor sections
|
||||
body.select('.entity-issues')
|
||||
.call(entityIssues
|
||||
.entityID(_entityID)
|
||||
);
|
||||
|
||||
body.select('.preset-editor')
|
||||
.call(presetEditor
|
||||
.preset(_activePreset)
|
||||
.entityID(_entityID)
|
||||
.tags(tags)
|
||||
.state(_state)
|
||||
);
|
||||
|
||||
body.select('.raw-tag-editor')
|
||||
.call(rawTagEditor
|
||||
.preset(_activePreset)
|
||||
.entityID(_entityID)
|
||||
.tags(tags)
|
||||
.state(_state)
|
||||
);
|
||||
|
||||
if (entity.type === 'relation') {
|
||||
body.select('.raw-member-editor')
|
||||
.style('display', 'block')
|
||||
.call(rawMemberEditor
|
||||
.entityID(_entityID)
|
||||
);
|
||||
} else {
|
||||
body.select('.raw-member-editor')
|
||||
.style('display', 'none');
|
||||
}
|
||||
|
||||
body.select('.raw-membership-editor')
|
||||
.call(rawMembershipEditor
|
||||
.entityID(_entityID)
|
||||
);
|
||||
|
||||
body.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();
|
||||
var sectionInfos = [
|
||||
{
|
||||
klass: 'selection-list',
|
||||
shouldHave: _entityIDs.length > 1,
|
||||
update: function(section) {
|
||||
section
|
||||
.call(selectionList
|
||||
.setSelectedIDs(_entityIDs)
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
klass: 'preset-list-item inspector-inner',
|
||||
shouldHave: entityID,
|
||||
create: 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');
|
||||
|
||||
// update quick links
|
||||
var 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();
|
||||
}
|
||||
}];
|
||||
|
||||
sectionEnter
|
||||
.append('div')
|
||||
.attr('class', 'preset-quick-links')
|
||||
.call(quickLinks.choices(choices));
|
||||
},
|
||||
update: function(section) {
|
||||
|
||||
// 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() {
|
||||
dispatch.call('choose', this, _activePreset);
|
||||
})
|
||||
.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)
|
||||
);
|
||||
|
||||
// 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; });
|
||||
|
||||
}
|
||||
}, {
|
||||
klass: 'entity-issues',
|
||||
shouldHave: entityID,
|
||||
update: function(section) {
|
||||
section
|
||||
.call(entityIssues
|
||||
.entityID(entityID)
|
||||
);
|
||||
}
|
||||
}, {
|
||||
klass: 'preset-editor',
|
||||
shouldHave: entityID,
|
||||
update: function(section) {
|
||||
section
|
||||
.call(presetEditor
|
||||
.preset(_activePreset)
|
||||
.entityID(entityID)
|
||||
.tags(tags)
|
||||
.state(_state)
|
||||
);
|
||||
}
|
||||
}, {
|
||||
klass: 'raw-tag-editor inspector-inner',
|
||||
shouldHave: true,
|
||||
update: function(section) {
|
||||
section
|
||||
.call(rawTagEditor
|
||||
.preset(_activePreset)
|
||||
.entityIDs(_entityIDs)
|
||||
.state(_state)
|
||||
);
|
||||
}
|
||||
}, {
|
||||
klass: 'raw-member-editor inspector-inner',
|
||||
shouldHave: entity && entity.type === 'relation',
|
||||
update: function(section) {
|
||||
section
|
||||
.call(rawMemberEditor
|
||||
.entityID(entityID)
|
||||
);
|
||||
}
|
||||
}, {
|
||||
klass: 'raw-membership-editor inspector-inner',
|
||||
shouldHave: entityID,
|
||||
update: function(section) {
|
||||
section
|
||||
.call(rawMembershipEditor
|
||||
.entityID(entityID)
|
||||
);
|
||||
}
|
||||
}, {
|
||||
klass: 'key-trap-wrap',
|
||||
shouldHave: true,
|
||||
create: function(sectionEnter) {
|
||||
sectionEnter
|
||||
.append('input')
|
||||
.attr('type', 'text')
|
||||
.attr('class', 'key-trap');
|
||||
},
|
||||
update: 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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 ||
|
||||
@@ -242,30 +303,12 @@ export function uiEntityEditor(context) {
|
||||
difference.didChange.deletion;
|
||||
if (!significant) return;
|
||||
|
||||
var entity = context.hasEntity(_entityID);
|
||||
_entityIDs = _entityIDs.filter(context.hasEntity);
|
||||
if (!_entityIDs.length) return;
|
||||
|
||||
loadActivePreset();
|
||||
|
||||
var graph = context.graph();
|
||||
if (!entity) return;
|
||||
|
||||
var match = context.presets().match(entity, graph);
|
||||
var activePreset = entityEditor.preset();
|
||||
var weakPreset = activePreset &&
|
||||
Object.keys(activePreset.addTags || {}).length === 0;
|
||||
|
||||
// A "weak" preset doesn't set any tags. (e.g. "Address")
|
||||
// Don't replace a weak preset with a fallback preset (e.g. "Point")
|
||||
if (!(weakPreset && match.isFallback())) {
|
||||
entityEditor.preset(match);
|
||||
|
||||
if (match.id !== activePreset.id) {
|
||||
// flash the button to indicate the preset changed
|
||||
selection
|
||||
.selectAll('button.preset-reset .label')
|
||||
.style('background-color', '#fff')
|
||||
.transition()
|
||||
.duration(500)
|
||||
.style('background-color', null);
|
||||
}
|
||||
}
|
||||
entityEditor.modified(_base !== graph);
|
||||
entityEditor(selection);
|
||||
}
|
||||
@@ -275,27 +318,45 @@ export function uiEntityEditor(context) {
|
||||
// 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 entity = context.entity(_entityID);
|
||||
var annotation = t('operations.change_tags.annotation');
|
||||
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;
|
||||
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 (!onInput) {
|
||||
tags = utilCleanTags(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 (!deepEqual(entity.tags, tags)) {
|
||||
if (_coalesceChanges) {
|
||||
context.overwrite(actionChangeTags(_entityID, tags), annotation);
|
||||
context.overwrite(combinedAction, annotation);
|
||||
} else {
|
||||
context.perform(actionChangeTags(_entityID, tags), annotation);
|
||||
context.perform(combinedAction, annotation);
|
||||
_coalesceChanges = !!onInput;
|
||||
}
|
||||
}
|
||||
@@ -310,8 +371,6 @@ export function uiEntityEditor(context) {
|
||||
entityEditor.modified = function(val) {
|
||||
if (!arguments.length) return _modified;
|
||||
_modified = val;
|
||||
d3_selectAll('button.preset-close use')
|
||||
.attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close'));
|
||||
return entityEditor;
|
||||
};
|
||||
|
||||
@@ -323,39 +382,74 @@ export function uiEntityEditor(context) {
|
||||
};
|
||||
|
||||
|
||||
entityEditor.entityID = function(val) {
|
||||
if (!arguments.length) return _entityID;
|
||||
if (_entityID === val) return entityEditor; // exit early if no change
|
||||
entityEditor.entityIDs = function(val) {
|
||||
if (!arguments.length) return _entityIDs;
|
||||
if (_entityIDs === val) return entityEditor; // exit early if no change
|
||||
|
||||
_entityID = val;
|
||||
_entityIDs = val;
|
||||
_base = context.graph();
|
||||
_coalesceChanges = false;
|
||||
|
||||
// reset the scroll to the top of the inspector (warning: triggers reflow)
|
||||
if (_scrolled) {
|
||||
window.requestIdleCallback(function() {
|
||||
var body = d3_selectAll('.entity-editor-pane .inspector-body');
|
||||
if (!body.empty()) {
|
||||
_scrolled = false;
|
||||
body.node().scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var presetMatch = context.presets().match(context.entity(_entityID), _base);
|
||||
loadActivePreset();
|
||||
|
||||
return entityEditor
|
||||
.preset(presetMatch)
|
||||
.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);
|
||||
}
|
||||
|
||||
entityEditor.preset(match);
|
||||
}
|
||||
|
||||
entityEditor.preset = function(val) {
|
||||
if (!arguments.length) return _activePreset;
|
||||
if (val !== _activePreset) {
|
||||
_activePreset = val;
|
||||
_tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context)
|
||||
.showing(false);
|
||||
var entityID = singularEntityID();
|
||||
if (entityID) {
|
||||
_tagReference = uiTagReference(_activePreset.reference(context.geometry(entityID)), context)
|
||||
.showing(false);
|
||||
}
|
||||
}
|
||||
return entityEditor;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user