I wrote a simple JavaScript tool to highlight multiple keywords at the same time in some text. It uses different color for each keyword, and also shows the count of matches.I'm using Backbone.js with the following class structure:App.Model: the main model, keeping track of:the keywordsthe words in the text and their countthe original textthe transformed text with highlighted wordsApp.OriginalTab, App.HighlightedTab: View classes, for showing the original text and the highlighted text, respectively, in tabsApp.KeywordView: a View class, handling the input of new keywordsApp.Keyword: a Model class, keeping track of a keyword and its count in the textApp.KeywordList: a Collection class, keeping track of the keywords. Changes to the collection trigger updating the highlighted textApp.KeywordsView: a View class, to render the list of keywords, highlighted using the same color as the highlight in the text, showing the count, and a button to remove from the listImplementation:var App = window.App = {};_.templateSettings = { interpolate: /\{\{(.+?)\}\}/g };App.Model = Backbone.Model.extend({    defaults: {        original: '',        keywords: [],        words: {},        highlighted: ''    },    initialize: function () {        this.on('change:original', this.onOriginalUpdated, this);        this.on('change:keywords', this.onKeywordsUpdated, this);    },    updateWords: function () {        var words = {};        _.each(this.get('original').split(/\W+/), function (word) {            word = word.toLowerCase();            words[word] = (words[word] || 0) + 1;        });        this.set({words: words});    },    updateHighlighted: function () {        var highlighted = this.escape('original');        var cnt = 1;        _.each(this.get('keywords'), function (keyword) {            var pattern = '\\b' + keyword;            var cname = 'hlt' + cnt++;            highlighted = highlighted.replace(new RegExp(pattern, 'gi'), '<span class=' + cname + '>' + keyword + '</span>');        });        this.set({highlighted: highlighted});    },    onOriginalUpdated: function () {        this.updateWords();        this.updateHighlighted();    },    onKeywordsUpdated: function () {        this.updateHighlighted();    },    getCount: function (word) {        var pattern = '\\b' + word;        var matches = this.get('original').match(new RegExp(pattern, 'gi'));        return matches ? matches.length : 0;    }});App.Tab = Backbone.View.extend({    activate: function () {        var id = this.$el.attr('id');        var anchor = $('a[href=#' + id + ']');        anchor.tab('show');    }});App.OriginalTab = App.Tab.extend({    initialize: function () {        this.text = this.$('.text');    },    fieldToFocus: this.$('.text'),    events: {        'blur .text': 'onTextChanged'    },    onTextChanged: function () {        this.model.set({original: this.text.val()});    }});jQuery.fn.selectText = function () {    var element = this[0];    var range;    if (document.body.createTextRange) {        range = document.body.createTextRange();        range.moveToElementText(element);        range.select();    } else if (window.getSelection) {        var selection = window.getSelection();        range = document.createRange();        range.selectNodeContents(element);        selection.removeAllRanges();        selection.addRange(range);    }};App.HighlightedTab = App.Tab.extend({    template: _.template($('#highlighted-template').html()),    initialize: function () {        this.model.bind('change:highlighted', this.render, this);    },    render: function () {        this.$el.html(this.template(this.model.toJSON()));        var element = this.$('.highlighted');        this.$('.select').click(function () {            element.selectText();        });        return this;    }});App.KeywordView = Backbone.View.extend({    tagName: 'tr',    template: _.template($('#keyword-template').html()),    events: {        'dblclick .view': 'edit',        'click a.destroy': 'clear',        'keypress .edit': 'updateOnEnter',        'blur .edit': 'close'    },    initialize: function () {        this.model.bind('change', this.render, this);        this.model.bind('destroy', this.remove, this);    },    render: function () {        this.$el.html(this.template(this.model.toJSON()));        this.input = this.$('.edit');        return this;    },    edit: function () {        this.$el.addClass('editing');        this.input.focus();    },    close: function () {        var value = this.input.val();        if (!value) {            this.clear();        }        this.model.set({keyword: value});        this.$el.removeClass('editing');    },    updateOnEnter: function (e) {        if (e.keyCode == 13) {            this.close();        }    },    clear: function () {        this.model.clear();    }});App.Keyword = Backbone.Model.extend({    defaults: function () {        return {            keyword: 'empty keyword...',            count: 0,            index: 1        };    },    initialize: function () {        App.model.on('change:words', this.refreshCount, this);        this.refreshCount();    },    refreshCount: function () {        var keyword = this.get('keyword');        this.set({count: App.model.getCount(keyword)});    },    clear: function () {        this.destroy();    }});App.KeywordList = Backbone.Collection.extend({    model: App.Keyword,    localStorage: new Store('highlighter-backbone'),    initialize: function () {        this.on('add', this.onChange, this);        this.on('remove', this.onChange, this);        this.on('reset', this.onChange, this);    },    onChange: function () {        var keywords = this.pluck('keyword');        App.model.set({keywords: keywords});        App.highlightedTab.activate();    }});App.KeywordsView = Backbone.View.extend({    el: '#keywords',    events: {        'keypress .keyword': 'createOnEnter',        'click th a.destroy': 'clear'    },    initialize: function (options) {        this.keywords = options.list;        this.input = this.$('.keyword');        this.keywords.bind('add', this.add, this);        this.keywords.bind('reset', this.reset, this);        this.keywords.fetch();        if (this.keywords.length) {            this.keywords.each(this.add);        }        else {            this.create('lorem');            this.create('ipsum');            this.create('dolor');        }    },    add: function (keyword) {        var view = new App.KeywordView({model: keyword});        this.$('#keyword-list').append(view.render().el);    },    reset: function () {        this.$('#keyword-list').empty();    },    createOnEnter: function (e) {        if (e.keyCode != 13) {            return;        }        if (!this.input.val()) {            return;        }        var keyword = this.input.val();        this.create(keyword);        this.input.val('');    },    create: function (keyword) {        var index = this.keywords.length + 1;        this.keywords.create({keyword: keyword, index: index});    },    clear: function () {        // todo: isn't there a better way?        var i = 0;        var maxiter = 10;        while (true) {            this.keywords.invoke('destroy');            if (!this.keywords.length || ++i > maxiter) {                break;            }        }    }});function onDomReady() {    App.model = new App.Model();    App.originalTab = new App.OriginalTab({        el: $('#original-tab'),        model: App.model    });    App.highlightedTab = new App.HighlightedTab({        el: $('#highlighted-tab'),        model: App.model    });    App.keywordList = new App.KeywordList();    App.keywordsView = new App.KeywordsView({        model: App.model,        list: App.keywordList    });    $('#reset').click(function () {        App.keywordsView.clear();        App.originalTab.text.val('');        App.originalTab.activate();        App.originalTab.text.focus();    });    // other initialization    App.keywordsView.input.focus();    App.model.set({original: App.originalTab.text.text()});}$(function () {    onDomReady();});Is there a better way to do this? Or simpler? Am I using Backbone.js correctly? Can I organize my classes better?You can see the code in action here.The full source code is hosted on GitHub.
Simple JavaScript Backbone.Js tool to highlight multiple search terms
javascript;jquery;beginner;backbone.js;underscore.js
One quick note (I'm so sorry I have no chance to read the full code)please be careful when using reference values (i.e object and arrays) in defaults, let me show you one example of how this could be turned into an unexpected behavior:A = Backbone.Model.extend({defaults:{arr: ['a', 'b']}})a = new A()a.get('arr').push('c')a.get('arr')>> [a, b, c]b = new A()b.get('arr')>>[a, b, c]This happens because arr attributes in both a and b instances of Model A refers to same value which is arr array reference in memory so when you are doing a.get('arr').push('c') you actually manipulate the arr value you referenced when you first declared defaults so any next instance of Model A will use the new value of arr which is [a, b, c] I hope this is clear.One way you could solve this is to check for your defaults in initialize method instead so it'll create new instance of arr whenever there's no one present if we apply this to your initialize method:   initialize: function() {    if (!this.get('keywords')) {        this.set({            keywords: new Array()        });    }    if (!this.get('words')) {        this.set({            words: new Object()        });    }    this.on('change:original', this.onOriginalUpdated, this);    this.on('change:keywords', this.onKeywordsUpdated, this);}