Files
iD/modules/ui/preset_list.js
Quincy Morgan 6bb2997306 Enable changing the preset of multiple selected features (re: #7276)
Fix issue where preset tag reference wouldn't load
Change "Change feature" tooltip to more accurate "Change feature type"
2020-01-21 15:19:51 -05:00

529 lines
20 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 * as countryCoder from '@ideditor/country-coder';
import {
event as d3_event,
select as d3_select,
selectAll as d3_selectAll
} from 'd3-selection';
import { t, textDirection } from '../util/locale';
import { actionChangePreset } from '../actions/change_preset';
import { operationDelete } from '../operations/delete';
import { svgIcon } from '../svg/index';
import { tooltip } from '../util/tooltip';
import { uiPresetIcon } from './preset_icon';
import { uiTagReference } from './tag_reference';
import { utilKeybinding, utilNoAuto, utilRebind } from '../util';
export function uiPresetList(context) {
var dispatch = d3_dispatch('cancel', 'choose');
var _entityIDs;
var _currentPresets;
var _autofocus = false;
function presetList(selection) {
if (!_entityIDs) return;
var presets = context.presets().matchAllGeometry(entityGeometries());
selection.html('');
var messagewrap = selection
.append('div')
.attr('class', 'header fillL');
var message = messagewrap
.append('h3')
.text(t('inspector.choose'));
messagewrap
.append('button')
.attr('class', 'preset-choose')
.on('click', function() { dispatch.call('cancel', this); })
.call(svgIcon((textDirection === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward'));
function initialKeydown() {
// hack to let delete shortcut work when search is autofocused
if (search.property('value').length === 0 &&
(d3_event.keyCode === utilKeybinding.keyCodes['⌫'] ||
d3_event.keyCode === utilKeybinding.keyCodes['⌦'])) {
d3_event.preventDefault();
d3_event.stopPropagation();
operationDelete(_entityIDs, context)();
// hack to let undo work when search is autofocused
} else if (search.property('value').length === 0 &&
(d3_event.ctrlKey || d3_event.metaKey) &&
d3_event.keyCode === utilKeybinding.keyCodes.z) {
d3_event.preventDefault();
d3_event.stopPropagation();
context.undo();
} else if (!d3_event.ctrlKey && !d3_event.metaKey) {
// don't check for delete/undo hack on future keydown events
d3_select(this).on('keydown', keydown);
keydown.call(this);
}
}
function keydown() {
// down arrow
if (d3_event.keyCode === utilKeybinding.keyCodes['↓'] &&
// if insertion point is at the end of the string
search.node().selectionStart === search.property('value').length) {
d3_event.preventDefault();
d3_event.stopPropagation();
// move focus to the first item in the preset list
var buttons = list.selectAll('.preset-list-button');
if (!buttons.empty()) buttons.nodes()[0].focus();
}
}
function keypress() {
// enter
var value = search.property('value');
if (d3_event.keyCode === 13 && value.length) {
list.selectAll('.preset-list-item:first-child')
.each(function(d) { d.choose.call(this); });
}
}
function inputevent() {
var value = search.property('value');
list.classed('filtered', value.length);
var extent = combinedEntityExtent();
var results, messageText;
if (value.length && extent) {
var center = extent.center();
var countryCode = countryCoder.iso1A2Code(center);
results = presets.search(value, entityGeometries()[0], countryCode && countryCode.toLowerCase());
messageText = t('inspector.results', {
n: results.collection.length,
search: value
});
} else {
results = context.presets().defaults(entityGeometries()[0], 36);
messageText = t('inspector.choose');
}
list.call(drawList, results);
message.text(messageText);
}
var searchWrap = selection
.append('div')
.attr('class', 'search-header');
var search = searchWrap
.append('input')
.attr('class', 'preset-search-input')
.attr('placeholder', t('inspector.search'))
.attr('type', 'search')
.call(utilNoAuto)
.on('keydown', initialKeydown)
.on('keypress', keypress)
.on('input', inputevent);
searchWrap
.call(svgIcon('#iD-icon-search', 'pre-text'));
if (_autofocus) {
search.node().focus();
}
var listWrap = selection
.append('div')
.attr('class', 'inspector-body');
var list = listWrap
.append('div')
.attr('class', 'preset-list fillL cf')
.call(drawList, context.presets().defaults(entityGeometries()[0], 36));
context.features().on('change.preset-list', updateForFeatureHiddenState);
}
function drawList(list, presets) {
presets = presets.matchAllGeometry(entityGeometries());
var collection = presets.collection.reduce(function(collection, preset) {
if (!preset) return collection;
if (preset.members) {
if (preset.members.collection.filter(function(preset) {
return preset.addable();
}).length > 1) {
collection.push(CategoryItem(preset));
}
} else if (preset.addable()) {
collection.push(PresetItem(preset));
}
return collection;
}, []);
var items = list.selectAll('.preset-list-item')
.data(collection, function(d) { return d.preset.id; });
items.order();
items.exit()
.remove();
items.enter()
.append('div')
.attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replace('/', '-'); })
.classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; })
.each(function(item) { d3_select(this).call(item); })
.style('opacity', 0)
.transition()
.style('opacity', 1);
updateForFeatureHiddenState();
}
function itemKeydown(){
// the actively focused item
var item = d3_select(this.closest('.preset-list-item'));
var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item'));
// arrow down, move focus to the next, lower item
if (d3_event.keyCode === utilKeybinding.keyCodes['↓']) {
d3_event.preventDefault();
d3_event.stopPropagation();
// the next item in the list at the same level
var nextItem = d3_select(item.node().nextElementSibling);
// if there is no next item in this list
if (nextItem.empty()) {
// if there is a parent item
if (!parentItem.empty()) {
// the item is the last item of a sublist,
// select the next item at the parent level
nextItem = d3_select(parentItem.node().nextElementSibling);
}
// if the focused item is expanded
} else if (d3_select(this).classed('expanded')) {
// select the first subitem instead
nextItem = item.select('.subgrid .preset-list-item:first-child');
}
if (!nextItem.empty()) {
// focus on the next item
nextItem.select('.preset-list-button').node().focus();
}
// arrow up, move focus to the previous, higher item
} else if (d3_event.keyCode === utilKeybinding.keyCodes['↑']) {
d3_event.preventDefault();
d3_event.stopPropagation();
// the previous item in the list at the same level
var previousItem = d3_select(item.node().previousElementSibling);
// if there is no previous item in this list
if (previousItem.empty()) {
// if there is a parent item
if (!parentItem.empty()) {
// the item is the first subitem of a sublist select the parent item
previousItem = parentItem;
}
// if the previous item is expanded
} else if (previousItem.select('.preset-list-button').classed('expanded')) {
// select the last subitem of the sublist of the previous item
previousItem = previousItem.select('.subgrid .preset-list-item:last-child');
}
if (!previousItem.empty()) {
// focus on the previous item
previousItem.select('.preset-list-button').node().focus();
} else {
// the focus is at the top of the list, move focus back to the search field
var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input');
search.node().focus();
}
// arrow left, move focus to the parent item if there is one
} else if (d3_event.keyCode === utilKeybinding.keyCodes[(textDirection === 'rtl') ? '→' : '←']) {
d3_event.preventDefault();
d3_event.stopPropagation();
// if there is a parent item, focus on the parent item
if (!parentItem.empty()) {
parentItem.select('.preset-list-button').node().focus();
}
// arrow right, choose this item
} else if (d3_event.keyCode === utilKeybinding.keyCodes[(textDirection === 'rtl') ? '←' : '→']) {
d3_event.preventDefault();
d3_event.stopPropagation();
item.datum().choose.call(d3_select(this).node());
}
}
function CategoryItem(preset) {
var box, sublist, shown = false;
function item(selection) {
var wrap = selection.append('div')
.attr('class', 'preset-list-button-wrap category');
function click() {
var isExpanded = d3_select(this).classed('expanded');
var iconName = isExpanded ?
(textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down';
d3_select(this)
.classed('expanded', !isExpanded);
d3_select(this).selectAll('div.label-inner svg.icon use')
.attr('href', iconName);
item.choose();
}
var geometries = entityGeometries();
var button = wrap
.append('button')
.attr('class', 'preset-list-button')
.classed('expanded', false)
.call(uiPresetIcon(context)
.geometry(geometries.length === 1 && geometries[0])
.preset(preset))
.on('click', click)
.on('keydown', function() {
// right arrow, expand the focused item
if (d3_event.keyCode === utilKeybinding.keyCodes[(textDirection === 'rtl') ? '←' : '→']) {
d3_event.preventDefault();
d3_event.stopPropagation();
// if the item isn't expanded
if (!d3_select(this).classed('expanded')) {
// toggle expansion (expand the item)
click.call(this);
}
// left arrow, collapse the focused item
} else if (d3_event.keyCode === utilKeybinding.keyCodes[(textDirection === 'rtl') ? '→' : '←']) {
d3_event.preventDefault();
d3_event.stopPropagation();
// if the item is expanded
if (d3_select(this).classed('expanded')) {
// toggle expansion (collapse the item)
click.call(this);
}
} else {
itemKeydown.call(this);
}
});
var label = button
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'label-inner');
label
.append('div')
.attr('class', 'namepart')
.call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'))
.append('span')
.html(function() { return preset.name() + '…'; });
box = selection.append('div')
.attr('class', 'subgrid')
.style('max-height', '0px')
.style('opacity', 0);
box.append('div')
.attr('class', 'arrow');
sublist = box.append('div')
.attr('class', 'preset-list fillL3');
}
item.choose = function() {
if (!box || !sublist) return;
if (shown) {
shown = false;
box.transition()
.duration(200)
.style('opacity', '0')
.style('max-height', '0px')
.style('padding-bottom', '0px');
} else {
shown = true;
var members = preset.members.matchAllGeometry(entityGeometries());
sublist.call(drawList, members);
box.transition()
.duration(200)
.style('opacity', '1')
.style('max-height', 200 + members.collection.length * 190 + 'px')
.style('padding-bottom', '10px');
}
};
item.preset = preset;
return item;
}
function PresetItem(preset) {
function item(selection) {
var wrap = selection.append('div')
.attr('class', 'preset-list-button-wrap');
var geometries = entityGeometries();
var button = wrap.append('button')
.attr('class', 'preset-list-button')
.call(uiPresetIcon(context)
.geometry(geometries.length === 1 && geometries[0])
.preset(preset))
.on('click', item.choose)
.on('keydown', itemKeydown);
var label = button
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'label-inner');
// NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc)
label.selectAll('.namepart')
.data(preset.name().split(' '))
.enter()
.append('div')
.attr('class', 'namepart')
.text(function(d) { return d; });
wrap.call(item.reference.button);
selection.call(item.reference.body);
}
item.choose = function() {
if (d3_select(this).classed('disabled')) return;
context.presets().setMostRecent(preset, entityGeometries()[0]);
context.perform(
function(graph) {
for (var i in _entityIDs) {
var entityID = _entityIDs[i];
var oldPreset = context.presets().match(graph.entity(entityID), graph);
graph = actionChangePreset(entityID, oldPreset, preset)(graph);
}
return graph;
},
t('operations.change_tags.annotation')
);
context.validator().validate(); // rerun validation
dispatch.call('choose', this, preset);
};
item.help = function() {
d3_event.stopPropagation();
item.reference.toggle();
};
item.preset = preset;
item.reference = uiTagReference(preset.reference(entityGeometries()[0]), context);
return item;
}
function updateForFeatureHiddenState() {
if (!_entityIDs.every(context.hasEntity)) return;
var geometries = entityGeometries();
var button = d3_selectAll('.preset-list .preset-list-button');
// remove existing tooltips
button.call(tooltip().destroyAny);
button.each(function(item, index) {
var hiddenPresetFeaturesId;
for (var i in geometries) {
hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]);
if (hiddenPresetFeaturesId) break;
}
var isHiddenPreset = !context.inIntro() &&
!!hiddenPresetFeaturesId &&
(_currentPresets.length !== 1 || item.preset !== _currentPresets[0]);
d3_select(this)
.classed('disabled', isHiddenPreset);
if (isHiddenPreset) {
var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);
var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual';
var tooltipObj = { features: t('feature.' + hiddenPresetFeaturesId + '.description') };
d3_select(this).call(tooltip()
.title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj))
.placement(index < 2 ? 'bottom' : 'top')
);
}
});
}
presetList.autofocus = function(val) {
if (!arguments.length) return _autofocus;
_autofocus = val;
return presetList;
};
presetList.entityIDs = function(val) {
if (!arguments.length) return _entityIDs;
_entityIDs = val;
if (_entityIDs && _entityIDs.length) {
var presets = _entityIDs.map(function(entityID) {
return context.presets().match(context.entity(entityID), context.graph());
});
presetList.presets(presets);
}
return presetList;
};
presetList.presets = function(val) {
if (!arguments.length) return _currentPresets;
_currentPresets = val;
return presetList;
};
function entityGeometries() {
var counts = {};
for (var i in _entityIDs) {
var entityID = _entityIDs[i];
var entity = context.entity(entityID);
var geometry = context.geometry(entityID);
// Treat entities on addr:interpolation lines as points, not vertices (#3241)
if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) {
geometry = 'point';
}
if (!counts[geometry]) counts[geometry] = 0;
counts[geometry] += 1;
}
return Object.keys(counts).sort(function(geom1, geom2) {
return counts[geom2] - counts[geom1];
});
}
function combinedEntityExtent() {
var extent;
_entityIDs.forEach(function(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 utilRebind(presetList, dispatch, 'on');
}