Don't autocomplete a longer value if search matches a value exactly

(closes #4549)
This commit is contained in:
Bryan Housel
2018-01-02 17:55:25 -05:00
parent 11d0ec87cf
commit baeff8f59c
2 changed files with 97 additions and 71 deletions
+62 -49
View File
@@ -14,15 +14,15 @@ import {
export function d3combobox() {
var dispatch = d3_dispatch('accept'),
container = d3_select(document.body),
data = [],
suggestions = [],
minItems = 2,
caseSensitive = false;
var dispatch = d3_dispatch('accept');
var _container = d3_select(document.body);
var _data = [];
var _suggestions = [];
var _minItems = 2;
var _caseSensitive = false;
var fetcher = function(val, cb) {
cb(data.filter(function(d) {
var _fetcher = function(val, cb) {
cb(_data.filter(function(d) {
return d.value
.toString()
.toLowerCase()
@@ -31,11 +31,11 @@ export function d3combobox() {
};
var combobox = function(input, attachTo) {
var idx = -1,
wrapper = container
.selectAll('div.combobox')
.filter(function(d) { return d === input.node(); }),
shown = !wrapper.empty();
var idx = -1;
var wrapper = _container
.selectAll('div.combobox')
.filter(function(d) { return d === input.node(); });
var shown = !wrapper.empty();
input
.classed('combobox-input', true)
@@ -45,17 +45,17 @@ export function d3combobox() {
.on('keyup.typeahead', keyup)
.on('input.typeahead', change)
.each(function() {
var parent = this.parentNode,
sibling = this.nextSibling;
var parent = this.parentNode;
var sibling = this.nextSibling;
var caret = d3_select(parent).selectAll('.combobox-caret')
.filter(function(d) { return d === input.node(); })
.data([input.node()]);
caret = caret.enter()
.insert('div', function() { return sibling; })
.insert('div', function() { return sibling; })
.attr('class', 'combobox-caret')
.merge(caret);
.merge(caret);
caret
.on('mousedown', function () {
@@ -82,7 +82,7 @@ export function d3combobox() {
function show() {
if (!shown) {
wrapper = container
wrapper = _container
.insert('div', ':first-child')
.datum(input.node())
.attr('class', 'combobox')
@@ -177,17 +177,17 @@ export function d3combobox() {
}
function nav(dir) {
if (!suggestions.length) return;
idx = Math.max(Math.min(idx + dir, suggestions.length - 1), 0);
input.property('value', suggestions[idx].value);
if (!_suggestions.length) return;
idx = Math.max(Math.min(idx + dir, _suggestions.length - 1), 0);
input.property('value', _suggestions[idx].value);
render();
ensureVisible();
}
function value() {
var value = input.property('value'),
start = input.property('selectionStart'),
end = input.property('selectionEnd');
var value = input.property('value');
var start = input.property('selectionStart');
var end = input.property('selectionEnd');
if (start && end) {
value = value.substring(0, start);
@@ -197,32 +197,45 @@ export function d3combobox() {
}
function fetch(v, cb) {
fetcher.call(input, v, function(_) {
suggestions = _;
_fetcher.call(input, v, function(_) {
_suggestions = _;
cb();
});
}
function autocomplete() {
var v = caseSensitive ? value() : value().toLowerCase();
var v = _caseSensitive ? value() : value().toLowerCase();
idx = -1;
if (!v) return;
for (var i = 0; i < suggestions.length; i++) {
var suggestion = suggestions[i].value,
compare = caseSensitive ? suggestion : suggestion.toLowerCase();
var best = -1;
var suggestion, compare;
if (compare.indexOf(v) === 0) {
idx = i;
input.property('value', suggestion);
input.node().setSelectionRange(v.length, suggestion.length);
return;
for (var i = 0; i < _suggestions.length; i++) {
suggestion = _suggestions[i].value;
compare = _caseSensitive ? suggestion : suggestion.toLowerCase();
// if search string matches suggestion exactly, pick it..
if (compare === v) {
best = i;
break;
// otherwise lock in the first result that starts with the search string..
} else if (best === -1 && compare.indexOf(v) === 0) {
best = i;
}
}
if (best !== -1) {
idx = best;
suggestion = _suggestions[best].value;
input.property('value', suggestion);
input.node().setSelectionRange(v.length, suggestion.length);
}
}
function render() {
if (suggestions.length >= minItems && document.activeElement === input.node()) {
if (_suggestions.length >= _minItems && document.activeElement === input.node()) {
show();
} else {
hide();
@@ -231,7 +244,7 @@ export function d3combobox() {
var options = wrapper
.selectAll('a.combobox-option')
.data(suggestions, function(d) { return d.value; });
.data(_suggestions, function(d) { return d.value; });
options.exit()
.remove();
@@ -248,8 +261,8 @@ export function d3combobox() {
.order();
var node = attachTo ? attachTo.node() : input.node(),
rect = node.getBoundingClientRect();
var node = attachTo ? attachTo.node() : input.node();
var rect = node.getBoundingClientRect();
wrapper
.style('left', rect.left + 'px')
@@ -277,32 +290,32 @@ export function d3combobox() {
};
combobox.fetcher = function(_) {
if (!arguments.length) return fetcher;
fetcher = _;
if (!arguments.length) return _fetcher;
_fetcher = _;
return combobox;
};
combobox.data = function(_) {
if (!arguments.length) return data;
data = _;
if (!arguments.length) return _data;
_data = _;
return combobox;
};
combobox.minItems = function(_) {
if (!arguments.length) return minItems;
minItems = _;
if (!arguments.length) return _minItems;
_minItems = _;
return combobox;
};
combobox.caseSensitive = function(_) {
if (!arguments.length) return caseSensitive;
caseSensitive = _;
if (!arguments.length) return _caseSensitive;
_caseSensitive = _;
return combobox;
};
combobox.container = function(_) {
if (!arguments.length) return container;
container = _;
if (!arguments.length) return _container;
_container = _;
return combobox;
};
+35 -22
View File
@@ -2,16 +2,18 @@ describe('d3.combobox', function() {
var body, container, content, input, combobox;
var data = [
{title: 'foobar', value: 'foobar'},
{title: 'foo', value: 'foo'},
{title: 'bar', value: 'bar'},
{title: 'Baz', value: 'Baz'}
{title: 'Baz', value: 'Baz'},
{title: 'test', value: 'test'}
];
function simulateKeypress(key) {
var keyCode = iD.lib.d3keybinding.keyCodes[key],
value = input.property('value'),
start = input.property('selectionStart'),
finis = input.property('selectionEnd');
var keyCode = iD.lib.d3keybinding.keyCodes[key];
var value = input.property('value');
var start = input.property('selectionStart');
var finis = input.property('selectionEnd');
iD.d3.customEvent(happen.makeEvent({
type: 'keydown',
@@ -101,36 +103,37 @@ describe('d3.combobox', function() {
it('shows a menu of entries on focus', function() {
input.call(combobox.data(data));
focusTypeahead(input);
expect(body.selectAll('.combobox-option').nodes().length).to.equal(3);
expect(body.selectAll('.combobox-option').text()).to.equal('foo');
expect(body.selectAll('.combobox-option').nodes().length).to.equal(5);
expect(body.selectAll('.combobox-option').text()).to.equal('foobar');
});
it('filters entries to those matching the value', function() {
input.property('value', 'b').call(combobox.data(data));
focusTypeahead(input);
expect(body.selectAll('.combobox-option').size()).to.equal(2);
expect(body.selectAll('.combobox-option').nodes()[0].text).to.equal('bar');
expect(body.selectAll('.combobox-option').nodes()[1].text).to.equal('Baz');
expect(body.selectAll('.combobox-option').size()).to.equal(3);
expect(body.selectAll('.combobox-option').nodes()[0].text).to.equal('foobar');
expect(body.selectAll('.combobox-option').nodes()[1].text).to.equal('bar');
expect(body.selectAll('.combobox-option').nodes()[2].text).to.equal('Baz');
});
it('shows no menu on focus if it would contain only one item', function() {
input.property('value', 'f').call(combobox.data(data));
input.property('value', 't').call(combobox.data(data));
focusTypeahead(input);
expect(body.selectAll('.combobox-option').size()).to.equal(0);
});
it('shows menu on focus if it would contain at least minItems items', function() {
combobox.minItems(1);
input.property('value', 'f').call(combobox.data(data));
input.property('value', 't').call(combobox.data(data));
focusTypeahead(input);
expect(body.selectAll('.combobox-option').size()).to.equal(1);
});
it('shows all entries when clicking on the caret', function() {
input.property('value', 'foo').call(combobox.data(data));
input.property('value', 'foobar').call(combobox.data(data));
body.selectAll('.combobox-caret').dispatch('mousedown');
expect(body.selectAll('.combobox-option').size()).to.equal(3);
expect(body.selectAll('.combobox-option').text()).to.equal('foo');
expect(body.selectAll('.combobox-option').size()).to.equal(5);
expect(body.selectAll('.combobox-option').text()).to.equal('foobar');
});
it('is initially shown with no selection', function() {
@@ -139,7 +142,7 @@ describe('d3.combobox', function() {
expect(body.selectAll('.combobox-option.selected').size()).to.equal(0);
});
it('selects the first option matching the input', function() {
it('selects the first option that matches the input', function() {
input.call(combobox.data(data));
focusTypeahead(input);
simulateKeypress('b');
@@ -147,6 +150,16 @@ describe('d3.combobox', function() {
expect(body.selectAll('.combobox-option.selected').text()).to.equal('bar');
});
it('prefers an option that exactly matches the input over the first option', function() {
input.call(combobox.data(data));
focusTypeahead(input);
simulateKeypress('f');
simulateKeypress('o');
simulateKeypress('o');
expect(body.selectAll('.combobox-option.selected').size()).to.equal(1);
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo'); // skip foobar
});
it('selects the completed portion of the value', function() {
input.call(combobox.data(data));
focusTypeahead(input);
@@ -217,18 +230,18 @@ describe('d3.combobox', function() {
simulateKeypress('↓');
expect(body.selectAll('.combobox-option.selected').size()).to.equal(1);
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo');
expect(input.property('value')).to.equal('foo');
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foobar');
expect(input.property('value')).to.equal('foobar');
simulateKeypress('↓');
expect(body.selectAll('.combobox-option.selected').size()).to.equal(1);
expect(body.selectAll('.combobox-option.selected').text()).to.equal('bar');
expect(input.property('value')).to.equal('bar');
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo');
expect(input.property('value')).to.equal('foo');
simulateKeypress('↑');
expect(body.selectAll('.combobox-option.selected').size()).to.equal(1);
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foo');
expect(input.property('value')).to.equal('foo');
expect(body.selectAll('.combobox-option.selected').text()).to.equal('foobar');
expect(input.property('value')).to.equal('foobar');
});
it('emits accepted event with selected datum on ⇥', function(done) {