w
clone your own copy | download snapshot

Snapshots | iceberg

No images in this repository’s iceberg at this time

Inside this repository

backbone.autocomplete.js
application/javascript

Download raw (5.0 KB)

var AutoCompleteItemView = Backbone.View.extend({
    tagName: "li",
    template: _.template('<a href="#"><%= label %></a>'),

    events: {
        "click": "select"
    },
    
    initialize: function(options) {
        this.options = options;
    },

    render: function () {
        this.$el.html(this.template({
            "label": this.highlight(this.model.label())
        }));
        return this;
    },
    
    highlight: function (label) {    // tkes, highlight keyword in result
        var op = this.options.parent;
        if (label && op.highlight && op.currentText) {
            label = label.replace(
                new RegExp(this.escapeRegExp(op.currentText), "gi"),
                function (matched) {
                    return $('<b>').addClass(op.highlight).html(matched);
                }
            );
        }
        return label;
    },

	escapeRegExp: function(str) {
		// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
		return String(str).replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
	},

    select: function () {
        this.options.parent.hide().select(this.model);
        return false;
    }

});

var AutoCompleteView = Backbone.View.extend({
    tagName: "ul",
    className: "autocomplete",
    wait: 300,

    queryParameter: "query",
    minKeywordLength: 2,
    currentText: "",
    itemView: AutoCompleteItemView,
    highlight: "",

    initialize: function (options) {
        _.extend(this, options);
        this.filter = _.debounce(this.filter, this.wait);
    },

    render: function () {
        // disable the native auto complete functionality
        this.input.attr("autocomplete", "off");

        this.$el.width(this.input.outerWidth());

        this.input
            .keyup(_.bind(this.keyup, this))
            .keydown(_.bind(this.keydown, this))
            .after(this.$el)
            .blur($.proxy(this.blur, this))
        ;

        return this;
    },

    keydown: function (event) {
        if (event.keyCode == 38) return this.move(-1);
        if (event.keyCode == 40) return this.move(+1);
        if (event.keyCode == 13) return this.onEnter();
        if (event.keyCode == 27) return this.hide();
    },

    blur: function() {
        this.hide();
    },

    keyup: function () {
        var keyword = this.input.val();
        if (this.isChanged(keyword)) {
            if (this.isValid(keyword)) {
                this.filter(keyword);
            } else {
                this.hide()
            }
            this.currentText = keyword; // tkes, moved here from loadResult
        }
    },

    filter: function (keyword) {
        var keyword = keyword.toLowerCase();
        if (this.model.url) {

            var parameters = {};
            parameters[this.queryParameter] = keyword;

            this.model.fetch({
                success: _.bind(function () { // tkes, _.bind instead of .bind()
                    this.loadResult(this.model.models, keyword);
                }, this),
                data: parameters,
                // tkes, some more params
                cache: (typeof this.model.cache != 'undefined') ? this.model.cache : undefined,
                dataType: (this.model.datatype) ? this.model.datatype : undefined,
                jsonpCallback: (this.model.callback) ? this.model.callback : undefined
            });

        } else {
            this.loadResult(this.model.filter(function (model) {
                return model.label().toLowerCase().indexOf(keyword) !== -1
            }), keyword);
        }
    },

    isValid: function (keyword) {
        return keyword.length > this.minKeywordLength
    },

    isChanged: function (keyword) {
        return this.currentText != keyword;
    },

    move: function (position) {
        var current = this.$el.children(".active"),
            siblings = this.$el.children(),
            index = current.index() + position;
        if (siblings.eq(index).length) {
            current.removeClass("active");
            siblings.eq(index).addClass("active");
        }
        return false;
    },

    onEnter: function () {
        this.$el.children(".active").click();
        return false;
    },

    loadResult: function (model, keyword) {
        this.show().reset();
        if (model.length) {
            _.forEach(model, this.addItem, this);
            this.show();
        } else {
            this.hide();
        }
    },

    addItem: function (model) {
        this.$el.append(new this.itemView({
            model: model,
            parent: this
        }).render().$el);
    },

    select: function (model) {
        var label = model.label();
        this.input.val(label);
        this.currentText = label;
        this.onSelect(model);
    },

    reset: function () {
        this.$el.empty();
        return this;
    },

    hide: function () {
        this.$el.hide();
        return this;
    },

    show: function () {
        this.$el.show();
        return this;
    },

    // callback definitions
    onSelect: function () {}

});