Files
iD/modules/ui/search_add.js

644 lines
22 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 _clone from 'lodash-es/clone';
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 { uiPresetFavorite } from './preset_favorite';
import { uiPresetIcon } from './preset_icon';
import { utilKeybinding, utilNoAuto, utilRebind } from '../util';
export function uiSearchAdd(context) {
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 = [];
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;
});
}
function searchAdd(selection) {
updateShownGeometry(_clone(allowedGeometry));
var key = t('modes.add_feature.key');
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(_clone(allowedGeometry));
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();
}
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()
.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 = uiPresetFavorite(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(searchAdd, dispatch, 'on');
}