├── demo
├── music
│ ├── music.mp3
│ ├── img
│ │ └── pattern.jpg
│ ├── css
│ │ └── style.css
│ ├── index.html
│ └── scripts
│ │ └── stub.js
├── mouse
│ ├── img
│ │ ├── pattern-0.jpg
│ │ ├── pattern-1.jpg
│ │ ├── pattern-2.jpg
│ │ └── pattern-3.jpg
│ ├── css
│ │ └── style.css
│ ├── index.html
│ └── js
│ │ └── stub.js
├── webcam
│ ├── img
│ │ ├── pattern-0.jpg
│ │ ├── pattern-1.jpg
│ │ ├── pattern-2.jpg
│ │ └── pattern-3.jpg
│ ├── css
│ │ └── style.css
│ ├── index.html
│ └── js
│ │ └── stub.js
├── common
│ ├── dragdrop.js
│ └── vendor
│ │ ├── dancer.min.js
│ │ ├── underscore.js
│ │ └── backbone.js
└── vkapi
│ ├── index.html
│ ├── css
│ └── style.css
│ └── js
│ └── stub.js
├── .gitignore
├── package.json
├── Gruntfile.coffee
├── README.md
├── lib
└── graphemescope.js
└── src
└── graphemescope.coffee
/demo/music/music.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/music/music.mp3
--------------------------------------------------------------------------------
/demo/music/img/pattern.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/music/img/pattern.jpg
--------------------------------------------------------------------------------
/demo/mouse/img/pattern-0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/mouse/img/pattern-0.jpg
--------------------------------------------------------------------------------
/demo/mouse/img/pattern-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/mouse/img/pattern-1.jpg
--------------------------------------------------------------------------------
/demo/mouse/img/pattern-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/mouse/img/pattern-2.jpg
--------------------------------------------------------------------------------
/demo/mouse/img/pattern-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/mouse/img/pattern-3.jpg
--------------------------------------------------------------------------------
/demo/webcam/img/pattern-0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/webcam/img/pattern-0.jpg
--------------------------------------------------------------------------------
/demo/webcam/img/pattern-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/webcam/img/pattern-1.jpg
--------------------------------------------------------------------------------
/demo/webcam/img/pattern-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/webcam/img/pattern-2.jpg
--------------------------------------------------------------------------------
/demo/webcam/img/pattern-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Grapheme/graphemescope/HEAD/demo/webcam/img/pattern-3.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | */config/development
2 | */logs/log-*.php
3 | */logs/!index.html
4 | */cache/*
5 | */cache/!index.html
6 | node_modules
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/demo/music/css/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | background-color: black;
3 | color : white;
4 | padding : 0;
5 | margin : 0;
6 | overflow: hidden;
7 | }
8 |
9 | #beat {
10 | position: fixed;
11 |
12 | left : 50%;
13 | top : 0;
14 | z-index: 4;
15 | width : 100px;
16 | height: 100px;
17 |
18 | margin-left : -50px;
19 | border-radius: 100px;
20 |
21 | background-color: #ff06cc;
22 | }
23 |
24 | #container {
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 |
30 |
--------------------------------------------------------------------------------
/demo/common/dragdrop.js:
--------------------------------------------------------------------------------
1 | // Drag-n-Drop helper
2 | DragDrop = function(context, onDrop) {
3 | var disable = function( event ) {
4 | event.stopPropagation();
5 | event.preventDefault();
6 | };
7 |
8 | var dropHandler = function( event ) {
9 | disable(event);
10 | onDrop(event.dataTransfer.files);
11 | };
12 |
13 | context.addEventListener("dragleave", disable);
14 | context.addEventListener("dragenter", disable);
15 | context.addEventListener("dragover", disable);
16 | context.addEventListener("drop", dropHandler);
17 | };
18 |
--------------------------------------------------------------------------------
/demo/mouse/css/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | background-color: black;
3 | padding : 0;
4 | margin : 0;
5 | overflow: hidden;
6 | }
7 |
8 | #container {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | .equalizer {
14 | position : absolute;
15 | top : 0;
16 | left : 0;
17 | height: 20px;
18 | z-index : 1;
19 | }
20 |
21 | audio {
22 | position: absolute;
23 | z-index: 2;
24 | right: 0;
25 | top: 0;
26 | }
27 |
28 | .equalizer div {
29 | display : inline-block;
30 | background-color: #ffffff;
31 | margin: 1px;
32 | width : 10px;
33 | }
34 |
--------------------------------------------------------------------------------
/demo/webcam/css/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | background-color: black;
3 | padding : 0;
4 | margin : 0;
5 | overflow: hidden;
6 | }
7 |
8 | #container {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | .equalizer {
14 | position : absolute;
15 | top : 0;
16 | left : 0;
17 | height: 20px;
18 | z-index : 1;
19 | }
20 |
21 | audio {
22 | position: absolute;
23 | z-index: 2;
24 | right: 0;
25 | top: 0;
26 | }
27 |
28 | .equalizer div {
29 | display : inline-block;
30 | background-color: #ffffff;
31 | margin: 1px;
32 | width : 10px;
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphemescope",
3 | "version": "0.0.0",
4 | "description": "Graphemescope - is a library, that contains bunch of methods to create dynamic kaleidoscopic visualizations using HTML5 APIs",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha"
8 | },
9 | "keywords": [
10 | "demo"
11 | ],
12 | "author": "grapheme",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "grunt-cli": "~0.1.9",
16 | "grunt": "~0.4.1",
17 | "grunt-contrib-watch": "~0.5.3",
18 | "grunt-contrib-coffee": "~0.7.0",
19 | "grunt-contrib-uglify": "~0.2.4"
20 | },
21 | "dependencies": {}
22 | }
23 |
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | module.exports = (grunt) ->
2 | grunt.initConfig
3 | pkg: grunt.file.readJSON "package.json"
4 |
5 | watch:
6 | options:
7 | livereload : true
8 | compile:
9 | files : [ "src/*.coffee" ]
10 | tasks : [ "default" ]
11 |
12 | coffee:
13 | compile:
14 | files:
15 | "lib/graphemescope.js" : [ "src/*.coffee" ]
16 |
17 | uglify:
18 | compile:
19 | files:
20 | "lib/graphemescope.js" : [ "lib/graphemescope.js" ]
21 |
22 | grunt.loadNpmTasks "grunt-contrib-coffee"
23 | grunt.loadNpmTasks "grunt-contrib-watch"
24 | grunt.loadNpmTasks "grunt-contrib-uglify"
25 |
26 | grunt.registerTask "default", [ "coffee", "uglify" ]
27 |
--------------------------------------------------------------------------------
/demo/mouse/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mouse and Drag'n'Drop demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/demo/music/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Music demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demo/vkapi/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | VK api demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/demo/webcam/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mouse and Drag'n'Drop demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/demo/vkapi/css/style.css:
--------------------------------------------------------------------------------
1 | @import url(http://fonts.googleapis.com/css?family=Neucha&subset=latin,cyrillic);
2 |
3 | body, html {
4 | background-color: black;
5 | padding : 0;
6 | margin : 0;
7 | overflow: hidden;
8 | }
9 |
10 | #container {
11 | margin: 0;
12 | padding: 0;
13 | }
14 |
15 | .equalizer {
16 | position : absolute;
17 | top : 0;
18 | left : 0;
19 | height: 20px;
20 | z-index : 1;
21 | }
22 |
23 | audio {
24 | position: absolute;
25 | z-index: 2;
26 | right: 0;
27 | top: 0;
28 | }
29 |
30 | .equalizer div {
31 | display : inline-block;
32 | background-color: #ffffff;
33 | margin: 1px;
34 | width : 10px;
35 | }
36 |
37 |
38 | #music-title {
39 | text-shadow: 0px 0px 20px black;
40 | position: absolute;
41 | font-family: "Neucha";
42 | z-index : 2;
43 | width : 100%;
44 | text-align: center;
45 | font-size : 3em;
46 | color : white;
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Graphemescope
2 | Библиотека для рисования калейдоскопических визуализаций. Пример использования:
3 | ```javascript
4 | // Родительский элемент
5 | var container = document.getElementById("container");
6 |
7 | // Создаем калейдоскоп
8 | var scope = new Graphemescope( container );
9 | scope.setImage("http://placekitten.com/200/30");
10 |
11 | // Двигаем
12 | scope.zoomTarget = 2.0;
13 | scope.angleTarget = 0.5;
14 | ```
15 |
16 | ### Сборка проекта
17 | В проекте присутствуют скрипты на языке `CoffeScript`, которые необходимо
18 | предварительно скомпилировать в `js` скрипты.
19 | Для автоматизации этого процесса используется система сборки [Grunt](http://gruntjs.com/)
20 |
21 | ### Процесс сборки
22 | #### Установка окружения
23 | 1. Установить [node.js](http://nodejs.org/)
24 | 2. Установить `grunt` и `coffee-script`:
25 |
26 | ```
27 | npm install -g grunt
28 | npm install -g coffee-script
29 | ```
30 | 3. Установить зависимости проекта:
31 |
32 | ```
33 | npm install
34 | ```
35 |
36 | #### Сборка файлов
37 | Сборка файлов проекта осуществляется командой:
38 |
39 | ```
40 | grunt
41 | ```
42 |
43 |
--------------------------------------------------------------------------------
/demo/music/scripts/stub.js:
--------------------------------------------------------------------------------
1 | window.addEventListener("load", function() {
2 | var imagePath = "img/pattern.jpg";
3 |
4 | var container = $("#container");
5 |
6 | resizeHandler = function() {
7 | container.height( $(window).height() );
8 | container.width( $(window).width() );
9 | };
10 |
11 | $(window).resize(resizeHandler);
12 | $(window).resize();
13 |
14 |
15 | var scope = new Graphemescope(container[0]);
16 |
17 |
18 | var x = 1;
19 | var v = 0;
20 | var target = 0;
21 |
22 | function moveScope() {
23 | var a = 4;
24 | var b = 1;
25 | var dt = 0.1;
26 |
27 | var force = a * (-x) + b * (-v);
28 | v += force * dt;
29 | x += v * dt;
30 |
31 | $("#beat").css({
32 | "transform" : "translateX(" + 100 * x + "px)"
33 | });
34 | }
35 |
36 |
37 | var dancer = new Dancer();
38 |
39 | dancer.bind("loaded", function() {
40 | dancer.play();
41 | });
42 |
43 |
44 | var kick = dancer.createKick({
45 | onKick: function (a) {
46 | x = a;
47 | },
48 | offKick: function () {}
49 | }).on();
50 |
51 | dancer.load({ src: "music.mp3" });
52 |
53 |
54 | scope.setImage(imagePath);
55 |
56 |
57 | var r = 0;
58 |
59 | dancer.after( 0, function() {
60 | var one = 0.15 * x;
61 | var two = 100 * this.getFrequency(32, 128);
62 |
63 | r += 0.1 * (two - r);
64 |
65 |
66 |
67 | scope.angleTarget = r;
68 | scope.zoomTarget = 1.0 + 5.0 * one;
69 |
70 | moveScope();
71 | });
72 |
73 | $(container).click(function() {
74 | if(dancer.isPlaying()) {
75 | dancer.pause();
76 | } else {
77 | dancer.play();
78 | }
79 | });
80 |
81 | $(window).mousemove(function(event) {
82 | var factorx = event.pageX / $(window).width();
83 | var factory = event.pageY / $(window).height();
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/lib/graphemescope.js:
--------------------------------------------------------------------------------
1 | (function(){var a,b;b=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(a){return window.setTimeout(a,1e3/30)}}(),window.Graphemescope=a=function(){function a(a){var c=this;this.parentElement=null!=a?a:window.document.body,this.enabled=!0,this.radiusFactor=1,this.zoomFactor=1,this.angleFactor=0,this.zoomTarget=1.2,this.angleTarget=.8,this.easeEnabled=!0,this.ease=.1,null==this.domElement&&(this.domElement=document.createElement("canvas")),null==this.ctx&&(this.ctx=this.domElement.getContext("2d")),this.alphaFactor=1,this.alphaTarget=1,this.parentElement.appendChild(this.domElement),this.oldResizeHandler=function(){},null!==window.onresize&&(this.oldResizeHandler=window.onresize),window.onresize=function(){return c.resizeHandler()},this.resizeHandler(),b(function(){return c.animationFrame()})}return a.prototype.animationFrame=function(){var a=this;return b(function(){return a.animationFrame()}),this.enabled?(this.update(),this.draw()):void 0},a.prototype.resizeHandler=function(){return this.width=this.domElement.width=this.parentElement.offsetWidth,this.height=this.domElement.height=this.parentElement.offsetHeight,this.radius=.5*this.radiusFactor*Math.min(this.width,this.height),this.radiusHeight=.5*Math.sqrt(3)*this.radius,this.oldResizeHandler()},a.prototype.update=function(){return this.easeEnabled?(this.angleFactor+=(this.angleTarget-this.angleFactor)*this.ease,this.zoomFactor+=(this.zoomTarget-this.zoomFactor)*this.ease,this.alphaFactor+=(this.alphaTarget-this.alphaFactor)*this.ease):(this.angleFactor=this.angleTarget,this.zoomFactor=this.zoomTarget,this.alphaFactor=this.alphaTarget)},a.prototype.drawImage=function(a){var b,c;return this.ctx.save(),b=2/3*this.radiusHeight,c=this.zoomFactor*b/(.5*Math.min(a.width,a.height)),this.ctx.translate(0,b),this.ctx.scale(c,c),this.ctx.rotate(2*this.angleFactor*Math.PI),this.ctx.translate(-.5*a.width,-.5*a.height),this.ctx.fill(),this.ctx.restore()},a.prototype.drawCell=function(a){var b,c,d;for(d=[],b=c=0;6>c;b=++c)this.ctx.save(),this.ctx.rotate(2*b*Math.PI/6),this.ctx.scale([-1,1][b%2],1),this.ctx.beginPath(),this.ctx.moveTo(0,0),this.ctx.lineTo(-.5*this.radius,1*this.radiusHeight),this.ctx.lineTo(.5*this.radius,1*this.radiusHeight),this.ctx.closePath(),this.drawImage(a),d.push(this.ctx.restore());return d},a.prototype.drawLayer=function(a){var b,c,d,e,f,g,h,i,j,k,l,m;for(this.ctx.save(),this.ctx.translate(.5*this.width,.5*this.height),f=Math.ceil(.5*this.height/this.radiusHeight),c=Math.ceil(.5*this.width/(3*this.radius)),d=function(){l=[];for(var a=-c;c>=-c?c>=a:a>=c;c>=-c?a++:a--)l.push(a);return l}.apply(this),g=function(){m=[];for(var a=-f;f>=-f?f>=a:a>=f;f>=-f?a++:a--)m.push(a);return m}.apply(this),h=0,j=g.length;j>h;h++){for(e=g[h],this.ctx.save(),this.ctx.translate(0,this.radiusHeight*e),Math.abs(e)%2&&this.ctx.translate(1.5*this.radius,0),i=0,k=d.length;k>i;i++)b=d[i],this.ctx.save(),this.ctx.translate(3*b*this.radius,0),this.drawCell(a),this.ctx.restore();this.ctx.restore()}return this.ctx.restore()},a.prototype.draw=function(){return null!=this.imageProxy&&(this.ctx.fillStyle=this.patternProxy,this.ctx.globalAlpha=1-this.alphaFactor,this.drawLayer(this.imageProxy)),null!=this.image?(this.ctx.fillStyle=this.pattern,this.ctx.globalAlpha=this.alphaFactor,this.drawLayer(this.image)):void 0},a.prototype.setImageDirect=function(a){return null!=this.image&&(this.imageProxy=this.image,this.patternProxy=this.pattern),this.image=a,this.pattern=this.ctx.createPattern(this.image,"repeat"),this.alphaFactor=0},a.prototype.setImage=function(a){var b,c=this;return"string"==typeof a?(b=new Image,b.src=a,b.onload=function(){return c.setImageDirect(b)}):this.setImageDirect(a)},a}()}).call(this);
--------------------------------------------------------------------------------
/demo/mouse/js/stub.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | window.leapEnabled = false;
3 |
4 | var container = $("#container");
5 |
6 | scope = new Graphemescope( container[0] );
7 |
8 | var dragdrop = new DragDrop(container[0], function (files) {
9 | var filter = /^image/i;
10 | var file = files[0];
11 |
12 | if(filter.test(file.type)) {
13 | var reader = new FileReader();
14 | reader.onload = function(event) {
15 | var img = new Image();
16 | img.src = event.target.result;
17 | scope.setImage(img);
18 | };
19 |
20 | reader.readAsDataURL(file);
21 | }
22 |
23 | });
24 |
25 | var index = 0;
26 | var imageCount = 4;
27 |
28 | function changePicture() {
29 | var imagePath = "img/pattern-" + index + ".jpg";
30 |
31 | scope.setImage(imagePath);
32 |
33 | index = (index + 1) % imageCount;
34 | }
35 |
36 | changePicture();
37 |
38 |
39 | function moveKaleidoscope(factorx, factory) {
40 | if(!leapEnabled) {
41 | scope.angleTarget = factorx;
42 | scope.zoomTarget = 1.0 + 0.5 * factory;
43 | }
44 | }
45 |
46 | $(window).mousemove(function(event) {
47 | moveKaleidoscope(
48 | event.pageX / $(window).width(),
49 | event.pageY / $(window).height()
50 | );
51 | });
52 |
53 | $(window).on("touchmove", function(evt) {
54 | evt.preventDefault();
55 | var originalEvent = evt.originalEvent;
56 |
57 | var touch = originalEvent.touches[0];
58 | moveKaleidoscope(
59 | touch.pageX / $(window).width(),
60 | touch.pageY / $(window).height()
61 | );
62 | });
63 |
64 | $(window).click(changePicture);
65 |
66 | var resizeHandler = function() {
67 | container.height( $(window).height() );
68 | container.width( $(window).width() );
69 | };
70 |
71 | $(window).resize(resizeHandler);
72 | $(window).resize();
73 |
74 | var throttledChange = _(changePicture).throttle(1000, {leading: false});
75 |
76 |
77 | var controller = new Leap.Controller({ enableGestures : true });
78 |
79 |
80 | controller.on('deviceDisconnected', function() {
81 | console.log("Leap Motion has been disconnected");
82 | leapEnabled = false;
83 | });
84 |
85 | controller.on('deviceConnected', function() {
86 | console.log("Leap Motion is connected");
87 | leapEnabled = true;
88 | });
89 |
90 | controller.on('ready', function() {
91 | console.log("Leap Motion is ready");
92 | leapEnabled = true;
93 | });
94 |
95 |
96 | function leapToScene( frame, leapPos ) {
97 | var iBox = frame.interactionBox;
98 |
99 | var left = iBox.center[0] - iBox.size[0]/2;
100 | var top = iBox.center[1] + iBox.size[1]/2;
101 |
102 | var x = leapPos[0] - left;
103 | var y = leapPos[1] - top;
104 |
105 | x /= iBox.size[0];
106 | y /= iBox.size[1];
107 |
108 | return [ x , -y ];
109 | }
110 |
111 | controller.loop(function(frame) {
112 | if (frame.valid) {
113 | if (typeof firstValidFrame === "undefined") {
114 | firstValidFrame = frame
115 | }
116 |
117 | if(frame.hands && frame.hands.length > 0) {
118 | var handPos = leapToScene( firstValidFrame , frame.hands[0].palmPosition );
119 |
120 | scope.zoomTarget = 1.0 + 1.0 * handPos[1];
121 | if(scope.zoomTarget < 1.0)
122 | scope.zoomTarget = 1.0;
123 |
124 | scope.angleTarget = handPos[0];
125 | }
126 |
127 | if(frame.gestures && frame.gestures.length > 0) {
128 | var gesture = frame.gestures[0];
129 |
130 | if(gesture.type === "swipe") {
131 | var startPos = leapToScene(frame, gesture.startPosition );
132 | var pos = leapToScene( frame, gesture.position );
133 |
134 | if(startPos[0] - pos[0] < 0) {
135 | console.log("Swipe event");
136 | throttledChange();
137 | }
138 | }
139 | }
140 | }
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/demo/vkapi/js/stub.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | window.wallSource = "";
3 |
4 | container = $("#container");
5 | window.scope = new Graphemescope( container[0] );
6 |
7 | resizeHandler = function() {
8 | container.height( $(window).height() );
9 | container.width( $(window).width() );
10 | };
11 | $(window).resize(resizeHandler);
12 | $(window).resize();
13 |
14 | function changeResources(imageSrc, music, callback) {
15 | var image = new Image();
16 | image.src = imageSrc;
17 | image.onload = function() {
18 | scope.setImage(image);
19 | scope.setAudio(music.url);
20 | $('#music-title').html(music.artist + " - " + music.title);
21 |
22 | callback();
23 | };
24 | }
25 |
26 |
27 | VK.init({
28 | apiId: 3300222
29 | });
30 |
31 | function authInfo(response) {
32 | if (response.session) {
33 | $('#login_button').hide();
34 | apiInitialized();
35 | }
36 | }
37 |
38 | VK.Auth.getLoginStatus(authInfo);
39 | VK.UI.button('login_button');
40 |
41 | $('#login_button').click(function() {
42 | VK.Auth.login(authInfo);
43 | });
44 |
45 |
46 | var totalCount = 0;
47 |
48 | function apiInitialized() {
49 | container.click(function() {
50 | getNext(0);
51 | });
52 |
53 |
54 | getNext = function getNext() {
55 | var index = _.random(0, totalCount);
56 |
57 |
58 | VK.Api.call('wall.get', {
59 | offset : index,
60 | domain : wallSource,
61 | count : 1,
62 | filter : 'owner'
63 | }, function(r) {
64 | if(!(r.response && r.response[1] && r.response[1].attachments)) {
65 | return getNext();
66 | }
67 |
68 |
69 | var att = r.response[1].attachments;
70 |
71 | var photos = _(att).chain()
72 | .filter(function(a) {
73 | return (a.type === 'photo');
74 | })
75 | .map(function(a) {
76 | return a.photo.src_xbig || a.photo.src_big;
77 | })
78 | .shuffle()
79 | .value();
80 |
81 | var music = _(att).chain()
82 | .filter(function(a) {
83 | return (a.type == 'audio');
84 | })
85 | .map(function(a) {
86 | return a.audio;
87 | })
88 | .shuffle()
89 | .value();
90 |
91 | if(photos.length <= 0 || music.length <= 0) {
92 | return getNext(index);
93 | }
94 |
95 | var imageSrc = photos[0];
96 | var musicSrc = music[0];
97 |
98 | changeResources(imageSrc, musicSrc, function() {});
99 | });
100 | }
101 |
102 |
103 | Router = Backbone.Router.extend({
104 | routes : {
105 | ":name" : "name",
106 | "" : "default"
107 | },
108 |
109 | name : function(name) {
110 | wallSource = name;
111 |
112 | VK.Api.call('wall.get', {
113 | offset : 0,
114 | domain : wallSource,
115 | count : 1,
116 | filter : 'owner'
117 | }, function(r) {
118 | if(r.response && r.response[0]) {
119 | totalCount = r.response[0];
120 | getNext();
121 | }
122 | });
123 | },
124 |
125 | default : function() {
126 | var that = this;
127 | VK.Api.call("users.get", {
128 | "fields" : "screen_name"
129 | }, function(r) {
130 | that.navigate(r.response[0].screen_name, {trigger: true});
131 | });
132 | }
133 | });
134 |
135 | new Router();
136 | Backbone.history.start();
137 |
138 | setInterval(getNext, 60000);
139 | }
140 |
141 |
142 | });
143 |
--------------------------------------------------------------------------------
/demo/webcam/js/stub.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | var streaming = false;
3 | var video = document.querySelector('#video');
4 | var canvas = document.querySelector('#canvas');
5 |
6 | navigator.getMedia = ( navigator.getUserMedia ||
7 | navigator.webkitGetUserMedia ||
8 | navigator.mozGetUserMedia ||
9 | navigator.msGetUserMedia);
10 |
11 | navigator.getMedia(
12 | {
13 | video: true,
14 | audio: false
15 | },
16 | function(stream) {
17 | if (navigator.mozGetUserMedia) {
18 | video.mozSrcObject = stream;
19 | } else {
20 | var vendorURL = window.URL || window.webkitURL;
21 | video.src = vendorURL.createObjectURL(stream);
22 | }
23 | video.play();
24 | },
25 | function(err) {
26 | console.log("An error occured! " + err);
27 | }
28 | );
29 |
30 | video.addEventListener('canplay', function(ev){
31 | if (!streaming) {
32 | canvas.setAttribute('width', video.videoWidth);
33 | canvas.setAttribute('height', video.videoHeight);
34 | streaming = true;
35 | }
36 | }, false);
37 |
38 | function takePicture() {
39 | if(streaming) {
40 |
41 | canvas.width = video.videoWidth;
42 | canvas.height = video.videoHeight;
43 |
44 | canvas.getContext('2d').drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
45 |
46 | var image = new Image();
47 | image.src = canvas.toDataURL('image/png');
48 | scope.setImage(image);
49 | }
50 |
51 | }
52 |
53 |
54 | window.leapEnabled = false;
55 |
56 | var container = $("#container");
57 |
58 | window.scope = new Graphemescope( container[0] );
59 |
60 |
61 | var index = 0;
62 | var imageCount = 4;
63 |
64 | function changePicture() {
65 | var imagePath = "img/pattern-" + index + ".jpg";
66 |
67 | scope.setImage(imagePath);
68 |
69 | index = (index + 1) % imageCount;
70 | }
71 |
72 | changePicture();
73 |
74 | $(window).mousemove(function(event) {
75 | var factorx = event.pageX / $(window).width();
76 | var factory = event.pageY / $(window).height();
77 |
78 | if(!leapEnabled) {
79 | scope.angleTarget = factorx;
80 | scope.zoomTarget = 1.0 + 0.5 * factory;
81 | }
82 | });
83 |
84 | $(window).click(takePicture);
85 |
86 | var resizeHandler = function() {
87 | container.height( $(window).height() );
88 | container.width( $(window).width() );
89 | };
90 |
91 | $(window).resize(resizeHandler);
92 | $(window).resize();
93 |
94 | setInterval(takePicture, 5000);
95 |
96 | var throttledChange = _(takePicture).throttle(5000);
97 |
98 |
99 | var controller = new Leap.Controller({ enableGestures : true });
100 |
101 |
102 | controller.on('deviceDisconnected', function() {
103 | console.log("Leap Motion has been disconnected");
104 | leapEnabled = false;
105 | });
106 |
107 | controller.on('deviceConnected', function() {
108 | console.log("Leap Motion is connected");
109 | leapEnabled = true;
110 | });
111 |
112 | controller.on('ready', function() {
113 | console.log("Leap Motion is ready");
114 | leapEnabled = true;
115 | });
116 |
117 |
118 | function leapToScene( frame, leapPos ) {
119 | var iBox = frame.interactionBox;
120 |
121 | var left = iBox.center[0] - iBox.size[0]/2;
122 | var top = iBox.center[1] + iBox.size[1]/2;
123 |
124 | var x = leapPos[0] - left;
125 | var y = leapPos[1] - top;
126 |
127 | x /= iBox.size[0];
128 | y /= iBox.size[1];
129 |
130 | return [ x , -y ];
131 | }
132 |
133 | controller.loop(function(frame) {
134 | if (frame.valid) {
135 | if (typeof firstValidFrame === "undefined") {
136 | firstValidFrame = frame
137 | }
138 |
139 | if(frame.hands && frame.hands.length > 0) {
140 | var handPos = leapToScene( firstValidFrame , frame.hands[0].palmPosition );
141 |
142 | scope.zoomTarget = 1.0 + 1.0 * handPos[1];
143 | if(scope.zoomTarget < 1.0)
144 | scope.zoomTarget = 1.0;
145 |
146 | if(scope.zoomTarget > 1.6)
147 | scope.zoomTarget = 1.6;
148 |
149 |
150 | scope.angleTarget = handPos[0];
151 | }
152 |
153 | if(frame.gestures && frame.gestures.length > 0) {
154 | var gesture = frame.gestures[0];
155 |
156 | if(gesture.type === "circle") {
157 | // throttledChange();
158 | }
159 | }
160 | }
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/graphemescope.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Polyfill for requestAnimationFrame function
3 | # http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
4 | requestAnimFrame = (->
5 | window.requestAnimationFrame ||
6 | window.webkitRequestAnimationFrame ||
7 | window.mozRequestAnimationFrame ||
8 | (callback) ->
9 | window.setTimeout callback, (1000 / 30)
10 | )()
11 |
12 | # Калейдоскоп
13 | window.Graphemescope = class Graphemescope
14 | constructor: ( @parentElement = window.document.body ) ->
15 | @enabled = true
16 | @radiusFactor = 1.0
17 |
18 | # Конкретные значения угла и увеличения (используются внутренне)
19 | # Меняются от 0 до 1
20 | @zoomFactor = 1.0
21 | @angleFactor = 0.0
22 |
23 | # Значения угла и увеличения, доступные из интерфейса
24 | @zoomTarget = 1.2
25 | @angleTarget = 0.8
26 |
27 | # Настройки плавности движения
28 | @easeEnabled = true
29 | @ease = 0.1
30 |
31 | @domElement ?= document.createElement "canvas"
32 | @ctx ?= @domElement.getContext "2d"
33 |
34 | @alphaFactor = 1.0
35 | @alphaTarget = 1.0
36 |
37 | @parentElement.appendChild @domElement
38 |
39 | # Запоминаем старый обработчик события resize
40 | # TODO: сделать через addEventListener!
41 | @oldResizeHandler = () ->
42 |
43 | if window.onresize != null
44 | @oldResizeHandler = window.onresize
45 |
46 | window.onresize = () => do @resizeHandler
47 | do @resizeHandler
48 |
49 | requestAnimFrame => do @animationFrame
50 |
51 | animationFrame : ->
52 | requestAnimFrame => do @animationFrame
53 | if @enabled
54 | do @update
55 | do @draw
56 |
57 | # Обработчик resize события
58 | resizeHandler : ->
59 | @width = @domElement.width = @parentElement.offsetWidth
60 | @height = @domElement.height = @parentElement.offsetHeight
61 |
62 | @radius = 0.5 * @radiusFactor * Math.min(@width, @height)
63 | @radiusHeight = 0.5 * Math.sqrt(3) * @radius
64 |
65 | do @oldResizeHandler
66 |
67 | # Функция обновления параметром (для изменения параметров движения)
68 | update: ->
69 | if @easeEnabled
70 | # Плавность включена
71 | @angleFactor += ( @angleTarget - @angleFactor ) * @ease
72 | @zoomFactor += ( @zoomTarget - @zoomFactor ) * @ease
73 | @alphaFactor += ( @alphaTarget - @alphaFactor ) * @ease
74 | else
75 | # Плавность выключена
76 | @angleFactor = @angleTarget
77 | @zoomFactor = @zoomTarget
78 | @alphaFactor = @alphaTarget
79 |
80 | # Функция рисует заданную картинку в центре правильного треугольника
81 | drawImage : (image) ->
82 | @ctx.save()
83 |
84 | # Cчитаем радиус описанной окружности
85 | outerRadius = 2 / 3 * @radiusHeight
86 |
87 | # Делаем масштабирование таким, что при zoomFactor = 1 картинка полностью оптимально
88 | # заполняет треугольник
89 | zoom = @zoomFactor * outerRadius / (0.5 * Math.min(image.width, image.height))
90 |
91 | # Помещаем центр вращения в центр треугольника, то есть в центр описанной окружности.
92 | # Центр лежит на высоте и делит ее в отношении 2/3
93 | @ctx.translate 0, outerRadius
94 | @ctx.scale zoom, zoom
95 | @ctx.rotate @angleFactor * 2 * Math.PI
96 | @ctx.translate -0.5 * image.width, -0.5 * image.height
97 |
98 | @ctx.fill()
99 | @ctx.restore()
100 |
101 | # Функция рисует одну ячейку (соту) калейдоскопа в центре
102 | # системы координат с радиусом @radius
103 | drawCell : (image) ->
104 | # Сота состоит из 6 лепестков, каждый лепесток -
105 | # равносторонний треугольник с радиусом @radius
106 | for cellIndex in [ 0...6 ]
107 | @ctx.save()
108 | @ctx.rotate(cellIndex * 2.0 * Math.PI / 6.0)
109 |
110 | # Каждый следующий лепесток отображаем зеркально
111 | @ctx.scale [-1, 1][ cellIndex % 2], 1
112 | @ctx.beginPath()
113 |
114 | # Рисуем правильный треугольник, вспоминая школьные формулы:
115 | # 1. В равнобедренном (то есть и в правильном треугольнике) высота есть и медиана
116 | # 2. Высота равна sqrt(3) / 2 стороны треугольника
117 | @ctx.moveTo 0, 0
118 | @ctx.lineTo -0.5 * @radius, 1.0 * @radiusHeight
119 | @ctx.lineTo 0.5 * @radius, 1.0 * @radiusHeight
120 | @ctx.closePath()
121 |
122 | @drawImage image
123 |
124 | @ctx.restore()
125 |
126 | # Функция отрисовки
127 | drawLayer: (image) ->
128 | @ctx.save()
129 |
130 | # Перемещаемся в центр
131 | @ctx.translate 0.5 * @width, 0.5 * @height
132 |
133 | # Вычисляем, сколько сот нужно рисовать
134 | # (не уверен, что формулы работают оптимально, но экран
135 | # они покрывают)
136 | verticalLimit = Math.ceil(0.5 * @height / @radiusHeight)
137 | horizontalLimit = Math.ceil(0.5 * @width / (3 * @radius))
138 |
139 | horizontalStrype = [ -horizontalLimit .. horizontalLimit ]
140 | verticalStrype = [ -verticalLimit .. verticalLimit ]
141 |
142 | for v in verticalStrype
143 | @ctx.save()
144 | @ctx.translate 0, @radiusHeight * v
145 |
146 | # Сдвиг у нечетных слоев
147 | if (Math.abs(v) % 2)
148 | @ctx.translate 1.5 * @radius, 0
149 |
150 | for h in horizontalStrype
151 | @ctx.save()
152 |
153 | @ctx.translate 3 * h * @radius, 0
154 | @drawCell image
155 |
156 | @ctx.restore()
157 |
158 | @ctx.restore()
159 | @ctx.restore()
160 |
161 | # Главная функция отрисовки
162 | draw : ->
163 | if @imageProxy?
164 | @ctx.fillStyle = @patternProxy
165 | @ctx.globalAlpha = 1 - @alphaFactor
166 | @drawLayer @imageProxy
167 |
168 | if @image?
169 | @ctx.fillStyle = @pattern
170 | @ctx.globalAlpha = @alphaFactor
171 | @drawLayer @image
172 |
173 | # Меняет картинку калейдоскопа напрямую (предполагается, что объект уже загружен)
174 | setImageDirect : (image) ->
175 | if @image?
176 | @imageProxy = @image
177 | @patternProxy = @pattern
178 |
179 | @image = image
180 | @pattern = @ctx.createPattern @image, "repeat"
181 |
182 | @alphaFactor = 0.0
183 |
184 | # Меняет картинку калейдоскопа
185 | setImage : (image) ->
186 | if typeof image is "string"
187 | # Аргумент - это строка с адресом картинки
188 | imageElement = new Image()
189 | imageElement.src = image
190 | imageElement.onload = =>
191 | @setImageDirect imageElement
192 | else
193 | @setImageDirect image
194 |
195 |
196 |
197 |
--------------------------------------------------------------------------------
/demo/common/vendor/dancer.min.js:
--------------------------------------------------------------------------------
1 | /* dancer.js - v0.3.2 - 2012-09-29
2 | * https://github.com/jsantell/dancer.js
3 | * Copyright (c) 2012 Jordan Santell; Licensed MIT */
4 | function FourierTransform(e,t){this.bufferSize=e,this.sampleRate=t,this.bandwidth=2/e*t/2,this.spectrum=new Float32Array(e/2),this.real=new Float32Array(e),this.imag=new Float32Array(e),this.peakBand=0,this.peak=0,this.getBandFrequency=function(e){return this.bandwidth*e+this.bandwidth/2},this.calculateSpectrum=function(){var t=this.spectrum,n=this.real,r=this.imag,i=2/this.bufferSize,s=Math.sqrt,o,u,a;for(var f=0,l=e/2;fthis.peak&&(this.peakBand=f,this.peak=a),t[f]=a}}function FFT(e,t){FourierTransform.call(this,e,t),this.reverseTable=new Uint32Array(e);var n=1,r=e>>1,i;while(n>=1}this.sinTable=new Float32Array(e),this.cosTable=new Float32Array(e);for(i=0;ie},callback:t}),this},before:function(e,t){var n=this;return this.sections.push({condition:function(){return n.getTime()e&&r.getTime()e&&!this.called},callback:function(){t.call(this),r.called=!0},called:!1}),r=this.sections[this.sections.length-1],this}},window.Dancer=e})(),function(e){function r(){var e=!!(navigator.vendor||"").match(/Apple/),t=navigator.userAgent.match(/Version\/([^ ]*)/);return t=t?parseFloat(t[1]):0,e&&t<=6}var t={mp3:"audio/mpeg;",ogg:'audio/ogg; codecs="vorbis"',wav:'audio/wav; codecs="1"',aac:'audio/mp4; codecs="mp4a.40.2"'},n=document.createElement("audio");e.options={},e.setOptions=function(t){for(var n in t)t.hasOwnProperty(n)&&(e.options[n]=t[n])},e.isSupported=function(){return!window.Float32Array||!window.Uint32Array?null:!r()&&(window.AudioContext||window.webkitAudioContext)?"webaudio":n&&n.mozSetup?"audiodata":FlashDetect.versionAtLeast(9)?"flash":""},e.canPlay=function(r){var i=n.canPlayType;return e.isSupported()==="flash"?r.toLowerCase()==="mp3":!!n.canPlayType&&!!n.canPlayType(t[r.toLowerCase()]).replace(/no/,"")},e.addPlugin=function(t,n){e.prototype[t]===undefined&&(e.prototype[t]=n)},e._makeSupportedPath=function(t,n){if(!n)return t;for(var r=0;r=this.currentThreshold&&e>=this.threshold?(this.currentThreshold=e,this.onKick&&this.onKick.call(this.dancer,e)):(this.offKick&&this.offKick.call(this.dancer,e),this.currentThreshold-=this.decay)},maxAmplitude:function(e){var t=0,n=this.dancer.getSpectrum();if(!e.length)return et&&(t=n[r]);return t}},window.Dancer.Kick=t}(),function(){function r(){this.source=this.context.createMediaElementSource(this.audio),this.source.connect(this.proc),this.source.connect(this.gain),this.gain.connect(this.context.destination),this.proc.connect(this.context.destination),this.isLoaded=!0,this.progress=1,this.dancer.trigger("loaded")}var e=2048,t=44100,n=function(e){this.dancer=e,this.audio=new Audio,this.context=window.AudioContext?new window.AudioContext:new window.webkitAudioContext};n.prototype={load:function(n){var i=this;return this.audio=n,this.isLoaded=!1,this.progress=0,this.proc=this.context.createJavaScriptNode(e/2,1,1),this.proc.onaudioprocess=function(e){i.update.call(i,e)},this.gain=this.context.createGainNode(),this.fft=new FFT(e/2,t),this.signal=new Float32Array(e/2),this.audio.readyState<3?this.audio.addEventListener("canplay",function(){r.call(i)}):r.call(i),this.audio.addEventListener("progress",function(e){e.currentTarget.duration&&(i.progress=e.currentTarget.seekable.end(0)/e.currentTarget.duration)}),this.audio},play:function(){this.audio.play(),this.isPlaying=!0},pause:function(){this.audio.pause(),this.isPlaying=!1},setVolume:function(e){this.gain.gain.value=e},getVolume:function(){return this.gain.gain.value},getProgress:function(){return this.progress},getWaveform:function(){return this.signal},getSpectrum:function(){return this.fft.spectrum},getTime:function(){return this.audio.currentTime},update:function(t){if(!this.isPlaying||!this.isLoaded)return;var n=[],r=t.inputBuffer.numberOfChannels,i=e/r,s=function(e,t){return e[o]+t[o]},o;for(o=r;o--;)n.push(t.inputBuffer.getChannelData(o));for(o=0;o1?n.reduce(s)/r:n[0][o];this.fft.forward(this.signal),this.dancer.trigger("update")}},Dancer.adapters.webkit=n}(),function(){function t(){this.fbLength=this.audio.mozFrameBufferLength,this.channels=this.audio.mozChannels,this.rate=this.audio.mozSampleRate,this.fft=new FFT(this.fbLength/this.channels,this.rate),this.signal=new Float32Array(this.fbLength/this.channels),this.isLoaded=!0,this.progress=1,this.dancer.trigger("loaded")}var e=function(e){this.dancer=e,this.audio=new Audio};e.prototype={load:function(e){var n=this;return this.audio=e,this.isLoaded=!1,this.progress=0,this.audio.readyState<3?this.audio.addEventListener("loadedmetadata",function(){t.call(n)},!1):t.call(n),this.audio.addEventListener("MozAudioAvailable",function(e){n.update(e)},!1),this.audio.addEventListener("progress",function(e){e.currentTarget.duration&&(n.progress=e.currentTarget.seekable.end(0)/e.currentTarget.duration)},!1),this.audio},play:function(){this.audio.play(),this.isPlaying=!0},pause:function(){this.audio.pause(),this.isPlaying=!1},setVolume:function(e){this.audio.volume=e},getVolume:function(){return this.audio.volume},getProgress:function(){return this.progress},getWaveform:function(){return this.signal},getSpectrum:function(){return this.fft.spectrum},getTime:function(){return this.audio.currentTime},update:function(e){if(!this.isPlaying||!this.isLoaded)return;for(var t=0,n=this.fbLength/2;t=t},e.minorAtLeast=function(t){return e.minor>=t},e.revisionAtLeast=function(t){return e.revision>=t},e.versionAtLeast=function(t){var n=[e.major,e.minor,e.revision],r=Math.min(n.length,arguments.length);for(i=0;i=arguments[i]){if(i+10){var n="application/x-shockwave-flash",i=navigator.mimeTypes;if(i&&i[n]&&i[n].enabledPlugin&&i[n].enabledPlugin.description){var u=i[n].enabledPlugin.description,a=o(u);e.raw=a.raw,e.major=a.major,e.minor=a.minor,e.revisionStr=a.revisionStr,e.revision=a.revision,e.installed=!0}}else if(navigator.appVersion.indexOf("Mac")==-1&&window.execScript){var u=-1;for(var f=0;f 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 |
--------------------------------------------------------------------------------
/demo/common/vendor/backbone.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 1.0.0
2 |
3 | // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
4 | // Backbone may be freely distributed under the MIT license.
5 | // For all details and documentation:
6 | // http://backbonejs.org
7 |
8 | (function(){
9 |
10 | // Initial Setup
11 | // -------------
12 |
13 | // Save a reference to the global object (`window` in the browser, `exports`
14 | // on the server).
15 | var root = this;
16 |
17 | // Save the previous value of the `Backbone` variable, so that it can be
18 | // restored later on, if `noConflict` is used.
19 | var previousBackbone = root.Backbone;
20 |
21 | // Create local references to array methods we'll want to use later.
22 | var array = [];
23 | var push = array.push;
24 | var slice = array.slice;
25 | var splice = array.splice;
26 |
27 | // The top-level namespace. All public Backbone classes and modules will
28 | // be attached to this. Exported for both the browser and the server.
29 | var Backbone;
30 | if (typeof exports !== 'undefined') {
31 | Backbone = exports;
32 | } else {
33 | Backbone = root.Backbone = {};
34 | }
35 |
36 | // Current version of the library. Keep in sync with `package.json`.
37 | Backbone.VERSION = '1.0.0';
38 |
39 | // Require Underscore, if we're on the server, and it's not already present.
40 | var _ = root._;
41 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
42 |
43 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
44 | // the `$` variable.
45 | Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
46 |
47 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
48 | // to its previous owner. Returns a reference to this Backbone object.
49 | Backbone.noConflict = function() {
50 | root.Backbone = previousBackbone;
51 | return this;
52 | };
53 |
54 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
55 | // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
56 | // set a `X-Http-Method-Override` header.
57 | Backbone.emulateHTTP = false;
58 |
59 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct
60 | // `application/json` requests ... will encode the body as
61 | // `application/x-www-form-urlencoded` instead and will send the model in a
62 | // form param named `model`.
63 | Backbone.emulateJSON = false;
64 |
65 | // Backbone.Events
66 | // ---------------
67 |
68 | // A module that can be mixed in to *any object* in order to provide it with
69 | // custom events. You may bind with `on` or remove with `off` callback
70 | // functions to an event; `trigger`-ing an event fires all callbacks in
71 | // succession.
72 | //
73 | // var object = {};
74 | // _.extend(object, Backbone.Events);
75 | // object.on('expand', function(){ alert('expanded'); });
76 | // object.trigger('expand');
77 | //
78 | var Events = Backbone.Events = {
79 |
80 | // Bind an event to a `callback` function. Passing `"all"` will bind
81 | // the callback to all events fired.
82 | on: function(name, callback, context) {
83 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
84 | this._events || (this._events = {});
85 | var events = this._events[name] || (this._events[name] = []);
86 | events.push({callback: callback, context: context, ctx: context || this});
87 | return this;
88 | },
89 |
90 | // Bind an event to only be triggered a single time. After the first time
91 | // the callback is invoked, it will be removed.
92 | once: function(name, callback, context) {
93 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
94 | var self = this;
95 | var once = _.once(function() {
96 | self.off(name, once);
97 | callback.apply(this, arguments);
98 | });
99 | once._callback = callback;
100 | return this.on(name, once, context);
101 | },
102 |
103 | // Remove one or many callbacks. If `context` is null, removes all
104 | // callbacks with that function. If `callback` is null, removes all
105 | // callbacks for the event. If `name` is null, removes all bound
106 | // callbacks for all events.
107 | off: function(name, callback, context) {
108 | var retain, ev, events, names, i, l, j, k;
109 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
110 | if (!name && !callback && !context) {
111 | this._events = {};
112 | return this;
113 | }
114 |
115 | names = name ? [name] : _.keys(this._events);
116 | for (i = 0, l = names.length; i < l; i++) {
117 | name = names[i];
118 | if (events = this._events[name]) {
119 | this._events[name] = retain = [];
120 | if (callback || context) {
121 | for (j = 0, k = events.length; j < k; j++) {
122 | ev = events[j];
123 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
124 | (context && context !== ev.context)) {
125 | retain.push(ev);
126 | }
127 | }
128 | }
129 | if (!retain.length) delete this._events[name];
130 | }
131 | }
132 |
133 | return this;
134 | },
135 |
136 | // Trigger one or many events, firing all bound callbacks. Callbacks are
137 | // passed the same arguments as `trigger` is, apart from the event name
138 | // (unless you're listening on `"all"`, which will cause your callback to
139 | // receive the true name of the event as the first argument).
140 | trigger: function(name) {
141 | if (!this._events) return this;
142 | var args = slice.call(arguments, 1);
143 | if (!eventsApi(this, 'trigger', name, args)) return this;
144 | var events = this._events[name];
145 | var allEvents = this._events.all;
146 | if (events) triggerEvents(events, args);
147 | if (allEvents) triggerEvents(allEvents, arguments);
148 | return this;
149 | },
150 |
151 | // Tell this object to stop listening to either specific events ... or
152 | // to every object it's currently listening to.
153 | stopListening: function(obj, name, callback) {
154 | var listeners = this._listeners;
155 | if (!listeners) return this;
156 | var deleteListener = !name && !callback;
157 | if (typeof name === 'object') callback = this;
158 | if (obj) (listeners = {})[obj._listenerId] = obj;
159 | for (var id in listeners) {
160 | listeners[id].off(name, callback, this);
161 | if (deleteListener) delete this._listeners[id];
162 | }
163 | return this;
164 | }
165 |
166 | };
167 |
168 | // Regular expression used to split event strings.
169 | var eventSplitter = /\s+/;
170 |
171 | // Implement fancy features of the Events API such as multiple event
172 | // names `"change blur"` and jQuery-style event maps `{change: action}`
173 | // in terms of the existing API.
174 | var eventsApi = function(obj, action, name, rest) {
175 | if (!name) return true;
176 |
177 | // Handle event maps.
178 | if (typeof name === 'object') {
179 | for (var key in name) {
180 | obj[action].apply(obj, [key, name[key]].concat(rest));
181 | }
182 | return false;
183 | }
184 |
185 | // Handle space separated event names.
186 | if (eventSplitter.test(name)) {
187 | var names = name.split(eventSplitter);
188 | for (var i = 0, l = names.length; i < l; i++) {
189 | obj[action].apply(obj, [names[i]].concat(rest));
190 | }
191 | return false;
192 | }
193 |
194 | return true;
195 | };
196 |
197 | // A difficult-to-believe, but optimized internal dispatch function for
198 | // triggering events. Tries to keep the usual cases speedy (most internal
199 | // Backbone events have 3 arguments).
200 | var triggerEvents = function(events, args) {
201 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
202 | switch (args.length) {
203 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
204 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
205 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
206 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
207 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
208 | }
209 | };
210 |
211 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
212 |
213 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to
214 | // listen to an event in another object ... keeping track of what it's
215 | // listening to.
216 | _.each(listenMethods, function(implementation, method) {
217 | Events[method] = function(obj, name, callback) {
218 | var listeners = this._listeners || (this._listeners = {});
219 | var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
220 | listeners[id] = obj;
221 | if (typeof name === 'object') callback = this;
222 | obj[implementation](name, callback, this);
223 | return this;
224 | };
225 | });
226 |
227 | // Aliases for backwards compatibility.
228 | Events.bind = Events.on;
229 | Events.unbind = Events.off;
230 |
231 | // Allow the `Backbone` object to serve as a global event bus, for folks who
232 | // want global "pubsub" in a convenient place.
233 | _.extend(Backbone, Events);
234 |
235 | // Backbone.Model
236 | // --------------
237 |
238 | // Backbone **Models** are the basic data object in the framework --
239 | // frequently representing a row in a table in a database on your server.
240 | // A discrete chunk of data and a bunch of useful, related methods for
241 | // performing computations and transformations on that data.
242 |
243 | // Create a new model with the specified attributes. A client id (`cid`)
244 | // is automatically generated and assigned for you.
245 | var Model = Backbone.Model = function(attributes, options) {
246 | var defaults;
247 | var attrs = attributes || {};
248 | options || (options = {});
249 | this.cid = _.uniqueId('c');
250 | this.attributes = {};
251 | _.extend(this, _.pick(options, modelOptions));
252 | if (options.parse) attrs = this.parse(attrs, options) || {};
253 | if (defaults = _.result(this, 'defaults')) {
254 | attrs = _.defaults({}, attrs, defaults);
255 | }
256 | this.set(attrs, options);
257 | this.changed = {};
258 | this.initialize.apply(this, arguments);
259 | };
260 |
261 | // A list of options to be attached directly to the model, if provided.
262 | var modelOptions = ['url', 'urlRoot', 'collection'];
263 |
264 | // Attach all inheritable methods to the Model prototype.
265 | _.extend(Model.prototype, Events, {
266 |
267 | // A hash of attributes whose current and previous value differ.
268 | changed: null,
269 |
270 | // The value returned during the last failed validation.
271 | validationError: null,
272 |
273 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and
274 | // CouchDB users may want to set this to `"_id"`.
275 | idAttribute: 'id',
276 |
277 | // Initialize is an empty function by default. Override it with your own
278 | // initialization logic.
279 | initialize: function(){},
280 |
281 | // Return a copy of the model's `attributes` object.
282 | toJSON: function(options) {
283 | return _.clone(this.attributes);
284 | },
285 |
286 | // Proxy `Backbone.sync` by default -- but override this if you need
287 | // custom syncing semantics for *this* particular model.
288 | sync: function() {
289 | return Backbone.sync.apply(this, arguments);
290 | },
291 |
292 | // Get the value of an attribute.
293 | get: function(attr) {
294 | return this.attributes[attr];
295 | },
296 |
297 | // Get the HTML-escaped value of an attribute.
298 | escape: function(attr) {
299 | return _.escape(this.get(attr));
300 | },
301 |
302 | // Returns `true` if the attribute contains a value that is not null
303 | // or undefined.
304 | has: function(attr) {
305 | return this.get(attr) != null;
306 | },
307 |
308 | // Set a hash of model attributes on the object, firing `"change"`. This is
309 | // the core primitive operation of a model, updating the data and notifying
310 | // anyone who needs to know about the change in state. The heart of the beast.
311 | set: function(key, val, options) {
312 | var attr, attrs, unset, changes, silent, changing, prev, current;
313 | if (key == null) return this;
314 |
315 | // Handle both `"key", value` and `{key: value}` -style arguments.
316 | if (typeof key === 'object') {
317 | attrs = key;
318 | options = val;
319 | } else {
320 | (attrs = {})[key] = val;
321 | }
322 |
323 | options || (options = {});
324 |
325 | // Run validation.
326 | if (!this._validate(attrs, options)) return false;
327 |
328 | // Extract attributes and options.
329 | unset = options.unset;
330 | silent = options.silent;
331 | changes = [];
332 | changing = this._changing;
333 | this._changing = true;
334 |
335 | if (!changing) {
336 | this._previousAttributes = _.clone(this.attributes);
337 | this.changed = {};
338 | }
339 | current = this.attributes, prev = this._previousAttributes;
340 |
341 | // Check for changes of `id`.
342 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
343 |
344 | // For each `set` attribute, update or delete the current value.
345 | for (attr in attrs) {
346 | val = attrs[attr];
347 | if (!_.isEqual(current[attr], val)) changes.push(attr);
348 | if (!_.isEqual(prev[attr], val)) {
349 | this.changed[attr] = val;
350 | } else {
351 | delete this.changed[attr];
352 | }
353 | unset ? delete current[attr] : current[attr] = val;
354 | }
355 |
356 | // Trigger all relevant attribute changes.
357 | if (!silent) {
358 | if (changes.length) this._pending = true;
359 | for (var i = 0, l = changes.length; i < l; i++) {
360 | this.trigger('change:' + changes[i], this, current[changes[i]], options);
361 | }
362 | }
363 |
364 | // You might be wondering why there's a `while` loop here. Changes can
365 | // be recursively nested within `"change"` events.
366 | if (changing) return this;
367 | if (!silent) {
368 | while (this._pending) {
369 | this._pending = false;
370 | this.trigger('change', this, options);
371 | }
372 | }
373 | this._pending = false;
374 | this._changing = false;
375 | return this;
376 | },
377 |
378 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop
379 | // if the attribute doesn't exist.
380 | unset: function(attr, options) {
381 | return this.set(attr, void 0, _.extend({}, options, {unset: true}));
382 | },
383 |
384 | // Clear all attributes on the model, firing `"change"`.
385 | clear: function(options) {
386 | var attrs = {};
387 | for (var key in this.attributes) attrs[key] = void 0;
388 | return this.set(attrs, _.extend({}, options, {unset: true}));
389 | },
390 |
391 | // Determine if the model has changed since the last `"change"` event.
392 | // If you specify an attribute name, determine if that attribute has changed.
393 | hasChanged: function(attr) {
394 | if (attr == null) return !_.isEmpty(this.changed);
395 | return _.has(this.changed, attr);
396 | },
397 |
398 | // Return an object containing all the attributes that have changed, or
399 | // false if there are no changed attributes. Useful for determining what
400 | // parts of a view need to be updated and/or what attributes need to be
401 | // persisted to the server. Unset attributes will be set to undefined.
402 | // You can also pass an attributes object to diff against the model,
403 | // determining if there *would be* a change.
404 | changedAttributes: function(diff) {
405 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
406 | var val, changed = false;
407 | var old = this._changing ? this._previousAttributes : this.attributes;
408 | for (var attr in diff) {
409 | if (_.isEqual(old[attr], (val = diff[attr]))) continue;
410 | (changed || (changed = {}))[attr] = val;
411 | }
412 | return changed;
413 | },
414 |
415 | // Get the previous value of an attribute, recorded at the time the last
416 | // `"change"` event was fired.
417 | previous: function(attr) {
418 | if (attr == null || !this._previousAttributes) return null;
419 | return this._previousAttributes[attr];
420 | },
421 |
422 | // Get all of the attributes of the model at the time of the previous
423 | // `"change"` event.
424 | previousAttributes: function() {
425 | return _.clone(this._previousAttributes);
426 | },
427 |
428 | // Fetch the model from the server. If the server's representation of the
429 | // model differs from its current attributes, they will be overridden,
430 | // triggering a `"change"` event.
431 | fetch: function(options) {
432 | options = options ? _.clone(options) : {};
433 | if (options.parse === void 0) options.parse = true;
434 | var model = this;
435 | var success = options.success;
436 | options.success = function(resp) {
437 | if (!model.set(model.parse(resp, options), options)) return false;
438 | if (success) success(model, resp, options);
439 | model.trigger('sync', model, resp, options);
440 | };
441 | wrapError(this, options);
442 | return this.sync('read', this, options);
443 | },
444 |
445 | // Set a hash of model attributes, and sync the model to the server.
446 | // If the server returns an attributes hash that differs, the model's
447 | // state will be `set` again.
448 | save: function(key, val, options) {
449 | var attrs, method, xhr, attributes = this.attributes;
450 |
451 | // Handle both `"key", value` and `{key: value}` -style arguments.
452 | if (key == null || typeof key === 'object') {
453 | attrs = key;
454 | options = val;
455 | } else {
456 | (attrs = {})[key] = val;
457 | }
458 |
459 | // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
460 | if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
461 |
462 | options = _.extend({validate: true}, options);
463 |
464 | // Do not persist invalid models.
465 | if (!this._validate(attrs, options)) return false;
466 |
467 | // Set temporary attributes if `{wait: true}`.
468 | if (attrs && options.wait) {
469 | this.attributes = _.extend({}, attributes, attrs);
470 | }
471 |
472 | // After a successful server-side save, the client is (optionally)
473 | // updated with the server-side state.
474 | if (options.parse === void 0) options.parse = true;
475 | var model = this;
476 | var success = options.success;
477 | options.success = function(resp) {
478 | // Ensure attributes are restored during synchronous saves.
479 | model.attributes = attributes;
480 | var serverAttrs = model.parse(resp, options);
481 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
482 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
483 | return false;
484 | }
485 | if (success) success(model, resp, options);
486 | model.trigger('sync', model, resp, options);
487 | };
488 | wrapError(this, options);
489 |
490 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
491 | if (method === 'patch') options.attrs = attrs;
492 | xhr = this.sync(method, this, options);
493 |
494 | // Restore attributes.
495 | if (attrs && options.wait) this.attributes = attributes;
496 |
497 | return xhr;
498 | },
499 |
500 | // Destroy this model on the server if it was already persisted.
501 | // Optimistically removes the model from its collection, if it has one.
502 | // If `wait: true` is passed, waits for the server to respond before removal.
503 | destroy: function(options) {
504 | options = options ? _.clone(options) : {};
505 | var model = this;
506 | var success = options.success;
507 |
508 | var destroy = function() {
509 | model.trigger('destroy', model, model.collection, options);
510 | };
511 |
512 | options.success = function(resp) {
513 | if (options.wait || model.isNew()) destroy();
514 | if (success) success(model, resp, options);
515 | if (!model.isNew()) model.trigger('sync', model, resp, options);
516 | };
517 |
518 | if (this.isNew()) {
519 | options.success();
520 | return false;
521 | }
522 | wrapError(this, options);
523 |
524 | var xhr = this.sync('delete', this, options);
525 | if (!options.wait) destroy();
526 | return xhr;
527 | },
528 |
529 | // Default URL for the model's representation on the server -- if you're
530 | // using Backbone's restful methods, override this to change the endpoint
531 | // that will be called.
532 | url: function() {
533 | var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
534 | if (this.isNew()) return base;
535 | return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
536 | },
537 |
538 | // **parse** converts a response into the hash of attributes to be `set` on
539 | // the model. The default implementation is just to pass the response along.
540 | parse: function(resp, options) {
541 | return resp;
542 | },
543 |
544 | // Create a new model with identical attributes to this one.
545 | clone: function() {
546 | return new this.constructor(this.attributes);
547 | },
548 |
549 | // A model is new if it has never been saved to the server, and lacks an id.
550 | isNew: function() {
551 | return this.id == null;
552 | },
553 |
554 | // Check if the model is currently in a valid state.
555 | isValid: function(options) {
556 | return this._validate({}, _.extend(options || {}, { validate: true }));
557 | },
558 |
559 | // Run validation against the next complete set of model attributes,
560 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
561 | _validate: function(attrs, options) {
562 | if (!options.validate || !this.validate) return true;
563 | attrs = _.extend({}, this.attributes, attrs);
564 | var error = this.validationError = this.validate(attrs, options) || null;
565 | if (!error) return true;
566 | this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
567 | return false;
568 | }
569 |
570 | });
571 |
572 | // Underscore methods that we want to implement on the Model.
573 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
574 |
575 | // Mix in each Underscore method as a proxy to `Model#attributes`.
576 | _.each(modelMethods, function(method) {
577 | Model.prototype[method] = function() {
578 | var args = slice.call(arguments);
579 | args.unshift(this.attributes);
580 | return _[method].apply(_, args);
581 | };
582 | });
583 |
584 | // Backbone.Collection
585 | // -------------------
586 |
587 | // If models tend to represent a single row of data, a Backbone Collection is
588 | // more analagous to a table full of data ... or a small slice or page of that
589 | // table, or a collection of rows that belong together for a particular reason
590 | // -- all of the messages in this particular folder, all of the documents
591 | // belonging to this particular author, and so on. Collections maintain
592 | // indexes of their models, both in order, and for lookup by `id`.
593 |
594 | // Create a new **Collection**, perhaps to contain a specific type of `model`.
595 | // If a `comparator` is specified, the Collection will maintain
596 | // its models in sort order, as they're added and removed.
597 | var Collection = Backbone.Collection = function(models, options) {
598 | options || (options = {});
599 | if (options.url) this.url = options.url;
600 | if (options.model) this.model = options.model;
601 | if (options.comparator !== void 0) this.comparator = options.comparator;
602 | this._reset();
603 | this.initialize.apply(this, arguments);
604 | if (models) this.reset(models, _.extend({silent: true}, options));
605 | };
606 |
607 | // Default options for `Collection#set`.
608 | var setOptions = {add: true, remove: true, merge: true};
609 | var addOptions = {add: true, merge: false, remove: false};
610 |
611 | // Define the Collection's inheritable methods.
612 | _.extend(Collection.prototype, Events, {
613 |
614 | // The default model for a collection is just a **Backbone.Model**.
615 | // This should be overridden in most cases.
616 | model: Model,
617 |
618 | // Initialize is an empty function by default. Override it with your own
619 | // initialization logic.
620 | initialize: function(){},
621 |
622 | // The JSON representation of a Collection is an array of the
623 | // models' attributes.
624 | toJSON: function(options) {
625 | return this.map(function(model){ return model.toJSON(options); });
626 | },
627 |
628 | // Proxy `Backbone.sync` by default.
629 | sync: function() {
630 | return Backbone.sync.apply(this, arguments);
631 | },
632 |
633 | // Add a model, or list of models to the set.
634 | add: function(models, options) {
635 | return this.set(models, _.defaults(options || {}, addOptions));
636 | },
637 |
638 | // Remove a model, or a list of models from the set.
639 | remove: function(models, options) {
640 | models = _.isArray(models) ? models.slice() : [models];
641 | options || (options = {});
642 | var i, l, index, model;
643 | for (i = 0, l = models.length; i < l; i++) {
644 | model = this.get(models[i]);
645 | if (!model) continue;
646 | delete this._byId[model.id];
647 | delete this._byId[model.cid];
648 | index = this.indexOf(model);
649 | this.models.splice(index, 1);
650 | this.length--;
651 | if (!options.silent) {
652 | options.index = index;
653 | model.trigger('remove', model, this, options);
654 | }
655 | this._removeReference(model);
656 | }
657 | return this;
658 | },
659 |
660 | // Update a collection by `set`-ing a new list of models, adding new ones,
661 | // removing models that are no longer present, and merging models that
662 | // already exist in the collection, as necessary. Similar to **Model#set**,
663 | // the core operation for updating the data contained by the collection.
664 | set: function(models, options) {
665 | options = _.defaults(options || {}, setOptions);
666 | if (options.parse) models = this.parse(models, options);
667 | if (!_.isArray(models)) models = models ? [models] : [];
668 | var i, l, model, attrs, existing, sort;
669 | var at = options.at;
670 | var sortable = this.comparator && (at == null) && options.sort !== false;
671 | var sortAttr = _.isString(this.comparator) ? this.comparator : null;
672 | var toAdd = [], toRemove = [], modelMap = {};
673 |
674 | // Turn bare objects into model references, and prevent invalid models
675 | // from being added.
676 | for (i = 0, l = models.length; i < l; i++) {
677 | if (!(model = this._prepareModel(models[i], options))) continue;
678 |
679 | // If a duplicate is found, prevent it from being added and
680 | // optionally merge it into the existing model.
681 | if (existing = this.get(model)) {
682 | if (options.remove) modelMap[existing.cid] = true;
683 | if (options.merge) {
684 | existing.set(model.attributes, options);
685 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
686 | }
687 |
688 | // This is a new model, push it to the `toAdd` list.
689 | } else if (options.add) {
690 | toAdd.push(model);
691 |
692 | // Listen to added models' events, and index models for lookup by
693 | // `id` and by `cid`.
694 | model.on('all', this._onModelEvent, this);
695 | this._byId[model.cid] = model;
696 | if (model.id != null) this._byId[model.id] = model;
697 | }
698 | }
699 |
700 | // Remove nonexistent models if appropriate.
701 | if (options.remove) {
702 | for (i = 0, l = this.length; i < l; ++i) {
703 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
704 | }
705 | if (toRemove.length) this.remove(toRemove, options);
706 | }
707 |
708 | // See if sorting is needed, update `length` and splice in new models.
709 | if (toAdd.length) {
710 | if (sortable) sort = true;
711 | this.length += toAdd.length;
712 | if (at != null) {
713 | splice.apply(this.models, [at, 0].concat(toAdd));
714 | } else {
715 | push.apply(this.models, toAdd);
716 | }
717 | }
718 |
719 | // Silently sort the collection if appropriate.
720 | if (sort) this.sort({silent: true});
721 |
722 | if (options.silent) return this;
723 |
724 | // Trigger `add` events.
725 | for (i = 0, l = toAdd.length; i < l; i++) {
726 | (model = toAdd[i]).trigger('add', model, this, options);
727 | }
728 |
729 | // Trigger `sort` if the collection was sorted.
730 | if (sort) this.trigger('sort', this, options);
731 | return this;
732 | },
733 |
734 | // When you have more items than you want to add or remove individually,
735 | // you can reset the entire set with a new list of models, without firing
736 | // any granular `add` or `remove` events. Fires `reset` when finished.
737 | // Useful for bulk operations and optimizations.
738 | reset: function(models, options) {
739 | options || (options = {});
740 | for (var i = 0, l = this.models.length; i < l; i++) {
741 | this._removeReference(this.models[i]);
742 | }
743 | options.previousModels = this.models;
744 | this._reset();
745 | this.add(models, _.extend({silent: true}, options));
746 | if (!options.silent) this.trigger('reset', this, options);
747 | return this;
748 | },
749 |
750 | // Add a model to the end of the collection.
751 | push: function(model, options) {
752 | model = this._prepareModel(model, options);
753 | this.add(model, _.extend({at: this.length}, options));
754 | return model;
755 | },
756 |
757 | // Remove a model from the end of the collection.
758 | pop: function(options) {
759 | var model = this.at(this.length - 1);
760 | this.remove(model, options);
761 | return model;
762 | },
763 |
764 | // Add a model to the beginning of the collection.
765 | unshift: function(model, options) {
766 | model = this._prepareModel(model, options);
767 | this.add(model, _.extend({at: 0}, options));
768 | return model;
769 | },
770 |
771 | // Remove a model from the beginning of the collection.
772 | shift: function(options) {
773 | var model = this.at(0);
774 | this.remove(model, options);
775 | return model;
776 | },
777 |
778 | // Slice out a sub-array of models from the collection.
779 | slice: function(begin, end) {
780 | return this.models.slice(begin, end);
781 | },
782 |
783 | // Get a model from the set by id.
784 | get: function(obj) {
785 | if (obj == null) return void 0;
786 | return this._byId[obj.id != null ? obj.id : obj.cid || obj];
787 | },
788 |
789 | // Get the model at the given index.
790 | at: function(index) {
791 | return this.models[index];
792 | },
793 |
794 | // Return models with matching attributes. Useful for simple cases of
795 | // `filter`.
796 | where: function(attrs, first) {
797 | if (_.isEmpty(attrs)) return first ? void 0 : [];
798 | return this[first ? 'find' : 'filter'](function(model) {
799 | for (var key in attrs) {
800 | if (attrs[key] !== model.get(key)) return false;
801 | }
802 | return true;
803 | });
804 | },
805 |
806 | // Return the first model with matching attributes. Useful for simple cases
807 | // of `find`.
808 | findWhere: function(attrs) {
809 | return this.where(attrs, true);
810 | },
811 |
812 | // Force the collection to re-sort itself. You don't need to call this under
813 | // normal circumstances, as the set will maintain sort order as each item
814 | // is added.
815 | sort: function(options) {
816 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
817 | options || (options = {});
818 |
819 | // Run sort based on type of `comparator`.
820 | if (_.isString(this.comparator) || this.comparator.length === 1) {
821 | this.models = this.sortBy(this.comparator, this);
822 | } else {
823 | this.models.sort(_.bind(this.comparator, this));
824 | }
825 |
826 | if (!options.silent) this.trigger('sort', this, options);
827 | return this;
828 | },
829 |
830 | // Figure out the smallest index at which a model should be inserted so as
831 | // to maintain order.
832 | sortedIndex: function(model, value, context) {
833 | value || (value = this.comparator);
834 | var iterator = _.isFunction(value) ? value : function(model) {
835 | return model.get(value);
836 | };
837 | return _.sortedIndex(this.models, model, iterator, context);
838 | },
839 |
840 | // Pluck an attribute from each model in the collection.
841 | pluck: function(attr) {
842 | return _.invoke(this.models, 'get', attr);
843 | },
844 |
845 | // Fetch the default set of models for this collection, resetting the
846 | // collection when they arrive. If `reset: true` is passed, the response
847 | // data will be passed through the `reset` method instead of `set`.
848 | fetch: function(options) {
849 | options = options ? _.clone(options) : {};
850 | if (options.parse === void 0) options.parse = true;
851 | var success = options.success;
852 | var collection = this;
853 | options.success = function(resp) {
854 | var method = options.reset ? 'reset' : 'set';
855 | collection[method](resp, options);
856 | if (success) success(collection, resp, options);
857 | collection.trigger('sync', collection, resp, options);
858 | };
859 | wrapError(this, options);
860 | return this.sync('read', this, options);
861 | },
862 |
863 | // Create a new instance of a model in this collection. Add the model to the
864 | // collection immediately, unless `wait: true` is passed, in which case we
865 | // wait for the server to agree.
866 | create: function(model, options) {
867 | options = options ? _.clone(options) : {};
868 | if (!(model = this._prepareModel(model, options))) return false;
869 | if (!options.wait) this.add(model, options);
870 | var collection = this;
871 | var success = options.success;
872 | options.success = function(resp) {
873 | if (options.wait) collection.add(model, options);
874 | if (success) success(model, resp, options);
875 | };
876 | model.save(null, options);
877 | return model;
878 | },
879 |
880 | // **parse** converts a response into a list of models to be added to the
881 | // collection. The default implementation is just to pass it through.
882 | parse: function(resp, options) {
883 | return resp;
884 | },
885 |
886 | // Create a new collection with an identical list of models as this one.
887 | clone: function() {
888 | return new this.constructor(this.models);
889 | },
890 |
891 | // Private method to reset all internal state. Called when the collection
892 | // is first initialized or reset.
893 | _reset: function() {
894 | this.length = 0;
895 | this.models = [];
896 | this._byId = {};
897 | },
898 |
899 | // Prepare a hash of attributes (or other model) to be added to this
900 | // collection.
901 | _prepareModel: function(attrs, options) {
902 | if (attrs instanceof Model) {
903 | if (!attrs.collection) attrs.collection = this;
904 | return attrs;
905 | }
906 | options || (options = {});
907 | options.collection = this;
908 | var model = new this.model(attrs, options);
909 | if (!model._validate(attrs, options)) {
910 | this.trigger('invalid', this, attrs, options);
911 | return false;
912 | }
913 | return model;
914 | },
915 |
916 | // Internal method to sever a model's ties to a collection.
917 | _removeReference: function(model) {
918 | if (this === model.collection) delete model.collection;
919 | model.off('all', this._onModelEvent, this);
920 | },
921 |
922 | // Internal method called every time a model in the set fires an event.
923 | // Sets need to update their indexes when models change ids. All other
924 | // events simply proxy through. "add" and "remove" events that originate
925 | // in other collections are ignored.
926 | _onModelEvent: function(event, model, collection, options) {
927 | if ((event === 'add' || event === 'remove') && collection !== this) return;
928 | if (event === 'destroy') this.remove(model, options);
929 | if (model && event === 'change:' + model.idAttribute) {
930 | delete this._byId[model.previous(model.idAttribute)];
931 | if (model.id != null) this._byId[model.id] = model;
932 | }
933 | this.trigger.apply(this, arguments);
934 | }
935 |
936 | });
937 |
938 | // Underscore methods that we want to implement on the Collection.
939 | // 90% of the core usefulness of Backbone Collections is actually implemented
940 | // right here:
941 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
942 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
943 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
944 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
945 | 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
946 | 'isEmpty', 'chain'];
947 |
948 | // Mix in each Underscore method as a proxy to `Collection#models`.
949 | _.each(methods, function(method) {
950 | Collection.prototype[method] = function() {
951 | var args = slice.call(arguments);
952 | args.unshift(this.models);
953 | return _[method].apply(_, args);
954 | };
955 | });
956 |
957 | // Underscore methods that take a property name as an argument.
958 | var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
959 |
960 | // Use attributes instead of properties.
961 | _.each(attributeMethods, function(method) {
962 | Collection.prototype[method] = function(value, context) {
963 | var iterator = _.isFunction(value) ? value : function(model) {
964 | return model.get(value);
965 | };
966 | return _[method](this.models, iterator, context);
967 | };
968 | });
969 |
970 | // Backbone.View
971 | // -------------
972 |
973 | // Backbone Views are almost more convention than they are actual code. A View
974 | // is simply a JavaScript object that represents a logical chunk of UI in the
975 | // DOM. This might be a single item, an entire list, a sidebar or panel, or
976 | // even the surrounding frame which wraps your whole app. Defining a chunk of
977 | // UI as a **View** allows you to define your DOM events declaratively, without
978 | // having to worry about render order ... and makes it easy for the view to
979 | // react to specific changes in the state of your models.
980 |
981 | // Creating a Backbone.View creates its initial element outside of the DOM,
982 | // if an existing element is not provided...
983 | var View = Backbone.View = function(options) {
984 | this.cid = _.uniqueId('view');
985 | this._configure(options || {});
986 | this._ensureElement();
987 | this.initialize.apply(this, arguments);
988 | this.delegateEvents();
989 | };
990 |
991 | // Cached regex to split keys for `delegate`.
992 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
993 |
994 | // List of view options to be merged as properties.
995 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
996 |
997 | // Set up all inheritable **Backbone.View** properties and methods.
998 | _.extend(View.prototype, Events, {
999 |
1000 | // The default `tagName` of a View's element is `"div"`.
1001 | tagName: 'div',
1002 |
1003 | // jQuery delegate for element lookup, scoped to DOM elements within the
1004 | // current view. This should be prefered to global lookups where possible.
1005 | $: function(selector) {
1006 | return this.$el.find(selector);
1007 | },
1008 |
1009 | // Initialize is an empty function by default. Override it with your own
1010 | // initialization logic.
1011 | initialize: function(){},
1012 |
1013 | // **render** is the core function that your view should override, in order
1014 | // to populate its element (`this.el`), with the appropriate HTML. The
1015 | // convention is for **render** to always return `this`.
1016 | render: function() {
1017 | return this;
1018 | },
1019 |
1020 | // Remove this view by taking the element out of the DOM, and removing any
1021 | // applicable Backbone.Events listeners.
1022 | remove: function() {
1023 | this.$el.remove();
1024 | this.stopListening();
1025 | return this;
1026 | },
1027 |
1028 | // Change the view's element (`this.el` property), including event
1029 | // re-delegation.
1030 | setElement: function(element, delegate) {
1031 | if (this.$el) this.undelegateEvents();
1032 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1033 | this.el = this.$el[0];
1034 | if (delegate !== false) this.delegateEvents();
1035 | return this;
1036 | },
1037 |
1038 | // Set callbacks, where `this.events` is a hash of
1039 | //
1040 | // *{"event selector": "callback"}*
1041 | //
1042 | // {
1043 | // 'mousedown .title': 'edit',
1044 | // 'click .button': 'save'
1045 | // 'click .open': function(e) { ... }
1046 | // }
1047 | //
1048 | // pairs. Callbacks will be bound to the view, with `this` set properly.
1049 | // Uses event delegation for efficiency.
1050 | // Omitting the selector binds the event to `this.el`.
1051 | // This only works for delegate-able events: not `focus`, `blur`, and
1052 | // not `change`, `submit`, and `reset` in Internet Explorer.
1053 | delegateEvents: function(events) {
1054 | if (!(events || (events = _.result(this, 'events')))) return this;
1055 | this.undelegateEvents();
1056 | for (var key in events) {
1057 | var method = events[key];
1058 | if (!_.isFunction(method)) method = this[events[key]];
1059 | if (!method) continue;
1060 |
1061 | var match = key.match(delegateEventSplitter);
1062 | var eventName = match[1], selector = match[2];
1063 | method = _.bind(method, this);
1064 | eventName += '.delegateEvents' + this.cid;
1065 | if (selector === '') {
1066 | this.$el.on(eventName, method);
1067 | } else {
1068 | this.$el.on(eventName, selector, method);
1069 | }
1070 | }
1071 | return this;
1072 | },
1073 |
1074 | // Clears all callbacks previously bound to the view with `delegateEvents`.
1075 | // You usually don't need to use this, but may wish to if you have multiple
1076 | // Backbone views attached to the same DOM element.
1077 | undelegateEvents: function() {
1078 | this.$el.off('.delegateEvents' + this.cid);
1079 | return this;
1080 | },
1081 |
1082 | // Performs the initial configuration of a View with a set of options.
1083 | // Keys with special meaning *(e.g. model, collection, id, className)* are
1084 | // attached directly to the view. See `viewOptions` for an exhaustive
1085 | // list.
1086 | _configure: function(options) {
1087 | if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1088 | _.extend(this, _.pick(options, viewOptions));
1089 | this.options = options;
1090 | },
1091 |
1092 | // Ensure that the View has a DOM element to render into.
1093 | // If `this.el` is a string, pass it through `$()`, take the first
1094 | // matching element, and re-assign it to `el`. Otherwise, create
1095 | // an element from the `id`, `className` and `tagName` properties.
1096 | _ensureElement: function() {
1097 | if (!this.el) {
1098 | var attrs = _.extend({}, _.result(this, 'attributes'));
1099 | if (this.id) attrs.id = _.result(this, 'id');
1100 | if (this.className) attrs['class'] = _.result(this, 'className');
1101 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1102 | this.setElement($el, false);
1103 | } else {
1104 | this.setElement(_.result(this, 'el'), false);
1105 | }
1106 | }
1107 |
1108 | });
1109 |
1110 | // Backbone.sync
1111 | // -------------
1112 |
1113 | // Override this function to change the manner in which Backbone persists
1114 | // models to the server. You will be passed the type of request, and the
1115 | // model in question. By default, makes a RESTful Ajax request
1116 | // to the model's `url()`. Some possible customizations could be:
1117 | //
1118 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1119 | // * Send up the models as XML instead of JSON.
1120 | // * Persist models via WebSockets instead of Ajax.
1121 | //
1122 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1123 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1124 | // as well as all requests with the body as `application/x-www-form-urlencoded`
1125 | // instead of `application/json` with the model in a param named `model`.
1126 | // Useful when interfacing with server-side languages like **PHP** that make
1127 | // it difficult to read the body of `PUT` requests.
1128 | Backbone.sync = function(method, model, options) {
1129 | var type = methodMap[method];
1130 |
1131 | // Default options, unless specified.
1132 | _.defaults(options || (options = {}), {
1133 | emulateHTTP: Backbone.emulateHTTP,
1134 | emulateJSON: Backbone.emulateJSON
1135 | });
1136 |
1137 | // Default JSON-request options.
1138 | var params = {type: type, dataType: 'json'};
1139 |
1140 | // Ensure that we have a URL.
1141 | if (!options.url) {
1142 | params.url = _.result(model, 'url') || urlError();
1143 | }
1144 |
1145 | // Ensure that we have the appropriate request data.
1146 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1147 | params.contentType = 'application/json';
1148 | params.data = JSON.stringify(options.attrs || model.toJSON(options));
1149 | }
1150 |
1151 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1152 | if (options.emulateJSON) {
1153 | params.contentType = 'application/x-www-form-urlencoded';
1154 | params.data = params.data ? {model: params.data} : {};
1155 | }
1156 |
1157 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1158 | // And an `X-HTTP-Method-Override` header.
1159 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1160 | params.type = 'POST';
1161 | if (options.emulateJSON) params.data._method = type;
1162 | var beforeSend = options.beforeSend;
1163 | options.beforeSend = function(xhr) {
1164 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1165 | if (beforeSend) return beforeSend.apply(this, arguments);
1166 | };
1167 | }
1168 |
1169 | // Don't process data on a non-GET request.
1170 | if (params.type !== 'GET' && !options.emulateJSON) {
1171 | params.processData = false;
1172 | }
1173 |
1174 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1175 | // that still has ActiveX enabled by default, override jQuery to use that
1176 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1177 | if (params.type === 'PATCH' && window.ActiveXObject &&
1178 | !(window.external && window.external.msActiveXFilteringEnabled)) {
1179 | params.xhr = function() {
1180 | return new ActiveXObject("Microsoft.XMLHTTP");
1181 | };
1182 | }
1183 |
1184 | // Make the request, allowing the user to override any Ajax options.
1185 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1186 | model.trigger('request', model, xhr, options);
1187 | return xhr;
1188 | };
1189 |
1190 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1191 | var methodMap = {
1192 | 'create': 'POST',
1193 | 'update': 'PUT',
1194 | 'patch': 'PATCH',
1195 | 'delete': 'DELETE',
1196 | 'read': 'GET'
1197 | };
1198 |
1199 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1200 | // Override this if you'd like to use a different library.
1201 | Backbone.ajax = function() {
1202 | return Backbone.$.ajax.apply(Backbone.$, arguments);
1203 | };
1204 |
1205 | // Backbone.Router
1206 | // ---------------
1207 |
1208 | // Routers map faux-URLs to actions, and fire events when routes are
1209 | // matched. Creating a new one sets its `routes` hash, if not set statically.
1210 | var Router = Backbone.Router = function(options) {
1211 | options || (options = {});
1212 | if (options.routes) this.routes = options.routes;
1213 | this._bindRoutes();
1214 | this.initialize.apply(this, arguments);
1215 | };
1216 |
1217 | // Cached regular expressions for matching named param parts and splatted
1218 | // parts of route strings.
1219 | var optionalParam = /\((.*?)\)/g;
1220 | var namedParam = /(\(\?)?:\w+/g;
1221 | var splatParam = /\*\w+/g;
1222 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
1223 |
1224 | // Set up all inheritable **Backbone.Router** properties and methods.
1225 | _.extend(Router.prototype, Events, {
1226 |
1227 | // Initialize is an empty function by default. Override it with your own
1228 | // initialization logic.
1229 | initialize: function(){},
1230 |
1231 | // Manually bind a single named route to a callback. For example:
1232 | //
1233 | // this.route('search/:query/p:num', 'search', function(query, num) {
1234 | // ...
1235 | // });
1236 | //
1237 | route: function(route, name, callback) {
1238 | if (!_.isRegExp(route)) route = this._routeToRegExp(route);
1239 | if (_.isFunction(name)) {
1240 | callback = name;
1241 | name = '';
1242 | }
1243 | if (!callback) callback = this[name];
1244 | var router = this;
1245 | Backbone.history.route(route, function(fragment) {
1246 | var args = router._extractParameters(route, fragment);
1247 | callback && callback.apply(router, args);
1248 | router.trigger.apply(router, ['route:' + name].concat(args));
1249 | router.trigger('route', name, args);
1250 | Backbone.history.trigger('route', router, name, args);
1251 | });
1252 | return this;
1253 | },
1254 |
1255 | // Simple proxy to `Backbone.history` to save a fragment into the history.
1256 | navigate: function(fragment, options) {
1257 | Backbone.history.navigate(fragment, options);
1258 | return this;
1259 | },
1260 |
1261 | // Bind all defined routes to `Backbone.history`. We have to reverse the
1262 | // order of the routes here to support behavior where the most general
1263 | // routes can be defined at the bottom of the route map.
1264 | _bindRoutes: function() {
1265 | if (!this.routes) return;
1266 | this.routes = _.result(this, 'routes');
1267 | var route, routes = _.keys(this.routes);
1268 | while ((route = routes.pop()) != null) {
1269 | this.route(route, this.routes[route]);
1270 | }
1271 | },
1272 |
1273 | // Convert a route string into a regular expression, suitable for matching
1274 | // against the current location hash.
1275 | _routeToRegExp: function(route) {
1276 | route = route.replace(escapeRegExp, '\\$&')
1277 | .replace(optionalParam, '(?:$1)?')
1278 | .replace(namedParam, function(match, optional){
1279 | return optional ? match : '([^\/]+)';
1280 | })
1281 | .replace(splatParam, '(.*?)');
1282 | return new RegExp('^' + route + '$');
1283 | },
1284 |
1285 | // Given a route, and a URL fragment that it matches, return the array of
1286 | // extracted decoded parameters. Empty or unmatched parameters will be
1287 | // treated as `null` to normalize cross-browser behavior.
1288 | _extractParameters: function(route, fragment) {
1289 | var params = route.exec(fragment).slice(1);
1290 | return _.map(params, function(param) {
1291 | return param ? decodeURIComponent(param) : null;
1292 | });
1293 | }
1294 |
1295 | });
1296 |
1297 | // Backbone.History
1298 | // ----------------
1299 |
1300 | // Handles cross-browser history management, based on either
1301 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
1302 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
1303 | // and URL fragments. If the browser supports neither (old IE, natch),
1304 | // falls back to polling.
1305 | var History = Backbone.History = function() {
1306 | this.handlers = [];
1307 | _.bindAll(this, 'checkUrl');
1308 |
1309 | // Ensure that `History` can be used outside of the browser.
1310 | if (typeof window !== 'undefined') {
1311 | this.location = window.location;
1312 | this.history = window.history;
1313 | }
1314 | };
1315 |
1316 | // Cached regex for stripping a leading hash/slash and trailing space.
1317 | var routeStripper = /^[#\/]|\s+$/g;
1318 |
1319 | // Cached regex for stripping leading and trailing slashes.
1320 | var rootStripper = /^\/+|\/+$/g;
1321 |
1322 | // Cached regex for detecting MSIE.
1323 | var isExplorer = /msie [\w.]+/;
1324 |
1325 | // Cached regex for removing a trailing slash.
1326 | var trailingSlash = /\/$/;
1327 |
1328 | // Has the history handling already been started?
1329 | History.started = false;
1330 |
1331 | // Set up all inheritable **Backbone.History** properties and methods.
1332 | _.extend(History.prototype, Events, {
1333 |
1334 | // The default interval to poll for hash changes, if necessary, is
1335 | // twenty times a second.
1336 | interval: 50,
1337 |
1338 | // Gets the true hash value. Cannot use location.hash directly due to bug
1339 | // in Firefox where location.hash will always be decoded.
1340 | getHash: function(window) {
1341 | var match = (window || this).location.href.match(/#(.*)$/);
1342 | return match ? match[1] : '';
1343 | },
1344 |
1345 | // Get the cross-browser normalized URL fragment, either from the URL,
1346 | // the hash, or the override.
1347 | getFragment: function(fragment, forcePushState) {
1348 | if (fragment == null) {
1349 | if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1350 | fragment = this.location.pathname;
1351 | var root = this.root.replace(trailingSlash, '');
1352 | if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
1353 | } else {
1354 | fragment = this.getHash();
1355 | }
1356 | }
1357 | return fragment.replace(routeStripper, '');
1358 | },
1359 |
1360 | // Start the hash change handling, returning `true` if the current URL matches
1361 | // an existing route, and `false` otherwise.
1362 | start: function(options) {
1363 | if (History.started) throw new Error("Backbone.history has already been started");
1364 | History.started = true;
1365 |
1366 | // Figure out the initial configuration. Do we need an iframe?
1367 | // Is pushState desired ... is it available?
1368 | this.options = _.extend({}, {root: '/'}, this.options, options);
1369 | this.root = this.options.root;
1370 | this._wantsHashChange = this.options.hashChange !== false;
1371 | this._wantsPushState = !!this.options.pushState;
1372 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1373 | var fragment = this.getFragment();
1374 | var docMode = document.documentMode;
1375 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1376 |
1377 | // Normalize root to always include a leading and trailing slash.
1378 | this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1379 |
1380 | if (oldIE && this._wantsHashChange) {
1381 | this.iframe = Backbone.$('').hide().appendTo('body')[0].contentWindow;
1382 | this.navigate(fragment);
1383 | }
1384 |
1385 | // Depending on whether we're using pushState or hashes, and whether
1386 | // 'onhashchange' is supported, determine how we check the URL state.
1387 | if (this._hasPushState) {
1388 | Backbone.$(window).on('popstate', this.checkUrl);
1389 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1390 | Backbone.$(window).on('hashchange', this.checkUrl);
1391 | } else if (this._wantsHashChange) {
1392 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1393 | }
1394 |
1395 | // Determine if we need to change the base url, for a pushState link
1396 | // opened by a non-pushState browser.
1397 | this.fragment = fragment;
1398 | var loc = this.location;
1399 | var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1400 |
1401 | // If we've started off with a route from a `pushState`-enabled browser,
1402 | // but we're currently in a browser that doesn't support it...
1403 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1404 | this.fragment = this.getFragment(null, true);
1405 | this.location.replace(this.root + this.location.search + '#' + this.fragment);
1406 | // Return immediately as browser will do redirect to new url
1407 | return true;
1408 |
1409 | // Or if we've started out with a hash-based route, but we're currently
1410 | // in a browser where it could be `pushState`-based instead...
1411 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1412 | this.fragment = this.getHash().replace(routeStripper, '');
1413 | this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1414 | }
1415 |
1416 | if (!this.options.silent) return this.loadUrl();
1417 | },
1418 |
1419 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1420 | // but possibly useful for unit testing Routers.
1421 | stop: function() {
1422 | Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1423 | clearInterval(this._checkUrlInterval);
1424 | History.started = false;
1425 | },
1426 |
1427 | // Add a route to be tested when the fragment changes. Routes added later
1428 | // may override previous routes.
1429 | route: function(route, callback) {
1430 | this.handlers.unshift({route: route, callback: callback});
1431 | },
1432 |
1433 | // Checks the current URL to see if it has changed, and if it has,
1434 | // calls `loadUrl`, normalizing across the hidden iframe.
1435 | checkUrl: function(e) {
1436 | var current = this.getFragment();
1437 | if (current === this.fragment && this.iframe) {
1438 | current = this.getFragment(this.getHash(this.iframe));
1439 | }
1440 | if (current === this.fragment) return false;
1441 | if (this.iframe) this.navigate(current);
1442 | this.loadUrl() || this.loadUrl(this.getHash());
1443 | },
1444 |
1445 | // Attempt to load the current URL fragment. If a route succeeds with a
1446 | // match, returns `true`. If no defined routes matches the fragment,
1447 | // returns `false`.
1448 | loadUrl: function(fragmentOverride) {
1449 | var fragment = this.fragment = this.getFragment(fragmentOverride);
1450 | var matched = _.any(this.handlers, function(handler) {
1451 | if (handler.route.test(fragment)) {
1452 | handler.callback(fragment);
1453 | return true;
1454 | }
1455 | });
1456 | return matched;
1457 | },
1458 |
1459 | // Save a fragment into the hash history, or replace the URL state if the
1460 | // 'replace' option is passed. You are responsible for properly URL-encoding
1461 | // the fragment in advance.
1462 | //
1463 | // The options object can contain `trigger: true` if you wish to have the
1464 | // route callback be fired (not usually desirable), or `replace: true`, if
1465 | // you wish to modify the current URL without adding an entry to the history.
1466 | navigate: function(fragment, options) {
1467 | if (!History.started) return false;
1468 | if (!options || options === true) options = {trigger: options};
1469 | fragment = this.getFragment(fragment || '');
1470 | if (this.fragment === fragment) return;
1471 | this.fragment = fragment;
1472 | var url = this.root + fragment;
1473 |
1474 | // If pushState is available, we use it to set the fragment as a real URL.
1475 | if (this._hasPushState) {
1476 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1477 |
1478 | // If hash changes haven't been explicitly disabled, update the hash
1479 | // fragment to store history.
1480 | } else if (this._wantsHashChange) {
1481 | this._updateHash(this.location, fragment, options.replace);
1482 | if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1483 | // Opening and closing the iframe tricks IE7 and earlier to push a
1484 | // history entry on hash-tag change. When replace is true, we don't
1485 | // want this.
1486 | if(!options.replace) this.iframe.document.open().close();
1487 | this._updateHash(this.iframe.location, fragment, options.replace);
1488 | }
1489 |
1490 | // If you've told us that you explicitly don't want fallback hashchange-
1491 | // based history, then `navigate` becomes a page refresh.
1492 | } else {
1493 | return this.location.assign(url);
1494 | }
1495 | if (options.trigger) this.loadUrl(fragment);
1496 | },
1497 |
1498 | // Update the hash location, either replacing the current entry, or adding
1499 | // a new one to the browser history.
1500 | _updateHash: function(location, fragment, replace) {
1501 | if (replace) {
1502 | var href = location.href.replace(/(javascript:|#).*$/, '');
1503 | location.replace(href + '#' + fragment);
1504 | } else {
1505 | // Some browsers require that `hash` contains a leading #.
1506 | location.hash = '#' + fragment;
1507 | }
1508 | }
1509 |
1510 | });
1511 |
1512 | // Create the default Backbone.history.
1513 | Backbone.history = new History;
1514 |
1515 | // Helpers
1516 | // -------
1517 |
1518 | // Helper function to correctly set up the prototype chain, for subclasses.
1519 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1520 | // class properties to be extended.
1521 | var extend = function(protoProps, staticProps) {
1522 | var parent = this;
1523 | var child;
1524 |
1525 | // The constructor function for the new subclass is either defined by you
1526 | // (the "constructor" property in your `extend` definition), or defaulted
1527 | // by us to simply call the parent's constructor.
1528 | if (protoProps && _.has(protoProps, 'constructor')) {
1529 | child = protoProps.constructor;
1530 | } else {
1531 | child = function(){ return parent.apply(this, arguments); };
1532 | }
1533 |
1534 | // Add static properties to the constructor function, if supplied.
1535 | _.extend(child, parent, staticProps);
1536 |
1537 | // Set the prototype chain to inherit from `parent`, without calling
1538 | // `parent`'s constructor function.
1539 | var Surrogate = function(){ this.constructor = child; };
1540 | Surrogate.prototype = parent.prototype;
1541 | child.prototype = new Surrogate;
1542 |
1543 | // Add prototype properties (instance properties) to the subclass,
1544 | // if supplied.
1545 | if (protoProps) _.extend(child.prototype, protoProps);
1546 |
1547 | // Set a convenience property in case the parent's prototype is needed
1548 | // later.
1549 | child.__super__ = parent.prototype;
1550 |
1551 | return child;
1552 | };
1553 |
1554 | // Set up inheritance for the model, collection, router, view and history.
1555 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1556 |
1557 | // Throw an error when a URL is needed, and none is supplied.
1558 | var urlError = function() {
1559 | throw new Error('A "url" property or function must be specified');
1560 | };
1561 |
1562 | // Wrap an optional error callback with a fallback error event.
1563 | var wrapError = function (model, options) {
1564 | var error = options.error;
1565 | options.error = function(resp) {
1566 | if (error) error(model, resp, options);
1567 | model.trigger('error', model, resp, options);
1568 | };
1569 | };
1570 |
1571 | }).call(this);
1572 |
--------------------------------------------------------------------------------