Files
iD/modules/ui/tools/search_add.js
T

656 lines
23 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 _debounce from 'lodash-es/debounce';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import {
event as d3_event,
select as d3_select,
selectAll as d3_selectAll
} from 'd3-selection';
import { modeAddArea, modeAddLine, modeAddPoint } from '../../modes';
import { t, textDirection } from '../../util/locale';
import { svgIcon } from '../../svg/index';
import { tooltip } from '../../util/tooltip';
import { uiTagReference } from '../tag_reference';
import { uiTooltipHtml } from '../tooltipHtml';
import { uiPresetFavoriteButton } from '../preset_favorite_button';
import { uiPresetIcon } from '../preset_icon';
import { utilKeybinding, utilNoAuto, utilRebind } from '../../util';
export function uiToolSearchAdd(context) {
var tool = {
id: 'search_add',
label: t('inspector.search')
};
var dispatch = d3_dispatch('choose');
var presets;
var searchWrap = d3_select(null),
search = d3_select(null),
popover = d3_select(null),
popoverContent = d3_select(null),
list = d3_select(null),
footer = d3_select(null),
message = d3_select(null);
var allowedGeometry = ['area', 'line', 'point', 'vertex'];
var shownGeometry = [];
var key = t('modes.add_feature.key');
function updateShownGeometry(geom) {
shownGeometry = geom.sort();
presets = context.presets().matchAnyGeometry(shownGeometry);
}
function toggleShownGeometry(d) {
var geom = shownGeometry;
var index = geom.indexOf(d);
if (index === -1) {
geom.push(d);
if (d === 'point') geom.push('vertex');
} else {
geom.splice(index, 1);
if (d === 'point') geom.splice(geom.indexOf('vertex'), 1);
}
updateShownGeometry(geom);
}
function updateFilterButtonsStates() {
footer.selectAll('button.filter')
.classed('active', function(d) {
return shownGeometry.indexOf(d) !== -1;
});
}
tool.render = function(selection) {
updateShownGeometry(allowedGeometry.slice()); // shallow copy
searchWrap = selection
.append('div')
.attr('class', 'search-wrap')
.call(tooltip()
.placement('bottom')
.html(true)
.title(function() { return uiTooltipHtml(t('modes.add_feature.description'), key); })
);
search = searchWrap
.append('input')
.attr('class', 'search-input')
.attr('placeholder', t('modes.add_feature.title'))
.attr('type', 'search')
.call(utilNoAuto)
.on('mousedown', function() {
search.attr('clicking', true);
})
.on('mouseup', function() {
search.attr('clicking', null);
})
.on('focus', function() {
searchWrap.classed('focused', true);
if (search.attr('clicking')) {
search.attr('focusing', true);
search.attr('clicking', null);
} else {
search.node().setSelectionRange(0, search.property('value').length);
}
popover.classed('hide', false);
})
.on('blur', function() {
searchWrap.classed('focused', false);
popover.classed('hide', true);
})
.on('click', function() {
if (search.attr('focusing')) {
search.node().setSelectionRange(0, search.property('value').length);
search.attr('focusing', null);
}
})
.on('keypress', keypress)
.on('keydown', keydown)
.on('input', updateResultsList);
searchWrap
.call(svgIcon('#iD-icon-search', 'search-icon pre-text'));
popover = searchWrap
.append('div')
.attr('class', 'popover fillL hide')
.on('mousedown', function() {
// don't blur the search input (and thus close results)
d3_event.preventDefault();
d3_event.stopPropagation();
});
popoverContent = popover
.append('div')
.attr('class', 'popover-content');
list = popoverContent.append('div')
.attr('class', 'list');
footer = popover
.append('div')
.attr('class', 'popover-footer');
message = footer.append('div')
.attr('class', 'message');
footer.append('div')
.attr('class', 'filter-wrap')
.selectAll('button.filter')
.data(['point', 'line', 'area'])
.enter()
.append('button')
.attr('class', 'filter active')
.attr('title', function(d) {
return t('modes.add_' + d + '.filter_tooltip');
})
.each(function(d) {
d3_select(this).call(svgIcon('#iD-icon-' + d));
})
.on('click', function(d) {
toggleShownGeometry(d);
if (shownGeometry.length === 0) {
updateShownGeometry(allowedGeometry.slice()); // shallow copy
toggleShownGeometry(d);
}
updateFilterButtonsStates();
updateResultsList();
});
context.features()
.on('change.search-add', updateForFeatureHiddenState);
context.keybinding().on(key, function() {
search.node().focus();
d3_event.preventDefault();
d3_event.stopPropagation();
});
var debouncedUpdate = _debounce(updateEnabledState, 500, { leading: true, trailing: true });
context.map()
.on('move.search-add', debouncedUpdate)
.on('drawn.search-add', debouncedUpdate);
updateEnabledState();
updateResultsList();
};
tool.uninstall = function() {
context.keybinding().off(key);
context.features()
.on('change.search-add', null);
context.map()
.on('move.search-add', null)
.on('drawn.search-add', null);
};
function osmEditable() {
var mode = context.mode();
return context.editable() && mode && mode.id !== 'save';
}
function updateEnabledState() {
var isEnabled = osmEditable();
searchWrap.classed('disabled', !isEnabled);
if (!isEnabled) {
search.node().blur();
}
search.attr('disabled', isEnabled ? null : true);
}
function keypress() {
if (d3_event.keyCode === utilKeybinding.keyCodes.enter) {
popover.selectAll('.list .list-item.focused button.choose')
.each(function(d) { d.choose.call(this); });
d3_event.preventDefault();
d3_event.stopPropagation();
}
}
function keydown() {
var nextFocus,
priorFocus,
parentSubsection;
if (d3_event.keyCode === utilKeybinding.keyCodes['↓'] ||
d3_event.keyCode === utilKeybinding.keyCodes.tab && !d3_event.shiftKey) {
d3_event.preventDefault();
d3_event.stopPropagation();
priorFocus = popover.selectAll('.list .list-item.focused');
if (priorFocus.empty()) {
nextFocus = popover.selectAll('.list > .list-item:first-child');
} else {
nextFocus = d3_select(priorFocus.nodes()[0].nextElementSibling);
if (!nextFocus.empty() && !nextFocus.classed('list-item')) {
nextFocus = nextFocus.selectAll('.list-item:first-child');
}
if (nextFocus.empty()) {
parentSubsection = priorFocus.nodes()[0].closest('.list .subsection');
if (parentSubsection && parentSubsection.nextElementSibling) {
nextFocus = d3_select(parentSubsection.nextElementSibling);
}
}
}
if (!nextFocus.empty()) {
focusListItem(nextFocus, true);
priorFocus.classed('focused', false);
}
} else if (d3_event.keyCode === utilKeybinding.keyCodes['↑'] ||
d3_event.keyCode === utilKeybinding.keyCodes.tab && d3_event.shiftKey) {
d3_event.preventDefault();
d3_event.stopPropagation();
priorFocus = popover.selectAll('.list .list-item.focused');
if (!priorFocus.empty()) {
nextFocus = d3_select(priorFocus.nodes()[0].previousElementSibling);
if (!nextFocus.empty() && !nextFocus.classed('list-item')) {
nextFocus = nextFocus.selectAll('.list-item:last-child');
}
if (nextFocus.empty()) {
parentSubsection = priorFocus.nodes()[0].closest('.list .subsection');
if (parentSubsection && parentSubsection.previousElementSibling) {
nextFocus = d3_select(parentSubsection.previousElementSibling);
}
}
if (!nextFocus.empty()) {
focusListItem(nextFocus, true);
priorFocus.classed('focused', false);
}
}
} else if (d3_event.keyCode === utilKeybinding.keyCodes.esc) {
search.node().blur();
d3_event.preventDefault();
d3_event.stopPropagation();
}
}
function updateResultsList() {
var value = search.property('value');
var results;
if (value.length) {
results = presets.search(value, shownGeometry).collection;
} else {
var recents = context.presets().getRecents();
recents = recents.filter(function(d) {
return shownGeometry.indexOf(d.geometry) !== -1;
});
results = recents.slice(0, 35);
}
list.call(drawList, results);
popover.selectAll('.list .list-item.focused')
.classed('focused', false);
focusListItem(popover.selectAll('.list > .list-item:first-child'), false);
popoverContent.node().scrollTop = 0;
var resultCount = results.length;
message.text(t('modes.add_feature.' + (resultCount === 1 ? 'result' : 'results'), { count: resultCount }));
}
function focusListItem(selection, scrollingToShow) {
if (!selection.empty()) {
selection.classed('focused', true);
if (scrollingToShow) {
// scroll to keep the focused item visible
scrollPopoverToShow(selection);
}
}
}
function scrollPopoverToShow(selection) {
if (selection.empty()) return;
var node = selection.nodes()[0];
var scrollableNode = popoverContent.node();
if (node.offsetTop < scrollableNode.scrollTop) {
scrollableNode.scrollTop = node.offsetTop;
} else if (node.offsetTop + node.offsetHeight > scrollableNode.scrollTop + scrollableNode.offsetHeight &&
node.offsetHeight < scrollableNode.offsetHeight) {
scrollableNode.scrollTop = node.offsetTop + node.offsetHeight - scrollableNode.offsetHeight;
}
}
function itemForPreset(preset) {
if (preset.members) {
return CategoryItem(preset);
}
if (preset.preset && preset.geometry) {
return AddablePresetItem(preset.preset, preset.geometry);
}
var supportedGeometry = preset.geometry.filter(function(geometry) {
return shownGeometry.indexOf(geometry) !== -1;
}).sort();
var vertexIndex = supportedGeometry.indexOf('vertex');
if (vertexIndex !== -1 && supportedGeometry.indexOf('point') !== -1) {
// both point and vertex allowed, just show point
supportedGeometry.splice(vertexIndex, 1);
}
if (supportedGeometry.length === 1) {
return AddablePresetItem(preset, supportedGeometry[0]);
}
return MultiGeometryPresetItem(preset, supportedGeometry);
}
function drawList(list, data) {
list.selectAll('.subsection.subitems').remove();
var dataItems = [];
for (var i = 0; i < data.length; i++) {
var preset = data[i];
if (i < data.length - 1) {
var nextPreset = data[i+1];
// group neighboring presets with the same name
if (preset.name && nextPreset.name && preset.name() === nextPreset.name()) {
var groupedPresets = [preset, nextPreset].sort(function(p1, p2) {
return (p1.geometry[0] < p2.geometry[0]) ? -1 : 1;
});
dataItems.push(MultiPresetItem(groupedPresets));
i++; // skip the next preset since we accounted for it
continue;
}
}
dataItems.push(itemForPreset(preset));
}
var items = list.selectAll('.list-item')
.data(dataItems, function(d) { return d.id(); });
items.order();
items.exit()
.remove();
drawItems(items.enter());
list.selectAll('.list-item.expanded')
.classed('expanded', false)
.selectAll('.label svg.icon use')
.attr('href', textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward');
updateForFeatureHiddenState();
}
function drawItems(selection) {
var item = selection
.append('div')
.attr('class', 'list-item')
.attr('id', function(d) {
return 'search-add-list-item-preset-' + d.id().replace(/[^a-zA-Z\d:]/g, '-');
})
.on('mouseover', function() {
list.selectAll('.list-item.focused')
.classed('focused', false);
d3_select(this)
.classed('focused', true);
})
.on('mouseout', function() {
d3_select(this)
.classed('focused', false);
});
var row = item.append('div')
.attr('class', 'row');
row.append('button')
.attr('class', 'choose')
.on('click', function(d) {
d.choose.call(this);
});
row.each(function(d) {
d3_select(this).call(
uiPresetIcon(context)
.geometry(d.geometry)
.preset(d.preset || d.presets[0])
.sizeClass('small')
);
});
var label = row.append('div')
.attr('class', 'label');
label.each(function(d) {
if (d.subitems) {
d3_select(this)
.call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));
}
});
label.each(function(d) {
// NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc)
d3_select(this)
.append('div')
.attr('class', 'label-inner')
.selectAll('.namepart')
.data(d.name().split(' '))
.enter()
.append('div')
.attr('class', 'namepart')
.text(function(d) { return d; });
});
row.each(function(d) {
if (d.geometry) {
var presetFavorite = uiPresetFavoriteButton(d.preset, d.geometry, context, 'accessory');
d3_select(this).call(presetFavorite.button);
}
});
item.each(function(d) {
if ((d.geometry && (!d.isSubitem || d.isInNameGroup)) || d.geometries) {
var reference = uiTagReference(d.preset.reference(d.geometry || d.geometries[0]), context);
var thisItem = d3_select(this);
thisItem.selectAll('.row').call(reference.button, 'accessory', 'info');
var subsection = thisItem
.append('div')
.attr('class', 'subsection reference');
subsection.call(reference.body);
}
});
}
function updateForFeatureHiddenState() {
var listItem = d3_selectAll('.search-add .popover .list-item');
// remove existing tooltips
listItem.selectAll('button.choose').call(tooltip().destroyAny);
listItem.each(function(item, index) {
if (!item.geometry) return;
var hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, item.geometry);
var isHiddenPreset = !!hiddenPresetFeaturesId;
var button = d3_select(this).selectAll('button.choose');
d3_select(this).classed('disabled', isHiddenPreset);
button.classed('disabled', isHiddenPreset);
if (isHiddenPreset) {
var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);
var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual';
var tooltipObj = { features: t('feature.' + hiddenPresetFeaturesId + '.description') };
button.call(tooltip('dark')
.html(true)
.title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj))
.placement(index < 2 ? 'bottom' : 'top')
);
}
});
}
function chooseExpandable(item, itemSelection) {
var shouldExpand = !itemSelection.classed('expanded');
itemSelection.classed('expanded', shouldExpand);
var iconName = shouldExpand ?
'#iD-icon-down' : (textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward');
itemSelection.selectAll('.label svg.icon use')
.attr('href', iconName);
if (shouldExpand) {
var subitems = item.subitems();
var selector = '#' + itemSelection.node().id + ' + *';
item.subsection = d3_select(itemSelection.node().parentElement).insert('div', selector)
.attr('class', 'subsection subitems');
var subitemsEnter = item.subsection.selectAll('.list-item')
.data(subitems)
.enter();
drawItems(subitemsEnter);
updateForFeatureHiddenState();
scrollPopoverToShow(item.subsection);
} else {
item.subsection.remove();
}
}
function CategoryItem(preset) {
var item = {};
item.id = function() {
return preset.id;
};
item.name = function() {
return preset.name();
};
item.subsection = d3_select(null);
item.preset = preset;
item.choose = function() {
var selection = d3_select(this);
if (selection.classed('disabled')) return;
chooseExpandable(item, d3_select(selection.node().closest('.list-item')));
};
item.subitems = function() {
return preset.members.matchAnyGeometry(shownGeometry).collection.map(function(preset) {
return itemForPreset(preset);
});
};
return item;
}
function MultiPresetItem(presets) {
var item = {};
item.id = function() {
return presets.map(function(preset) { return preset.id; }).join();
};
item.name = function() {
return presets[0].name();
};
item.subsection = d3_select(null);
item.presets = presets;
item.choose = function() {
var selection = d3_select(this);
if (selection.classed('disabled')) return;
chooseExpandable(item, d3_select(selection.node().closest('.list-item')));
};
item.subitems = function() {
var items = [];
presets.forEach(function(preset) {
preset.geometry.filter(function(geometry) {
return shownGeometry.indexOf(geometry) !== -1;
}).forEach(function(geometry) {
items.push(AddablePresetItem(preset, geometry, true, true));
});
});
return items;
};
return item;
}
function MultiGeometryPresetItem(preset, geometries) {
var item = {};
item.id = function() {
return preset.id + geometries;
};
item.name = function() {
return preset.name();
};
item.subsection = d3_select(null);
item.preset = preset;
item.geometries = geometries;
item.choose = function() {
var selection = d3_select(this);
if (selection.classed('disabled')) return;
chooseExpandable(item, d3_select(selection.node().closest('.list-item')));
};
item.subitems = function() {
return geometries.map(function(geometry) {
return AddablePresetItem(preset, geometry, true);
});
};
return item;
}
function AddablePresetItem(preset, geometry, isSubitem, isInNameGroup) {
var item = {};
item.id = function() {
return preset.id + geometry + isSubitem;
};
item.name = function() {
if (isSubitem) {
if (preset.setTags({}, geometry).building) {
return t('presets.presets.building.name');
}
return t('modes.add_' + context.presets().fallback(geometry).id + '.title');
}
return preset.name();
};
item.isInNameGroup = isInNameGroup;
item.isSubitem = isSubitem;
item.preset = preset;
item.geometry = geometry;
item.choose = function() {
if (d3_select(this).classed('disabled')) return;
var markerClass = 'add-preset add-' + geometry +
' add-preset-' + preset.name().replace(/\s+/g, '_') + '-' + geometry;
var modeInfo = {
button: markerClass,
preset: preset,
geometry: geometry
};
var mode;
switch (geometry) {
case 'point':
case 'vertex':
mode = modeAddPoint(context, modeInfo);
break;
case 'line':
mode = modeAddLine(context, modeInfo);
break;
case 'area':
mode = modeAddArea(context, modeInfo);
}
search.node().blur();
context.presets().setMostRecent(preset, geometry);
context.enter(mode);
};
return item;
}
return utilRebind(tool, dispatch, 'on');
}