diff --git a/modules/lib/d3.combobox.js b/modules/lib/d3.combobox.js index d991257bf..c7cd0ed02 100644 --- a/modules/lib/d3.combobox.js +++ b/modules/lib/d3.combobox.js @@ -13,16 +13,27 @@ import { } from '../../modules/util'; +// This code assumes that the combobox values will not have duplicate entries. +// It is keyed on the `value` of the entry. +// Data should be an array of objects like: +// [{ +// title: 'hover text', +// value: 'display text' +// }, ...] + export function d3combobox() { var dispatch = d3_dispatch('accept'); var _container = d3_select(document.body); - var _data = []; + var _wrapper = d3_select(null); var _suggestions = []; - var _minItems = 2; + var _choice = ''; + var _canAutocomplete = true; var _caseSensitive = false; + var _values = []; + var _minItems = 2; var _fetcher = function(val, cb) { - cb(_data.filter(function(d) { + cb(_values.filter(function(d) { return d.value .toString() .toLowerCase() @@ -31,12 +42,9 @@ export function d3combobox() { }; var combobox = function(input, attachTo) { - var idx = -1; - var wrapper = _container + _wrapper = _container .selectAll('div.combobox') .filter(function(d) { return d === input.node(); }); - var shown = !wrapper.empty(); - var tagName = input.node() ? input.node().tagName.toLowerCase() : ''; input .classed('combobox-input', true) @@ -60,11 +68,10 @@ export function d3combobox() { caret .on('mousedown', function () { - // prevent the form element from blurring. it blurs - // on mousedown + // prevent the form element from blurring. it blurs on mousedown d3_event.stopPropagation(); d3_event.preventDefault(); - if (!shown) { + if (_wrapper.empty()) { input.node().focus(); fetch('', render); } else { @@ -82,8 +89,8 @@ export function d3combobox() { } function show() { - if (!shown) { - wrapper = _container + if (_wrapper.empty()) { + _wrapper = _container .insert('div', ':first-child') .datum(input.node()) .attr('class', 'combobox') @@ -97,72 +104,72 @@ export function d3combobox() { d3_select('body') .on('scroll.combobox', render, true); - - shown = true; } } function hide() { - if (shown) { - idx = -1; - wrapper.remove(); + if (!_wrapper.empty()) { + _choice = ''; + _wrapper.remove(); d3_select('body') .on('scroll.combobox', null); - - shown = false; } } function keydown() { - switch (d3_event.keyCode) { - // backspace, delete - case 8: - case 46: - input.on('input.typeahead', function() { - idx = -1; - render(); - var start = input.property('selectionStart'); - input.node().setSelectionRange(start, start); - input.on('input.typeahead', change); - }); - break; - // tab - case 9: - wrapper.selectAll('a.selected').each(function (d) { - dispatch.call('accept', this, d); - }); - hide(); - break; - // return - case 13: - d3_event.preventDefault(); - break; - // up arrow - case 38: - if (tagName === 'textarea' && !shown) return; - nav(-1); - d3_event.preventDefault(); - break; - // down arrow - case 40: - if (tagName === 'textarea' && !shown) return; - nav(+1); - d3_event.preventDefault(); - break; - } - d3_event.stopPropagation(); + var shown = !_wrapper.empty(); + var tagName = input.node() ? input.node().tagName.toLowerCase() : ''; + + switch (d3_event.keyCode) { + case 8: // ⌫ Backspace + case 46: // ⌦ Delete + _choice = ''; + render(); + input.on('input.typeahead', function() { + var start = input.property('selectionStart'); + input.node().setSelectionRange(start, start); + input.on('input.typeahead', change); + }); + break; + + case 9: // ⇥ Tab + _wrapper.selectAll('a.selected').each(function (d) { + dispatch.call('accept', this, d); + }); + hide(); + break; + + case 13: // ↩ Return + d3_event.preventDefault(); + break; + + case 38: // ↑ Up arrow + if (tagName === 'textarea' && !shown) return; + d3_event.preventDefault(); + nav(-1); + break; + + case 40: // ↓ Down arrow + if (tagName === 'textarea' && !shown) return; + d3_event.preventDefault(); + if (tagName === 'input' && !shown) { + show(); + } + nav(+1); + break; + } + d3_event.stopPropagation(); } function keyup() { switch (d3_event.keyCode) { - // escape - case 27: + case 27: // ⎋ Escape hide(); break; - // return - case 13: - wrapper.selectAll('a.selected').each(function (d) { + + case 13: // ↩ Return + _wrapper.selectAll('a.selected').each(function (d) { dispatch.call('accept', this, d); }); hide(); @@ -173,7 +180,7 @@ export function d3combobox() { function change() { fetch(value(), function() { if (input.property('selectionEnd') === input.property('value').length) { - autocomplete(); + doAutocomplete(); } render(); }); @@ -181,8 +188,18 @@ 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); + + var index = -1; + for (var i = 0; i < _suggestions.length; i++) { + if (_choice !== '' && _suggestions[i].value === _choice) { + index = i; + break; + } + } + + index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0); + _choice = _suggestions[index].value; + input.property('value', _choice); render(); ensureVisible(); } @@ -200,26 +217,26 @@ export function d3combobox() { } function fetch(v, cb) { - _fetcher.call(input, v, function(_) { - _suggestions = _; + _fetcher.call(input, v, function(results) { + _suggestions = results; cb(); }); } - function autocomplete() { + function doAutocomplete() { + if (!_canAutocomplete) return; + var v = _caseSensitive ? value() : value().toLowerCase(); - idx = -1; + _choice = ''; if (!v) return; // Don't autocomplete if user is typing a number - #4935 if (!isNaN(parseFloat(v)) && isFinite(v)) return; var best = -1; - var suggestion, compare; - for (var i = 0; i < _suggestions.length; i++) { - suggestion = _suggestions[i].value; - compare = _caseSensitive ? suggestion : suggestion.toLowerCase(); + var suggestion = _suggestions[i].value; + var compare = _caseSensitive ? suggestion : suggestion.toLowerCase(); // if search string matches suggestion exactly, pick it.. if (compare === v) { @@ -233,10 +250,9 @@ export function d3combobox() { } if (best !== -1) { - idx = best; - suggestion = _suggestions[best].value; - input.property('value', suggestion); - input.node().setSelectionRange(v.length, suggestion.length); + _choice = _suggestions[best].value; + input.property('value', _choice); + input.node().setSelectionRange(v.length, _choice.length); } } @@ -250,21 +266,21 @@ export function d3combobox() { return; } - var options = wrapper + var options = _wrapper .selectAll('a.combobox-option') .data(_suggestions, function(d) { return d.value; }); options.exit() .remove(); + // enter/update options.enter() .append('a') .attr('class', 'combobox-option') .text(function(d) { return d.value; }) .merge(options) .attr('title', function(d) { return d.title; }) - .classed('selected', function(d, i) { return i === idx; }) - .on('mouseover', select) + .classed('selected', function(d) { return d.value === _choice; }) .on('click', accept) .order(); @@ -272,24 +288,19 @@ export function d3combobox() { var node = attachTo ? attachTo.node() : input.node(); var rect = node.getBoundingClientRect(); - wrapper + _wrapper .style('left', (rect.left + 5) + 'px') .style('width', (rect.width - 10) + 'px') .style('top', rect.height + rect.top + 'px'); } - function select(d, i) { - idx = i; - render(); - } - function ensureVisible() { - var node = wrapper.selectAll('a.selected').node(); + var node = _wrapper.selectAll('a.selected').node(); if (node) node.scrollIntoView(); } function accept(d) { - if (!shown) return; + if (_wrapper.empty()) return; input.property('value', d.value); utilTriggerEvent(input, 'change'); dispatch.call('accept', this, d); @@ -297,36 +308,43 @@ export function d3combobox() { } }; - combobox.fetcher = function(_) { - if (!arguments.length) return _fetcher; - _fetcher = _; + combobox.canAutocomplete = function(val) { + if (!arguments.length) return _canAutocomplete; + _canAutocomplete = val; return combobox; }; - combobox.data = function(_) { - if (!arguments.length) return _data; - _data = _; - return combobox; - }; - - combobox.minItems = function(_) { - if (!arguments.length) return _minItems; - _minItems = _; - return combobox; - }; - - combobox.caseSensitive = function(_) { + combobox.caseSensitive = function(val) { if (!arguments.length) return _caseSensitive; - _caseSensitive = _; + _caseSensitive = val; return combobox; }; - combobox.container = function(_) { + combobox.container = function(val) { if (!arguments.length) return _container; - _container = _; + _container = val; return combobox; }; + combobox.data = function(val) { + if (!arguments.length) return _values; + _values = val; + return combobox; + }; + + combobox.fetcher = function(val) { + if (!arguments.length) return _fetcher; + _fetcher = val; + return combobox; + }; + + combobox.minItems = function(val) { + if (!arguments.length) return _minItems; + _minItems = val; + return combobox; + }; + + return utilRebind(combobox, dispatch, 'on'); } diff --git a/modules/ui/fields/localized.js b/modules/ui/fields/localized.js index 901bf3906..7a6a31d79 100644 --- a/modules/ui/fields/localized.js +++ b/modules/ui/fields/localized.js @@ -24,10 +24,27 @@ import { export function uiFieldLocalized(field, context) { +console.log('new uiFieldLocalized!'); var dispatch = d3_dispatch('change', 'input'); var wikipedia = services.wikipedia; var input = d3_select(null); var localizedInputs = d3_select(null); + + var allSuggestions = context.presets().collection.filter(function(p) { + return p.suggestion === true; + }); + + // reuse these combos + var langcombo = d3_combobox() + .container(context.container()) + .fetcher(fetchLanguages) + .minItems(0); + + var brandcombo = d3_combobox() + .container(context.container()) + // .canAutocomplete(false) + .minItems(1); + var _selection = d3_select(null); var _multilingual = []; var _isLocked = false; @@ -120,10 +137,6 @@ export function uiFieldLocalized(field, context) { } else { // Not a suggestion preset - Add a suggestions dropdown if it makes sense to. - var allSuggestions = context.presets().collection.filter(function(p) { - return p.suggestion === true; - }); - // This code attempts to determine if the matched preset is the // kind of preset that even can benefit from name suggestions.. // - true = shops, cafes, hotels, etc. (also generic and fallback presets) @@ -140,10 +153,8 @@ export function uiFieldLocalized(field, context) { // Show the suggestions.. If the user picks one, change the tags.. if (allSuggestions.length && goodSuggestions.length) { input - .call(d3_combobox() - .container(context.container()) - .fetcher(suggestNames(preset, allSuggestions)) - .minItems(1) + .call(brandcombo + .fetcher(fetchBrandNames(preset, allSuggestions)) .on('accept', function(d) { var entity = context.entity(_entity.id); // get latest var tags = entity.tags; @@ -206,7 +217,7 @@ export function uiFieldLocalized(field, context) { - function suggestNames(preset, suggestions) { + function fetchBrandNames(preset, suggestions) { var pTag = preset.id.split('/', 2); var pKey = pTag[0]; var pValue = pTag[1]; @@ -313,7 +324,7 @@ export function uiFieldLocalized(field, context) { } - function fetcher(value, cb) { + function fetchLanguages(value, cb) { var v = value.toLowerCase(); cb(dataWikipedia.filter(function(d) { @@ -345,10 +356,6 @@ export function uiFieldLocalized(field, context) { .attr('class', 'entry') .each(function() { var wrap = d3_select(this); - var langcombo = d3_combobox() - .container(context.container()) - .fetcher(fetcher) - .minItems(0); var label = wrap .append('label') diff --git a/test/spec/lib/d3.combobox.js b/test/spec/lib/d3.combobox.js index af588c766..efca555a9 100644 --- a/test/spec/lib/d3.combobox.js +++ b/test/spec/lib/d3.combobox.js @@ -172,6 +172,14 @@ describe('d3.combobox', function() { expect(body.selectAll('.combobox-option.selected').size()).to.equal(0); }); + it('does not autocomplete if canAutocomplete(false)', function() { + input.call(combobox.data(data).canAutocomplete(false)); + focusTypeahead(input); + simulateKeypress('b'); + expect(input.property('value')).to.equal('b'); + expect(body.selectAll('.combobox-option.selected').size()).to.equal(0); + }); + it('selects the completed portion of the value', function() { input.call(combobox.data(data)); focusTypeahead(input);