<|start_filename|>spec/autocomplete_list_item_spec.js<|end_filename|>
describe("AutocompleteListItem", function() {
  var subject;
  var testText;
  var testValue;
  var onSelectSpy;

  beforeEach(function() {
    testText = "<NAME>";
    testValue = 1;
    onSelectSpy = sinon.spy();
    subject = new AutocompleteListItem({
      onSelect: onSelectSpy,
      item: {
        value: testValue,
        text: testText
      }
    });
  });

  it("has an element", function() {
    expect(subject.$el).to.exist;
    expect(subject.$el).to.have.class("autocomplete-list-item");
    expect(subject.$el[0].tagName).to.equal("LI");
  });

  it("has a value", function() {
    expect(subject.value).to.equal(testValue);
  });

  it("has text", function() {
    expect(subject.text).to.equal(testText);
  });

  it("has a template", function() {
    expect(subject.template).to.be.a("function");
    var renderedTemplate = subject.template({ text: "foo" });
    expect(renderedTemplate).to.match(/foo/);
  });

  it("has an onSelect handler", function() {
    expect(subject.onSelect).to.be.a("function");
  });

  it("has a active state", function() {
    expect(subject.active).to.be.false;
  });

  describe("#activate", function() {
    beforeEach(function() {
      subject.activate();
    });

    it("sets active to true", function() {
      expect(subject.active).to.be.true;
    });

    it("adds the active class to el", function() {
      expect(subject.$el).to.have.class("active");
    });
  });

  describe("#deactivate", function() {
    beforeEach(function() {
      subject.active = true;
      subject.$el.addClass("active");
      subject.deactivate();
    });

    it("sets active to false", function() {
      expect(subject.active).to.be.false;
    });

    it("removes the active class from el", function() {
      expect(subject.$el).to.not.have.class("active");
    });
  });

  describe("#select", function() {
    it("calls the onSelect function, passing itself", function() {
      subject.select();
      expect(subject.onSelect).to.have.been.calledWith(subject);
    });
  });

  describe("#registerEvents", function() {
    it("handles click", function() {
      subject.$el.trigger("click");
      expect(subject.onSelect).to.have.been.called;
    });
  });
});


<|start_filename|>spec/autocomplete_ajax_adapter_spec.js<|end_filename|>
describe("AutocompleteAjaxAdapter", function() {
  var subject;
  var onAutocompleteSpy;
  var xhr;
  var requests;
  beforeEach(function() {
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function(xhr) {
      requests.push(xhr);
    };
    onAutocompleteSpy = sinon.spy();
    subject = new AutocompleteAjaxAdapter({
      url: "test",
      onAutocomplete: onAutocompleteSpy
    });
  });

  it("requires a url", function() {
    expect(function() {
      new AutocompleteAjaxAdapter;
    }).to.throw("AutocompleteAjaxAdapter: url is undefined");
  });

  it("has a url", function() {
    expect(subject.url).to.equal("test");
  });

  it("has an onAutocomplete handler", function() {
    expect(subject.onAutocomplete).to.equal(onAutocompleteSpy);
  });

  it("has a throttle delay", function() {
    expect(subject.throttleDelay).to.be.a("number");
  });

  describe("defaults", function() {
    beforeEach(function() {
      subject = new AutocompleteAjaxAdapter({
        url: "test"
      });
    });

    it("has a default onAutocomplete handler", function() {
      expect(subject.onAutocomplete).to.be.a("function");
    });
  });

  describe("#handleTextEntry", function() {
    var text;
    beforeEach(function() {
      text = "test query";
      sinon.spy(window, "clearTimeout");
      sinon.spy(subject, "queueRequest");
    });

    afterEach(function() {
      clearTimeout.restore();
    });

    it("sets the query", function() {
      subject.handleTextEntry(text);
      expect(subject.query).to.equal(text);
    });

    it("queues a request", function() {
      subject.handleTextEntry(text);
      expect(subject.queueRequest).to.have.been.called;
    });

    describe("when there's a queued request", function() {
      it("clears the timeout", function() {
        subject.queuedRequest = 5;
        subject.handleTextEntry(text);
        expect(clearTimeout).to.have.been.calledWith(5);
      });
    });

    describe("when there is not a queued request", function() {
      it("does nothing", function() {
        subject.handleTextEntry(text);
        expect(clearTimeout).not.to.have.been.called;
      });
    });

    describe("when the text is empty", function() {
      it("does not queue a request", function() {
        subject.handleTextEntry("");
        expect(subject.queuedRequest).to.be.undefined;
      });

      it("clears a request out", function() {
        subject.queuedRequest = 15;
        subject.handleTextEntry("");
        expect(clearTimeout).to.have.been.calledWith(subject.queuedRequest);
      });

      it("sends an empty array to the on autocomplete callback", function() {
        subject.handleTextEntry("");
        expect(subject.onAutocomplete).to.have.been.calledWith([]);
      });
    });
  });

  describe("#fetchItems", function() {
    it("gets the items via ajax", function() {
      subject.fetchItems();
      var request = _.last(requests);
      expect(request.url).to.equal(subject.url);
      expect(request.method).to.equal("GET");
      expect(request.requestHeaders.Accept).to.match(/application\/json/);
    });
  });

  describe("#queueRequest", function() {
    beforeEach(function() {
      subject.throttleDelay = 0;
      sinon.spy(window, "setTimeout");
      sinon.spy(subject, "fetchItems");
      subject.queueRequest();
    });

    afterEach(function() {
      setTimeout.restore();
    });

    it("queues a fetch items request", function() {
      expect(subject.queuedRequest).to.be.a("number");
    });

    it("fetches the items after the throttle delay", function() {
      expect(setTimeout).to.have.been.called;

      var args = _.first(setTimeout.args);
      var callback = _.first(args);
      var throttleDelay = _.last(args);
      callback();
      expect(subject.fetchItems).to.have.been.called;
      expect(throttleDelay).to.equal(subject.throttleDelay);
    });
  });
});


