modules/ui/preset_list.js (399 lines of code) (raw):

import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; import _debounce from 'lodash-es/debounce'; import { presetManager } from '../presets'; import { t, localizer } from '../core/localizer'; import { actionChangePreset } from '../actions/change_preset'; import { operationDelete } from '../operations/delete'; import { svgIcon } from '../svg/index'; import { uiTooltip } from './tooltip'; import { uiPresetIcon } from './preset_icon'; import { uiTagReference } from './tag_reference'; import { utilKeybinding, utilNoAuto, utilRebind, utilTotalExtent } from '../util'; export function uiPresetList(context) { var dispatch = d3_dispatch('cancel', 'choose'); var _entityIDs; var _currLoc; var _currentPresets; var _autofocus = false; function presetList(selection) { if (!_entityIDs) return; var presets = presetManager.matchAllGeometry(entityGeometries()); selection.html(''); var messagewrap = selection .append('div') .attr('class', 'header fillL'); var message = messagewrap .append('h3') .html(t.html('inspector.choose')); messagewrap .append('button') .attr('class', 'preset-choose') .on('click', function() { dispatch.call('cancel', this); }) .call(svgIcon((localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward')); function initialKeydown(d3_event) { // 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(context, _entityIDs)(); // 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, d3_event); } } function keydown(d3_event) { // 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(d3_event) { // enter var value = search.property('value'); if (d3_event.keyCode === 13 && // ↩ Return 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 results, messageText; if (value.length) { results = presets.search(value, entityGeometries()[0], _currLoc); messageText = t('inspector.results', { n: results.collection.length, search: value }); } else { results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc); messageText = t('inspector.choose'); } list.call(drawList, results); message.html(messageText); } var searchWrap = selection .append('div') .attr('class', 'search-header'); searchWrap .call(svgIcon('#iD-icon-search', 'pre-text')); 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', _debounce(inputevent)); if (_autofocus) { search.node().focus(); // Safari 14 doesn't always like to focus immediately, // so try again on the next pass setTimeout(function() { search.node().focus(); }, 0); } var listWrap = selection .append('div') .attr('class', 'inspector-body'); var list = listWrap .append('div') .attr('class', 'preset-list') .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc)); 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(d3_event) { // 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[(localizer.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[(localizer.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 ? (localizer.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() .geometry(geometries.length === 1 && geometries[0]) .preset(preset)) .on('click', click) .on('keydown', function(d3_event) { // right arrow, expand the focused item if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.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, d3_event); } // left arrow, collapse the focused item } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.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, d3_event); } } else { itemKeydown.call(this, d3_event); } }); var label = button .append('div') .attr('class', 'label') .append('div') .attr('class', 'label-inner'); label .append('div') .attr('class', 'namepart') .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline')) .append('span') .html(function() { return preset.nameLabel() + '&hellip;'; }); 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() .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'); var nameparts = [ preset.nameLabel(), preset.subtitleLabel() ].filter(Boolean); label.selectAll('.namepart') .data(nameparts) .enter() .append('div') .attr('class', 'namepart') .html(function(d) { return d; }); wrap.call(item.reference.button); selection.call(item.reference.body); } item.choose = function() { if (d3_select(this).classed('disabled')) return; if (!context.inIntro()) { presetManager.setMostRecent(preset, entityGeometries()[0]); } context.perform( function(graph) { for (var i in _entityIDs) { var entityID = _entityIDs[i]; var oldPreset = presetManager.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) { d3_event.stopPropagation(); item.reference.toggle(); }; item.preset = preset; item.reference = uiTagReference(preset.reference(), context); return item; } function updateForFeatureHiddenState() { if (!_entityIDs.every(context.hasEntity)) return; var geometries = entityGeometries(); var button = context.container().selectAll('.preset-list .preset-list-button'); // remove existing tooltips button.call(uiTooltip().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); d3_select(this).call(uiTooltip() .title(t.html('inspector.hidden_preset.' + (isAutoHidden ? 'zoom' : 'manual'), { features: t.html('feature.' + hiddenPresetFeaturesId + '.description') })) .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; _currLoc = null; if (_entityIDs && _entityIDs.length) { // calculate current location _currLoc = utilTotalExtent(_entityIDs, context.graph()).center(); // match presets var presets = _entityIDs.map(function(entityID) { return presetManager.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 = entity.geometry(context.graph()); // 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]; }); } return utilRebind(presetList, dispatch, 'on'); }