d3.combobox = function() { var event = d3.dispatch('accept'), data = [], suggestions = [], minItems = 2; var fetcher = function(val, cb) { cb(data.filter(function(d) { return d.value .toString() .toLowerCase() .indexOf(val.toLowerCase()) !== -1; })); }; var combobox = function(input) { var idx = -1, container = d3.select(document.body) .selectAll('div.combobox') .filter(function(d) { return d === input.node(); }), shown = !container.empty(); input .classed('combobox-input', true) .on('focus.typeahead', focus) .on('blur.typeahead', blur) .on('keydown.typeahead', keydown) .on('keyup.typeahead', keyup) .on('input.typeahead', change) .each(function() { var parent = this.parentNode, sibling = this.nextSibling; var caret = d3.select(parent).selectAll('.combobox-caret') .filter(function(d) { return d === input.node(); }) .data([input.node()]); caret.enter().insert('div', function() { return sibling; }) .attr('class', 'combobox-caret'); caret .on('mousedown', function () { // prevent the form element from blurring. it blurs // on mousedown d3.event.stopPropagation(); d3.event.preventDefault(); if (!shown) { input.node().focus(); fetch('', render); } else { hide(); } }); }); function focus() { fetch(value(), render); } function blur() { window.setTimeout(hide, 150); } function show() { if (!shown) { container = d3.select(document.body) .insert('div', ':first-child') .datum(input.node()) .attr('class', 'combobox') .style({ position: 'absolute', display: 'block', left: '0px' }) .on('mousedown', function () { // prevent moving focus out of the text field d3.event.preventDefault(); }); d3.select(document.body) .on('scroll.combobox', render, true); shown = true; } } function hide() { if (shown) { idx = -1; container.remove(); d3.select(document.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: container.selectAll('a.selected').each(event.accept); break; // return case 13: d3.event.preventDefault(); break; // up arrow case 38: nav(-1); d3.event.preventDefault(); break; // down arrow case 40: nav(+1); d3.event.preventDefault(); break; } d3.event.stopPropagation(); } function keyup() { switch (d3.event.keyCode) { // escape case 27: hide(); break; // return case 13: container.selectAll('a.selected').each(event.accept); hide(); break; } } function change() { fetch(value(), function() { autocomplete(); render(); }); } function nav(dir) { 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'); if (start && end) { value = value.substring(0, start); } return value; } function fetch(v, cb) { fetcher.call(input, v, function(_) { suggestions = _; cb(); }); } function autocomplete() { var v = value(); idx = -1; if (!v) return; for (var i = 0; i < suggestions.length; i++) { if (suggestions[i].value.toLowerCase().indexOf(v.toLowerCase()) === 0) { var completion = v + suggestions[i].value.substr(v.length); idx = i; input.property('value', completion); input.node().setSelectionRange(v.length, completion.length); return; } } } function render() { if (suggestions.length >= minItems && document.activeElement === input.node()) { show(); } else { hide(); return; } var options = container .selectAll('a.combobox-option') .data(suggestions, function(d) { return d.value; }); options.enter().append('a') .attr('class', 'combobox-option') .text(function(d) { return d.value; }); options .attr('title', function(d) { return d.title; }) .classed('selected', function(d, i) { return i == idx; }) .on('mouseover', select) .on('click', accept) .order(); options.exit() .remove(); var rect = input.node().getBoundingClientRect(); container.style({ 'left': rect.left + 'px', 'width': rect.width + 'px', 'top': rect.height + rect.top + 'px' }); } function select(d, i) { idx = i; render(); } function ensureVisible() { var node = container.selectAll('a.selected').node(); if (node) node.scrollIntoView(); } function accept(d) { if (!shown) return; input .property('value', d.value) .trigger('change'); event.accept(d); hide(); } }; combobox.fetcher = function(_) { if (!arguments.length) return fetcher; fetcher = _; return combobox; }; combobox.data = function(_) { if (!arguments.length) return data; data = _; return combobox; }; combobox.minItems = function(_) { if (!arguments.length) return minItems; minItems = _; return combobox; }; return d3.rebind(combobox, event, 'on'); };