<|start_filename|>js/autocomplete_local_adapter.js<|end_filename|>
function AutocompleteLocalAdapter(attributes) {
  _.bindAll(this);
  attributes = attributes || {};
  _.defaults(attributes, {
    onAutocomplete: function() {}
  });

  if (_.isUndefined(attributes.items)) {
    throw new Error("AutocompleteLocalAdapter: items is undefined");
  }

  this.items = attributes.items;
  this.onAutocomplete = attributes.onAutocomplete;
}

_.merge(AutocompleteLocalAdapter.prototype, {

  handleTextEntry: function(text) {
    var items = [];
    if (text) {
      this.filter = new RegExp("^" + text, "i");
      items = _.filter(this.items, this.itemMatchesFilter);
    }
    this.onAutocomplete(items);
  },

  itemMatchesFilter: function(item) {
    return item.text.match(this.filter);
  }

});


<|start_filename|>server.js<|end_filename|>
var express = require("express");
var app = express();
var _ = require("lodash");
var url = require('url');
var port = process.env.PORT || 3000;

app.use(express.static("."));

app.get("/fruits", function (req, res) {
  var parsedUrl = url.parse(req.url, true);
  var params = parsedUrl.query;
  var fruits = require("./fruit.json");
  var filteredFruits = _.filter(fruits, function(fruit) {
    var pattern = new RegExp("^" + params.query, "i");
    return !!fruit.text.match(pattern);
  });
  res.send(filteredFruits);
});
app.listen(port);
console.log("Server started at: " + "http://localhost:".concat(port));


