├── .DS_Store
├── .gitignore
├── CHANGELOG.md
├── Gruntfile.js
├── LICENSE.html
├── README.md
├── base.css
├── bower.json
├── dist
├── .DS_Store
├── backbone.collectionView.js
└── backbone.collectionView.min.js
├── package.json
├── src
└── backbone.collectionView.js
├── test
├── .DS_Store
├── index.html
├── lib
│ ├── backbone.babysitter.js
│ ├── backbone.js
│ ├── jquery-1.9.1.js
│ ├── jquery-ui.css
│ ├── jquery-ui.js
│ ├── qunit-1.11.0.css
│ ├── qunit-1.11.0.js
│ └── underscore.js
└── tests.js
└── zips
└── Backbone.CollectionView.zip
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rotundasoftware/backbone.collectionView/63109a35255b91ca220c2b3988c964000f48477b/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .sass-cache
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ### v1.2.0
4 |
5 | * Added 'click' event
6 |
7 | ### v0.11.0
8 |
9 | * Added `reuseModelViews` option. Set to false to completely recreate model views on each render.
10 |
11 | ### v0.10.1
12 |
13 | * Brought npm package version in sync with Bower package. How'd that happen?
14 | * Fixed issue that could surface when setting multiple models using collection.set (#50)
15 |
16 | ### v0.9.3
17 |
18 | * Fixed UMD wrapper.
19 |
20 | ### v0.9.2
21 |
22 | * Added UMD wrapper.
23 |
24 | ### v0.9.0
25 |
26 | * No longer re-render upon add() and remove() events in the collection.
27 | * Due to the above, "render" events no longer trigger during add() or remove() calls on a collection.
28 | * Made use of backbone.viewOptions by including it and updating the source to use it's facilities.
29 |
30 | ### v0.8.2
31 |
32 | * Patch for better bower support.
33 |
34 | ### v0.8.1
35 |
36 | * If a modelView's element is an
, don't wrap in an extra
when rendering
37 | * Fix accidental item removal after sort stop when rendering as table.
38 | * Add sortableOptions constructor option which is passed through to the created jQuery sortable.
39 |
40 | ### v0.8
41 |
42 | * data-item-id attribute has been changed to data-model-cid for clarity
43 | * selectableModelsFilter can no longer be a string.
44 | * After the collection view is reordered via dragging: if the collection has a comparator, sort after adding all the models in the visual order.
45 | * Never "rerender" the collection view unless it has already been rendered. For example, adding models to the collection should not render the collection view if it was not previously rendered.
46 | * Don't listen to events from old collection when setOptions( "collection", newCollection ) is called
47 |
48 | ### v0.7.1
49 |
50 | * Use css classes to keep track of and determine visibility of an item
51 | * Call remove() on views in viewManager when they are removed (so they stop listening to events)
52 | * Add `detachedRendering` option (defaults to `false`) to improve performance by rendering all modelViews before inserting into the DOM.
53 |
54 | ### v0.7.0
55 |
56 | * Fix to work with underscore.js 1.5.1 (remove `_.bindAll` and use `.bind` where necessary).
57 | * Listen for `mousedown` event instead of `click` when `clickToSelect` is enabled.
58 | * Hide empty list caption when dragging item into a `sortable` collection view.
59 |
60 | ### v0.6.5
61 |
62 | * Model views that represent a particular model are now reused between renders.
63 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | grunt.initConfig({
4 | pkg: grunt.file.readJSON('package.json'),
5 | banner: '/*!\n' +
6 | '* Backbone.CollectionView, v<%= pkg.version %>\n' +
7 | '* Copyright (c)2013 Rotunda Software, LLC.\n' +
8 | '* Distributed under MIT license\n' +
9 | '* http://github.com/rotundasoftware/backbone-collection-view\n' +
10 | '*/\n\n',
11 |
12 | // Task configuration.
13 | concat: {
14 | options: {
15 | banner: '<%= banner %>',
16 | stripBanners: true
17 | },
18 | js: {
19 | src: ['src/backbone.collectionView.js'],
20 | dest: 'dist/backbone.collectionView.js'
21 | }
22 | },
23 | uglify: {
24 | options: {
25 | banner: '<%= banner %>'
26 | },
27 | dist: {
28 | src: '<%= concat.js.dest %>',
29 | dest: 'dist/backbone.collectionView.min.js'
30 | }
31 | },
32 | compress: {
33 | dist: {
34 | options: {
35 | archive: 'zips/Backbone.CollectionView.zip'
36 | },
37 | expand: true,
38 | cwd: 'dist/',
39 | src: ['**/*'],
40 | dest: './'
41 | }
42 | },
43 | jshint: {
44 | options: {
45 | curly: true,
46 | eqeqeq: true,
47 | immed: true,
48 | latedef: true,
49 | newcap: true,
50 | noarg: true,
51 | sub: true,
52 | undef: true,
53 | unused: true,
54 | boss: true,
55 | eqnull: true,
56 | browser: true,
57 | globals: {
58 | jQuery: true
59 | }
60 | },
61 | library: {
62 | src: 'collectionView.js'
63 | }
64 | }
65 | });
66 |
67 | grunt.loadNpmTasks('grunt-contrib-concat');
68 | grunt.loadNpmTasks('grunt-contrib-uglify');
69 | grunt.loadNpmTasks('grunt-contrib-jshint');
70 | grunt.loadNpmTasks('grunt-contrib-compress');
71 |
72 | grunt.registerTask('default', ['concat', 'uglify', 'compress']);
73 |
74 | };
75 |
--------------------------------------------------------------------------------
/LICENSE.html:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 David Beck, Rotunda Software
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Backbone.CollectionView
2 |
3 | __For demos see [the Backbone.CollectionView home page](http://rotundasoftware.github.com/backbone.collectionView/).__
4 |
5 | Depends on jQuery and jQueryUI for event handling and sorting, respectively.
6 |
7 | ## Benefits
8 |
9 | * Renders a collection of models, updating automatically when models are added or removed.
10 | * Keeps track of selected model(s) and fires events when the selection is changed.
11 | * Adds "selected" css class to selected model views for easy stying.
12 | * Supports single and multiple selection through meta-key and shift clicks.
13 | * Allows a user to reorder the collection by dragging and dropping.
14 | * Supports changing the currently selected model(s) through up and down arrow key presses.
15 | * Allows you to filter which models are visible, selectable, and sortable.
16 | * Integrates with [Backbone.Courier](https://github.com/rotundasoftware/backbone.courier) out of the box.
17 |
18 | ## Sample Usage
19 | ```javascript
20 | var myCollectionView = new Backbone.CollectionView( {
21 | el : $( "#listForCollection" ), // must be a 'ul' (i.e. unordered list) or 'table' element
22 | modelView : EmployeeView, // a View class to be used for rendering each model in the collection
23 | collection : employeeCollection
24 | } );
25 |
26 | myCollectionView.render();
27 | myCollectionView.setSelectedModel( employeeCollection.first() );
28 | ```
29 |
30 | ## Initialization Options
31 | * `el` : A `
` or `
` element into which your collection will be rendered. If you supply a `
` element, your modelView must have an element of type of `
`.
32 | * `collection` : The collection of models to be rendered.
33 | * `modelView` : A view constructor that will be used to create the views for each individual model.
34 | * `selectable` : (default: _true_) Determines whether models in the CollectionView are selectable.
35 | * `reuseModelViews` : (default: _true_) When `true`, modelViews are reused instead of being re-created from scratch when the CollectionView is re-rendered.
36 | * `detachedRendering` : (default: _false_) When `true`, all the modelViews are rendered before being added to the DOM to improve performance. If your modelView rendering relies on its location in the DOM (for sizing or other reasons), use the default value of `false`.
37 | * `sortable` : (default: _false_) Determines if models can be rearranged by dragging and dropping.
38 | * `sortableOptions` : Options passed through to the created jQueryUI sortable. Only applies if `sortable`.
39 | * `emptyListCaption` : A string (or a function returning a string) to be shown when the list is empty.
40 |
41 | The following options apply when `selectable` option is set:
42 |
43 | * `clickToSelect` : (default: _true_) Determines whether mouse clicks should automatically select models as would be appropriate in a standard HTML mutli-SELECT element.
44 | * `processKeyEvents` : (default: _true_) Determines if the collection view should respond to arrow key events as would be appropriate in a standard HTML multi-SELECT element.
45 | * `selectMultiple` : (default: _false_) Determines if multiple models can be selected at once.
46 | * `clickToToggle` : (default: _false_) Determines if clicking a model view should toggle its selected / unselected state. Only applies if `selectMultiple` is set.
47 |
48 | The following options expect a filter function that takes a single parameter, the model in question, and returns `true` or `false`. They are all optional, defaulting to passing all models.
49 | * `visibleModelsFilter` : Determines which models are visible.
50 | * `selectableModelsFilter` : In a selectable CollectionView, determines which models are selectable.
51 | * `sortableModelsFilter` : In a sortable CollectionView, determines which models are sortable.
52 |
53 | ## Methods and Properties Reference
54 |
55 | * __setSelectedModel( modelReference, [options] )__ Sets which model(s) are selected. (See below.)
56 | * __getSelectedModel( [options] )__ Returns references to the selected model(s). (See below.)
57 | * __setOption( optionName, optionValue )__ Updates the value of a configuration option. All constructor options above are valid except `el`. The CollectionView is automatically re-rendered if necessary.
58 | * __collection__ The Backbone collection instance that this CollectionView represents.
59 | * __viewManager__ A [Backbone.BabySitter](https://github.com/marionettejs/backbone.babysitter) instance that contains the views corresponding to the individual models in the collection. Backbone.Babysitter implements a "view collection" that enables you to, for example, iterate through all the model views, or easily trigger an event on all model views at once. See the [Backbone.BabySitter](https://github.com/marionettejs/backbone.babysitter) documentation for more information.
60 |
61 |
62 | ### setSelectedModel(s) and getSelectedModel(s)
63 |
64 | The `getSelectedModel(s)` and `setSelectedModel(s)` methods are used to get or set the currently selected models. The methods are able to reference models in a variety of ways using the `by` option:
65 |
66 | ```javascript
67 | // Returns an array of the selected models
68 | myCollectionView.getSelectedModels();
69 |
70 | // Returns an array of the ids of the selected models
71 | myCollectionView.getSelectedModels( { by : "id" } );
72 |
73 | // Select model id 2 and model id 4
74 | myCollectionView.setSelectedModels( [ 2, 4 ], { by : "id" } );
75 |
76 | // Select the model with cid equal to "c21"
77 | myCollectionView.setSelectedModel( "c21", { by : "cid" } );
78 |
79 | // Returns the view object that represents the selected model
80 | myCollectionView.getSelectedModel( { by : "view" } );
81 | ```
82 |
83 | As shown in the examples, the plural versions of the methods expect / return an array of "model references", whereas the singular versions expect / return just a single "model reference".
84 |
85 | There are four valid values for `by` option which determine the type of "model reference" used.
86 | * `"id"` : The `id` of the model.
87 | * `"cid"` : The `cid` of the model.
88 | * `"offset"` : The zero-based index of the model in the collection, only counting visible models.
89 | * `"view"` : The view that was created to represent the model when the CollectionView was rendered.
90 |
91 | If no `by` option is provided the model object itself is expected / returned.
92 |
93 | Additionally, the `setSelectedModel(s)` function accepts one additional option, `silent`, which, when true, will prevent the `selectionChanged` event from being fired.
94 |
95 | ##Events Fired
96 | CollectionViews `trigger` the following events on themselves. If [Backbone.Courier](https://github.com/rotundasoftware/backbone.courier)
97 | is available, these events are also "spawned".
98 | * __"selectionChanged"__ ( _newSelectedModels, oldSelectedModels_ ) Fired whenever the selection is changed, either by the user or by a programmatic call to `setSelectedModel(s)`.
99 | * __"updateDependentControls"__ ( _selectedModels_ ) Fired whenever controls that are dependent on the selection should be updated (e.g. buttons that should be disabled on no selection). This event is always fired just after `selectionChanged` is fired. In addition, it is fired after rendering and sorting.
100 | * __"click"__ ( _clickedModel_ ) Fired when a model view is clicked.
101 | * __"doubleClick"__ ( _clickedModel_ ) Fired when a model view is double clicked.
102 |
103 | In addition, sortable CollectionViews fire these events:
104 |
105 | * __"sortStart"__ Fired just as a model view is starting to be dragged.
106 | * __"sortStop"__ Fired after a drag is finished and after the collection is reordered.
107 | * __"reorder"__ Fired when a drag is finished, but before the collection is reordered.
108 |
109 | ##Styling
110 |
111 | How you style the collection view is up to you.
112 |
113 | The `ul` or `table` element that is used as the collection view's element will be given the `collection-list` class. Generally you will want to eliminate bullets, etc., from your collection view list elements, which you can do with these "base" styles:
114 |
115 | ```css
116 | ul.collection-list {
117 | margin: 0;
118 | padding: 0;
119 | list-style-type: none;
120 | outline: none;
121 | cursor: pointer;
122 | }
123 | ```
124 |
125 | When a model is selected, its view's `li` or `tr` element will be given the `selected` class.
126 |
127 | You can style the caption created by the `emptyListCaption` option with the `var.empty-list-caption` selector. These styles will center the empty list caption text near the top of the collection view.
128 |
129 | ```css
130 | var.empty-list-caption {
131 | color: #A0A0A0;
132 | padding: 30px;
133 | display: block;
134 | text-align: center;
135 | font-style: normal;
136 | line-height: 1.45;
137 | }
138 | ```
139 |
140 | (Both of the above css fragments are included in `base.css`, which will be included automatically in your css bundles if you are using [parcelify](https://github.com/rotundasoftware/parcelify) or [cartero](https://github.com/rotundasoftware/cartero/).)
141 |
142 | See the [the Backbone.CollectionView home page](http://rotundasoftware.github.com/backbone.collectionView/) for styling examples.
143 |
144 | ## Contributing
145 |
146 | See the [the Backbone.CollectionView home page](http://rotundasoftware.github.com/backbone.collectionView/) for styling examples.
147 |
148 | ##Dependencies
149 | * Backbone.js (tested with v1.0, v0.9.10)
150 | * jQuery (tested with v1.9.1)
151 | * jQueryUI - for sorting (tested with v1.10.1)
152 | * _(optional)_ [Backbone.Courier](https://github.com/rotundasoftware/backbone.courier)
153 |
154 | ##License
155 | MIT
156 |
--------------------------------------------------------------------------------
/base.css:
--------------------------------------------------------------------------------
1 | ul.collection-list {
2 | margin: 0;
3 | padding: 0;
4 | list-style-type: none;
5 | outline: none;
6 | }
7 |
8 | ul.collection-list.selectable {
9 | cursor: pointer;
10 | }
11 |
12 | var.empty-list-caption {
13 | color: #A0A0A0;
14 | padding: 30px;
15 | display: block;
16 | text-align: center;
17 | font-style: normal;
18 | line-height: 1.45;
19 | }
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "backbone.collectionView",
3 | "homepage": "http://rotundasoftware.github.io/backbone.collectionView/",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/rotundasoftware/backbone.collectionView"
7 | },
8 | "authors": [
9 | "David Beck "
10 | ],
11 | "description": "A backbone view class that renders a collection of models. This class is similar to the collection view classes found in [Backbone.Marionette](https://github.com/marionettejs/backbone.marionette) and other frameworks, with added features for automatic selection of models in response to clicks, and support for rearranging models (and reordering the underlying collection) through drag and drop.",
12 | "main": "dist/backbone.collectionView.js",
13 | "keywords": [
14 | "backbone",
15 | "views",
16 | "view"
17 | ],
18 | "license": "MIT",
19 | "dependencies": {
20 | "backbone" : "~1.0",
21 | "jquery" : "~1.9.1",
22 | "jqueryui" : "~1.10.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dist/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rotundasoftware/backbone.collectionView/63109a35255b91ca220c2b3988c964000f48477b/dist/.DS_Store
--------------------------------------------------------------------------------
/dist/backbone.collectionView.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Backbone.CollectionView, v3.0.0
3 | * Copyright (c)2013 Rotunda Software, LLC.
4 | * Distributed under MIT license
5 | * http://github.com/rotundasoftware/backbone-collection-view
6 | */
7 |
8 | !function(a,b){"function"==typeof define&&define.amd?define(["underscore","backbone","jquery"],b):"undefined"!=typeof exports?module.exports=b(require("underscore"),require("backbone"),require("backbone").$):b(a._,a.Backbone,a.jQuery||a.Zepto||a.$)}(this,function(a,b,c){function d(b){var c={};if(!a.isArray(b))throw new Error("Option declarations must be an array.");return a.each(b,function(b){var d,e,f;if(e=!1,f=void 0,a.isString(b))d=b;else{if(!a.isObject(b))throw new Error("Each element in the option declarations array must be either a string or an object.");d=a.first(a.keys(b)),f=a.isFunction(b[d])?b[d]:a.clone(b[d])}"!"===d[d.length-1]&&(e=!0,d=d.slice(0,d.length-1)),c[d]=c[d]||{},c[d].required=e,a.isUndefined(f)||(c[d].defaultValue=f)}),c}var e=b.View,f="model",g=["collection","modelView","modelViewOptions","itemTemplate","itemTemplateFunction","detachedRendering"],h={background:"transparent",border:"none","box-shadow":"none"};return b.CollectionView=b.View.extend({tagName:"ul",events:{"mousedown > li, tbody > tr > td":"_listItem_onMousedown","dblclick > li, tbody > tr > td":"_listItem_onDoubleClick",click:"_listBackground_onClick","click ul.collection-view, table.collection-view":"_listBackground_onClick",keydown:"_onKeydown"},spawnMessages:{focus:"focus"},passMessages:!0,initializationOptions:[{collection:null},{modelView:null},{modelViewOptions:{}},{itemTemplate:null},{itemTemplateFunction:null},{selectable:!0},{clickToSelect:!0},{selectableModelsFilter:null},{visibleModelsFilter:null},{sortableModelsFilter:null},{selectMultiple:!1},{clickToToggle:!1},{processKeyEvents:!0},{sortable:!1},{sortableOptions:null},{reuseModelViews:!0},{detachedRendering:!1},{emptyListCaption:null}],initialize:function(a){b.ViewOptions.add(this,"initializationOptions"),this.setOptions(a),this.collection||(this.collection=new b.Collection),this._hasBeenRendered=!1,this._isBackboneCourierAvailable()&&b.Courier.add(this),this.$el.data("view",this),this.$el.addClass("collection-view collection-list"),this.selectable&&this.$el.addClass("selectable"),this.selectable&&this.processKeyEvents&&this.$el.attr("tabindex",0),this.selectedItems=[],this._updateItemTemplate(),this.collection&&this._registerCollectionEvents(),this.viewManager=new ChildViewContainer},_onOptionsChanged:function(b,c){var d=this,e=!1;a.each(a.keys(b),function(f){var h=b[f],i=c[f];switch(f){case"collection":h!==i&&(d.stopListening(i),d._registerCollectionEvents());break;case"selectMultiple":!h&&d.selectedItems.length>1&&d.setSelectedModel(a.first(d.selectedItems),{by:"cid"});break;case"selectable":!h&&d.selectedItems.length>0&&d.setSelectedModels([]),h&&this.processKeyEvents?d.$el.attr("tabindex",0):d.$el.removeAttr("tabindex",0);break;case"sortable":b.sortable?d._setupSortable():d.$el.sortable("destroy");break;case"selectableModelsFilter":d.reapplyFilter("selectableModels");break;case"sortableOptions":d.$el.sortable("destroy"),d._setupSortable();break;case"sortableModelsFilter":d.reapplyFilter("sortableModels");break;case"visibleModelsFilter":d.reapplyFilter("visibleModels");break;case"itemTemplate":d._updateItemTemplate();break;case"processKeyEvents":h&&this.selectable?d.$el.attr("tabindex",0):d.$el.removeAttr("tabindex",0);break;case"modelView":d.viewManager.each(function(a){d.viewManager.remove(a),a.remove()})}a.contains(g,f)&&(e=!0)}),this._hasBeenRendered&&e&&this.render()},setOption:function(a,b){var c={};c[a]=b,this.setOptions(c)},getSelectedModel:function(b){return this.selectedItems.length?a.first(this.getSelectedModels(b)):null},getSelectedModels:function(b){var d=this;b=a.extend({},{by:f},b);var e=b.by,g=[];switch(e){case"id":a.each(this.selectedItems,function(a){g.push(d.collection.get(a).id)});break;case"cid":g=g.concat(this.selectedItems);break;case"offset":var h=0,i=this._getVisibleItemEls();i.each(function(){var a=c(this);a.is(".selected")&&g.push(h),h++});break;case"model":a.each(this.selectedItems,function(a){g.push(d.collection.get(a))});break;case"view":a.each(this.selectedItems,function(a){g.push(d.viewManager.findByModel(d.collection.get(a)))});break;default:throw new Error("Invalid referenceBy option: "+e)}return g},setSelectedModels:function(b,d){if(!a.isArray(b))throw"Invalid parameter value";if(this.selectable||!(b.length>0)){d=a.extend({},{silent:!1,by:f},d);var e=d.by,g=[];switch(e){case"cid":g=b;break;case"id":this.collection.each(function(c){a.contains(b,c.id)&&g.push(c.cid)});break;case"model":g=a.pluck(b,"cid");break;case"view":a.each(b,function(a){g.push(a.model.cid)});break;case"offset":var h=0,i=this._getVisibleItemEls();i.each(function(){var d=c(this);a.contains(b,h)&&g.push(d.attr("data-model-cid")),h++});break;default:throw new Error("Invalid referenceBy option: "+e)}var j=this.getSelectedModels(),k=a.clone(this.selectedItems);this.selectedItems=this._convertStringsToInts(g),this._validateSelection();var l=this.getSelectedModels();this._containSameElements(k,this.selectedItems)||(this._addSelectedClassToSelectedItems(k),d.silent||(this._isBackboneCourierAvailable()?this.spawn("selectionChanged",{selectedModels:l,oldSelectedModels:j}):this.trigger("selectionChanged",l,j)),this.updateDependentControls())}},setSelectedModel:function(a,b){a||0===a?this.setSelectedModels([a],b):this.setSelectedModels([],b)},getView:function(b,d){switch(d=a.extend({},{by:f},d),d.by){case"id":case"cid":var e=this.collection.get(b)||null;return e&&this.viewManager.findByModel(e);case"offset":var g=this._getVisibleItemEls();return c(g.get(b));case"model":return this.viewManager.findByModel(b);default:throw new Error("Invalid referenceBy option: "+referenceBy)}},render:function(){this._hasBeenRendered=!0,this.selectable&&this._saveSelection();var b;b=this._getContainerEl();var c=this.viewManager;this.viewManager=new ChildViewContainer,c.each(function(a){this.reuseModelViews&&this.collection.get(a.model.cid)?a.$el.detach():a.remove()},this),b.empty();var d;this.detachedRendering&&(d=document.createDocumentFragment()),this.collection.each(function(e){var f=c.findByModelCid(e.cid);(!this.reuseModelViews||a.isUndefined(f))&&(f=this._createNewModelView(e,this._getModelViewOptions(e))),this._insertAndRenderModelView(f,d||b)},this),this.detachedRendering&&b.append(d),this.sortable&&this._setupSortable(),this._showEmptyListCaptionIfAppropriate(),this._isBackboneCourierAvailable()?this.spawn("render"):this.trigger("render"),this.selectable&&(this._restoreSelection(),this.updateDependentControls()),this.forceRerenderOnNextSortEvent=!1},_showEmptyListCaptionIfAppropriate:function(){if(this._removeEmptyListCaption(),this.emptyListCaption){var b=this._getVisibleItemEls();if(0===b.length){var d;d=a.isFunction(this.emptyListCaption)?this.emptyListCaption():this.emptyListCaption;var e,f=c(""+d+"");e=this._isRenderedAsList()?f.wrapAll("").parent().css(h):f.wrapAll("
").parent().parent().css(h),this._getContainerEl().append(e)}}},_removeEmptyListCaption:function(){this._isRenderedAsList()?this._getContainerEl().find("> li > var.empty-list-caption").parent().remove():this._getContainerEl().find("> tr > td > var.empty-list-caption").parent().parent().remove()},_insertAndRenderModelView:function(b,c,d){var e=this._wrapModelView(b);if(11===c.nodeType)c.appendChild(e.get(0));else{var f=c.children().length;!a.isUndefined(d)&&d>=0&&f>d?c.children().eq(d).before(e):(!a.isUndefined(d)&&d>f&&(this.forceRerenderOnNextSortEvent=!0),c.append(e))}this.viewManager.add(b);var g=b.render();g===!1&&(e.hide(),e.addClass("not-visible"));var h=!1;a.isFunction(this.visibleModelsFilter)&&(h=!this.visibleModelsFilter(b.model)),1===e.children().length?e.toggle(!h):b.$el.toggle(!h),e.toggleClass("not-visible",h),!h&&this.emptyListCaption&&this._removeEmptyListCaption()},updateDependentControls:function(){this._isBackboneCourierAvailable()?this.spawn("updateDependentControls",{selectedModels:this.getSelectedModels()}):this.trigger("updateDependentControls",this.getSelectedModels())},remove:function(){this.viewManager.each(function(a){a.remove()}),b.View.prototype.remove.apply(this,arguments)},reapplyFilter:function(b){var c=this;if(!a.contains(["selectableModels","sortableModels","visibleModels"],b))throw new Error("Invalid filter identifier supplied to reapplyFilter: "+b);switch(b){case"visibleModels":c.viewManager.each(function(a){var b=c.visibleModelsFilter&&!c.visibleModelsFilter.call(c,a.model);a.$el.toggleClass("not-visible",b),c._modelViewHasWrapperLI(a)?a.$el.closest("li").toggleClass("not-visible",b).toggle(!b):a.$el.toggle(!b)}),this._showEmptyListCaptionIfAppropriate();break;case"sortableModels":c.$el.sortable("destroy"),c.viewManager.each(function(a){var b=c.sortableModelsFilter&&!c.sortableModelsFilter.call(c,a.model);a.$el.toggleClass("not-sortable",b),c._modelViewHasWrapperLI(a)&&a.$el.closest("li").toggleClass("not-sortable",b)}),c._setupSortable();break;case"selectableModels":c.viewManager.each(function(a){var b=c.selectableModelsFilter&&!c.selectableModelsFilter.call(c,a.model);a.$el.toggleClass("not-selectable",b),c._modelViewHasWrapperLI(a)&&a.$el.closest("li").toggleClass("not-selectable",b)}),c._validateSelection()}},_removeModelView:function(a){this.selectable&&this._saveSelection(),this.viewManager.remove(a),this._modelViewHasWrapperLI(a)&&a.$el.parent().remove(),a.remove(),this.selectable&&this._restoreSelection(),this._showEmptyListCaptionIfAppropriate()},_validateSelectionAndRender:function(){this._validateSelection(),this.render()},_registerCollectionEvents:function(){this.listenTo(this.collection,"add",function(a){var b;this._hasBeenRendered&&(b=this._createNewModelView(a,this._getModelViewOptions(a)),this._insertAndRenderModelView(b,this._getContainerEl(),this.collection.indexOf(a))),this._isBackboneCourierAvailable()?this.spawn("add",b):this.trigger("add",b)}),this.listenTo(this.collection,"remove",function(a){var b;this._hasBeenRendered&&(b=this.viewManager.findByModelCid(a.cid),this._removeModelView(b)),this._isBackboneCourierAvailable()?this.spawn("remove"):this.trigger("remove")}),this.listenTo(this.collection,"reset",function(){this._hasBeenRendered&&this.render(),this._isBackboneCourierAvailable()?this.spawn("reset"):this.trigger("reset")}),this.listenTo(this.collection,"sort",function(a,b){this._hasBeenRendered&&(b.add!==!0||this.forceRerenderOnNextSortEvent)&&this.render(),this._isBackboneCourierAvailable()?this.spawn("sort"):this.trigger("sort")})},_getContainerEl:function(){if(this._isRenderedAsTable()){var a=this.$el.find("> tbody");if(a.length>0)return a}return this.$el},_getClickedItemId:function(a){var b=null,d=c(a.currentTarget);if(d.closest(".collection-view").get(0)===this.$el.get(0)){var e=d.closest("[data-model-cid]");return e.length>0&&(b=e.attr("data-model-cid"),c.isNumeric(b)&&(b=parseInt(b,10))),b}},_updateItemTemplate:function(){var b;if(this.itemTemplate){if(0===c(this.itemTemplate).length)throw"Could not find item template from selector: "+this.itemTemplate;b=c(this.itemTemplate).html()}else b=this.$(".item-template").html();b&&(this.itemTemplateFunction=a.template(b))},_validateSelection:function(){var b=a.pluck(this.collection.models,"cid");this.selectedItems=a.intersection(b,this.selectedItems),a.isFunction(this.selectableModelsFilter)&&(this.selectedItems=a.filter(this.selectedItems,function(a){return this.selectableModelsFilter.call(this,this.collection.get(a))},this))},_saveSelection:function(){if(!this.selectable)throw"Attempt to save selection on non-selectable list";this.savedSelection={items:a.clone(this.selectedItems),offset:this.getSelectedModel({by:"offset"})}},_restoreSelection:function(){if(!this.savedSelection)throw"Attempt to restore selection but no selection has been saved!";this.setSelectedModels([],{silent:!0}),this.savedSelection.items.length>0&&(this.setSelectedModels(this.savedSelection.items,{by:"cid",silent:!0}),0===this.selectedItems.length&&this.setSelectedModel(this.savedSelection.offset,{by:"offset"}),this.selectedItems.length!==this.savedSelection.items.length&&(this._isBackboneCourierAvailable()?this.spawn("selectionChanged",{selectedModels:this.getSelectedModels(),oldSelectedModels:[]}):this.trigger("selectionChanged",this.getSelectedModels(),[])))},_addSelectedClassToSelectedItems:function(b){a.isUndefined(b)&&(b=[]);var c=b;c=a.without(c,this.selectedItems),a.each(c,function(a){this._getContainerEl().find("[data-model-cid="+a+"]").removeClass("selected"),this._isRenderedAsList()&&this._getContainerEl().find("li[data-model-cid="+a+"] > *").removeClass("selected")},this);var d=this.selectedItems;d=a.without(d,b),a.each(d,function(a){this._getContainerEl().find("[data-model-cid="+a+"]").addClass("selected"),this._isRenderedAsList()&&this._getContainerEl().find("li[data-model-cid="+a+"] > *").addClass("selected")},this)},_reorderCollectionBasedOnHTML:function(){var a=this;this._getContainerEl().children().each(function(){var b=c(this).attr("data-model-cid");if(b){var d=a.collection.get(b);d&&(a.collection.remove(d,{silent:!0}),a.collection.add(d,{silent:!0,sort:!a.collection.comparator}))}}),this._isBackboneCourierAvailable()?this.spawn("reorder"):this.collection.trigger("reorder"),this.collection.comparator&&this.collection.sort()},_getModelViewConstructor:function(a){return this.modelView||e},_getModelViewOptions:function(b){var c=this.modelViewOptions;return a.isFunction(c)&&(c=c(b)),a.extend({model:b},c)},_createNewModelView:function(b,c){var d=this._getModelViewConstructor(b);if(a.isUndefined(d))throw"Could not find modelView constructor for model";var e=new d(c);return e.collectionListView=e.collectionView=this,e},_wrapModelView:function(b){var c,d=this;return this._isRenderedAsTable()?(c=b.$el,b.$el.attr("data-model-cid",b.model.cid)):this._isRenderedAsList()&&(b.$el.is("li")?(c=b.$el,b.$el.attr("data-model-cid",b.model.cid)):c=b.$el.wrapAll("").parent()),a.isFunction(this.sortableModelsFilter)&&(this.sortableModelsFilter.call(d,b.model)||(c.addClass("not-sortable"),b.$el.addClass("not-selectable"))),a.isFunction(this.selectableModelsFilter)&&(this.selectableModelsFilter.call(d,b.model)||(c.addClass("not-selectable"),b.$el.addClass("not-selectable"))),c},_convertStringsToInts:function(b){return a.map(b,function(b){if(!a.isString(b))return b;var c=parseInt(b,10);return c==b?c:b})},_containSameElements:function(b,c){if(b.length!=c.length)return!1;var d=a.intersection(b,c).length;return d==b.length},_isRenderedAsTable:function(){return"table"===this.$el.prop("tagName").toLowerCase()},_isRenderedAsList:function(){return!this._isRenderedAsTable()},_modelViewHasWrapperLI:function(a){return this._isRenderedAsList()&&!a.$el.is("li")},_getVisibleItemEls:function(){var a=[];return a=this._getContainerEl().find("> [data-model-cid]:not(.not-visible)")},_charCodes:{upArrow:38,downArrow:40},_isBackboneCourierAvailable:function(){return!a.isUndefined(b.Courier)},_setupSortable:function(){var b=a.extend({axis:"y",distance:10,forcePlaceholderSize:!0,items:this._isRenderedAsTable()?"> tbody > tr:not(.not-sortable)":"> li:not(.not-sortable)",start:a.bind(this._sortStart,this),change:a.bind(this._sortChange,this),stop:a.bind(this._sortStop,this),receive:a.bind(this._receive,this),over:a.bind(this._over,this)},a.result(this,"sortableOptions"));this.$el=this.$el.sortable(b)},_sortStart:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid"));this._isBackboneCourierAvailable()?this.spawn("sortStart",{modelBeingSorted:c}):this.trigger("sortStart",c)},_sortChange:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid"));this._isBackboneCourierAvailable()?this.spawn("sortChange",{modelBeingSorted:c}):this.trigger("sortChange",c)},_sortStop:function(a,b){var c=this.collection.get(b.item.attr("data-model-cid")),d=this._getContainerEl(),e=d.children().index(b.item);-1==e&&c&&this.collection.remove(c),c&&(this._reorderCollectionBasedOnHTML(),this.updateDependentControls(),this._isBackboneCourierAvailable()?this.spawn("sortStop",{modelBeingSorted:c,newIndex:e}):this.trigger("sortStop",c,e))},_receive:function(a,b){var c=b.sender,d=c.data("view");if(d&&d.collection){var e=this._getContainerEl().children().index(b.item),f=d.collection.get(b.item.attr("data-model-cid"));d.collection.remove(f),this.collection.add(f,{at:e}),f.collection=this.collection,this.setSelectedModel(f)}},_over:function(a,b){this._getContainerEl().find("> var.empty-list-caption").hide()},_onKeydown:function(a){if(!this.processKeyEvents)return!0;var b=!1;if(1==this.getSelectedModels({by:"offset"}).length){var c=this.getSelectedModel({by:"offset"});a.which===this._charCodes.upArrow&&0!==c?(this.setSelectedModel(c-1,{by:"offset"}),b=!0):a.which===this._charCodes.downArrow&&c!==this.collection.length-1&&(this.setSelectedModel(c+1,{by:"offset"}),b=!0)}return!b},_listItem_onMousedown:function(b){var c=this._getClickedItemId(b);if(c){var d=this.collection.get(c);if(this._isBackboneCourierAvailable()){var e={clickedModel:d,metaKeyPressed:b.ctrlKey||b.metaKey};a.each(["preventDefault","stopPropagation","stopImmediatePropagation"],function(a){e[a]=function(){b[a]()}}),this.spawn("click",e)}else this.trigger("click",d)}if(this.selectable&&this.clickToSelect)if(c){if(a.isFunction(this.selectableModelsFilter)&&!this.selectableModelsFilter.call(this,this.collection.get(c)))return;if(this.selectMultiple&&b.shiftKey){var f=-1;this.selectedItems.length>0&&this.collection.find(function(b){return f++,a.contains(this.selectedItems,b.cid)},this);var g=-1;this.collection.find(function(a){return g++,a.cid==c},this);for(var h=-1==f?g:f,i=Math.min(g,h),j=Math.max(g,h),k=[],l=i;j>=l;l++)k.push(this.collection.at(l).cid);if(this.setSelectedModels(k,{by:"cid"}),document.selection&&document.selection.empty)document.selection.empty();else if(window.getSelection){var m=window.getSelection();m&&m.removeAllRanges&&m.removeAllRanges()}}else(this.selectMultiple||a.contains(this.selectedItems,c))&&(this.clickToToggle||b.metaKey||b.ctrlKey)?a.contains(this.selectedItems,c)?this.setSelectedModels(a.without(this.selectedItems,c),{by:"cid"}):this.setSelectedModels(a.union(this.selectedItems,[c]),{by:"cid"}):this.setSelectedModels([c],{by:"cid"})}else this.setSelectedModels([])},_listItem_onDoubleClick:function(a){var b=this._getClickedItemId(a);if(b){var c=this.collection.get(b);this._isBackboneCourierAvailable()?this.spawn("doubleClick",{clickedModel:c,metaKeyPressed:a.ctrlKey||a.metaKey}):this.trigger("doubleClick",c)}},_listBackground_onClick:function(a){this.selectable&&this.clickToSelect&&c(a.target).is(".collection-view")&&this.setSelectedModels([])}},{setDefaultModelViewConstructor:function(a){e=a}}),b.ViewOptions={},b.ViewOptions.add=function(b,c){a.isUndefined(c)&&(c="options"),b.setOptions=function(b){var e=this,f={},g={},h=a.result(this,c);if(!a.isUndefined(h)){var i=d(h);a.each(i,function(c,d){var h=c.required,i=c.defaultValue;if(h){if((!b||!a.contains(a.keys(b),d))&&a.isUndefined(e[d]))throw new Error('Required option "'+d+'" was not supplied.');if(b&&a.contains(a.keys(b),d)&&a.isUndefined(b[d]))throw new Error('Required option "'+d+'" can not be set to undefined.')}if(b&&d in b&&!a.isUndefined(b[d])){var j=e[d],k=b[d];a.isUndefined(j)||j===k||(g[d]=j,f[d]=k),e[d]=k}else a.isUndefined(e[d])&&(e[d]=i)})}a.keys(f).length>0&&(a.isFunction(e.onOptionsChanged)?e.onOptionsChanged(f,g):a.isFunction(e._onOptionsChanged)&&e._onOptionsChanged(f,g))},b.getOptions=function(){var b=a.result(this,c);if(a.isUndefined(b))return{};var e=d(b),f=a.keys(e);return a.pick(this,f)}},ChildViewContainer=function(a,b){var c=function(a){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),b.each(a,this.add,this)};b.extend(c.prototype,{add:function(a,b){var c=a.cid;this._views[c]=a,a.model&&(this._indexByModel[a.model.cid]=c),b&&(this._indexByCustom[b]=c),this._updateLength()},findByModel:function(a){return this.findByModelCid(a.cid)},findByModelCid:function(a){var b=this._indexByModel[a];return this.findByCid(b)},findByCustom:function(a){var b=this._indexByCustom[a];return this.findByCid(b)},findByIndex:function(a){return b.values(this._views)[a]},findByCid:function(a){return this._views[a]},findIndexByCid:function(a){var c=-1,d=b.find(this._views,function(b){return c++,b.model.cid==a?b:void 0});return d?c:-1},remove:function(a){var c=a.cid;a.model&&delete this._indexByModel[a.model.cid],b.any(this._indexByCustom,function(a,b){return a===c?(delete this._indexByCustom[b],!0):void 0},this),delete this._views[c],this._updateLength()},call:function(a){this.apply(a,b.tail(arguments))},apply:function(a,c){b.each(this._views,function(d){b.isFunction(d[a])&&d[a].apply(d,c||[])})},_updateLength:function(){this.length=b.size(this._views)}});var d=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck"];return b.each(d,function(a){c.prototype[a]=function(){var c=b.values(this._views),d=[c].concat(b.toArray(arguments));return b[a].apply(b,d)}}),c}(b,a),b.CollectionView});
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bb-collection-view",
3 | "version": "3.0.0",
4 | "description": "Easily render backbone.js collections with support for automatic selection of models in response to clicks, reordering models via drag and drop, and more.",
5 | "main": "dist/backbone.collectionView.js",
6 | "devDependencies": {
7 | "grunt": "^0.4.5",
8 | "grunt-contrib-compress": "^0.13.0",
9 | "grunt-contrib-concat": "^0.5.1",
10 | "grunt-contrib-jshint": "^0.11.2",
11 | "grunt-contrib-uglify": "^0.9.1"
12 | },
13 | "keywords": [
14 | "backbone",
15 | "collection",
16 | "view"
17 | ],
18 | "style": "base.css",
19 | "scripts": {
20 | "test": "open test/index.html"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/rotundasoftware/backbone.collectionView.git"
25 | },
26 | "author": {
27 | "name": "Rotunda Software"
28 | },
29 | "license": "MIT",
30 | "readmeFilename": "README.md",
31 | "dependencies": {
32 | "backbone": "^1.2.3",
33 | "jquery": "^3.2.1",
34 | "jquery-ui": "^1.10.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/test/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rotundasoftware/backbone.collectionView/63109a35255b91ca220c2b3988c964000f48477b/test/.DS_Store
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Backbone CollectionView
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
25 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/lib/backbone.babysitter.js:
--------------------------------------------------------------------------------
1 | // Backbone.BabySitter
2 | // -------------------
3 | // v0.0.5
4 | //
5 | // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
6 | // Distributed under MIT license
7 | //
8 | // http://github.com/babysitterjs/backbone.babysitter
9 |
10 | // Backbone.ChildViewContainer
11 | // ---------------------------
12 | //
13 | // Provide a container to store, retrieve and
14 | // shut down child views.
15 |
16 | Backbone.ChildViewContainer = (function(Backbone, _){
17 |
18 | // Container Constructor
19 | // ---------------------
20 |
21 | var Container = function(initialViews){
22 | this._views = {};
23 | this._indexByModel = {};
24 | this._indexByCollection = {};
25 | this._indexByCustom = {};
26 | this._updateLength();
27 |
28 | this._addInitialViews(initialViews);
29 | };
30 |
31 | // Container Methods
32 | // -----------------
33 |
34 | _.extend(Container.prototype, {
35 |
36 | // Add a view to this container. Stores the view
37 | // by `cid` and makes it searchable by the model
38 | // and/or collection of the view. Optionally specify
39 | // a custom key to store an retrieve the view.
40 | add: function(view, customIndex){
41 | var viewCid = view.cid;
42 |
43 | // store the view
44 | this._views[viewCid] = view;
45 |
46 | // index it by model
47 | if (view.model){
48 | this._indexByModel[view.model.cid] = viewCid;
49 | }
50 |
51 | // index it by collection
52 | if (view.collection){
53 | this._indexByCollection[view.collection.cid] = viewCid;
54 | }
55 |
56 | // index by custom
57 | if (customIndex){
58 | this._indexByCustom[customIndex] = viewCid;
59 | }
60 |
61 | this._updateLength();
62 | },
63 |
64 | // Find a view by the model that was attached to
65 | // it. Uses the model's `cid` to find it, and
66 | // retrieves the view by it's `cid` from the result
67 | findByModel: function(model){
68 | var viewCid = this._indexByModel[model.cid];
69 | return this.findByCid(viewCid);
70 | },
71 |
72 | // Find a view by the collection that was attached to
73 | // it. Uses the collection's `cid` to find it, and
74 | // retrieves the view by it's `cid` from the result
75 | findByCollection: function(col){
76 | var viewCid = this._indexByCollection[col.cid];
77 | return this.findByCid(viewCid);
78 | },
79 |
80 | // Find a view by a custom indexer.
81 | findByCustom: function(index){
82 | var viewCid = this._indexByCustom[index];
83 | return this.findByCid(viewCid);
84 | },
85 |
86 | // Find by index. This is not guaranteed to be a
87 | // stable index.
88 | findByIndex: function(index){
89 | return _.values(this._views)[index];
90 | },
91 |
92 | // retrieve a view by it's `cid` directly
93 | findByCid: function(cid){
94 | return this._views[cid];
95 | },
96 |
97 | // Remove a view
98 | remove: function(view){
99 | var viewCid = view.cid;
100 |
101 | // delete model index
102 | if (view.model){
103 | delete this._indexByModel[view.model.cid];
104 | }
105 |
106 | // delete collection index
107 | if (view.collection){
108 | delete this._indexByCollection[view.collection.cid];
109 | }
110 |
111 | // delete custom index
112 | var cust;
113 |
114 | for (var key in this._indexByCustom){
115 | if (this._indexByCustom.hasOwnProperty(key)){
116 | if (this._indexByCustom[key] === viewCid){
117 | cust = key;
118 | break;
119 | }
120 | }
121 | }
122 |
123 | if (cust){
124 | delete this._indexByCustom[cust];
125 | }
126 |
127 | // remove the view from the container
128 | delete this._views[viewCid];
129 |
130 | // update the length
131 | this._updateLength();
132 | },
133 |
134 | // Call a method on every view in the container,
135 | // passing parameters to the call method one at a
136 | // time, like `function.call`.
137 | call: function(method, args){
138 | args = Array.prototype.slice.call(arguments, 1);
139 | this.apply(method, args);
140 | },
141 |
142 | // Apply a method on every view in the container,
143 | // passing parameters to the call method one at a
144 | // time, like `function.apply`.
145 | apply: function(method, args){
146 | var view;
147 |
148 | // fix for IE < 9
149 | args = args || [];
150 |
151 | _.each(this._views, function(view, key){
152 | if (_.isFunction(view[method])){
153 | view[method].apply(view, args);
154 | }
155 | });
156 |
157 | },
158 |
159 | // Update the `.length` attribute on this container
160 | _updateLength: function(){
161 | this.length = _.size(this._views);
162 | },
163 |
164 | // set up an initial list of views
165 | _addInitialViews: function(views){
166 | if (!views){ return; }
167 |
168 | var view, i,
169 | length = views.length;
170 |
171 | for (i=0; i li:last-child {
206 | border-radius: 0 0 5px 5px;
207 | -moz-border-radius: 0 0 5px 5px;
208 | -webkit-border-bottom-right-radius: 5px;
209 | -webkit-border-bottom-left-radius: 5px;
210 | }
211 |
212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
213 | #qunit-tests .fail .test-name,
214 | #qunit-tests .fail .module-name { color: #000000; }
215 |
216 | #qunit-tests .fail .test-actual { color: #EE5757; }
217 | #qunit-tests .fail .test-expected { color: green; }
218 |
219 | #qunit-banner.qunit-fail { background-color: #EE5757; }
220 |
221 |
222 | /** Result */
223 |
224 | #qunit-testresult {
225 | padding: 0.5em 0.5em 0.5em 2.5em;
226 |
227 | color: #2b81af;
228 | background-color: #D2E0E6;
229 |
230 | border-bottom: 1px solid white;
231 | }
232 | #qunit-testresult .module-name {
233 | font-weight: bold;
234 | }
235 |
236 | /** Fixture */
237 |
238 | #qunit-fixture {
239 | position: absolute;
240 | top: -10000px;
241 | left: -10000px;
242 | width: 1000px;
243 | height: 1000px;
244 | }
245 |
--------------------------------------------------------------------------------
/test/lib/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.5.2
2 | // http://underscorejs.org
3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 |
6 | (function() {
7 |
8 | // Baseline setup
9 | // --------------
10 |
11 | // Establish the root object, `window` in the browser, or `exports` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Establish the object that gets returned to break out of a loop iteration.
18 | var breaker = {};
19 |
20 | // Save bytes in the minified (but not gzipped) version:
21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22 |
23 | // Create quick reference variables for speed access to core prototypes.
24 | var
25 | push = ArrayProto.push,
26 | slice = ArrayProto.slice,
27 | concat = ArrayProto.concat,
28 | toString = ObjProto.toString,
29 | hasOwnProperty = ObjProto.hasOwnProperty;
30 |
31 | // All **ECMAScript 5** native function implementations that we hope to use
32 | // are declared here.
33 | var
34 | nativeForEach = ArrayProto.forEach,
35 | nativeMap = ArrayProto.map,
36 | nativeReduce = ArrayProto.reduce,
37 | nativeReduceRight = ArrayProto.reduceRight,
38 | nativeFilter = ArrayProto.filter,
39 | nativeEvery = ArrayProto.every,
40 | nativeSome = ArrayProto.some,
41 | nativeIndexOf = ArrayProto.indexOf,
42 | nativeLastIndexOf = ArrayProto.lastIndexOf,
43 | nativeIsArray = Array.isArray,
44 | nativeKeys = Object.keys,
45 | nativeBind = FuncProto.bind;
46 |
47 | // Create a safe reference to the Underscore object for use below.
48 | var _ = function(obj) {
49 | if (obj instanceof _) return obj;
50 | if (!(this instanceof _)) return new _(obj);
51 | this._wrapped = obj;
52 | };
53 |
54 | // Export the Underscore object for **Node.js**, with
55 | // backwards-compatibility for the old `require()` API. If we're in
56 | // the browser, add `_` as a global object via a string identifier,
57 | // for Closure Compiler "advanced" mode.
58 | if (typeof exports !== 'undefined') {
59 | if (typeof module !== 'undefined' && module.exports) {
60 | exports = module.exports = _;
61 | }
62 | exports._ = _;
63 | } else {
64 | root._ = _;
65 | }
66 |
67 | // Current version.
68 | _.VERSION = '1.5.2';
69 |
70 | // Collection Functions
71 | // --------------------
72 |
73 | // The cornerstone, an `each` implementation, aka `forEach`.
74 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
75 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
76 | var each = _.each = _.forEach = function(obj, iterator, context) {
77 | if (obj == null) return;
78 | if (nativeForEach && obj.forEach === nativeForEach) {
79 | obj.forEach(iterator, context);
80 | } else if (obj.length === +obj.length) {
81 | for (var i = 0, length = obj.length; i < length; i++) {
82 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
83 | }
84 | } else {
85 | var keys = _.keys(obj);
86 | for (var i = 0, length = keys.length; i < length; i++) {
87 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
88 | }
89 | }
90 | };
91 |
92 | // Return the results of applying the iterator to each element.
93 | // Delegates to **ECMAScript 5**'s native `map` if available.
94 | _.map = _.collect = function(obj, iterator, context) {
95 | var results = [];
96 | if (obj == null) return results;
97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
98 | each(obj, function(value, index, list) {
99 | results.push(iterator.call(context, value, index, list));
100 | });
101 | return results;
102 | };
103 |
104 | var reduceError = 'Reduce of empty array with no initial value';
105 |
106 | // **Reduce** builds up a single result from a list of values, aka `inject`,
107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
109 | var initial = arguments.length > 2;
110 | if (obj == null) obj = [];
111 | if (nativeReduce && obj.reduce === nativeReduce) {
112 | if (context) iterator = _.bind(iterator, context);
113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
114 | }
115 | each(obj, function(value, index, list) {
116 | if (!initial) {
117 | memo = value;
118 | initial = true;
119 | } else {
120 | memo = iterator.call(context, memo, value, index, list);
121 | }
122 | });
123 | if (!initial) throw new TypeError(reduceError);
124 | return memo;
125 | };
126 |
127 | // The right-associative version of reduce, also known as `foldr`.
128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
130 | var initial = arguments.length > 2;
131 | if (obj == null) obj = [];
132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
133 | if (context) iterator = _.bind(iterator, context);
134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
135 | }
136 | var length = obj.length;
137 | if (length !== +length) {
138 | var keys = _.keys(obj);
139 | length = keys.length;
140 | }
141 | each(obj, function(value, index, list) {
142 | index = keys ? keys[--length] : --length;
143 | if (!initial) {
144 | memo = obj[index];
145 | initial = true;
146 | } else {
147 | memo = iterator.call(context, memo, obj[index], index, list);
148 | }
149 | });
150 | if (!initial) throw new TypeError(reduceError);
151 | return memo;
152 | };
153 |
154 | // Return the first value which passes a truth test. Aliased as `detect`.
155 | _.find = _.detect = function(obj, iterator, context) {
156 | var result;
157 | any(obj, function(value, index, list) {
158 | if (iterator.call(context, value, index, list)) {
159 | result = value;
160 | return true;
161 | }
162 | });
163 | return result;
164 | };
165 |
166 | // Return all the elements that pass a truth test.
167 | // Delegates to **ECMAScript 5**'s native `filter` if available.
168 | // Aliased as `select`.
169 | _.filter = _.select = function(obj, iterator, context) {
170 | var results = [];
171 | if (obj == null) return results;
172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
173 | each(obj, function(value, index, list) {
174 | if (iterator.call(context, value, index, list)) results.push(value);
175 | });
176 | return results;
177 | };
178 |
179 | // Return all the elements for which a truth test fails.
180 | _.reject = function(obj, iterator, context) {
181 | return _.filter(obj, function(value, index, list) {
182 | return !iterator.call(context, value, index, list);
183 | }, context);
184 | };
185 |
186 | // Determine whether all of the elements match a truth test.
187 | // Delegates to **ECMAScript 5**'s native `every` if available.
188 | // Aliased as `all`.
189 | _.every = _.all = function(obj, iterator, context) {
190 | iterator || (iterator = _.identity);
191 | var result = true;
192 | if (obj == null) return result;
193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
194 | each(obj, function(value, index, list) {
195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
196 | });
197 | return !!result;
198 | };
199 |
200 | // Determine if at least one element in the object matches a truth test.
201 | // Delegates to **ECMAScript 5**'s native `some` if available.
202 | // Aliased as `any`.
203 | var any = _.some = _.any = function(obj, iterator, context) {
204 | iterator || (iterator = _.identity);
205 | var result = false;
206 | if (obj == null) return result;
207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
208 | each(obj, function(value, index, list) {
209 | if (result || (result = iterator.call(context, value, index, list))) return breaker;
210 | });
211 | return !!result;
212 | };
213 |
214 | // Determine if the array or object contains a given value (using `===`).
215 | // Aliased as `include`.
216 | _.contains = _.include = function(obj, target) {
217 | if (obj == null) return false;
218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
219 | return any(obj, function(value) {
220 | return value === target;
221 | });
222 | };
223 |
224 | // Invoke a method (with arguments) on every item in a collection.
225 | _.invoke = function(obj, method) {
226 | var args = slice.call(arguments, 2);
227 | var isFunc = _.isFunction(method);
228 | return _.map(obj, function(value) {
229 | return (isFunc ? method : value[method]).apply(value, args);
230 | });
231 | };
232 |
233 | // Convenience version of a common use case of `map`: fetching a property.
234 | _.pluck = function(obj, key) {
235 | return _.map(obj, function(value){ return value[key]; });
236 | };
237 |
238 | // Convenience version of a common use case of `filter`: selecting only objects
239 | // containing specific `key:value` pairs.
240 | _.where = function(obj, attrs, first) {
241 | if (_.isEmpty(attrs)) return first ? void 0 : [];
242 | return _[first ? 'find' : 'filter'](obj, function(value) {
243 | for (var key in attrs) {
244 | if (attrs[key] !== value[key]) return false;
245 | }
246 | return true;
247 | });
248 | };
249 |
250 | // Convenience version of a common use case of `find`: getting the first object
251 | // containing specific `key:value` pairs.
252 | _.findWhere = function(obj, attrs) {
253 | return _.where(obj, attrs, true);
254 | };
255 |
256 | // Return the maximum element or (element-based computation).
257 | // Can't optimize arrays of integers longer than 65,535 elements.
258 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
259 | _.max = function(obj, iterator, context) {
260 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
261 | return Math.max.apply(Math, obj);
262 | }
263 | if (!iterator && _.isEmpty(obj)) return -Infinity;
264 | var result = {computed : -Infinity, value: -Infinity};
265 | each(obj, function(value, index, list) {
266 | var computed = iterator ? iterator.call(context, value, index, list) : value;
267 | computed > result.computed && (result = {value : value, computed : computed});
268 | });
269 | return result.value;
270 | };
271 |
272 | // Return the minimum element (or element-based computation).
273 | _.min = function(obj, iterator, context) {
274 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
275 | return Math.min.apply(Math, obj);
276 | }
277 | if (!iterator && _.isEmpty(obj)) return Infinity;
278 | var result = {computed : Infinity, value: Infinity};
279 | each(obj, function(value, index, list) {
280 | var computed = iterator ? iterator.call(context, value, index, list) : value;
281 | computed < result.computed && (result = {value : value, computed : computed});
282 | });
283 | return result.value;
284 | };
285 |
286 | // Shuffle an array, using the modern version of the
287 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
288 | _.shuffle = function(obj) {
289 | var rand;
290 | var index = 0;
291 | var shuffled = [];
292 | each(obj, function(value) {
293 | rand = _.random(index++);
294 | shuffled[index - 1] = shuffled[rand];
295 | shuffled[rand] = value;
296 | });
297 | return shuffled;
298 | };
299 |
300 | // Sample **n** random values from an array.
301 | // If **n** is not specified, returns a single random element from the array.
302 | // The internal `guard` argument allows it to work with `map`.
303 | _.sample = function(obj, n, guard) {
304 | if (arguments.length < 2 || guard) {
305 | return obj[_.random(obj.length - 1)];
306 | }
307 | return _.shuffle(obj).slice(0, Math.max(0, n));
308 | };
309 |
310 | // An internal function to generate lookup iterators.
311 | var lookupIterator = function(value) {
312 | return _.isFunction(value) ? value : function(obj){ return obj[value]; };
313 | };
314 |
315 | // Sort the object's values by a criterion produced by an iterator.
316 | _.sortBy = function(obj, value, context) {
317 | var iterator = lookupIterator(value);
318 | return _.pluck(_.map(obj, function(value, index, list) {
319 | return {
320 | value: value,
321 | index: index,
322 | criteria: iterator.call(context, value, index, list)
323 | };
324 | }).sort(function(left, right) {
325 | var a = left.criteria;
326 | var b = right.criteria;
327 | if (a !== b) {
328 | if (a > b || a === void 0) return 1;
329 | if (a < b || b === void 0) return -1;
330 | }
331 | return left.index - right.index;
332 | }), 'value');
333 | };
334 |
335 | // An internal function used for aggregate "group by" operations.
336 | var group = function(behavior) {
337 | return function(obj, value, context) {
338 | var result = {};
339 | var iterator = value == null ? _.identity : lookupIterator(value);
340 | each(obj, function(value, index) {
341 | var key = iterator.call(context, value, index, obj);
342 | behavior(result, key, value);
343 | });
344 | return result;
345 | };
346 | };
347 |
348 | // Groups the object's values by a criterion. Pass either a string attribute
349 | // to group by, or a function that returns the criterion.
350 | _.groupBy = group(function(result, key, value) {
351 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
352 | });
353 |
354 | // Indexes the object's values by a criterion, similar to `groupBy`, but for
355 | // when you know that your index values will be unique.
356 | _.indexBy = group(function(result, key, value) {
357 | result[key] = value;
358 | });
359 |
360 | // Counts instances of an object that group by a certain criterion. Pass
361 | // either a string attribute to count by, or a function that returns the
362 | // criterion.
363 | _.countBy = group(function(result, key) {
364 | _.has(result, key) ? result[key]++ : result[key] = 1;
365 | });
366 |
367 | // Use a comparator function to figure out the smallest index at which
368 | // an object should be inserted so as to maintain order. Uses binary search.
369 | _.sortedIndex = function(array, obj, iterator, context) {
370 | iterator = iterator == null ? _.identity : lookupIterator(iterator);
371 | var value = iterator.call(context, obj);
372 | var low = 0, high = array.length;
373 | while (low < high) {
374 | var mid = (low + high) >>> 1;
375 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
376 | }
377 | return low;
378 | };
379 |
380 | // Safely create a real, live array from anything iterable.
381 | _.toArray = function(obj) {
382 | if (!obj) return [];
383 | if (_.isArray(obj)) return slice.call(obj);
384 | if (obj.length === +obj.length) return _.map(obj, _.identity);
385 | return _.values(obj);
386 | };
387 |
388 | // Return the number of elements in an object.
389 | _.size = function(obj) {
390 | if (obj == null) return 0;
391 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
392 | };
393 |
394 | // Array Functions
395 | // ---------------
396 |
397 | // Get the first element of an array. Passing **n** will return the first N
398 | // values in the array. Aliased as `head` and `take`. The **guard** check
399 | // allows it to work with `_.map`.
400 | _.first = _.head = _.take = function(array, n, guard) {
401 | if (array == null) return void 0;
402 | return (n == null) || guard ? array[0] : slice.call(array, 0, n);
403 | };
404 |
405 | // Returns everything but the last entry of the array. Especially useful on
406 | // the arguments object. Passing **n** will return all the values in
407 | // the array, excluding the last N. The **guard** check allows it to work with
408 | // `_.map`.
409 | _.initial = function(array, n, guard) {
410 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
411 | };
412 |
413 | // Get the last element of an array. Passing **n** will return the last N
414 | // values in the array. The **guard** check allows it to work with `_.map`.
415 | _.last = function(array, n, guard) {
416 | if (array == null) return void 0;
417 | if ((n == null) || guard) {
418 | return array[array.length - 1];
419 | } else {
420 | return slice.call(array, Math.max(array.length - n, 0));
421 | }
422 | };
423 |
424 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
425 | // Especially useful on the arguments object. Passing an **n** will return
426 | // the rest N values in the array. The **guard**
427 | // check allows it to work with `_.map`.
428 | _.rest = _.tail = _.drop = function(array, n, guard) {
429 | return slice.call(array, (n == null) || guard ? 1 : n);
430 | };
431 |
432 | // Trim out all falsy values from an array.
433 | _.compact = function(array) {
434 | return _.filter(array, _.identity);
435 | };
436 |
437 | // Internal implementation of a recursive `flatten` function.
438 | var flatten = function(input, shallow, output) {
439 | if (shallow && _.every(input, _.isArray)) {
440 | return concat.apply(output, input);
441 | }
442 | each(input, function(value) {
443 | if (_.isArray(value) || _.isArguments(value)) {
444 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
445 | } else {
446 | output.push(value);
447 | }
448 | });
449 | return output;
450 | };
451 |
452 | // Flatten out an array, either recursively (by default), or just one level.
453 | _.flatten = function(array, shallow) {
454 | return flatten(array, shallow, []);
455 | };
456 |
457 | // Return a version of the array that does not contain the specified value(s).
458 | _.without = function(array) {
459 | return _.difference(array, slice.call(arguments, 1));
460 | };
461 |
462 | // Produce a duplicate-free version of the array. If the array has already
463 | // been sorted, you have the option of using a faster algorithm.
464 | // Aliased as `unique`.
465 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
466 | if (_.isFunction(isSorted)) {
467 | context = iterator;
468 | iterator = isSorted;
469 | isSorted = false;
470 | }
471 | var initial = iterator ? _.map(array, iterator, context) : array;
472 | var results = [];
473 | var seen = [];
474 | each(initial, function(value, index) {
475 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
476 | seen.push(value);
477 | results.push(array[index]);
478 | }
479 | });
480 | return results;
481 | };
482 |
483 | // Produce an array that contains the union: each distinct element from all of
484 | // the passed-in arrays.
485 | _.union = function() {
486 | return _.uniq(_.flatten(arguments, true));
487 | };
488 |
489 | // Produce an array that contains every item shared between all the
490 | // passed-in arrays.
491 | _.intersection = function(array) {
492 | var rest = slice.call(arguments, 1);
493 | return _.filter(_.uniq(array), function(item) {
494 | return _.every(rest, function(other) {
495 | return _.indexOf(other, item) >= 0;
496 | });
497 | });
498 | };
499 |
500 | // Take the difference between one array and a number of other arrays.
501 | // Only the elements present in just the first array will remain.
502 | _.difference = function(array) {
503 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
504 | return _.filter(array, function(value){ return !_.contains(rest, value); });
505 | };
506 |
507 | // Zip together multiple lists into a single array -- elements that share
508 | // an index go together.
509 | _.zip = function() {
510 | var length = _.max(_.pluck(arguments, "length").concat(0));
511 | var results = new Array(length);
512 | for (var i = 0; i < length; i++) {
513 | results[i] = _.pluck(arguments, '' + i);
514 | }
515 | return results;
516 | };
517 |
518 | // Converts lists into objects. Pass either a single array of `[key, value]`
519 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
520 | // the corresponding values.
521 | _.object = function(list, values) {
522 | if (list == null) return {};
523 | var result = {};
524 | for (var i = 0, length = list.length; i < length; i++) {
525 | if (values) {
526 | result[list[i]] = values[i];
527 | } else {
528 | result[list[i][0]] = list[i][1];
529 | }
530 | }
531 | return result;
532 | };
533 |
534 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
535 | // we need this function. Return the position of the first occurrence of an
536 | // item in an array, or -1 if the item is not included in the array.
537 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
538 | // If the array is large and already in sort order, pass `true`
539 | // for **isSorted** to use binary search.
540 | _.indexOf = function(array, item, isSorted) {
541 | if (array == null) return -1;
542 | var i = 0, length = array.length;
543 | if (isSorted) {
544 | if (typeof isSorted == 'number') {
545 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
546 | } else {
547 | i = _.sortedIndex(array, item);
548 | return array[i] === item ? i : -1;
549 | }
550 | }
551 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
552 | for (; i < length; i++) if (array[i] === item) return i;
553 | return -1;
554 | };
555 |
556 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
557 | _.lastIndexOf = function(array, item, from) {
558 | if (array == null) return -1;
559 | var hasIndex = from != null;
560 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
561 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
562 | }
563 | var i = (hasIndex ? from : array.length);
564 | while (i--) if (array[i] === item) return i;
565 | return -1;
566 | };
567 |
568 | // Generate an integer Array containing an arithmetic progression. A port of
569 | // the native Python `range()` function. See
570 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
571 | _.range = function(start, stop, step) {
572 | if (arguments.length <= 1) {
573 | stop = start || 0;
574 | start = 0;
575 | }
576 | step = arguments[2] || 1;
577 |
578 | var length = Math.max(Math.ceil((stop - start) / step), 0);
579 | var idx = 0;
580 | var range = new Array(length);
581 |
582 | while(idx < length) {
583 | range[idx++] = start;
584 | start += step;
585 | }
586 |
587 | return range;
588 | };
589 |
590 | // Function (ahem) Functions
591 | // ------------------
592 |
593 | // Reusable constructor function for prototype setting.
594 | var ctor = function(){};
595 |
596 | // Create a function bound to a given object (assigning `this`, and arguments,
597 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
598 | // available.
599 | _.bind = function(func, context) {
600 | var args, bound;
601 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
602 | if (!_.isFunction(func)) throw new TypeError;
603 | args = slice.call(arguments, 2);
604 | return bound = function() {
605 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
606 | ctor.prototype = func.prototype;
607 | var self = new ctor;
608 | ctor.prototype = null;
609 | var result = func.apply(self, args.concat(slice.call(arguments)));
610 | if (Object(result) === result) return result;
611 | return self;
612 | };
613 | };
614 |
615 | // Partially apply a function by creating a version that has had some of its
616 | // arguments pre-filled, without changing its dynamic `this` context.
617 | _.partial = function(func) {
618 | var args = slice.call(arguments, 1);
619 | return function() {
620 | return func.apply(this, args.concat(slice.call(arguments)));
621 | };
622 | };
623 |
624 | // Bind all of an object's methods to that object. Useful for ensuring that
625 | // all callbacks defined on an object belong to it.
626 | _.bindAll = function(obj) {
627 | var funcs = slice.call(arguments, 1);
628 | if (funcs.length === 0) throw new Error("bindAll must be passed function names");
629 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
630 | return obj;
631 | };
632 |
633 | // Memoize an expensive function by storing its results.
634 | _.memoize = function(func, hasher) {
635 | var memo = {};
636 | hasher || (hasher = _.identity);
637 | return function() {
638 | var key = hasher.apply(this, arguments);
639 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
640 | };
641 | };
642 |
643 | // Delays a function for the given number of milliseconds, and then calls
644 | // it with the arguments supplied.
645 | _.delay = function(func, wait) {
646 | var args = slice.call(arguments, 2);
647 | return setTimeout(function(){ return func.apply(null, args); }, wait);
648 | };
649 |
650 | // Defers a function, scheduling it to run after the current call stack has
651 | // cleared.
652 | _.defer = function(func) {
653 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
654 | };
655 |
656 | // Returns a function, that, when invoked, will only be triggered at most once
657 | // during a given window of time. Normally, the throttled function will run
658 | // as much as it can, without ever going more than once per `wait` duration;
659 | // but if you'd like to disable the execution on the leading edge, pass
660 | // `{leading: false}`. To disable execution on the trailing edge, ditto.
661 | _.throttle = function(func, wait, options) {
662 | var context, args, result;
663 | var timeout = null;
664 | var previous = 0;
665 | options || (options = {});
666 | var later = function() {
667 | previous = options.leading === false ? 0 : new Date;
668 | timeout = null;
669 | result = func.apply(context, args);
670 | };
671 | return function() {
672 | var now = new Date;
673 | if (!previous && options.leading === false) previous = now;
674 | var remaining = wait - (now - previous);
675 | context = this;
676 | args = arguments;
677 | if (remaining <= 0) {
678 | clearTimeout(timeout);
679 | timeout = null;
680 | previous = now;
681 | result = func.apply(context, args);
682 | } else if (!timeout && options.trailing !== false) {
683 | timeout = setTimeout(later, remaining);
684 | }
685 | return result;
686 | };
687 | };
688 |
689 | // Returns a function, that, as long as it continues to be invoked, will not
690 | // be triggered. The function will be called after it stops being called for
691 | // N milliseconds. If `immediate` is passed, trigger the function on the
692 | // leading edge, instead of the trailing.
693 | _.debounce = function(func, wait, immediate) {
694 | var timeout, args, context, timestamp, result;
695 | return function() {
696 | context = this;
697 | args = arguments;
698 | timestamp = new Date();
699 | var later = function() {
700 | var last = (new Date()) - timestamp;
701 | if (last < wait) {
702 | timeout = setTimeout(later, wait - last);
703 | } else {
704 | timeout = null;
705 | if (!immediate) result = func.apply(context, args);
706 | }
707 | };
708 | var callNow = immediate && !timeout;
709 | if (!timeout) {
710 | timeout = setTimeout(later, wait);
711 | }
712 | if (callNow) result = func.apply(context, args);
713 | return result;
714 | };
715 | };
716 |
717 | // Returns a function that will be executed at most one time, no matter how
718 | // often you call it. Useful for lazy initialization.
719 | _.once = function(func) {
720 | var ran = false, memo;
721 | return function() {
722 | if (ran) return memo;
723 | ran = true;
724 | memo = func.apply(this, arguments);
725 | func = null;
726 | return memo;
727 | };
728 | };
729 |
730 | // Returns the first function passed as an argument to the second,
731 | // allowing you to adjust arguments, run code before and after, and
732 | // conditionally execute the original function.
733 | _.wrap = function(func, wrapper) {
734 | return function() {
735 | var args = [func];
736 | push.apply(args, arguments);
737 | return wrapper.apply(this, args);
738 | };
739 | };
740 |
741 | // Returns a function that is the composition of a list of functions, each
742 | // consuming the return value of the function that follows.
743 | _.compose = function() {
744 | var funcs = arguments;
745 | return function() {
746 | var args = arguments;
747 | for (var i = funcs.length - 1; i >= 0; i--) {
748 | args = [funcs[i].apply(this, args)];
749 | }
750 | return args[0];
751 | };
752 | };
753 |
754 | // Returns a function that will only be executed after being called N times.
755 | _.after = function(times, func) {
756 | return function() {
757 | if (--times < 1) {
758 | return func.apply(this, arguments);
759 | }
760 | };
761 | };
762 |
763 | // Object Functions
764 | // ----------------
765 |
766 | // Retrieve the names of an object's properties.
767 | // Delegates to **ECMAScript 5**'s native `Object.keys`
768 | _.keys = nativeKeys || function(obj) {
769 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
770 | var keys = [];
771 | for (var key in obj) if (_.has(obj, key)) keys.push(key);
772 | return keys;
773 | };
774 |
775 | // Retrieve the values of an object's properties.
776 | _.values = function(obj) {
777 | var keys = _.keys(obj);
778 | var length = keys.length;
779 | var values = new Array(length);
780 | for (var i = 0; i < length; i++) {
781 | values[i] = obj[keys[i]];
782 | }
783 | return values;
784 | };
785 |
786 | // Convert an object into a list of `[key, value]` pairs.
787 | _.pairs = function(obj) {
788 | var keys = _.keys(obj);
789 | var length = keys.length;
790 | var pairs = new Array(length);
791 | for (var i = 0; i < length; i++) {
792 | pairs[i] = [keys[i], obj[keys[i]]];
793 | }
794 | return pairs;
795 | };
796 |
797 | // Invert the keys and values of an object. The values must be serializable.
798 | _.invert = function(obj) {
799 | var result = {};
800 | var keys = _.keys(obj);
801 | for (var i = 0, length = keys.length; i < length; i++) {
802 | result[obj[keys[i]]] = keys[i];
803 | }
804 | return result;
805 | };
806 |
807 | // Return a sorted list of the function names available on the object.
808 | // Aliased as `methods`
809 | _.functions = _.methods = function(obj) {
810 | var names = [];
811 | for (var key in obj) {
812 | if (_.isFunction(obj[key])) names.push(key);
813 | }
814 | return names.sort();
815 | };
816 |
817 | // Extend a given object with all the properties in passed-in object(s).
818 | _.extend = function(obj) {
819 | each(slice.call(arguments, 1), function(source) {
820 | if (source) {
821 | for (var prop in source) {
822 | obj[prop] = source[prop];
823 | }
824 | }
825 | });
826 | return obj;
827 | };
828 |
829 | // Return a copy of the object only containing the whitelisted properties.
830 | _.pick = function(obj) {
831 | var copy = {};
832 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
833 | each(keys, function(key) {
834 | if (key in obj) copy[key] = obj[key];
835 | });
836 | return copy;
837 | };
838 |
839 | // Return a copy of the object without the blacklisted properties.
840 | _.omit = function(obj) {
841 | var copy = {};
842 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
843 | for (var key in obj) {
844 | if (!_.contains(keys, key)) copy[key] = obj[key];
845 | }
846 | return copy;
847 | };
848 |
849 | // Fill in a given object with default properties.
850 | _.defaults = function(obj) {
851 | each(slice.call(arguments, 1), function(source) {
852 | if (source) {
853 | for (var prop in source) {
854 | if (obj[prop] === void 0) obj[prop] = source[prop];
855 | }
856 | }
857 | });
858 | return obj;
859 | };
860 |
861 | // Create a (shallow-cloned) duplicate of an object.
862 | _.clone = function(obj) {
863 | if (!_.isObject(obj)) return obj;
864 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
865 | };
866 |
867 | // Invokes interceptor with the obj, and then returns obj.
868 | // The primary purpose of this method is to "tap into" a method chain, in
869 | // order to perform operations on intermediate results within the chain.
870 | _.tap = function(obj, interceptor) {
871 | interceptor(obj);
872 | return obj;
873 | };
874 |
875 | // Internal recursive comparison function for `isEqual`.
876 | var eq = function(a, b, aStack, bStack) {
877 | // Identical objects are equal. `0 === -0`, but they aren't identical.
878 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
879 | if (a === b) return a !== 0 || 1 / a == 1 / b;
880 | // A strict comparison is necessary because `null == undefined`.
881 | if (a == null || b == null) return a === b;
882 | // Unwrap any wrapped objects.
883 | if (a instanceof _) a = a._wrapped;
884 | if (b instanceof _) b = b._wrapped;
885 | // Compare `[[Class]]` names.
886 | var className = toString.call(a);
887 | if (className != toString.call(b)) return false;
888 | switch (className) {
889 | // Strings, numbers, dates, and booleans are compared by value.
890 | case '[object String]':
891 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
892 | // equivalent to `new String("5")`.
893 | return a == String(b);
894 | case '[object Number]':
895 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
896 | // other numeric values.
897 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
898 | case '[object Date]':
899 | case '[object Boolean]':
900 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
901 | // millisecond representations. Note that invalid dates with millisecond representations
902 | // of `NaN` are not equivalent.
903 | return +a == +b;
904 | // RegExps are compared by their source patterns and flags.
905 | case '[object RegExp]':
906 | return a.source == b.source &&
907 | a.global == b.global &&
908 | a.multiline == b.multiline &&
909 | a.ignoreCase == b.ignoreCase;
910 | }
911 | if (typeof a != 'object' || typeof b != 'object') return false;
912 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
913 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
914 | var length = aStack.length;
915 | while (length--) {
916 | // Linear search. Performance is inversely proportional to the number of
917 | // unique nested structures.
918 | if (aStack[length] == a) return bStack[length] == b;
919 | }
920 | // Objects with different constructors are not equivalent, but `Object`s
921 | // from different frames are.
922 | var aCtor = a.constructor, bCtor = b.constructor;
923 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
924 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
925 | return false;
926 | }
927 | // Add the first object to the stack of traversed objects.
928 | aStack.push(a);
929 | bStack.push(b);
930 | var size = 0, result = true;
931 | // Recursively compare objects and arrays.
932 | if (className == '[object Array]') {
933 | // Compare array lengths to determine if a deep comparison is necessary.
934 | size = a.length;
935 | result = size == b.length;
936 | if (result) {
937 | // Deep compare the contents, ignoring non-numeric properties.
938 | while (size--) {
939 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
940 | }
941 | }
942 | } else {
943 | // Deep compare objects.
944 | for (var key in a) {
945 | if (_.has(a, key)) {
946 | // Count the expected number of properties.
947 | size++;
948 | // Deep compare each member.
949 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
950 | }
951 | }
952 | // Ensure that both objects contain the same number of properties.
953 | if (result) {
954 | for (key in b) {
955 | if (_.has(b, key) && !(size--)) break;
956 | }
957 | result = !size;
958 | }
959 | }
960 | // Remove the first object from the stack of traversed objects.
961 | aStack.pop();
962 | bStack.pop();
963 | return result;
964 | };
965 |
966 | // Perform a deep comparison to check if two objects are equal.
967 | _.isEqual = function(a, b) {
968 | return eq(a, b, [], []);
969 | };
970 |
971 | // Is a given array, string, or object empty?
972 | // An "empty" object has no enumerable own-properties.
973 | _.isEmpty = function(obj) {
974 | if (obj == null) return true;
975 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
976 | for (var key in obj) if (_.has(obj, key)) return false;
977 | return true;
978 | };
979 |
980 | // Is a given value a DOM element?
981 | _.isElement = function(obj) {
982 | return !!(obj && obj.nodeType === 1);
983 | };
984 |
985 | // Is a given value an array?
986 | // Delegates to ECMA5's native Array.isArray
987 | _.isArray = nativeIsArray || function(obj) {
988 | return toString.call(obj) == '[object Array]';
989 | };
990 |
991 | // Is a given variable an object?
992 | _.isObject = function(obj) {
993 | return obj === Object(obj);
994 | };
995 |
996 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
997 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
998 | _['is' + name] = function(obj) {
999 | return toString.call(obj) == '[object ' + name + ']';
1000 | };
1001 | });
1002 |
1003 | // Define a fallback version of the method in browsers (ahem, IE), where
1004 | // there isn't any inspectable "Arguments" type.
1005 | if (!_.isArguments(arguments)) {
1006 | _.isArguments = function(obj) {
1007 | return !!(obj && _.has(obj, 'callee'));
1008 | };
1009 | }
1010 |
1011 | // Optimize `isFunction` if appropriate.
1012 | if (typeof (/./) !== 'function') {
1013 | _.isFunction = function(obj) {
1014 | return typeof obj === 'function';
1015 | };
1016 | }
1017 |
1018 | // Is a given object a finite number?
1019 | _.isFinite = function(obj) {
1020 | return isFinite(obj) && !isNaN(parseFloat(obj));
1021 | };
1022 |
1023 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
1024 | _.isNaN = function(obj) {
1025 | return _.isNumber(obj) && obj != +obj;
1026 | };
1027 |
1028 | // Is a given value a boolean?
1029 | _.isBoolean = function(obj) {
1030 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
1031 | };
1032 |
1033 | // Is a given value equal to null?
1034 | _.isNull = function(obj) {
1035 | return obj === null;
1036 | };
1037 |
1038 | // Is a given variable undefined?
1039 | _.isUndefined = function(obj) {
1040 | return obj === void 0;
1041 | };
1042 |
1043 | // Shortcut function for checking if an object has a given property directly
1044 | // on itself (in other words, not on a prototype).
1045 | _.has = function(obj, key) {
1046 | return hasOwnProperty.call(obj, key);
1047 | };
1048 |
1049 | // Utility Functions
1050 | // -----------------
1051 |
1052 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1053 | // previous owner. Returns a reference to the Underscore object.
1054 | _.noConflict = function() {
1055 | root._ = previousUnderscore;
1056 | return this;
1057 | };
1058 |
1059 | // Keep the identity function around for default iterators.
1060 | _.identity = function(value) {
1061 | return value;
1062 | };
1063 |
1064 | // Run a function **n** times.
1065 | _.times = function(n, iterator, context) {
1066 | var accum = Array(Math.max(0, n));
1067 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1068 | return accum;
1069 | };
1070 |
1071 | // Return a random integer between min and max (inclusive).
1072 | _.random = function(min, max) {
1073 | if (max == null) {
1074 | max = min;
1075 | min = 0;
1076 | }
1077 | return min + Math.floor(Math.random() * (max - min + 1));
1078 | };
1079 |
1080 | // List of HTML entities for escaping.
1081 | var entityMap = {
1082 | escape: {
1083 | '&': '&',
1084 | '<': '<',
1085 | '>': '>',
1086 | '"': '"',
1087 | "'": '''
1088 | }
1089 | };
1090 | entityMap.unescape = _.invert(entityMap.escape);
1091 |
1092 | // Regexes containing the keys and values listed immediately above.
1093 | var entityRegexes = {
1094 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1095 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1096 | };
1097 |
1098 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1099 | _.each(['escape', 'unescape'], function(method) {
1100 | _[method] = function(string) {
1101 | if (string == null) return '';
1102 | return ('' + string).replace(entityRegexes[method], function(match) {
1103 | return entityMap[method][match];
1104 | });
1105 | };
1106 | });
1107 |
1108 | // If the value of the named `property` is a function then invoke it with the
1109 | // `object` as context; otherwise, return it.
1110 | _.result = function(object, property) {
1111 | if (object == null) return void 0;
1112 | var value = object[property];
1113 | return _.isFunction(value) ? value.call(object) : value;
1114 | };
1115 |
1116 | // Add your own custom functions to the Underscore object.
1117 | _.mixin = function(obj) {
1118 | each(_.functions(obj), function(name) {
1119 | var func = _[name] = obj[name];
1120 | _.prototype[name] = function() {
1121 | var args = [this._wrapped];
1122 | push.apply(args, arguments);
1123 | return result.call(this, func.apply(_, args));
1124 | };
1125 | });
1126 | };
1127 |
1128 | // Generate a unique integer id (unique within the entire client session).
1129 | // Useful for temporary DOM ids.
1130 | var idCounter = 0;
1131 | _.uniqueId = function(prefix) {
1132 | var id = ++idCounter + '';
1133 | return prefix ? prefix + id : id;
1134 | };
1135 |
1136 | // By default, Underscore uses ERB-style template delimiters, change the
1137 | // following template settings to use alternative delimiters.
1138 | _.templateSettings = {
1139 | evaluate : /<%([\s\S]+?)%>/g,
1140 | interpolate : /<%=([\s\S]+?)%>/g,
1141 | escape : /<%-([\s\S]+?)%>/g
1142 | };
1143 |
1144 | // When customizing `templateSettings`, if you don't want to define an
1145 | // interpolation, evaluation or escaping regex, we need one that is
1146 | // guaranteed not to match.
1147 | var noMatch = /(.)^/;
1148 |
1149 | // Certain characters need to be escaped so that they can be put into a
1150 | // string literal.
1151 | var escapes = {
1152 | "'": "'",
1153 | '\\': '\\',
1154 | '\r': 'r',
1155 | '\n': 'n',
1156 | '\t': 't',
1157 | '\u2028': 'u2028',
1158 | '\u2029': 'u2029'
1159 | };
1160 |
1161 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1162 |
1163 | // JavaScript micro-templating, similar to John Resig's implementation.
1164 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1165 | // and correctly escapes quotes within interpolated code.
1166 | _.template = function(text, data, settings) {
1167 | var render;
1168 | settings = _.defaults({}, settings, _.templateSettings);
1169 |
1170 | // Combine delimiters into one regular expression via alternation.
1171 | var matcher = new RegExp([
1172 | (settings.escape || noMatch).source,
1173 | (settings.interpolate || noMatch).source,
1174 | (settings.evaluate || noMatch).source
1175 | ].join('|') + '|$', 'g');
1176 |
1177 | // Compile the template source, escaping string literals appropriately.
1178 | var index = 0;
1179 | var source = "__p+='";
1180 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1181 | source += text.slice(index, offset)
1182 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1183 |
1184 | if (escape) {
1185 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1186 | }
1187 | if (interpolate) {
1188 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1189 | }
1190 | if (evaluate) {
1191 | source += "';\n" + evaluate + "\n__p+='";
1192 | }
1193 | index = offset + match.length;
1194 | return match;
1195 | });
1196 | source += "';\n";
1197 |
1198 | // If a variable is not specified, place data values in local scope.
1199 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1200 |
1201 | source = "var __t,__p='',__j=Array.prototype.join," +
1202 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1203 | source + "return __p;\n";
1204 |
1205 | try {
1206 | render = new Function(settings.variable || 'obj', '_', source);
1207 | } catch (e) {
1208 | e.source = source;
1209 | throw e;
1210 | }
1211 |
1212 | if (data) return render(data, _);
1213 | var template = function(data) {
1214 | return render.call(this, data, _);
1215 | };
1216 |
1217 | // Provide the compiled function source as a convenience for precompilation.
1218 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1219 |
1220 | return template;
1221 | };
1222 |
1223 | // Add a "chain" function, which will delegate to the wrapper.
1224 | _.chain = function(obj) {
1225 | return _(obj).chain();
1226 | };
1227 |
1228 | // OOP
1229 | // ---------------
1230 | // If Underscore is called as a function, it returns a wrapped object that
1231 | // can be used OO-style. This wrapper holds altered versions of all the
1232 | // underscore functions. Wrapped objects may be chained.
1233 |
1234 | // Helper function to continue chaining intermediate results.
1235 | var result = function(obj) {
1236 | return this._chain ? _(obj).chain() : obj;
1237 | };
1238 |
1239 | // Add all of the Underscore functions to the wrapper object.
1240 | _.mixin(_);
1241 |
1242 | // Add all mutator Array functions to the wrapper.
1243 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1244 | var method = ArrayProto[name];
1245 | _.prototype[name] = function() {
1246 | var obj = this._wrapped;
1247 | method.apply(obj, arguments);
1248 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1249 | return result.call(this, obj);
1250 | };
1251 | });
1252 |
1253 | // Add all accessor Array functions to the wrapper.
1254 | each(['concat', 'join', 'slice'], function(name) {
1255 | var method = ArrayProto[name];
1256 | _.prototype[name] = function() {
1257 | return result.call(this, method.apply(this._wrapped, arguments));
1258 | };
1259 | });
1260 |
1261 | _.extend(_.prototype, {
1262 |
1263 | // Start chaining a wrapped Underscore object.
1264 | chain: function() {
1265 | this._chain = true;
1266 | return this;
1267 | },
1268 |
1269 | // Extracts the result from a wrapped and chained object.
1270 | value: function() {
1271 | return this._wrapped;
1272 | }
1273 |
1274 | });
1275 |
1276 | }).call(this);
1277 |
--------------------------------------------------------------------------------
/test/tests.js:
--------------------------------------------------------------------------------
1 | $(document).ready( function() {
2 |
3 | var Employee = Backbone.Model.extend( { } );
4 | var Employees = Backbone.Collection.extend( { model : Employee } );
5 |
6 | function commonSetup() {
7 |
8 | this.emp1 = new Employee( { id : 1, firstName : 'Sherlock', lastName : 'Holmes' } );
9 | this.emp2 = new Employee( { id : 2, firstName : 'John', lastName : 'Watson' } );
10 | this.emp3 = new Employee( { id : 3, firstName : 'Mycroft', lastName : 'Holmes' } );
11 | this.employees = new Employees( [ this.emp1, this.emp2, this.emp3 ] );
12 |
13 | this.EmployeeView = Backbone.View.extend( {
14 | template : _.template( $( "#employee-template" ).html() ),
15 | render : function() {
16 | var emp = this.model.toJSON();
17 | var html = this.template(emp);
18 | this.$el.html(html);
19 | }
20 | } );
21 |
22 | this.EmployeeViewWithLi = Backbone.View.extend( {
23 | tagName : "li",
24 | template : _.template( $( "#employee-template" ).html() ),
25 | render : function() {
26 | var emp = this.model.toJSON();
27 | var html = this.template(emp);
28 | this.$el.html(html);
29 | }
30 | } );
31 |
32 | this.EmployeeViewForTable = Backbone.View.extend( {
33 | tagName : 'tr',
34 | template : _.template( $( "#employee-template-for-table" ).html() ),
35 | render : function() {
36 | var emp = this.model.toJSON();
37 | var html = this.template(emp);
38 | this.$el.html(html);
39 | }
40 | } );
41 |
42 | this.EmployeeView2 = Backbone.View.extend( {
43 | template : _.template( $( "#employee-template-2" ).html() ),
44 | render : function() {
45 | var emp = this.model.toJSON();
46 | var html = this.template(emp);
47 | this.$el.html(html);
48 | }
49 | } );
50 |
51 |
52 | var $fixture = $( "#qunit-fixture" );
53 | $fixture.append( "