<|start_filename|>spec/autocomplete_list_spec.js<|end_filename|>
describe("AutocompleteList", function() {
  var subject;
  var item1;
  var item2;
  var item3;
  var items;
  var onItemSelectSpy;
  beforeEach(function() {
    item1 = { value: 1, text: "Test item 1" };
    item2 = { value: 2, text: "Test item 2" };
    item3 = { value: 3, text: "Test item 3" };
    items = [item1, item2, item3];
    onItemSelectSpy = sinon.spy();
    subject = new AutocompleteList({ onItemSelect: onItemSelectSpy });
  });

  it("has an element", function() {
    expect(subject.$el).to.exist;
    expect(subject.$el[0].tagName).to.equal("UL");
    expect(subject.$el).to.have.class("autocomplete-list");
    expect(subject.$el).to.have.class("hidden");
  });

  it("has an onItemSelect handler", function() {
    expect(subject.onItemSelect).to.equal(onItemSelectSpy);
  });

  describe("defaults", function() {
    beforeEach(function() {
      subject = new AutocompleteList();
    });

    it("has an onItemSelect handler", function() {
      expect(subject.onItemSelect).to.be.a("function");
    });
  });

  describe("#render", function() {
    beforeEach(function() {
      subject.render(items);
    });

    it("creates an completionListItem for each item", function() {
      expect(subject.items).to.be.an("Array");
      expect(subject.items.length).to.equal(items.length);
      _.each(subject.items, function(item) {
        expect(item).to.be.an.instanceof(AutocompleteListItem);
      });
    });

    it("appends each item to the element", function() {
      expect(subject.$el).to.have.descendants("li");
    });

    it("removes the hidden class", function() {
      expect(subject.$el).not.to.have.class("hidden");
    });

    it("does not double render the items", function() {
      subject.render(items);
      expect(subject.$el.find("li").length).to.equal(items.length);
    });

    describe("when there are no items", function() {
      beforeEach(function() {
        subject.$el.removeClass("hidden");
        subject.$el.empty();
      });

      it("it adds the hidden class", function() {
        subject.render([]);
        expect(subject.$el).to.have.class("hidden");
      });

      it("does not throw an error when items are undefined", function() {
        expect(function() {
          subject.render();
        }).not.to.throw();
      });
    });
  });

  describe("#hide", function() {
    it("adds the hidden class to the element", function() {
      subject.$el.removeClass("hidden");
      subject.hide();
      expect(subject.$el).to.have.class("hidden");
    });
  });

  describe("#show", function() {
    it("removes the hidden class from the element", function() {
      subject.$el.addClass("hidden");
      subject.show();
      expect(subject.$el).not.to.have.class("hidden");
    });
  });

  describe("#handleItemSelect", function() {
    var selectedItem;
    beforeEach(function() {
      subject.render(items);
      subject.show();
      selectedItem = _.first(subject.items);
      subject.handleItemSelect(selectedItem);
    });

    it("calls the onItemSelect callback, passing the item clicked", function() {
      expect(subject.onItemSelect).to.have.been.calledWith(selectedItem);
    });

    it("hides the completion list", function() {
      expect(subject.$el).to.have.class("hidden");
    });
  });

  describe("#activeItem", function() {
    beforeEach(function() {
      subject.createListItems(items);
    });

    it("returns undefined when no item is active", function() {
      expect(subject.activeItem()).to.be.undefined;
    });

    it("returns the active item when one is active", function() {
      var activeItem = _.first(subject.items);
      activeItem.activate();
      expect(subject.activeItem()).to.equal(activeItem);
    });
  });

  describe("#nextItem", function() {
    beforeEach(function() {
      subject.createListItems(items);
    });

    describe("when no items are active", function() {
      it("returns the first item", function() {
        expect(subject.nextItem()).to.equal(_.first(subject.items));
      });
    });

    describe("when an item is active", function() {
      beforeEach(function() {
        _.first(subject.items).activate();
      });

      it("returns the next item in the items array", function() {
        expect(subject.nextItem()).to.equal(subject.items[1]);
      });
    });

    describe("when the last item is already active", function() {
      it("returns the first item", function() {
        _.last(subject.items).activate();
        expect(subject.nextItem()).to.equal(_.first(subject.items));
      });
    });
  });

  describe("#activateNextItem", function() {
    beforeEach(function() {
      subject.createListItems(items);
    });

    describe("when no items are active", function() {
      it("activates the first item", function() {
        subject.activateNextItem();
        expect(_.first(subject.items).active).to.be.true;
      });
    });

    describe("when an item is active", function() {
      beforeEach(function() {
        _.first(subject.items).activate();
        subject.activateNextItem();
      });

      it("activates the next item in the items array", function() {
        expect(subject.items[1].active).to.be.true;
      });

      it("deactivates the previously active item", function() {
        expect(_.first(subject.items).active).to.be.false;
      });
    });

    describe("when the last item is already active", function() {
      it("activates the first item", function() {
        _.last(subject.items).activate();
        subject.activateNextItem();
        expect(_.first(subject.items).active).to.be.true;
      });
    });
  });

  describe("#previousItem", function() {
    beforeEach(function() {
      subject.createListItems(items);
    });

    describe("when no items are active", function() {
      it("returns the last item", function() {
        expect(subject.previousItem()).to.equal(_.last(subject.items));
      });
    });

    describe("when an item is active", function() {
      beforeEach(function() {
        _.last(subject.items).activate();
      });

      it("returns the next item in the items array", function() {
        expect(subject.previousItem()).to.equal(subject.items[1]);
      });
    });

    describe("when the first item is already active", function() {
      it("returns the last item", function() {
        _.first(subject.items).activate();
        expect(subject.previousItem()).to.equal(_.last(subject.items));
      });
    });
  });

  describe("#activatePreviousItem", function() {
    beforeEach(function() {
      subject.createListItems(items);
    });

    describe("when no items are active", function() {
      it("activates the last item", function() {
        subject.activatePreviousItem();
        expect(_.last(subject.items).active).to.be.true;
      });
    });

    describe("when an item is active", function() {
      beforeEach(function() {
        _.last(subject.items).activate();
        subject.activatePreviousItem();
      });

      it("activates the previous item in the items array", function() {
        expect(subject.items[1].active).to.be.true;
      });

      it("deactivates the previously active item", function() {
        expect(_.last(subject.items).active).to.be.false;
      });
    });

    describe("when the first item is already active", function() {
      it("activates the last item", function() {
        _.first(subject.items).activate();
        subject.activatePreviousItem();
        expect(_.last(subject.items).active).to.be.true;
      });
    });
  });

  describe("#handleCommandEntry", function() {
    beforeEach(function() {
      sinon.stub(subject, "handleUp");
      sinon.stub(subject, "handleDown");
      sinon.stub(subject, "handleEnter");
      sinon.stub(subject, "handleEscape");
    });

    describe("up", function() {
      beforeEach(function() {
        subject.handleCommandEntry("up");
      });

      it("handles up", function() {
        expect(subject.handleUp).to.have.been.called;
        expect(subject.handleDown).to.not.have.been.called;
        expect(subject.handleEnter).to.not.have.been.called;
        expect(subject.handleEscape).to.not.have.been.called;
      });
    });

    describe("down", function() {
      beforeEach(function() {
        subject.handleCommandEntry("down");
      });

      it("handles down", function() {
        expect(subject.handleDown).to.have.been.called;
        expect(subject.handleUp).to.not.have.been.called;
        expect(subject.handleEnter).to.not.have.been.called;
        expect(subject.handleEscape).to.not.have.been.called;
      });
    });

    describe("enter", function() {
      beforeEach(function() {
        subject.handleCommandEntry("enter");
      });

      it("handles enter", function() {
        expect(subject.handleEnter).to.have.been.called;
        expect(subject.handleUp).to.not.have.been.called;
        expect(subject.handleDown).to.not.have.been.called;
        expect(subject.handleEscape).to.not.have.been.called;
      });
    });

    describe("escape", function() {
      beforeEach(function() {
        subject.handleCommandEntry("escape");
      });

      it("handles escape", function() {
        expect(subject.handleEscape).to.have.been.called;
        expect(subject.handleUp).to.not.have.been.called;
        expect(subject.handleDown).to.not.have.been.called;
        expect(subject.handleEnter).to.not.have.been.called;
      });
    });
  });

  describe("#handleDown", function() {
    beforeEach(function() {
      sinon.spy(subject, "activateNextItem");
      subject.createListItems(items);
      subject.handleDown();
    });

    it("activates the next item", function() {
      expect(subject.activateNextItem).to.have.been.called;
    });
  });

  describe("#handleUp", function() {
    beforeEach(function() {
      subject.createListItems(items);
      sinon.spy(subject, "activatePreviousItem");
      subject.handleUp();
    });

    it("activates the previous item", function() {
      expect(subject.activatePreviousItem).to.have.been.called;
    });
  });

  describe("#handleEscape", function() {
    beforeEach(function() {
      sinon.spy(subject, "hide");
      subject.handleEscape();
    });

    it("hides the list", function() {
      expect(subject.hide).to.have.been.called;
    });
  });

  describe("#handleEnter", function() {
    describe("when an item is active", function() {
      var item;
      beforeEach(function() {
        subject.createListItems(items);
        item = _.first(subject.items);
        item.activate();
        sinon.spy(item, "select");
        subject.handleEnter();
      });

      it("selects the active item", function() {
        expect(item.select).to.have.been.called;
      });
    });

    describe("when there is no active item", function() {
      it("does not throw an error", function() {
        expect(function() {
          subject.handleEnter();
        }).not.to.throw();
      });
    });
  });
});


<|start_filename|>stylesheets/autocomplete.css<|end_filename|>
h2 {
  clear: both;
}

.autocomplete-container {
  float: left;
  overflow: auto;
  zoom: 1;
  min-width: 200px;
  padding: 0 1px 0 0;
}

.autocomplete-container .autocomplete-input {
  float: left;
  display: block;
  padding: .5em;
  margin: 0;
  width: 200px;
}

.autocomplete-container .autocomplete-list {
  clear: both;
  overflow: hidden;
  border-right: solid 1px #efefef;
  border-left: solid 1px #efefef;
}

.autocomplete-container .autocomplete-list.hidden {
  display: none;
}

.autocomplete-container .autocomplete-list li {
  float: left;
  display: block;
  width: 100%;
}

.autocomplete-container .autocomplete-list li a {
  padding: .25em;
  border-bottom: solid 1px #efefef;
  float: left;
  display: block;
  width: 100%;
  text-decoration: none;
  color: black;
}

.autocomplete-container .autocomplete-list li a:active,
.autocomplete-container .autocomplete-list .active {
  background-color: #66ccff;
}


<|start_filename|>js/autocomplete_ajax_adapter.js<|end_filename|>
function AutocompleteAjaxAdapter(attributes) {
  _.bindAll(this);
  attributes = attributes || {};
  _.defaults(attributes, {
    onAutocomplete: function() {}
  });

  if (_.isUndefined(attributes.url)) {
    throw new Error("AutocompleteAjaxAdapter: url is undefined");
  }

  this.url = attributes.url;
  this.onAutocomplete = attributes.onAutocomplete;
  this.throttleDelay = 250;
}

_.merge(AutocompleteAjaxAdapter.prototype, {

  handleTextEntry: function(text) {
    this.query = text;
    if (this.queuedRequest) {
      clearTimeout(this.queuedRequest);
    }
    if (!text) {
      this.onAutocomplete([]);
      return;
    }
    this.queueRequest();
  },

  fetchItems: function() {
    $.ajax({
      url: this.url,
      type: "get",
      dataType: "json",
      data: { query: this.query }
    })
      .done(this.onAutocomplete);
  },

  queueRequest: function() {
    var _this = this;
    this.queuedRequest = setTimeout(function() {
      _this.fetchItems();
    }, this.throttleDelay);
  }

});



<|start_filename|>js/autocomplete_list_item.js<|end_filename|>
function AutocompleteListItem(attributes) {
  _.bindAll(this);
  attributes = attributes || {};
  _.defaults(attributes, {
    onSelect: function() {}
  });

  this.value = attributes.item.value;
  this.text = attributes.item.text;
  this.onSelect = attributes.onSelect;
  this.$el = $(this.template({
    value: this.value,
    text: this.text
  }));
  this.active = false;

  this.registerEvents();
}

_.merge(AutocompleteListItem.prototype, {

  template: _.template('<li class="autocomplete-list-item"><a href="#"><%= text %></a></li>'),

  activate: function() {
    this.active = true;
    this.$el.addClass("active");
  },

  deactivate: function() {
    this.active = false;
    this.$el.removeClass("active");
  },

  registerEvents: function() {
    this.$el.on("click", this.select);
  },

  select: function() {
    this.onSelect(this);
  }

});
