├── .gitignore
├── public
├── images
│ └── close.png
├── index.html
├── js
│ ├── util.js
│ ├── bootstrap
│ │ └── bootstrap-modal.js
│ ├── underscore-min.js
│ ├── mustache.js
│ └── backbone.js
├── app.css
└── app.js
├── package.json
├── test
├── index.html
├── qunit.css
└── qunit.js
├── LICENSE
├── README.md
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/public/images/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akavlie/web-irc/HEAD/public/images/close.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-irc",
3 | "description": "In-browser IRC client",
4 | "author": "Aaron Kavlie",
5 | "version": "0.0.1",
6 | "dependencies": {
7 | "express": "2.5.x",
8 | "irc": "0.3.x",
9 | "socket.io": "0.8.x"
10 | },
11 | "bin": {
12 | "web-irc": "./server.js"
13 | },
14 | "devDependencies": {
15 | },
16 | "engine": "node >= 0.4.x"
17 | }
18 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | test markup, will be hidden
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2011 Aaron Kavlie
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 | of the Software, and to permit persons to whom the Software is furnished to do
9 | so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Web-IRC
2 | =======
3 |
4 | Note
5 | ----
6 |
7 | I have stopped development on Web-IRC in favor of [Subway](https://github.com/thedjpetersen/subway),
8 | a joint effort with another githubber. If you are interested in a web-based IRC client, look there.
9 |
10 | https://github.com/thedjpetersen/subway
11 |
12 | -----
13 |
14 |
15 | ### A web IRC client
16 |
17 | The goal for this project is to become the best in-browser IRC client available,
18 | and bring the best ideas from modern web applications to IRC. It was inspired by a [request for improvements to qwebirc](https://github.com/paulirish/lazyweb-requests/issues/31)
19 | by Paul Irish.
20 |
21 | Web-IRC is based on [node.js](http://nodejs.org/) and
22 | Martyn Smith's [node-irc](https://github.com/martynsmith/node-irc) on the backend,
23 | and [Backbone.js](http://documentcloud.github.com/backbone/) and
24 | [jQuery](http://jquery.com/) on the frontend.
25 |
26 |
27 | Status
28 | ------
29 |
30 | The app is still in its early stages. Potential contributors should find plenty to do.
31 |
32 | Here's what works:
33 |
34 | - Choose nick/network/channel(s) to use at login
35 | - Join channels
36 | - Send messages to channels
37 | - Switch between channel tabs, see chat output
38 | - Leave channels
39 | - Private messages
40 | - Channel topics
41 |
42 | Here's (a partial list of) what doesn't work yet:
43 |
44 | - Status messages
45 | - Listing channels
46 |
47 | Design/UI/UX help also **desperately needed**.
48 |
49 | Installation
50 | ------------
51 |
52 | 1. Assuming you already have node.js & npm, run:
53 |
54 | $ npm install -g web-irc
55 |
56 | 2. Launch the web server
57 |
58 | $ web-irc
59 |
60 | 3. Point your browser at `http://localhost:8337/`
61 |
62 | Development
63 | -----------
64 |
65 | Replace step 1 above with this:
66 |
67 | $ git clone https://github.com/akavlie/web-irc
68 | $ cd web-irc
69 | $ npm link
70 |
71 | this should install dependencies, and link the git checkout to your global
72 | node_modules directory.
73 |
74 | Rationale
75 | ---------
76 |
77 | Web-based IRC clients are quite popular, particularly as an in-page embed for
78 | various open source projects and live shows. The ubiquitous choice at this time
79 | is the aforementioned [qwebirc](http://qwebirc.org/).
80 |
81 | Here are some popular sites that use (or link to) a web IRC client:
82 |
83 | - [jQuery](http://docs.jquery.com/Discussion)
84 | - [freenode](http://webchat.freenode.net/)
85 | - [TWiT](http://twit.tv/)
86 |
87 |
88 | License
89 | -------
90 |
91 | MIT licensed. See `LICENSE`.
92 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var express = require('express'),
4 | app = express.createServer(),
5 | io = require('socket.io').listen(app),
6 | irc = require('irc');
7 |
8 | app.configure(function() {
9 | app.use(app.router);
10 | app.use(express.static(__dirname + '/public'));
11 | });
12 |
13 | app.configure('development', function() {
14 | app.listen(8337);
15 | });
16 |
17 | app.configure('production', function() {
18 | app.listen(12445);
19 | });
20 |
21 | app.get('/', function(req, res, next) {
22 | next();
23 | });
24 |
25 | console.log('web-irc started on port %s', app.address().port);
26 |
27 |
28 | // Socket.IO
29 | io.sockets.on('connection', function(socket) {
30 |
31 | // Events to signal TO the front-end
32 | var events = {
33 | 'join': ['channel', 'nick'],
34 | 'part': ['channel', 'nick'],
35 | 'topic': ['channel', 'topic', 'nick'],
36 | 'nick': ['oldNick', 'newNick', 'channels'],
37 | 'names': ['channel', 'nicks'],
38 | 'message': ['from', 'to', 'text'],
39 | 'pm': ['nick', 'text'],
40 | 'motd': ['motd'],
41 | 'error': ['message']
42 | };
43 |
44 | socket.on('connect', function(data) {
45 | var client = new irc.Client(data.server, data.nick, {
46 | debug: true,
47 | showErrors: true,
48 | channels: data.channels
49 | });
50 |
51 | // Socket events sent FROM the front-end
52 | socket.on('join', function(name) { client.join(name); });
53 | socket.on('part', function(name) { client.part(name); });
54 | socket.on('say', function(data) { client.say(data.target, data.message); });
55 | socket.on('command', function(text) { console.log(text); client.send(text); });
56 | socket.on('disconnect', function() { client.disconnect(); });
57 |
58 |
59 | // Add a listener on client for the given event & argument names
60 | var activateListener = function(event, argNames) {
61 | client.addListener(event, function() {
62 | console.log('Event ' + event + ' sent');
63 | // Associate specified names with callback arguments
64 | // to avoid getting tripped up on the other side
65 | var callbackArgs = arguments;
66 | args = {};
67 | argNames.forEach(function(arg, index) {
68 | args[arg] = callbackArgs[index];
69 | });
70 | console.log(args);
71 | socket.emit(event, args);
72 | });
73 | };
74 |
75 | for (var event in events) { activateListener(event, events[event]); }
76 | console.log('Starting IRC client; wiring up socket events.')
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | web-irc
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
21 |
22 |
23 |
channel topic here
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
63 |
64 |
65 |
69 |
70 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/public/js/util.js:
--------------------------------------------------------------------------------
1 | // make it safe to use console.log always
2 | (function(b){function c(){}for(var d="assert,clear,count,debug,dir,dirxml,error,exception,firebug,group,groupCollapsed,groupEnd,info,log,memoryProfile,memoryProfileEnd,profile,profileEnd,table,time,timeEnd,timeStamp,trace,warn".split(","),a;a=d.pop();){b[a]=b[a]||c}})((function(){try
3 | {console.log();return window.console;}catch(err){return window.console={};}})());
4 |
5 |
6 | // POLYFILLS
7 | // =========
8 |
9 | if(!String.prototype.trim) {
10 | String.prototype.trim = function () {
11 | return this.replace(/^\s+|\s+$/g,'');
12 | };
13 | }
14 |
15 | // Production steps of ECMA-262, Edition 5, 15.4.4.18
16 | // Reference: http://es5.github.com/#x15.4.4.18
17 | if ( !Array.prototype.forEach ) {
18 |
19 | Array.prototype.forEach = function( callback, thisArg ) {
20 |
21 | var T, k;
22 |
23 | if ( this == null ) {
24 | throw new TypeError( " this is null or not defined" );
25 | }
26 |
27 | // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
28 | var O = Object(this);
29 |
30 | // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
31 | // 3. Let len be ToUint32(lenValue).
32 | var len = O.length >>> 0; // Hack to convert O.length to a UInt32
33 |
34 | // 4. If IsCallable(callback) is false, throw a TypeError exception.
35 | // See: http://es5.github.com/#x9.11
36 | if ( {}.toString.call(callback) != "[object Function]" ) {
37 | throw new TypeError( callback + " is not a function" );
38 | }
39 |
40 | // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
41 | if ( thisArg ) {
42 | T = thisArg;
43 | }
44 |
45 | // 6. Let k be 0
46 | k = 0;
47 |
48 | // 7. Repeat, while k < len
49 | while( k < len ) {
50 |
51 | var kValue;
52 |
53 | // a. Let Pk be ToString(k).
54 | // This is implicit for LHS operands of the in operator
55 | // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
56 | // This step can be combined with c
57 | // c. If kPresent is true, then
58 | if ( k in O ) {
59 |
60 | // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
61 | kValue = O[ k ];
62 |
63 | // ii. Call the Call internal method of callback with T as the this value and
64 | // argument list containing kValue, k, and O.
65 | callback.call( T, kValue, k, O );
66 | }
67 | // d. Increase k by 1.
68 | k++;
69 | }
70 | // 8. return undefined
71 | };
72 | }
73 |
74 | // UTILITY FUNCTIONS
75 | // =================
76 |
77 | window.irc = (function(module) {
78 | module.util = {
79 | // Replaces oldString with newString at beginning of text
80 | swapCommand: function(oldString, newString, text) {
81 | if (text.substring(0, oldString.length) === oldString)
82 | return newString + text.substring(oldString.length, text.length);
83 | else
84 | throw 'String "' + oldString + '" not found at beginning of text';
85 | },
86 | escapeHTML: function(text) {
87 | return text.replace(/&/g,'&').replace(//g,'>');
88 | }
89 |
90 | }
91 |
92 | return module
93 | })(window.irc || {});
94 |
--------------------------------------------------------------------------------
/public/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | padding: 0;
4 | margin: 0;
5 | overflow-y: hidden;
6 | }
7 |
8 | /*
9 | body {
10 | color: #444;
11 | font-family: arial;
12 | }
13 | */
14 |
15 | ul {
16 | list-style-type: none;
17 | padding: 0;
18 | }
19 |
20 | #content {
21 | padding: 5px;
22 | display: none;
23 | }
24 |
25 | header {
26 | margin-bottom: 3px;
27 | }
28 |
29 | .frames {
30 | margin: 0;
31 | }
32 |
33 | #frame {
34 | position: relative;
35 | }
36 |
37 | #frame .wrapper {
38 | position: relative;
39 | }
40 |
41 | #output {
42 | font-size: 14px;
43 | position: relative;
44 | margin-bottom: 5px;
45 | border: 1px solid #888;
46 | }
47 |
48 | #output #topic {
49 | position: relative;
50 | min-height: 40px;
51 | padding: 5px;
52 | background-color: #e2e2e2;
53 | border-bottom: 1px solid #444;
54 | display: none;
55 | z-index: 2;
56 | }
57 |
58 | #output #messages {
59 | position: absolute;
60 | padding: 5px;
61 | overflow: auto;
62 | z-index: 10;
63 | top: 0;
64 | bottom: 0;
65 | left: 0;
66 | right: 0;
67 | }
68 |
69 | #frame .gutter {
70 | position: absolute;
71 | background-color: #e2e2e2;
72 | border-right: 1px solid #888;
73 | width: 110px;
74 | left: 0;
75 | top: 0;
76 | height: 100%;
77 | }
78 |
79 | #frame.status .gutter {
80 | display: none;
81 | }
82 |
83 | /* Special message types */
84 | #output .error {
85 | background-color: red;
86 | }
87 |
88 | #output .sender {
89 | color: #444;
90 | display: block;
91 | float: left;
92 | width: 100px;
93 | text-align: right;
94 | z-index: 10;
95 | }
96 |
97 | #output .text {
98 | color: #222;
99 | display: block;
100 | margin-left: 115px;
101 | margin-bottom: 4px;
102 | }
103 |
104 | #frame.status .text.no-sender {
105 | margin-left: 0;
106 | }
107 |
108 | #sidebar {
109 | width: 200px;
110 | margin: 0 0 5px 5px;
111 | border: 1px solid #888;
112 | float: right;
113 | position: relative;
114 | display: none;
115 | }
116 |
117 | #sidebar .stats {
118 | position: relative;
119 | top: 0;
120 | height: 40px;
121 | background-color: #e2e2e2;
122 | border-bottom: 1px solid #888;
123 | display: none;
124 | }
125 |
126 | #sidebar .nicks {
127 | position: absolute;
128 | overflow: auto;
129 | padding: 5px;
130 | top: 0;
131 | /*top: 40px;*/
132 | bottom: 0;
133 | left: 0;
134 | right: 0;
135 | }
136 |
137 | #prime-input {
138 | font-size: 15px;
139 | position: relative;
140 | width: 99%;
141 | bottom: 0;
142 | }
143 |
144 | /* Twitter Bootstrap overrides
145 | ----------------------------------------------------------------------*/
146 |
147 | .pills > li { margin-right: 6px; }
148 | .pills > li > * { float: left; }
149 |
150 | .pills > li > a {
151 | cursor: pointer;
152 | background: #e2e2e2;
153 | padding: 0 12px;
154 | margin: 0 1px 0 0;
155 | border-radius: 3px;
156 | line-height: 26px;
157 | }
158 | .pills > li > a.channel,
159 | .pills > li > a.pm {
160 | border-radius: 3px 0 0 3px;
161 | }
162 |
163 | .pills > li > .close-frame {
164 | cursor: pointer;
165 | background: #e2e2e2 url(images/close.png) no-repeat center center;
166 | width: 18px;
167 | height: 26px;
168 | border-radius: 0 3px 3px 0;
169 | }
170 |
171 | .pills .active .close-frame {
172 | background-color: #0069d6;
173 | }
174 |
175 | .pills > li:hover > a,
176 | .pills > li:hover > .close-frame {
177 | color: white;
178 | text-shadow: 0;
179 | background-color: #00438a;
180 | }
181 |
--------------------------------------------------------------------------------
/test/qunit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit 1.2.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 15px 15px 0 0;
42 | -moz-border-radius: 15px 15px 0 0;
43 | -webkit-border-top-right-radius: 15px;
44 | -webkit-border-top-left-radius: 15px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-banner {
58 | height: 5px;
59 | }
60 |
61 | #qunit-testrunner-toolbar {
62 | padding: 0.5em 0 0.5em 2em;
63 | color: #5E740B;
64 | background-color: #eee;
65 | }
66 |
67 | #qunit-userAgent {
68 | padding: 0.5em 0 0.5em 2.5em;
69 | background-color: #2b81af;
70 | color: #fff;
71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
72 | }
73 |
74 |
75 | /** Tests: Pass/Fail */
76 |
77 | #qunit-tests {
78 | list-style-position: inside;
79 | }
80 |
81 | #qunit-tests li {
82 | padding: 0.4em 0.5em 0.4em 2.5em;
83 | border-bottom: 1px solid #fff;
84 | list-style-position: inside;
85 | }
86 |
87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
88 | display: none;
89 | }
90 |
91 | #qunit-tests li strong {
92 | cursor: pointer;
93 | }
94 |
95 | #qunit-tests li a {
96 | padding: 0.5em;
97 | color: #c2ccd1;
98 | text-decoration: none;
99 | }
100 | #qunit-tests li a:hover,
101 | #qunit-tests li a:focus {
102 | color: #000;
103 | }
104 |
105 | #qunit-tests ol {
106 | margin-top: 0.5em;
107 | padding: 0.5em;
108 |
109 | background-color: #fff;
110 |
111 | border-radius: 15px;
112 | -moz-border-radius: 15px;
113 | -webkit-border-radius: 15px;
114 |
115 | box-shadow: inset 0px 2px 13px #999;
116 | -moz-box-shadow: inset 0px 2px 13px #999;
117 | -webkit-box-shadow: inset 0px 2px 13px #999;
118 | }
119 |
120 | #qunit-tests table {
121 | border-collapse: collapse;
122 | margin-top: .2em;
123 | }
124 |
125 | #qunit-tests th {
126 | text-align: right;
127 | vertical-align: top;
128 | padding: 0 .5em 0 0;
129 | }
130 |
131 | #qunit-tests td {
132 | vertical-align: top;
133 | }
134 |
135 | #qunit-tests pre {
136 | margin: 0;
137 | white-space: pre-wrap;
138 | word-wrap: break-word;
139 | }
140 |
141 | #qunit-tests del {
142 | background-color: #e0f2be;
143 | color: #374e0c;
144 | text-decoration: none;
145 | }
146 |
147 | #qunit-tests ins {
148 | background-color: #ffcaca;
149 | color: #500;
150 | text-decoration: none;
151 | }
152 |
153 | /*** Test Counts */
154 |
155 | #qunit-tests b.counts { color: black; }
156 | #qunit-tests b.passed { color: #5E740B; }
157 | #qunit-tests b.failed { color: #710909; }
158 |
159 | #qunit-tests li li {
160 | margin: 0.5em;
161 | padding: 0.4em 0.5em 0.4em 0.5em;
162 | background-color: #fff;
163 | border-bottom: none;
164 | list-style-position: inside;
165 | }
166 |
167 | /*** Passing Styles */
168 |
169 | #qunit-tests li li.pass {
170 | color: #5E740B;
171 | background-color: #fff;
172 | border-left: 26px solid #C6E746;
173 | }
174 |
175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
176 | #qunit-tests .pass .test-name { color: #366097; }
177 |
178 | #qunit-tests .pass .test-actual,
179 | #qunit-tests .pass .test-expected { color: #999999; }
180 |
181 | #qunit-banner.qunit-pass { background-color: #C6E746; }
182 |
183 | /*** Failing Styles */
184 |
185 | #qunit-tests li li.fail {
186 | color: #710909;
187 | background-color: #fff;
188 | border-left: 26px solid #EE5757;
189 | white-space: pre;
190 | }
191 |
192 | #qunit-tests > li:last-child {
193 | border-radius: 0 0 15px 15px;
194 | -moz-border-radius: 0 0 15px 15px;
195 | -webkit-border-bottom-right-radius: 15px;
196 | -webkit-border-bottom-left-radius: 15px;
197 | }
198 |
199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
200 | #qunit-tests .fail .test-name,
201 | #qunit-tests .fail .module-name { color: #000000; }
202 |
203 | #qunit-tests .fail .test-actual { color: #EE5757; }
204 | #qunit-tests .fail .test-expected { color: green; }
205 |
206 | #qunit-banner.qunit-fail { background-color: #EE5757; }
207 |
208 |
209 | /** Result */
210 |
211 | #qunit-testresult {
212 | padding: 0.5em 0.5em 0.5em 2.5em;
213 |
214 | color: #2b81af;
215 | background-color: #D2E0E6;
216 |
217 | border-bottom: 1px solid white;
218 | }
219 |
220 | /** Fixture */
221 |
222 | #qunit-fixture {
223 | position: absolute;
224 | top: -10000px;
225 | left: -10000px;
226 | }
227 |
--------------------------------------------------------------------------------
/public/js/bootstrap/bootstrap-modal.js:
--------------------------------------------------------------------------------
1 | /* =========================================================
2 | * bootstrap-modal.js v1.4.0
3 | * http://twitter.github.com/bootstrap/javascript.html#modal
4 | * =========================================================
5 | * Copyright 2011 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================= */
19 |
20 |
21 | !function( $ ){
22 |
23 | "use strict"
24 |
25 | /* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
26 | * ======================================================= */
27 |
28 | var transitionEnd
29 |
30 | $(document).ready(function () {
31 |
32 | $.support.transition = (function () {
33 | var thisBody = document.body || document.documentElement
34 | , thisStyle = thisBody.style
35 | , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
36 | return support
37 | })()
38 |
39 | // set CSS transition event type
40 | if ( $.support.transition ) {
41 | transitionEnd = "TransitionEnd"
42 | if ( $.browser.webkit ) {
43 | transitionEnd = "webkitTransitionEnd"
44 | } else if ( $.browser.mozilla ) {
45 | transitionEnd = "transitionend"
46 | } else if ( $.browser.opera ) {
47 | transitionEnd = "oTransitionEnd"
48 | }
49 | }
50 |
51 | })
52 |
53 |
54 | /* MODAL PUBLIC CLASS DEFINITION
55 | * ============================= */
56 |
57 | var Modal = function ( content, options ) {
58 | this.settings = $.extend({}, $.fn.modal.defaults, options)
59 | this.$element = $(content)
60 | .delegate('.close', 'click.modal', $.proxy(this.hide, this))
61 |
62 | if ( this.settings.show ) {
63 | this.show()
64 | }
65 |
66 | return this
67 | }
68 |
69 | Modal.prototype = {
70 |
71 | toggle: function () {
72 | return this[!this.isShown ? 'show' : 'hide']()
73 | }
74 |
75 | , show: function () {
76 | var that = this
77 | this.isShown = true
78 | this.$element.trigger('show')
79 |
80 | escape.call(this)
81 | backdrop.call(this, function () {
82 | var transition = $.support.transition && that.$element.hasClass('fade')
83 |
84 | that.$element
85 | .appendTo(document.body)
86 | .show()
87 |
88 | if (transition) {
89 | that.$element[0].offsetWidth // force reflow
90 | }
91 |
92 | that.$element.addClass('in')
93 |
94 | transition ?
95 | that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) :
96 | that.$element.trigger('shown')
97 |
98 | })
99 |
100 | return this
101 | }
102 |
103 | , hide: function (e) {
104 | e && e.preventDefault()
105 |
106 | if ( !this.isShown ) {
107 | return this
108 | }
109 |
110 | var that = this
111 | this.isShown = false
112 |
113 | escape.call(this)
114 |
115 | this.$element
116 | .trigger('hide')
117 | .removeClass('in')
118 |
119 | $.support.transition && this.$element.hasClass('fade') ?
120 | hideWithTransition.call(this) :
121 | hideModal.call(this)
122 |
123 | return this
124 | }
125 |
126 | }
127 |
128 |
129 | /* MODAL PRIVATE METHODS
130 | * ===================== */
131 |
132 | function hideWithTransition() {
133 | // firefox drops transitionEnd events :{o
134 | var that = this
135 | , timeout = setTimeout(function () {
136 | that.$element.unbind(transitionEnd)
137 | hideModal.call(that)
138 | }, 500)
139 |
140 | this.$element.one(transitionEnd, function () {
141 | clearTimeout(timeout)
142 | hideModal.call(that)
143 | })
144 | }
145 |
146 | function hideModal (that) {
147 | this.$element
148 | .hide()
149 | .trigger('hidden')
150 |
151 | backdrop.call(this)
152 | }
153 |
154 | function backdrop ( callback ) {
155 | var that = this
156 | , animate = this.$element.hasClass('fade') ? 'fade' : ''
157 | if ( this.isShown && this.settings.backdrop ) {
158 | var doAnimate = $.support.transition && animate
159 |
160 | this.$backdrop = $('
')
161 | .appendTo(document.body)
162 |
163 | if ( this.settings.backdrop != 'static' ) {
164 | this.$backdrop.click($.proxy(this.hide, this))
165 | }
166 |
167 | if ( doAnimate ) {
168 | this.$backdrop[0].offsetWidth // force reflow
169 | }
170 |
171 | this.$backdrop.addClass('in')
172 |
173 | doAnimate ?
174 | this.$backdrop.one(transitionEnd, callback) :
175 | callback()
176 |
177 | } else if ( !this.isShown && this.$backdrop ) {
178 | this.$backdrop.removeClass('in')
179 |
180 | $.support.transition && this.$element.hasClass('fade')?
181 | this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) :
182 | removeBackdrop.call(this)
183 |
184 | } else if ( callback ) {
185 | callback()
186 | }
187 | }
188 |
189 | function removeBackdrop() {
190 | this.$backdrop.remove()
191 | this.$backdrop = null
192 | }
193 |
194 | function escape() {
195 | var that = this
196 | if ( this.isShown && this.settings.keyboard ) {
197 | $(document).bind('keyup.modal', function ( e ) {
198 | if ( e.which == 27 ) {
199 | that.hide()
200 | }
201 | })
202 | } else if ( !this.isShown ) {
203 | $(document).unbind('keyup.modal')
204 | }
205 | }
206 |
207 |
208 | /* MODAL PLUGIN DEFINITION
209 | * ======================= */
210 |
211 | $.fn.modal = function ( options ) {
212 | var modal = this.data('modal')
213 |
214 | if (!modal) {
215 |
216 | if (typeof options == 'string') {
217 | options = {
218 | show: /show|toggle/.test(options)
219 | }
220 | }
221 |
222 | return this.each(function () {
223 | $(this).data('modal', new Modal(this, options))
224 | })
225 | }
226 |
227 | if ( options === true ) {
228 | return modal
229 | }
230 |
231 | if ( typeof options == 'string' ) {
232 | modal[options]()
233 | } else if ( modal ) {
234 | modal.toggle()
235 | }
236 |
237 | return this
238 | }
239 |
240 | $.fn.modal.Modal = Modal
241 |
242 | $.fn.modal.defaults = {
243 | backdrop: false
244 | , keyboard: false
245 | , show: false
246 | }
247 |
248 |
249 | /* MODAL DATA- IMPLEMENTATION
250 | * ========================== */
251 |
252 | $(document).ready(function () {
253 | $('body').delegate('[data-controls-modal]', 'click', function (e) {
254 | e.preventDefault()
255 | var $this = $(this).data('show', true)
256 | $('#' + $this.attr('data-controls-modal')).modal( $this.data() )
257 | })
258 | })
259 |
260 | }( window.jQuery || window.ender );
261 |
--------------------------------------------------------------------------------
/public/js/underscore-min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.2.0
2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Underscore is freely distributable under the MIT license.
4 | // Portions of Underscore are inspired or borrowed from Prototype,
5 | // Oliver Steele's Functional, and John Resig's Micro-Templating.
6 | // For all details and documentation:
7 | // http://documentcloud.github.com/underscore
8 | (function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null)return a===c;var e=typeof a;if(e!=typeof c)return false;if(!a!=!c)return false;if(b.isNaN(a))return b.isNaN(c);var f=b.isString(a),g=b.isString(c);if(f||g)return f&&g&&String(a)==String(c);f=b.isNumber(a);g=b.isNumber(c);if(f||g)return f&&g&&+a==+c;f=b.isBoolean(a);g=b.isBoolean(c);if(f||g)return f&&g&&+a==+c;f=b.isDate(a);g=b.isDate(c);if(f||g)return f&&g&&a.getTime()==c.getTime();f=b.isRegExp(a);g=b.isRegExp(c);if(f||g)return f&&
9 | g&&a.source==c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase;if(e!="object")return false;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(b.isFunction(a.isEqual))return a.isEqual(c);for(e=d.length;e--;)if(d[e]==a)return true;d.push(a);e=0;f=true;if(a.length===+a.length||c.length===+c.length){if(e=a.length,f=e==c.length)for(;e--;)if(!(f=e in a==e in c&&q(a[e],c[e],d)))break}else{for(var h in a)if(l.call(a,h)&&(e++,!(f=l.call(c,h)&&q(a[h],c[h],d))))break;if(f){for(h in c)if(l.call(c,
10 | h)&&!e--)break;f=!e}}d.pop();return f}var r=this,F=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,G=k.unshift,u=o.toString,l=o.hasOwnProperty,v=k.forEach,w=k.map,x=k.reduce,y=k.reduceRight,z=k.filter,A=k.every,B=k.some,p=k.indexOf,C=k.lastIndexOf,o=Array.isArray,H=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):r._=b;b.VERSION="1.2.0";var j=b.each=b.forEach=function(a,c,b){if(a!=null)if(v&&a.forEach===
11 | v)a.forEach(c,b);else if(a.length===+a.length)for(var e=0,f=a.length;e=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,b){var d={};j(a,function(a,f){var g=b(a,f);(d[g]||(d[g]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};
19 | b.difference=function(a,c){return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,
23 | arguments)}};b.keys=H||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=
24 | b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return u.call(a)==="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};
25 | b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||u.call(a)=="[object Boolean]"};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===false))};b.isNull=
26 | function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){r._=F;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e /g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a),function(c){I(c,b[c]=a[c])})};var J=0;b.uniqueId=function(a){var b=
27 | J++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape,function(a,b){return"',_.escape("+b.replace(/\\'/g,"'")+"),'"}).replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(a,
28 | b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",d=new Function("obj",d);return c?d(c):d};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var t=function(a,c){return c?b(a).chain():a},I=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);G.call(a,this._wrapped);return t(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),
29 | function(a){var b=k[a];m.prototype[a]=function(){b.apply(this._wrapped,arguments);return t(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return t(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}})();
30 |
--------------------------------------------------------------------------------
/public/js/mustache.js:
--------------------------------------------------------------------------------
1 | /*
2 | mustache.js — Logic-less templates in JavaScript
3 |
4 | See http://mustache.github.com/ for more info.
5 | */
6 |
7 | var Mustache = function() {
8 | var regexCache = {};
9 | var Renderer = function() {};
10 |
11 | Renderer.prototype = {
12 | otag: "{{",
13 | ctag: "}}",
14 | pragmas: {},
15 | buffer: [],
16 | pragmas_implemented: {
17 | "IMPLICIT-ITERATOR": true
18 | },
19 | context: {},
20 |
21 | render: function(template, context, partials, in_recursion) {
22 | // reset buffer & set context
23 | if(!in_recursion) {
24 | this.context = context;
25 | this.buffer = []; // TODO: make this non-lazy
26 | }
27 |
28 | // fail fast
29 | if(!this.includes("", template)) {
30 | if(in_recursion) {
31 | return template;
32 | } else {
33 | this.send(template);
34 | return;
35 | }
36 | }
37 |
38 | // get the pragmas together
39 | template = this.render_pragmas(template);
40 |
41 | // render the template
42 | var html = this.render_section(template, context, partials);
43 |
44 | // render_section did not find any sections, we still need to render the tags
45 | if (html === false) {
46 | html = this.render_tags(template, context, partials, in_recursion);
47 | }
48 |
49 | if (in_recursion) {
50 | return html;
51 | } else {
52 | this.sendLines(html);
53 | }
54 | },
55 |
56 | /*
57 | Sends parsed lines
58 | */
59 | send: function(line) {
60 | if(line !== "") {
61 | this.buffer.push(line);
62 | }
63 | },
64 |
65 | sendLines: function(text) {
66 | if (text) {
67 | var lines = text.split("\n");
68 | for (var i = 0; i < lines.length; i++) {
69 | this.send(lines[i]);
70 | }
71 | }
72 | },
73 |
74 | /*
75 | Looks for %PRAGMAS
76 | */
77 | render_pragmas: function(template) {
78 | // no pragmas
79 | if(!this.includes("%", template)) {
80 | return template;
81 | }
82 |
83 | var that = this;
84 | var regex = this.getCachedRegex("render_pragmas", function(otag, ctag) {
85 | return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g");
86 | });
87 |
88 | return template.replace(regex, function(match, pragma, options) {
89 | if(!that.pragmas_implemented[pragma]) {
90 | throw({message:
91 | "This implementation of mustache doesn't understand the '" +
92 | pragma + "' pragma"});
93 | }
94 | that.pragmas[pragma] = {};
95 | if(options) {
96 | var opts = options.split("=");
97 | that.pragmas[pragma][opts[0]] = opts[1];
98 | }
99 | return "";
100 | // ignore unknown pragmas silently
101 | });
102 | },
103 |
104 | /*
105 | Tries to find a partial in the curent scope and render it
106 | */
107 | render_partial: function(name, context, partials) {
108 | name = this.trim(name);
109 | if(!partials || partials[name] === undefined) {
110 | throw({message: "unknown_partial '" + name + "'"});
111 | }
112 | if(typeof(context[name]) != "object") {
113 | return this.render(partials[name], context, partials, true);
114 | }
115 | return this.render(partials[name], context[name], partials, true);
116 | },
117 |
118 | /*
119 | Renders inverted (^) and normal (#) sections
120 | */
121 | render_section: function(template, context, partials) {
122 | if(!this.includes("#", template) && !this.includes("^", template)) {
123 | // did not render anything, there were no sections
124 | return false;
125 | }
126 |
127 | var that = this;
128 |
129 | var regex = this.getCachedRegex("render_section", function(otag, ctag) {
130 | // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder
131 | return new RegExp(
132 | "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1)
133 |
134 | otag + // {{
135 | "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3)
136 | ctag + // }}
137 |
138 | "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped
139 |
140 | otag + // {{
141 | "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag).
142 | ctag + // }}
143 |
144 | "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped.
145 |
146 | "g");
147 | });
148 |
149 |
150 | // for each {{#foo}}{{/foo}} section do...
151 | return template.replace(regex, function(match, before, type, name, content, after) {
152 | // before contains only tags, no sections
153 | var renderedBefore = before ? that.render_tags(before, context, partials, true) : "",
154 |
155 | // after may contain both sections and tags, so use full rendering function
156 | renderedAfter = after ? that.render(after, context, partials, true) : "",
157 |
158 | // will be computed below
159 | renderedContent,
160 |
161 | value = that.find(name, context);
162 |
163 | if (type === "^") { // inverted section
164 | if (!value || that.is_array(value) && value.length === 0) {
165 | // false or empty list, render it
166 | renderedContent = that.render(content, context, partials, true);
167 | } else {
168 | renderedContent = "";
169 | }
170 | } else if (type === "#") { // normal section
171 | if (that.is_array(value)) { // Enumerable, Let's loop!
172 | renderedContent = that.map(value, function(row) {
173 | return that.render(content, that.create_context(row), partials, true);
174 | }).join("");
175 | } else if (that.is_object(value)) { // Object, Use it as subcontext!
176 | renderedContent = that.render(content, that.create_context(value),
177 | partials, true);
178 | } else if (typeof value === "function") {
179 | // higher order section
180 | renderedContent = value.call(context, content, function(text) {
181 | return that.render(text, context, partials, true);
182 | });
183 | } else if (value) { // boolean section
184 | renderedContent = that.render(content, context, partials, true);
185 | } else {
186 | renderedContent = "";
187 | }
188 | }
189 |
190 | return renderedBefore + renderedContent + renderedAfter;
191 | });
192 | },
193 |
194 | /*
195 | Replace {{foo}} and friends with values from our view
196 | */
197 | render_tags: function(template, context, partials, in_recursion) {
198 | // tit for tat
199 | var that = this;
200 |
201 |
202 |
203 | var new_regex = function() {
204 | return that.getCachedRegex("render_tags", function(otag, ctag) {
205 | return new RegExp(otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + ctag + "+", "g");
206 | });
207 | };
208 |
209 | var regex = new_regex();
210 | var tag_replace_callback = function(match, operator, name) {
211 | switch(operator) {
212 | case "!": // ignore comments
213 | return "";
214 | case "=": // set new delimiters, rebuild the replace regexp
215 | that.set_delimiters(name);
216 | regex = new_regex();
217 | return "";
218 | case ">": // render partial
219 | return that.render_partial(name, context, partials);
220 | case "{": // the triple mustache is unescaped
221 | return that.find(name, context);
222 | default: // escape the value
223 | return that.escape(that.find(name, context));
224 | }
225 | };
226 | var lines = template.split("\n");
227 | for(var i = 0; i < lines.length; i++) {
228 | lines[i] = lines[i].replace(regex, tag_replace_callback, this);
229 | if(!in_recursion) {
230 | this.send(lines[i]);
231 | }
232 | }
233 |
234 | if(in_recursion) {
235 | return lines.join("\n");
236 | }
237 | },
238 |
239 | set_delimiters: function(delimiters) {
240 | var dels = delimiters.split(" ");
241 | this.otag = this.escape_regex(dels[0]);
242 | this.ctag = this.escape_regex(dels[1]);
243 | },
244 |
245 | escape_regex: function(text) {
246 | // thank you Simon Willison
247 | if(!arguments.callee.sRE) {
248 | var specials = [
249 | '/', '.', '*', '+', '?', '|',
250 | '(', ')', '[', ']', '{', '}', '\\'
251 | ];
252 | arguments.callee.sRE = new RegExp(
253 | '(\\' + specials.join('|\\') + ')', 'g'
254 | );
255 | }
256 | return text.replace(arguments.callee.sRE, '\\$1');
257 | },
258 |
259 | /*
260 | find `name` in current `context`. That is find me a value
261 | from the view object
262 | */
263 | find: function(name, context) {
264 | name = this.trim(name);
265 |
266 | // Checks whether a value is thruthy or false or 0
267 | function is_kinda_truthy(bool) {
268 | return bool === false || bool === 0 || bool;
269 | }
270 |
271 | var value;
272 |
273 | // check for dot notation eg. foo.bar
274 | if(name.match(/([a-z_]+)\./ig)){
275 | value = is_kinda_truthy(this.walk_context(name, context));
276 | }
277 | else{
278 | if(is_kinda_truthy(context[name])) {
279 | value = context[name];
280 | } else if(is_kinda_truthy(this.context[name])) {
281 | value = this.context[name];
282 | }
283 | }
284 |
285 | if(typeof value === "function") {
286 | return value.apply(context);
287 | }
288 | if(value !== undefined) {
289 | return value;
290 | }
291 | // silently ignore unkown variables
292 | return "";
293 | },
294 |
295 | walk_context: function(name, context){
296 | var path = name.split('.');
297 | // if the var doesn't exist in current context, check the top level context
298 | var value_context = (context[path[0]] != undefined) ? context : this.context;
299 | var value = value_context[path.shift()];
300 | while(value != undefined && path.length > 0){
301 | value_context = value;
302 | value = value[path.shift()];
303 | }
304 | // if the value is a function, call it, binding the correct context
305 | if(typeof value === "function") {
306 | return value.apply(value_context);
307 | }
308 | return value;
309 | },
310 |
311 | // Utility methods
312 |
313 | /* includes tag */
314 | includes: function(needle, haystack) {
315 | return haystack.indexOf(this.otag + needle) != -1;
316 | },
317 |
318 | /*
319 | Does away with nasty characters
320 | */
321 | escape: function(s) {
322 | s = String(s === null ? "" : s);
323 | return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
324 | switch(s) {
325 | case "&": return "&";
326 | case '"': return '"';
327 | case "'": return ''';
328 | case "<": return "<";
329 | case ">": return ">";
330 | default: return s;
331 | }
332 | });
333 | },
334 |
335 | // by @langalex, support for arrays of strings
336 | create_context: function(_context) {
337 | if(this.is_object(_context)) {
338 | return _context;
339 | } else {
340 | var iterator = ".";
341 | if(this.pragmas["IMPLICIT-ITERATOR"]) {
342 | iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
343 | }
344 | var ctx = {};
345 | ctx[iterator] = _context;
346 | return ctx;
347 | }
348 | },
349 |
350 | is_object: function(a) {
351 | return a && typeof a == "object";
352 | },
353 |
354 | is_array: function(a) {
355 | return Object.prototype.toString.call(a) === '[object Array]';
356 | },
357 |
358 | /*
359 | Gets rid of leading and trailing whitespace
360 | */
361 | trim: function(s) {
362 | return s.replace(/^\s*|\s*$/g, "");
363 | },
364 |
365 | /*
366 | Why, why, why? Because IE. Cry, cry cry.
367 | */
368 | map: function(array, fn) {
369 | if (typeof array.map == "function") {
370 | return array.map(fn);
371 | } else {
372 | var r = [];
373 | var l = array.length;
374 | for(var i = 0; i < l; i++) {
375 | r.push(fn(array[i]));
376 | }
377 | return r;
378 | }
379 | },
380 |
381 | getCachedRegex: function(name, generator) {
382 | var byOtag = regexCache[this.otag];
383 | if (!byOtag) {
384 | byOtag = regexCache[this.otag] = {};
385 | }
386 |
387 | var byCtag = byOtag[this.ctag];
388 | if (!byCtag) {
389 | byCtag = byOtag[this.ctag] = {};
390 | }
391 |
392 | var regex = byCtag[name];
393 | if (!regex) {
394 | regex = byCtag[name] = generator(this.otag, this.ctag);
395 | }
396 |
397 | return regex;
398 | }
399 | };
400 |
401 | return({
402 | name: "mustache.js",
403 | version: "0.4.0-dev",
404 |
405 | /*
406 | Turns a template and view into HTML
407 | */
408 | to_html: function(template, view, partials, send_fun) {
409 | var renderer = new Renderer();
410 | if(send_fun) {
411 | renderer.send = send_fun;
412 | }
413 | renderer.render(template, view || {}, partials);
414 | if(!send_fun) {
415 | return renderer.buffer.join("\n");
416 | }
417 | }
418 | });
419 | }();
--------------------------------------------------------------------------------
/public/app.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | // Our global object
3 | window.irc = window.irc || {};
4 |
5 | // socket.io init
6 | // var socket = io.connect('http://localhost/');
7 | var socket = io.connect();
8 |
9 |
10 | // MODELS & COLLECTIONS
11 | // ====================
12 | var Message = Backbone.Model.extend({
13 | defaults: {
14 | // expected properties:
15 | // - sender
16 | // - raw
17 | 'type': 'message'
18 | },
19 |
20 | initialize: function() {
21 | if (this.get('raw')) {
22 | this.set({text: this.parse( irc.util.escapeHTML(this.get('raw')) )});
23 | }
24 | },
25 |
26 | parse: function(text) {
27 | return this._linkify(text);
28 | },
29 |
30 | // Set output text for status messages
31 | setText: function() {
32 | var text = '';
33 | switch (this.get('type')) {
34 | case 'join':
35 | text = this.get('nick') + ' joined the channel';
36 | break;
37 | case 'part':
38 | text = this.get('nick') + ' left the channel';
39 | break;
40 | case 'nick':
41 | text = this.get('oldNick') + ' is now known as ' + this.get('newNick');
42 | break;
43 | }
44 | this.set({text: text});
45 | },
46 |
47 | // Find and link URLs
48 | _linkify: function(text) {
49 | // see http://daringfireball.net/2010/07/improved_regex_for_matching_urls
50 | var re = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/gi;
51 | var parsed = text.replace(re, function(url) {
52 | // turn into a link
53 | var href = url;
54 | if (url.indexOf('http') !== 0) {
55 | href = 'http://' + url;
56 | }
57 | return '' + url + ' ';
58 | });
59 | return parsed;
60 | }
61 | });
62 |
63 | var Stream = Backbone.Collection.extend({
64 | model: Message
65 | });
66 |
67 | var Person = Backbone.Model.extend({
68 | defaults: {
69 | opStatus: ''
70 | }
71 | });
72 |
73 | var Participants = Backbone.Collection.extend({
74 | model: Person,
75 | getByNick: function(nick) {
76 | return this.detect(function(person) {
77 | return person.get('nick') == nick;
78 | });
79 | }
80 | });
81 |
82 | var Frame = Backbone.Model.extend({
83 | // expected properties:
84 | // - name
85 | defaults: {
86 | 'type': 'channel',
87 | 'active': true
88 | },
89 |
90 | initialize: function() {
91 | this.stream = new Stream;
92 | this.participants = new Participants;
93 | },
94 |
95 | part: function() {
96 | console.log('Leaving ' + this.get('name'));
97 | this.destroy();
98 | }
99 |
100 | });
101 |
102 | var FrameList = Backbone.Collection.extend({
103 | model: Frame,
104 |
105 | getByName: function(name) {
106 | return this.detect(function(frame) {
107 | return frame.get('name') == name;
108 | });
109 | },
110 |
111 | getActive: function() {
112 | return this.detect(function(frame) {
113 | return frame.get('active') == true;
114 | });
115 | },
116 |
117 | setActive: function(frame) {
118 | this.each(function(frm) {
119 | frm.set({active: false});
120 | });
121 |
122 | frame.set({active: true});
123 | },
124 |
125 | getChannels: function() {
126 | return this.filter(function(frame) {
127 | return frame.get('type') == 'channel';
128 | });
129 | }
130 |
131 | });
132 |
133 | // hoisted to window for now, for ease of debugging
134 | window.frames = new FrameList;
135 |
136 |
137 | // VIEWS
138 | // =====
139 | var MessageView = Backbone.View.extend({
140 | tmpl: $('#message-tmpl').html(),
141 | initialize: function() {
142 | this.render();
143 | },
144 |
145 | render: function() {
146 | var context = {
147 | sender: this.model.get('sender'),
148 | text: this.model.get('text')
149 | };
150 | var html = Mustache.to_html(this.tmpl, context);
151 | $(this.el).addClass(this.model.get('type'))
152 | .html(html);
153 | return this;
154 | }
155 | });
156 |
157 | // Nick in the sidebar
158 | var NickListView = Backbone.View.extend({
159 | el: $('.nicks'),
160 | initialize: function() {
161 | _.bindAll(this);
162 | },
163 |
164 | // this is a temp. hack
165 | tmpl: function(opStatus, nick) {
166 | return '' + opStatus + ' ' + nick + '
'
167 | },
168 |
169 | switchChannel: function(ch) {
170 | ch.participants.bind('add', this.addOne, this);
171 | ch.participants.bind('change', this.changeNick, this);
172 | },
173 |
174 | addOne: function(p) {
175 | var text = this.tmpl(p.get('opStatus'), p.get('nick'));
176 | $(this.el).append(text);
177 | },
178 |
179 | addAll: function(participants) {
180 | var nicks = [];
181 | participants.each(function(p) {
182 | var text = this.tmpl(p.get('opStatus'), p.get('nick'));
183 | nicks.push(text);
184 | }, this);
185 | $(this.el).html(nicks.join('\n'));
186 | },
187 |
188 | changeNick: function() {
189 | console.log('Change of nick seen');
190 | console.log(arguments);
191 | }
192 |
193 | });
194 | var nickList = new NickListView;
195 |
196 | var FrameView = Backbone.View.extend({
197 | el: $('#frame'),
198 | // to track scroll position
199 | position: {},
200 |
201 | initialize: function() {
202 | _.bindAll(this);
203 | },
204 |
205 | addMessage: function(message, single) {
206 | // Only do this on single message additions
207 | if (single) {
208 | var position = $('#messages').scrollTop();
209 | var atBottom = $('#messages')[0].scrollHeight - position
210 | == $('#messages').innerHeight();
211 | }
212 | var view = new MessageView({model: message});
213 | $('#messages').append(view.el);
214 | // Scroll to bottom on new message if already at bottom
215 | if (atBottom) {
216 | $('#messages').scrollTop(position + 100);
217 | }
218 | },
219 |
220 | updateTopic: function(channel) {
221 | this.$('#topic').text(channel.get('topic')).show();
222 | $('#messages').css('top', $('#topic').outerHeight(true));
223 | },
224 |
225 | // Switch focus to a different frame
226 | focus: function(frame) {
227 | // Save scroll position for frame before switching
228 | if (this.focused) {
229 | this.position[this.focused.get('name')] = this.$('#output').scrollTop();
230 | }
231 | this.focused = frame;
232 | frames.setActive(this.focused);
233 | $('#messages').empty();
234 |
235 | frame.stream.each(function(message) {
236 | this.addMessage(message, false);
237 | }, this);
238 |
239 | nickList.addAll(frame.participants);
240 |
241 | if (frame.get('type') == 'channel') {
242 | this.$('#sidebar').show();
243 | frame.get('topic') && this.updateTopic(frame);
244 | $('.wrapper').css('margin-right', 205);
245 | $('#messages').css('top', $('#topic').outerHeight(true));
246 | } else {
247 | this.$('#sidebar').hide();
248 | this.$('#topic').hide();
249 | $('.wrapper').css('margin-right', 0);
250 | $('#messages').css('top', 0);
251 | }
252 | $(this.el).removeClass().addClass(frame.get('type'));
253 |
254 | this.$('#output #messsages').scrollTop(this.position[frame.get('name')] || 0);
255 |
256 | // Only the selected frame should send messages
257 | frames.each(function(frm) {
258 | frm.stream.unbind('add');
259 | frm.participants.unbind();
260 | frm.unbind();
261 | });
262 | frame.bind('change:topic', this.updateTopic, this);
263 | frame.stream.bind('add', this.addMessage, this);
264 | nickList.switchChannel(frame);
265 | },
266 |
267 | updateNicks: function(model, nicks) {
268 | console.log('Nicks rendered');
269 | }
270 | });
271 |
272 | var FrameTabView = Backbone.View.extend({
273 | tagName: 'li',
274 | tmpl: $('#tab-tmpl').html(),
275 |
276 | initialize: function() {
277 | this.model.bind('destroy', this.close, this);
278 | this.render();
279 | },
280 |
281 | events: {
282 | 'click': 'setActive',
283 | 'click .close-frame': 'close'
284 | },
285 |
286 | // Send PART command to server
287 | part: function() {
288 | if (this.model.get('type') === 'channel') {
289 | socket.emit('part', this.model.get('name'));
290 | } else {
291 | // PMs don't need an explicit PART
292 | this.model.destroy();
293 | }
294 | },
295 |
296 | // Close frame
297 | close: function() {
298 | // Focus on next frame if this one has the focus
299 | if ($(this.el).hasClass('active')) {
300 | // Go to previous frame unless it's status
301 | if ($(this.el).prev().text().trim() !== 'status') {
302 | $(this.el).prev().click();
303 | } else {
304 | $(this.el).next().click();
305 | }
306 | }
307 | $(this.el).remove();
308 | },
309 |
310 | // Set as active tab; focus window on frame
311 | setActive: function() {
312 | console.log('View setting active status');
313 | $(this.el).addClass('active')
314 | .siblings().removeClass('active');
315 | irc.frameWindow.focus(this.model);
316 | },
317 |
318 | render: function() {
319 | console.log(this.model);
320 | var self = this;
321 | var context = {
322 | text: this.model.get('name'),
323 | type: this.model.get('type'),
324 | isStatus: function() {
325 | return self.model.get('type') == 'status';
326 | }
327 | };
328 | var html = Mustache.to_html(this.tmpl, context);
329 | $(this.el).html(html);
330 | return this;
331 | }
332 | });
333 |
334 | var AppView = Backbone.View.extend({
335 | el: $('#content'),
336 | testFrames: $('#sidebar .frames'),
337 | frameList: $('header .frames'),
338 |
339 | initialize: function() {
340 | frames.bind('add', this.addTab, this);
341 | this.input = this.$('#prime-input');
342 | this.render();
343 | },
344 |
345 | events: {
346 | 'keypress #prime-input': 'sendInput',
347 | },
348 |
349 | addTab: function(frame) {
350 | var tab = new FrameTabView({model: frame});
351 | this.frameList.append(tab.el);
352 | tab.setActive();
353 | },
354 |
355 | joinChannel: function(name) {
356 | socket.emit('join', name);
357 | },
358 |
359 | // Map common IRC commands to standard (RFC 1459)
360 | parse: function(text) {
361 | var command = text.split(' ')[0];
362 | console.log(command);
363 | var revised = '';
364 | switch (command) {
365 | case 'msg':
366 | revised = 'privmsg';
367 | break;
368 | default:
369 | revised = command;
370 | break;
371 | }
372 | return irc.util.swapCommand(command, revised, text);
373 | },
374 |
375 | sendInput: function(e) {
376 | if (e.keyCode != 13) return;
377 | var frame = irc.frameWindow.focused,
378 | input = this.input.val();
379 |
380 | if (input.indexOf('/') === 0) {
381 | var parsed = this.parse(input.substr(1));
382 | socket.emit('command', parsed);
383 | // special case -- no output emitted, yet we want a new frame
384 | var msgParts = parsed.split(' ');
385 | if (msgParts[0].toLowerCase() === 'privmsg') {
386 | pm = frames.getByName(msgParts[1]) || new Frame({type: 'pm', name: msgParts[1]});
387 | pm.stream.add({sender: irc.me.get('nick'), raw: msgParts[2]})
388 | frames.add(pm);
389 | }
390 | } else {
391 | socket.emit('say', {
392 | target: frame.get('name'),
393 | message: input
394 | });
395 | frame.stream.add({sender: irc.me.get('nick'), raw: input});
396 | }
397 |
398 | this.input.val('');
399 | },
400 |
401 | render: function() {
402 | // Dynamically assign height
403 | this.el.show();
404 |
405 | $(window).resize(function() {
406 | sizeContent($('#frame #output'));
407 | sizeContent($('#frame #sidebar'));
408 | sizeContent($('#sidebar .nicks', '.stats'));
409 | });
410 | }
411 |
412 | });
413 |
414 | var ConnectView = Backbone.View.extend({
415 | el: $('#connect'),
416 | events: {
417 | 'click .btn': 'connect',
418 | 'keypress': 'connectOnEnter'
419 | },
420 |
421 | initialize: function() {
422 | _.bindAll(this);
423 | this.render();
424 | },
425 |
426 | render: function() {
427 | this.el.modal({backdrop: true, show: true});
428 | $('#connect-nick').focus();
429 | },
430 |
431 | connectOnEnter: function(e) {
432 | if (e.keyCode != 13) return;
433 | this.connect();
434 | },
435 |
436 | connect: function(e) {
437 | e && e.preventDefault();
438 |
439 | var channelInput = $('#connect-channels').val(),
440 | channels = channelInput ? channelInput.split(' ') : [];
441 | var connectInfo = {
442 | nick: $('#connect-nick').val(),
443 | server: $('#connect-server').val(),
444 | channels: channels
445 | };
446 |
447 | socket.emit('connect', connectInfo);
448 | $('#connect').modal('hide');
449 |
450 | irc.me = new Person({nick: connectInfo.nick});
451 |
452 | irc.frameWindow = new FrameView;
453 | irc.app = new AppView;
454 | // Create the status "frame"
455 | frames.add({name: 'status', type: 'status'});
456 |
457 | sizeContent($('#frame #output'));
458 | sizeContent($('#frame #sidebar'));
459 | sizeContent($('#sidebar .nicks', '.stats'));
460 | }
461 |
462 | });
463 |
464 | var connect = new ConnectView;
465 |
466 | // UTILS
467 | // =====
468 | function humanizeError(message) {
469 | var text = '';
470 | switch (message.command) {
471 | case 'err_unknowncommand':
472 | text = 'That is not a known IRC command.';
473 | break;
474 | }
475 | return text;
476 | }
477 |
478 | // Set output window to full height, minus other elements
479 | function sizeContent(sel, additional) {
480 | var newHeight = $('html').height() - $('header').outerHeight(true)
481 | - $('#prime-input').outerHeight(true)
482 | - (sel.outerHeight(true) - sel.height()) - 10;
483 | // 10 = #content padding
484 | if (additional) {
485 | newHeight -= $(additional).outerHeight(true);
486 | }
487 | sel.height(newHeight);
488 | }
489 |
490 |
491 | // SOCKET EVENTS
492 | // =============
493 | socket.on('message', function(msg) {
494 | // Filter out messages not aimed at a channel or status (i.e. PMs)
495 | if (msg.to.indexOf('#') !== 0 &&
496 | msg.to.indexOf('&') !== 0 &&
497 | msg.to !== 'status') return;
498 | frame = frames.getByName(msg.to);
499 | if (frame) {
500 | frame.stream.add({sender: msg.from, raw: msg.text});
501 | }
502 | });
503 |
504 | socket.on('pm', function(msg) {
505 | pm = frames.getByName(msg.nick) || new Frame({type: 'pm', name: msg.nick});
506 | pm.stream.add({sender: msg.nick, raw: msg.text})
507 | frames.add(pm);
508 | })
509 |
510 | // Message of the Day event (on joining a server)
511 | socket.on('motd', function(data) {
512 | data.motd.split('\n').forEach(function(line) {
513 | frames.getByName('status').stream.add({sender: '', raw: line});
514 | });
515 | });
516 |
517 | // Join channel event
518 | socket.on('join', function(data) {
519 | console.log('Join event received for ' + data.channel + ' - ' + data.nick);
520 | if (data.nick == irc.me.get('nick')) {
521 | frames.add({name: data.channel});
522 | } else {
523 | channel = frames.getByName(data.channel);
524 | channel.participants.add({nick: data.nick});
525 | var joinMessage = new Message({type: 'join', nick: data.nick});
526 | joinMessage.setText();
527 | channel.stream.add(joinMessage);
528 | }
529 | });
530 |
531 | // Part channel event
532 | socket.on('part', function(data) {
533 | console.log('Part event received for ' + data.channel + ' - ' + data.nick);
534 | if (data.nick == irc.me.get('nick')) {
535 | frames.getByName(data.channel).part();
536 | } else {
537 | channel = frames.getByName(data.channel);
538 | channel.participants.getByNick(data.nick).destroy();
539 | var partMessage = new Message({type: 'part', nick: data.nick});
540 | partMessage.setText();
541 | channel.stream.add(partMessage);
542 | }
543 | });
544 |
545 | // Set topic event
546 | socket.on('topic', function(data) {
547 | var channel = frames.getByName(data.channel);
548 | channel.set({topic: data.topic});
549 | // TODO: Show this was changed by data.nick in the channel stream
550 | });
551 |
552 | // Nick change event
553 | socket.on('nick', function(data) {
554 | // Update my info, if it's me
555 | console.log('Nick change', data);
556 | if (data.oldNick == irc.me.get('nick')) {
557 | irc.me.set({nick: data.newNick});
558 | }
559 |
560 | // Set new name in all channels
561 | data.channels.forEach(function(ch) {
562 | var channel = frames.getByName(ch);
563 | // Change nick in user list
564 | channel.participants.getByNick(data.oldNick).set({nick: data.newNick});
565 | // Send nick change message to channel stream
566 | var nickMessage = new Message({
567 | type: 'nick',
568 | oldNick: data.oldNick,
569 | newNick: data.newNick
570 | });
571 | nickMessage.setText();
572 | channel.stream.add(nickMessage);
573 | });
574 | });
575 |
576 | socket.on('names', function(data) {
577 | var frame = frames.getByName(data.channel);
578 | console.log(data);
579 | for (var nick in data.nicks) {
580 | frame.participants.add({nick: nick, opStatus: data.nicks[nick]});
581 | }
582 | });
583 |
584 | socket.on('error', function(data) {
585 | console.log(data.message);
586 | frame = frames.getActive();
587 | error = humanizeError(data.message);
588 | frame.stream.add({type: 'error', raw: error});
589 | });
590 |
591 | });
--------------------------------------------------------------------------------
/public/js/backbone.js:
--------------------------------------------------------------------------------
1 | // Backbone.js 0.5.3
2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Backbone may be freely distributed under the MIT license.
4 | // For all details and documentation:
5 | // http://documentcloud.github.com/backbone
6 |
7 | (function(){
8 |
9 | // Initial Setup
10 | // -------------
11 |
12 | // Save a reference to the global object.
13 | var root = this;
14 |
15 | // Save the previous value of the `Backbone` variable.
16 | var previousBackbone = root.Backbone;
17 |
18 | // The top-level namespace. All public Backbone classes and modules will
19 | // be attached to this. Exported for both CommonJS and the browser.
20 | var Backbone;
21 | if (typeof exports !== 'undefined') {
22 | Backbone = exports;
23 | } else {
24 | Backbone = root.Backbone = {};
25 | }
26 |
27 | // Current version of the library. Keep in sync with `package.json`.
28 | Backbone.VERSION = '0.5.3';
29 |
30 | // Require Underscore, if we're on the server, and it's not already present.
31 | var _ = root._;
32 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
33 |
34 | // For Backbone's purposes, jQuery or Zepto owns the `$` variable.
35 | var $ = root.jQuery || root.Zepto;
36 |
37 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
38 | // to its previous owner. Returns a reference to this Backbone object.
39 | Backbone.noConflict = function() {
40 | root.Backbone = previousBackbone;
41 | return this;
42 | };
43 |
44 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
45 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
46 | // `X-Http-Method-Override` header.
47 | Backbone.emulateHTTP = false;
48 |
49 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct
50 | // `application/json` requests ... will encode the body as
51 | // `application/x-www-form-urlencoded` instead and will send the model in a
52 | // form param named `model`.
53 | Backbone.emulateJSON = false;
54 |
55 | // Backbone.Events
56 | // -----------------
57 |
58 | // A module that can be mixed in to *any object* in order to provide it with
59 | // custom events. You may `bind` or `unbind` a callback function to an event;
60 | // `trigger`-ing an event fires all callbacks in succession.
61 | //
62 | // var object = {};
63 | // _.extend(object, Backbone.Events);
64 | // object.bind('expand', function(){ alert('expanded'); });
65 | // object.trigger('expand');
66 | //
67 | Backbone.Events = {
68 |
69 | // Bind an event, specified by a string name, `ev`, to a `callback` function.
70 | // Passing `"all"` will bind the callback to all events fired.
71 | bind : function(ev, callback, context) {
72 | var calls = this._callbacks || (this._callbacks = {});
73 | var list = calls[ev] || (calls[ev] = []);
74 | list.push([callback, context]);
75 | return this;
76 | },
77 |
78 | // Remove one or many callbacks. If `callback` is null, removes all
79 | // callbacks for the event. If `ev` is null, removes all bound callbacks
80 | // for all events.
81 | unbind : function(ev, callback) {
82 | var calls;
83 | if (!ev) {
84 | this._callbacks = {};
85 | } else if (calls = this._callbacks) {
86 | if (!callback) {
87 | calls[ev] = [];
88 | } else {
89 | var list = calls[ev];
90 | if (!list) return this;
91 | for (var i = 0, l = list.length; i < l; i++) {
92 | if (list[i] && callback === list[i][0]) {
93 | list[i] = null;
94 | break;
95 | }
96 | }
97 | }
98 | }
99 | return this;
100 | },
101 |
102 | // Trigger an event, firing all bound callbacks. Callbacks are passed the
103 | // same arguments as `trigger` is, apart from the event name.
104 | // Listening for `"all"` passes the true event name as the first argument.
105 | trigger : function(eventName) {
106 | var list, calls, ev, callback, args;
107 | var both = 2;
108 | if (!(calls = this._callbacks)) return this;
109 | while (both--) {
110 | ev = both ? eventName : 'all';
111 | if (list = calls[ev]) {
112 | for (var i = 0, l = list.length; i < l; i++) {
113 | if (!(callback = list[i])) {
114 | list.splice(i, 1); i--; l--;
115 | } else {
116 | args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
117 | callback[0].apply(callback[1] || this, args);
118 | }
119 | }
120 | }
121 | }
122 | return this;
123 | }
124 |
125 | };
126 |
127 | // Backbone.Model
128 | // --------------
129 |
130 | // Create a new model, with defined attributes. A client id (`cid`)
131 | // is automatically generated and assigned for you.
132 | Backbone.Model = function(attributes, options) {
133 | var defaults;
134 | attributes || (attributes = {});
135 | if (defaults = this.defaults) {
136 | if (_.isFunction(defaults)) defaults = defaults.call(this);
137 | attributes = _.extend({}, defaults, attributes);
138 | }
139 | this.attributes = {};
140 | this._escapedAttributes = {};
141 | this.cid = _.uniqueId('c');
142 | this.set(attributes, {silent : true});
143 | this._changed = false;
144 | this._previousAttributes = _.clone(this.attributes);
145 | if (options && options.collection) this.collection = options.collection;
146 | this.initialize(attributes, options);
147 | };
148 |
149 | // Attach all inheritable methods to the Model prototype.
150 | _.extend(Backbone.Model.prototype, Backbone.Events, {
151 |
152 | // A snapshot of the model's previous attributes, taken immediately
153 | // after the last `"change"` event was fired.
154 | _previousAttributes : null,
155 |
156 | // Has the item been changed since the last `"change"` event?
157 | _changed : false,
158 |
159 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and
160 | // CouchDB users may want to set this to `"_id"`.
161 | idAttribute : 'id',
162 |
163 | // Initialize is an empty function by default. Override it with your own
164 | // initialization logic.
165 | initialize : function(){},
166 |
167 | // Return a copy of the model's `attributes` object.
168 | toJSON : function() {
169 | return _.clone(this.attributes);
170 | },
171 |
172 | // Get the value of an attribute.
173 | get : function(attr) {
174 | return this.attributes[attr];
175 | },
176 |
177 | // Get the HTML-escaped value of an attribute.
178 | escape : function(attr) {
179 | var html;
180 | if (html = this._escapedAttributes[attr]) return html;
181 | var val = this.attributes[attr];
182 | return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);
183 | },
184 |
185 | // Returns `true` if the attribute contains a value that is not null
186 | // or undefined.
187 | has : function(attr) {
188 | return this.attributes[attr] != null;
189 | },
190 |
191 | // Set a hash of model attributes on the object, firing `"change"` unless you
192 | // choose to silence it.
193 | set : function(attrs, options) {
194 |
195 | // Extract attributes and options.
196 | options || (options = {});
197 | if (!attrs) return this;
198 | if (attrs.attributes) attrs = attrs.attributes;
199 | var now = this.attributes, escaped = this._escapedAttributes;
200 |
201 | // Run validation.
202 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
203 |
204 | // Check for changes of `id`.
205 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
206 |
207 | // We're about to start triggering change events.
208 | var alreadyChanging = this._changing;
209 | this._changing = true;
210 |
211 | // Update attributes.
212 | for (var attr in attrs) {
213 | var val = attrs[attr];
214 | if (!_.isEqual(now[attr], val)) {
215 | now[attr] = val;
216 | delete escaped[attr];
217 | this._changed = true;
218 | if (!options.silent) this.trigger('change:' + attr, this, val, options);
219 | }
220 | }
221 |
222 | // Fire the `"change"` event, if the model has been changed.
223 | if (!alreadyChanging && !options.silent && this._changed) this.change(options);
224 | this._changing = false;
225 | return this;
226 | },
227 |
228 | // Remove an attribute from the model, firing `"change"` unless you choose
229 | // to silence it. `unset` is a noop if the attribute doesn't exist.
230 | unset : function(attr, options) {
231 | if (!(attr in this.attributes)) return this;
232 | options || (options = {});
233 | var value = this.attributes[attr];
234 |
235 | // Run validation.
236 | var validObj = {};
237 | validObj[attr] = void 0;
238 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
239 |
240 | // Remove the attribute.
241 | delete this.attributes[attr];
242 | delete this._escapedAttributes[attr];
243 | if (attr == this.idAttribute) delete this.id;
244 | this._changed = true;
245 | if (!options.silent) {
246 | this.trigger('change:' + attr, this, void 0, options);
247 | this.change(options);
248 | }
249 | return this;
250 | },
251 |
252 | // Clear all attributes on the model, firing `"change"` unless you choose
253 | // to silence it.
254 | clear : function(options) {
255 | options || (options = {});
256 | var attr;
257 | var old = this.attributes;
258 |
259 | // Run validation.
260 | var validObj = {};
261 | for (attr in old) validObj[attr] = void 0;
262 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
263 |
264 | this.attributes = {};
265 | this._escapedAttributes = {};
266 | this._changed = true;
267 | if (!options.silent) {
268 | for (attr in old) {
269 | this.trigger('change:' + attr, this, void 0, options);
270 | }
271 | this.change(options);
272 | }
273 | return this;
274 | },
275 |
276 | // Fetch the model from the server. If the server's representation of the
277 | // model differs from its current attributes, they will be overriden,
278 | // triggering a `"change"` event.
279 | fetch : function(options) {
280 | options || (options = {});
281 | var model = this;
282 | var success = options.success;
283 | options.success = function(resp, status, xhr) {
284 | if (!model.set(model.parse(resp, xhr), options)) return false;
285 | if (success) success(model, resp);
286 | };
287 | options.error = wrapError(options.error, model, options);
288 | return (this.sync || Backbone.sync).call(this, 'read', this, options);
289 | },
290 |
291 | // Set a hash of model attributes, and sync the model to the server.
292 | // If the server returns an attributes hash that differs, the model's
293 | // state will be `set` again.
294 | save : function(attrs, options) {
295 | options || (options = {});
296 | if (attrs && !this.set(attrs, options)) return false;
297 | var model = this;
298 | var success = options.success;
299 | options.success = function(resp, status, xhr) {
300 | if (!model.set(model.parse(resp, xhr), options)) return false;
301 | if (success) success(model, resp, xhr);
302 | };
303 | options.error = wrapError(options.error, model, options);
304 | var method = this.isNew() ? 'create' : 'update';
305 | return (this.sync || Backbone.sync).call(this, method, this, options);
306 | },
307 |
308 | // Destroy this model on the server if it was already persisted. Upon success, the model is removed
309 | // from its collection, if it has one.
310 | destroy : function(options) {
311 | options || (options = {});
312 | if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
313 | var model = this;
314 | var success = options.success;
315 | options.success = function(resp) {
316 | model.trigger('destroy', model, model.collection, options);
317 | if (success) success(model, resp);
318 | };
319 | options.error = wrapError(options.error, model, options);
320 | return (this.sync || Backbone.sync).call(this, 'delete', this, options);
321 | },
322 |
323 | // Default URL for the model's representation on the server -- if you're
324 | // using Backbone's restful methods, override this to change the endpoint
325 | // that will be called.
326 | url : function() {
327 | var base = getUrl(this.collection) || this.urlRoot || urlError();
328 | if (this.isNew()) return base;
329 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
330 | },
331 |
332 | // **parse** converts a response into the hash of attributes to be `set` on
333 | // the model. The default implementation is just to pass the response along.
334 | parse : function(resp, xhr) {
335 | return resp;
336 | },
337 |
338 | // Create a new model with identical attributes to this one.
339 | clone : function() {
340 | return new this.constructor(this);
341 | },
342 |
343 | // A model is new if it has never been saved to the server, and lacks an id.
344 | isNew : function() {
345 | return this.id == null;
346 | },
347 |
348 | // Call this method to manually fire a `change` event for this model.
349 | // Calling this will cause all objects observing the model to update.
350 | change : function(options) {
351 | this.trigger('change', this, options);
352 | this._previousAttributes = _.clone(this.attributes);
353 | this._changed = false;
354 | },
355 |
356 | // Determine if the model has changed since the last `"change"` event.
357 | // If you specify an attribute name, determine if that attribute has changed.
358 | hasChanged : function(attr) {
359 | if (attr) return this._previousAttributes[attr] != this.attributes[attr];
360 | return this._changed;
361 | },
362 |
363 | // Return an object containing all the attributes that have changed, or false
364 | // if there are no changed attributes. Useful for determining what parts of a
365 | // view need to be updated and/or what attributes need to be persisted to
366 | // the server.
367 | changedAttributes : function(now) {
368 | now || (now = this.attributes);
369 | var old = this._previousAttributes;
370 | var changed = false;
371 | for (var attr in now) {
372 | if (!_.isEqual(old[attr], now[attr])) {
373 | changed = changed || {};
374 | changed[attr] = now[attr];
375 | }
376 | }
377 | return changed;
378 | },
379 |
380 | // Get the previous value of an attribute, recorded at the time the last
381 | // `"change"` event was fired.
382 | previous : function(attr) {
383 | if (!attr || !this._previousAttributes) return null;
384 | return this._previousAttributes[attr];
385 | },
386 |
387 | // Get all of the attributes of the model at the time of the previous
388 | // `"change"` event.
389 | previousAttributes : function() {
390 | return _.clone(this._previousAttributes);
391 | },
392 |
393 | // Run validation against a set of incoming attributes, returning `true`
394 | // if all is well. If a specific `error` callback has been passed,
395 | // call that instead of firing the general `"error"` event.
396 | _performValidation : function(attrs, options) {
397 | var error = this.validate(attrs);
398 | if (error) {
399 | if (options.error) {
400 | options.error(this, error, options);
401 | } else {
402 | this.trigger('error', this, error, options);
403 | }
404 | return false;
405 | }
406 | return true;
407 | }
408 |
409 | });
410 |
411 | // Backbone.Collection
412 | // -------------------
413 |
414 | // Provides a standard collection class for our sets of models, ordered
415 | // or unordered. If a `comparator` is specified, the Collection will maintain
416 | // its models in sort order, as they're added and removed.
417 | Backbone.Collection = function(models, options) {
418 | options || (options = {});
419 | if (options.comparator) this.comparator = options.comparator;
420 | _.bindAll(this, '_onModelEvent', '_removeReference');
421 | this._reset();
422 | if (models) this.reset(models, {silent: true});
423 | this.initialize.apply(this, arguments);
424 | };
425 |
426 | // Define the Collection's inheritable methods.
427 | _.extend(Backbone.Collection.prototype, Backbone.Events, {
428 |
429 | // The default model for a collection is just a **Backbone.Model**.
430 | // This should be overridden in most cases.
431 | model : Backbone.Model,
432 |
433 | // Initialize is an empty function by default. Override it with your own
434 | // initialization logic.
435 | initialize : function(){},
436 |
437 | // The JSON representation of a Collection is an array of the
438 | // models' attributes.
439 | toJSON : function() {
440 | return this.map(function(model){ return model.toJSON(); });
441 | },
442 |
443 | // Add a model, or list of models to the set. Pass **silent** to avoid
444 | // firing the `added` event for every new model.
445 | add : function(models, options) {
446 | if (_.isArray(models)) {
447 | for (var i = 0, l = models.length; i < l; i++) {
448 | this._add(models[i], options);
449 | }
450 | } else {
451 | this._add(models, options);
452 | }
453 | return this;
454 | },
455 |
456 | // Remove a model, or a list of models from the set. Pass silent to avoid
457 | // firing the `removed` event for every model removed.
458 | remove : function(models, options) {
459 | if (_.isArray(models)) {
460 | for (var i = 0, l = models.length; i < l; i++) {
461 | this._remove(models[i], options);
462 | }
463 | } else {
464 | this._remove(models, options);
465 | }
466 | return this;
467 | },
468 |
469 | // Get a model from the set by id.
470 | get : function(id) {
471 | if (id == null) return null;
472 | return this._byId[id.id != null ? id.id : id];
473 | },
474 |
475 | // Get a model from the set by client id.
476 | getByCid : function(cid) {
477 | return cid && this._byCid[cid.cid || cid];
478 | },
479 |
480 | // Get the model at the given index.
481 | at: function(index) {
482 | return this.models[index];
483 | },
484 |
485 | // Force the collection to re-sort itself. You don't need to call this under normal
486 | // circumstances, as the set will maintain sort order as each item is added.
487 | sort : function(options) {
488 | options || (options = {});
489 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
490 | this.models = this.sortBy(this.comparator);
491 | if (!options.silent) this.trigger('reset', this, options);
492 | return this;
493 | },
494 |
495 | // Pluck an attribute from each model in the collection.
496 | pluck : function(attr) {
497 | return _.map(this.models, function(model){ return model.get(attr); });
498 | },
499 |
500 | // When you have more items than you want to add or remove individually,
501 | // you can reset the entire set with a new list of models, without firing
502 | // any `added` or `removed` events. Fires `reset` when finished.
503 | reset : function(models, options) {
504 | models || (models = []);
505 | options || (options = {});
506 | this.each(this._removeReference);
507 | this._reset();
508 | this.add(models, {silent: true});
509 | if (!options.silent) this.trigger('reset', this, options);
510 | return this;
511 | },
512 |
513 | // Fetch the default set of models for this collection, resetting the
514 | // collection when they arrive. If `add: true` is passed, appends the
515 | // models to the collection instead of resetting.
516 | fetch : function(options) {
517 | options || (options = {});
518 | var collection = this;
519 | var success = options.success;
520 | options.success = function(resp, status, xhr) {
521 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
522 | if (success) success(collection, resp);
523 | };
524 | options.error = wrapError(options.error, collection, options);
525 | return (this.sync || Backbone.sync).call(this, 'read', this, options);
526 | },
527 |
528 | // Create a new instance of a model in this collection. After the model
529 | // has been created on the server, it will be added to the collection.
530 | // Returns the model, or 'false' if validation on a new model fails.
531 | create : function(model, options) {
532 | var coll = this;
533 | options || (options = {});
534 | model = this._prepareModel(model, options);
535 | if (!model) return false;
536 | var success = options.success;
537 | options.success = function(nextModel, resp, xhr) {
538 | coll.add(nextModel, options);
539 | if (success) success(nextModel, resp, xhr);
540 | };
541 | model.save(null, options);
542 | return model;
543 | },
544 |
545 | // **parse** converts a response into a list of models to be added to the
546 | // collection. The default implementation is just to pass it through.
547 | parse : function(resp, xhr) {
548 | return resp;
549 | },
550 |
551 | // Proxy to _'s chain. Can't be proxied the same way the rest of the
552 | // underscore methods are proxied because it relies on the underscore
553 | // constructor.
554 | chain: function () {
555 | return _(this.models).chain();
556 | },
557 |
558 | // Reset all internal state. Called when the collection is reset.
559 | _reset : function(options) {
560 | this.length = 0;
561 | this.models = [];
562 | this._byId = {};
563 | this._byCid = {};
564 | },
565 |
566 | // Prepare a model to be added to this collection
567 | _prepareModel: function(model, options) {
568 | if (!(model instanceof Backbone.Model)) {
569 | var attrs = model;
570 | model = new this.model(attrs, {collection: this});
571 | if (model.validate && !model._performValidation(attrs, options)) model = false;
572 | } else if (!model.collection) {
573 | model.collection = this;
574 | }
575 | return model;
576 | },
577 |
578 | // Internal implementation of adding a single model to the set, updating
579 | // hash indexes for `id` and `cid` lookups.
580 | // Returns the model, or 'false' if validation on a new model fails.
581 | _add : function(model, options) {
582 | options || (options = {});
583 | model = this._prepareModel(model, options);
584 | if (!model) return false;
585 | var already = this.getByCid(model);
586 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
587 | this._byId[model.id] = model;
588 | this._byCid[model.cid] = model;
589 | var index = options.at != null ? options.at :
590 | this.comparator ? this.sortedIndex(model, this.comparator) :
591 | this.length;
592 | this.models.splice(index, 0, model);
593 | model.bind('all', this._onModelEvent);
594 | this.length++;
595 | if (!options.silent) model.trigger('add', model, this, options);
596 | return model;
597 | },
598 |
599 | // Internal implementation of removing a single model from the set, updating
600 | // hash indexes for `id` and `cid` lookups.
601 | _remove : function(model, options) {
602 | options || (options = {});
603 | model = this.getByCid(model) || this.get(model);
604 | if (!model) return null;
605 | delete this._byId[model.id];
606 | delete this._byCid[model.cid];
607 | this.models.splice(this.indexOf(model), 1);
608 | this.length--;
609 | if (!options.silent) model.trigger('remove', model, this, options);
610 | this._removeReference(model);
611 | return model;
612 | },
613 |
614 | // Internal method to remove a model's ties to a collection.
615 | _removeReference : function(model) {
616 | if (this == model.collection) {
617 | delete model.collection;
618 | }
619 | model.unbind('all', this._onModelEvent);
620 | },
621 |
622 | // Internal method called every time a model in the set fires an event.
623 | // Sets need to update their indexes when models change ids. All other
624 | // events simply proxy through. "add" and "remove" events that originate
625 | // in other collections are ignored.
626 | _onModelEvent : function(ev, model, collection, options) {
627 | if ((ev == 'add' || ev == 'remove') && collection != this) return;
628 | if (ev == 'destroy') {
629 | this._remove(model, options);
630 | }
631 | if (model && ev === 'change:' + model.idAttribute) {
632 | delete this._byId[model.previous(model.idAttribute)];
633 | this._byId[model.id] = model;
634 | }
635 | this.trigger.apply(this, arguments);
636 | }
637 |
638 | });
639 |
640 | // Underscore methods that we want to implement on the Collection.
641 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
642 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
643 | 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
644 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
645 |
646 | // Mix in each Underscore method as a proxy to `Collection#models`.
647 | _.each(methods, function(method) {
648 | Backbone.Collection.prototype[method] = function() {
649 | return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
650 | };
651 | });
652 |
653 | // Backbone.Router
654 | // -------------------
655 |
656 | // Routers map faux-URLs to actions, and fire events when routes are
657 | // matched. Creating a new one sets its `routes` hash, if not set statically.
658 | Backbone.Router = function(options) {
659 | options || (options = {});
660 | if (options.routes) this.routes = options.routes;
661 | this._bindRoutes();
662 | this.initialize.apply(this, arguments);
663 | };
664 |
665 | // Cached regular expressions for matching named param parts and splatted
666 | // parts of route strings.
667 | var namedParam = /:([\w\d]+)/g;
668 | var splatParam = /\*([\w\d]+)/g;
669 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
670 |
671 | // Set up all inheritable **Backbone.Router** properties and methods.
672 | _.extend(Backbone.Router.prototype, Backbone.Events, {
673 |
674 | // Initialize is an empty function by default. Override it with your own
675 | // initialization logic.
676 | initialize : function(){},
677 |
678 | // Manually bind a single named route to a callback. For example:
679 | //
680 | // this.route('search/:query/p:num', 'search', function(query, num) {
681 | // ...
682 | // });
683 | //
684 | route : function(route, name, callback) {
685 | Backbone.history || (Backbone.history = new Backbone.History);
686 | if (!_.isRegExp(route)) route = this._routeToRegExp(route);
687 | Backbone.history.route(route, _.bind(function(fragment) {
688 | var args = this._extractParameters(route, fragment);
689 | callback.apply(this, args);
690 | this.trigger.apply(this, ['route:' + name].concat(args));
691 | }, this));
692 | },
693 |
694 | // Simple proxy to `Backbone.history` to save a fragment into the history.
695 | navigate : function(fragment, triggerRoute) {
696 | Backbone.history.navigate(fragment, triggerRoute);
697 | },
698 |
699 | // Bind all defined routes to `Backbone.history`. We have to reverse the
700 | // order of the routes here to support behavior where the most general
701 | // routes can be defined at the bottom of the route map.
702 | _bindRoutes : function() {
703 | if (!this.routes) return;
704 | var routes = [];
705 | for (var route in this.routes) {
706 | routes.unshift([route, this.routes[route]]);
707 | }
708 | for (var i = 0, l = routes.length; i < l; i++) {
709 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
710 | }
711 | },
712 |
713 | // Convert a route string into a regular expression, suitable for matching
714 | // against the current location hash.
715 | _routeToRegExp : function(route) {
716 | route = route.replace(escapeRegExp, "\\$&")
717 | .replace(namedParam, "([^\/]*)")
718 | .replace(splatParam, "(.*?)");
719 | return new RegExp('^' + route + '$');
720 | },
721 |
722 | // Given a route, and a URL fragment that it matches, return the array of
723 | // extracted parameters.
724 | _extractParameters : function(route, fragment) {
725 | return route.exec(fragment).slice(1);
726 | }
727 |
728 | });
729 |
730 | // Backbone.History
731 | // ----------------
732 |
733 | // Handles cross-browser history management, based on URL fragments. If the
734 | // browser does not support `onhashchange`, falls back to polling.
735 | Backbone.History = function() {
736 | this.handlers = [];
737 | _.bindAll(this, 'checkUrl');
738 | };
739 |
740 | // Cached regex for cleaning hashes.
741 | var hashStrip = /^#*/;
742 |
743 | // Cached regex for detecting MSIE.
744 | var isExplorer = /msie [\w.]+/;
745 |
746 | // Has the history handling already been started?
747 | var historyStarted = false;
748 |
749 | // Set up all inheritable **Backbone.History** properties and methods.
750 | _.extend(Backbone.History.prototype, {
751 |
752 | // The default interval to poll for hash changes, if necessary, is
753 | // twenty times a second.
754 | interval: 50,
755 |
756 | // Get the cross-browser normalized URL fragment, either from the URL,
757 | // the hash, or the override.
758 | getFragment : function(fragment, forcePushState) {
759 | if (fragment == null) {
760 | if (this._hasPushState || forcePushState) {
761 | fragment = window.location.pathname;
762 | var search = window.location.search;
763 | if (search) fragment += search;
764 | if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);
765 | } else {
766 | fragment = window.location.hash;
767 | }
768 | }
769 | return decodeURIComponent(fragment.replace(hashStrip, ''));
770 | },
771 |
772 | // Start the hash change handling, returning `true` if the current URL matches
773 | // an existing route, and `false` otherwise.
774 | start : function(options) {
775 |
776 | // Figure out the initial configuration. Do we need an iframe?
777 | // Is pushState desired ... is it available?
778 | if (historyStarted) throw new Error("Backbone.history has already been started");
779 | this.options = _.extend({}, {root: '/'}, this.options, options);
780 | this._wantsPushState = !!this.options.pushState;
781 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
782 | var fragment = this.getFragment();
783 | var docMode = document.documentMode;
784 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
785 | if (oldIE) {
786 | this.iframe = $('').hide().appendTo('body')[0].contentWindow;
787 | this.navigate(fragment);
788 | }
789 |
790 | // Depending on whether we're using pushState or hashes, and whether
791 | // 'onhashchange' is supported, determine how we check the URL state.
792 | if (this._hasPushState) {
793 | $(window).bind('popstate', this.checkUrl);
794 | } else if ('onhashchange' in window && !oldIE) {
795 | $(window).bind('hashchange', this.checkUrl);
796 | } else {
797 | setInterval(this.checkUrl, this.interval);
798 | }
799 |
800 | // Determine if we need to change the base url, for a pushState link
801 | // opened by a non-pushState browser.
802 | this.fragment = fragment;
803 | historyStarted = true;
804 | var loc = window.location;
805 | var atRoot = loc.pathname == this.options.root;
806 | if (this._wantsPushState && !this._hasPushState && !atRoot) {
807 | this.fragment = this.getFragment(null, true);
808 | window.location.replace(this.options.root + '#' + this.fragment);
809 | // Return immediately as browser will do redirect to new url
810 | return true;
811 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
812 | this.fragment = loc.hash.replace(hashStrip, '');
813 | window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
814 | }
815 |
816 | if (!this.options.silent) {
817 | return this.loadUrl();
818 | }
819 | },
820 |
821 | // Add a route to be tested when the fragment changes. Routes added later may
822 | // override previous routes.
823 | route : function(route, callback) {
824 | this.handlers.unshift({route : route, callback : callback});
825 | },
826 |
827 | // Checks the current URL to see if it has changed, and if it has,
828 | // calls `loadUrl`, normalizing across the hidden iframe.
829 | checkUrl : function(e) {
830 | var current = this.getFragment();
831 | if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
832 | if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
833 | if (this.iframe) this.navigate(current);
834 | this.loadUrl() || this.loadUrl(window.location.hash);
835 | },
836 |
837 | // Attempt to load the current URL fragment. If a route succeeds with a
838 | // match, returns `true`. If no defined routes matches the fragment,
839 | // returns `false`.
840 | loadUrl : function(fragmentOverride) {
841 | var fragment = this.fragment = this.getFragment(fragmentOverride);
842 | var matched = _.any(this.handlers, function(handler) {
843 | if (handler.route.test(fragment)) {
844 | handler.callback(fragment);
845 | return true;
846 | }
847 | });
848 | return matched;
849 | },
850 |
851 | // Save a fragment into the hash history. You are responsible for properly
852 | // URL-encoding the fragment in advance. This does not trigger
853 | // a `hashchange` event.
854 | navigate : function(fragment, triggerRoute) {
855 | var frag = (fragment || '').replace(hashStrip, '');
856 | if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
857 | if (this._hasPushState) {
858 | var loc = window.location;
859 | if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
860 | this.fragment = frag;
861 | window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
862 | } else {
863 | window.location.hash = this.fragment = frag;
864 | if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
865 | this.iframe.document.open().close();
866 | this.iframe.location.hash = frag;
867 | }
868 | }
869 | if (triggerRoute) this.loadUrl(fragment);
870 | }
871 |
872 | });
873 |
874 | // Backbone.View
875 | // -------------
876 |
877 | // Creating a Backbone.View creates its initial element outside of the DOM,
878 | // if an existing element is not provided...
879 | Backbone.View = function(options) {
880 | this.cid = _.uniqueId('view');
881 | this._configure(options || {});
882 | this._ensureElement();
883 | this.delegateEvents();
884 | this.initialize.apply(this, arguments);
885 | };
886 |
887 | // Element lookup, scoped to DOM elements within the current view.
888 | // This should be prefered to global lookups, if you're dealing with
889 | // a specific view.
890 | var selectorDelegate = function(selector) {
891 | return $(selector, this.el);
892 | };
893 |
894 | // Cached regex to split keys for `delegate`.
895 | var eventSplitter = /^(\S+)\s*(.*)$/;
896 |
897 | // List of view options to be merged as properties.
898 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
899 |
900 | // Set up all inheritable **Backbone.View** properties and methods.
901 | _.extend(Backbone.View.prototype, Backbone.Events, {
902 |
903 | // The default `tagName` of a View's element is `"div"`.
904 | tagName : 'div',
905 |
906 | // Attach the `selectorDelegate` function as the `$` property.
907 | $ : selectorDelegate,
908 |
909 | // Initialize is an empty function by default. Override it with your own
910 | // initialization logic.
911 | initialize : function(){},
912 |
913 | // **render** is the core function that your view should override, in order
914 | // to populate its element (`this.el`), with the appropriate HTML. The
915 | // convention is for **render** to always return `this`.
916 | render : function() {
917 | return this;
918 | },
919 |
920 | // Remove this view from the DOM. Note that the view isn't present in the
921 | // DOM by default, so calling this method may be a no-op.
922 | remove : function() {
923 | $(this.el).remove();
924 | return this;
925 | },
926 |
927 | // For small amounts of DOM Elements, where a full-blown template isn't
928 | // needed, use **make** to manufacture elements, one at a time.
929 | //
930 | // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
931 | //
932 | make : function(tagName, attributes, content) {
933 | var el = document.createElement(tagName);
934 | if (attributes) $(el).attr(attributes);
935 | if (content) $(el).html(content);
936 | return el;
937 | },
938 |
939 | // Set callbacks, where `this.callbacks` is a hash of
940 | //
941 | // *{"event selector": "callback"}*
942 | //
943 | // {
944 | // 'mousedown .title': 'edit',
945 | // 'click .button': 'save'
946 | // }
947 | //
948 | // pairs. Callbacks will be bound to the view, with `this` set properly.
949 | // Uses event delegation for efficiency.
950 | // Omitting the selector binds the event to `this.el`.
951 | // This only works for delegate-able events: not `focus`, `blur`, and
952 | // not `change`, `submit`, and `reset` in Internet Explorer.
953 | delegateEvents : function(events) {
954 | if (!(events || (events = this.events))) return;
955 | if (_.isFunction(events)) events = events.call(this);
956 | $(this.el).unbind('.delegateEvents' + this.cid);
957 | for (var key in events) {
958 | var method = this[events[key]];
959 | if (!method) throw new Error('Event "' + events[key] + '" does not exist');
960 | var match = key.match(eventSplitter);
961 | var eventName = match[1], selector = match[2];
962 | method = _.bind(method, this);
963 | eventName += '.delegateEvents' + this.cid;
964 | if (selector === '') {
965 | $(this.el).bind(eventName, method);
966 | } else {
967 | $(this.el).delegate(selector, eventName, method);
968 | }
969 | }
970 | },
971 |
972 | // Performs the initial configuration of a View with a set of options.
973 | // Keys with special meaning *(model, collection, id, className)*, are
974 | // attached directly to the view.
975 | _configure : function(options) {
976 | if (this.options) options = _.extend({}, this.options, options);
977 | for (var i = 0, l = viewOptions.length; i < l; i++) {
978 | var attr = viewOptions[i];
979 | if (options[attr]) this[attr] = options[attr];
980 | }
981 | this.options = options;
982 | },
983 |
984 | // Ensure that the View has a DOM element to render into.
985 | // If `this.el` is a string, pass it through `$()`, take the first
986 | // matching element, and re-assign it to `el`. Otherwise, create
987 | // an element from the `id`, `className` and `tagName` proeprties.
988 | _ensureElement : function() {
989 | if (!this.el) {
990 | var attrs = this.attributes || {};
991 | if (this.id) attrs.id = this.id;
992 | if (this.className) attrs['class'] = this.className;
993 | this.el = this.make(this.tagName, attrs);
994 | } else if (_.isString(this.el)) {
995 | this.el = $(this.el).get(0);
996 | }
997 | }
998 |
999 | });
1000 |
1001 | // The self-propagating extend function that Backbone classes use.
1002 | var extend = function (protoProps, classProps) {
1003 | var child = inherits(this, protoProps, classProps);
1004 | child.extend = this.extend;
1005 | return child;
1006 | };
1007 |
1008 | // Set up inheritance for the model, collection, and view.
1009 | Backbone.Model.extend = Backbone.Collection.extend =
1010 | Backbone.Router.extend = Backbone.View.extend = extend;
1011 |
1012 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1013 | var methodMap = {
1014 | 'create': 'POST',
1015 | 'update': 'PUT',
1016 | 'delete': 'DELETE',
1017 | 'read' : 'GET'
1018 | };
1019 |
1020 | // Backbone.sync
1021 | // -------------
1022 |
1023 | // Override this function to change the manner in which Backbone persists
1024 | // models to the server. You will be passed the type of request, and the
1025 | // model in question. By default, uses makes a RESTful Ajax request
1026 | // to the model's `url()`. Some possible customizations could be:
1027 | //
1028 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1029 | // * Send up the models as XML instead of JSON.
1030 | // * Persist models via WebSockets instead of Ajax.
1031 | //
1032 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1033 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1034 | // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
1035 | // `application/json` with the model in a param named `model`.
1036 | // Useful when interfacing with server-side languages like **PHP** that make
1037 | // it difficult to read the body of `PUT` requests.
1038 | Backbone.sync = function(method, model, options) {
1039 | var type = methodMap[method];
1040 |
1041 | // Default JSON-request options.
1042 | var params = _.extend({
1043 | type: type,
1044 | dataType: 'json'
1045 | }, options);
1046 |
1047 | // Ensure that we have a URL.
1048 | if (!params.url) {
1049 | params.url = getUrl(model) || urlError();
1050 | }
1051 |
1052 | // Ensure that we have the appropriate request data.
1053 | if (!params.data && model && (method == 'create' || method == 'update')) {
1054 | params.contentType = 'application/json';
1055 | params.data = JSON.stringify(model.toJSON());
1056 | }
1057 |
1058 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1059 | if (Backbone.emulateJSON) {
1060 | params.contentType = 'application/x-www-form-urlencoded';
1061 | params.data = params.data ? {model : params.data} : {};
1062 | }
1063 |
1064 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1065 | // And an `X-HTTP-Method-Override` header.
1066 | if (Backbone.emulateHTTP) {
1067 | if (type === 'PUT' || type === 'DELETE') {
1068 | if (Backbone.emulateJSON) params.data._method = type;
1069 | params.type = 'POST';
1070 | params.beforeSend = function(xhr) {
1071 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1072 | };
1073 | }
1074 | }
1075 |
1076 | // Don't process data on a non-GET request.
1077 | if (params.type !== 'GET' && !Backbone.emulateJSON) {
1078 | params.processData = false;
1079 | }
1080 |
1081 | // Make the request.
1082 | return $.ajax(params);
1083 | };
1084 |
1085 | // Helpers
1086 | // -------
1087 |
1088 | // Shared empty constructor function to aid in prototype-chain creation.
1089 | var ctor = function(){};
1090 |
1091 | // Helper function to correctly set up the prototype chain, for subclasses.
1092 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1093 | // class properties to be extended.
1094 | var inherits = function(parent, protoProps, staticProps) {
1095 | var child;
1096 |
1097 | // The constructor function for the new subclass is either defined by you
1098 | // (the "constructor" property in your `extend` definition), or defaulted
1099 | // by us to simply call `super()`.
1100 | if (protoProps && protoProps.hasOwnProperty('constructor')) {
1101 | child = protoProps.constructor;
1102 | } else {
1103 | child = function(){ return parent.apply(this, arguments); };
1104 | }
1105 |
1106 | // Inherit class (static) properties from parent.
1107 | _.extend(child, parent);
1108 |
1109 | // Set the prototype chain to inherit from `parent`, without calling
1110 | // `parent`'s constructor function.
1111 | ctor.prototype = parent.prototype;
1112 | child.prototype = new ctor();
1113 |
1114 | // Add prototype properties (instance properties) to the subclass,
1115 | // if supplied.
1116 | if (protoProps) _.extend(child.prototype, protoProps);
1117 |
1118 | // Add static properties to the constructor function, if supplied.
1119 | if (staticProps) _.extend(child, staticProps);
1120 |
1121 | // Correctly set child's `prototype.constructor`.
1122 | child.prototype.constructor = child;
1123 |
1124 | // Set a convenience property in case the parent's prototype is needed later.
1125 | child.__super__ = parent.prototype;
1126 |
1127 | return child;
1128 | };
1129 |
1130 | // Helper function to get a URL from a Model or Collection as a property
1131 | // or as a function.
1132 | var getUrl = function(object) {
1133 | if (!(object && object.url)) return null;
1134 | return _.isFunction(object.url) ? object.url() : object.url;
1135 | };
1136 |
1137 | // Throw an error when a URL is needed, and none is supplied.
1138 | var urlError = function() {
1139 | throw new Error('A "url" property or function must be specified');
1140 | };
1141 |
1142 | // Wrap an optional error callback with a fallback error event.
1143 | var wrapError = function(onError, model, options) {
1144 | return function(resp) {
1145 | if (onError) {
1146 | onError(model, resp, options);
1147 | } else {
1148 | model.trigger('error', model, resp, options);
1149 | }
1150 | };
1151 | };
1152 |
1153 | // Helper function to escape a string for HTML rendering.
1154 | var escapeHTML = function(string) {
1155 | return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
1156 | };
1157 |
1158 | }).call(this);
1159 |
--------------------------------------------------------------------------------
/test/qunit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit 1.2.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | (function(window) {
12 |
13 | var defined = {
14 | setTimeout: typeof window.setTimeout !== "undefined",
15 | sessionStorage: (function() {
16 | try {
17 | return !!sessionStorage.getItem;
18 | } catch(e) {
19 | return false;
20 | }
21 | })()
22 | };
23 |
24 | var testId = 0,
25 | toString = Object.prototype.toString,
26 | hasOwn = Object.prototype.hasOwnProperty;
27 |
28 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
29 | this.name = name;
30 | this.testName = testName;
31 | this.expected = expected;
32 | this.testEnvironmentArg = testEnvironmentArg;
33 | this.async = async;
34 | this.callback = callback;
35 | this.assertions = [];
36 | };
37 | Test.prototype = {
38 | init: function() {
39 | var tests = id("qunit-tests");
40 | if (tests) {
41 | var b = document.createElement("strong");
42 | b.innerHTML = "Running " + this.name;
43 | var li = document.createElement("li");
44 | li.appendChild( b );
45 | li.className = "running";
46 | li.id = this.id = "test-output" + testId++;
47 | tests.appendChild( li );
48 | }
49 | },
50 | setup: function() {
51 | if (this.module != config.previousModule) {
52 | if ( config.previousModule ) {
53 | runLoggingCallbacks('moduleDone', QUnit, {
54 | name: config.previousModule,
55 | failed: config.moduleStats.bad,
56 | passed: config.moduleStats.all - config.moduleStats.bad,
57 | total: config.moduleStats.all
58 | } );
59 | }
60 | config.previousModule = this.module;
61 | config.moduleStats = { all: 0, bad: 0 };
62 | runLoggingCallbacks( 'moduleStart', QUnit, {
63 | name: this.module
64 | } );
65 | }
66 |
67 | config.current = this;
68 | this.testEnvironment = extend({
69 | setup: function() {},
70 | teardown: function() {}
71 | }, this.moduleTestEnvironment);
72 | if (this.testEnvironmentArg) {
73 | extend(this.testEnvironment, this.testEnvironmentArg);
74 | }
75 |
76 | runLoggingCallbacks( 'testStart', QUnit, {
77 | name: this.testName,
78 | module: this.module
79 | });
80 |
81 | // allow utility functions to access the current test environment
82 | // TODO why??
83 | QUnit.current_testEnvironment = this.testEnvironment;
84 |
85 | try {
86 | if ( !config.pollution ) {
87 | saveGlobal();
88 | }
89 |
90 | this.testEnvironment.setup.call(this.testEnvironment);
91 | } catch(e) {
92 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message );
93 | }
94 | },
95 | run: function() {
96 | config.current = this;
97 | if ( this.async ) {
98 | QUnit.stop();
99 | }
100 |
101 | if ( config.notrycatch ) {
102 | this.callback.call(this.testEnvironment);
103 | return;
104 | }
105 | try {
106 | this.callback.call(this.testEnvironment);
107 | } catch(e) {
108 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback);
109 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) );
110 | // else next test will carry the responsibility
111 | saveGlobal();
112 |
113 | // Restart the tests if they're blocking
114 | if ( config.blocking ) {
115 | QUnit.start();
116 | }
117 | }
118 | },
119 | teardown: function() {
120 | config.current = this;
121 | try {
122 | this.testEnvironment.teardown.call(this.testEnvironment);
123 | checkPollution();
124 | } catch(e) {
125 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message );
126 | }
127 | },
128 | finish: function() {
129 | config.current = this;
130 | if ( this.expected != null && this.expected != this.assertions.length ) {
131 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" );
132 | }
133 |
134 | var good = 0, bad = 0,
135 | tests = id("qunit-tests");
136 |
137 | config.stats.all += this.assertions.length;
138 | config.moduleStats.all += this.assertions.length;
139 |
140 | if ( tests ) {
141 | var ol = document.createElement("ol");
142 |
143 | for ( var i = 0; i < this.assertions.length; i++ ) {
144 | var assertion = this.assertions[i];
145 |
146 | var li = document.createElement("li");
147 | li.className = assertion.result ? "pass" : "fail";
148 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed");
149 | ol.appendChild( li );
150 |
151 | if ( assertion.result ) {
152 | good++;
153 | } else {
154 | bad++;
155 | config.stats.bad++;
156 | config.moduleStats.bad++;
157 | }
158 | }
159 |
160 | // store result when possible
161 | if ( QUnit.config.reorder && defined.sessionStorage ) {
162 | if (bad) {
163 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad);
164 | } else {
165 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName);
166 | }
167 | }
168 |
169 | if (bad == 0) {
170 | ol.style.display = "none";
171 | }
172 |
173 | var b = document.createElement("strong");
174 | b.innerHTML = this.name + " (" + bad + " , " + good + " , " + this.assertions.length + ") ";
175 |
176 | var a = document.createElement("a");
177 | a.innerHTML = "Rerun";
178 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
179 |
180 | addEvent(b, "click", function() {
181 | var next = b.nextSibling.nextSibling,
182 | display = next.style.display;
183 | next.style.display = display === "none" ? "block" : "none";
184 | });
185 |
186 | addEvent(b, "dblclick", function(e) {
187 | var target = e && e.target ? e.target : window.event.srcElement;
188 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
189 | target = target.parentNode;
190 | }
191 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
192 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
193 | }
194 | });
195 |
196 | var li = id(this.id);
197 | li.className = bad ? "fail" : "pass";
198 | li.removeChild( li.firstChild );
199 | li.appendChild( b );
200 | li.appendChild( a );
201 | li.appendChild( ol );
202 |
203 | } else {
204 | for ( var i = 0; i < this.assertions.length; i++ ) {
205 | if ( !this.assertions[i].result ) {
206 | bad++;
207 | config.stats.bad++;
208 | config.moduleStats.bad++;
209 | }
210 | }
211 | }
212 |
213 | try {
214 | QUnit.reset();
215 | } catch(e) {
216 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset);
217 | }
218 |
219 | runLoggingCallbacks( 'testDone', QUnit, {
220 | name: this.testName,
221 | module: this.module,
222 | failed: bad,
223 | passed: this.assertions.length - bad,
224 | total: this.assertions.length
225 | } );
226 | },
227 |
228 | queue: function() {
229 | var test = this;
230 | synchronize(function() {
231 | test.init();
232 | });
233 | function run() {
234 | // each of these can by async
235 | synchronize(function() {
236 | test.setup();
237 | });
238 | synchronize(function() {
239 | test.run();
240 | });
241 | synchronize(function() {
242 | test.teardown();
243 | });
244 | synchronize(function() {
245 | test.finish();
246 | });
247 | }
248 | // defer when previous test run passed, if storage is available
249 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName);
250 | if (bad) {
251 | run();
252 | } else {
253 | synchronize(run, true);
254 | };
255 | }
256 |
257 | };
258 |
259 | var QUnit = {
260 |
261 | // call on start of module test to prepend name to all tests
262 | module: function(name, testEnvironment) {
263 | config.currentModule = name;
264 | config.currentModuleTestEnviroment = testEnvironment;
265 | },
266 |
267 | asyncTest: function(testName, expected, callback) {
268 | if ( arguments.length === 2 ) {
269 | callback = expected;
270 | expected = null;
271 | }
272 |
273 | QUnit.test(testName, expected, callback, true);
274 | },
275 |
276 | test: function(testName, expected, callback, async) {
277 | var name = '' + testName + ' ', testEnvironmentArg;
278 |
279 | if ( arguments.length === 2 ) {
280 | callback = expected;
281 | expected = null;
282 | }
283 | // is 2nd argument a testEnvironment?
284 | if ( expected && typeof expected === 'object') {
285 | testEnvironmentArg = expected;
286 | expected = null;
287 | }
288 |
289 | if ( config.currentModule ) {
290 | name = '' + config.currentModule + " : " + name;
291 | }
292 |
293 | if ( !validTest(config.currentModule + ": " + testName) ) {
294 | return;
295 | }
296 |
297 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
298 | test.module = config.currentModule;
299 | test.moduleTestEnvironment = config.currentModuleTestEnviroment;
300 | test.queue();
301 | },
302 |
303 | /**
304 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
305 | */
306 | expect: function(asserts) {
307 | config.current.expected = asserts;
308 | },
309 |
310 | /**
311 | * Asserts true.
312 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
313 | */
314 | ok: function(a, msg) {
315 | a = !!a;
316 | var details = {
317 | result: a,
318 | message: msg
319 | };
320 | msg = escapeInnerText(msg);
321 | runLoggingCallbacks( 'log', QUnit, details );
322 | config.current.assertions.push({
323 | result: a,
324 | message: msg
325 | });
326 | },
327 |
328 | /**
329 | * Checks that the first two arguments are equal, with an optional message.
330 | * Prints out both actual and expected values.
331 | *
332 | * Prefered to ok( actual == expected, message )
333 | *
334 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
335 | *
336 | * @param Object actual
337 | * @param Object expected
338 | * @param String message (optional)
339 | */
340 | equal: function(actual, expected, message) {
341 | QUnit.push(expected == actual, actual, expected, message);
342 | },
343 |
344 | notEqual: function(actual, expected, message) {
345 | QUnit.push(expected != actual, actual, expected, message);
346 | },
347 |
348 | deepEqual: function(actual, expected, message) {
349 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message);
350 | },
351 |
352 | notDeepEqual: function(actual, expected, message) {
353 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message);
354 | },
355 |
356 | strictEqual: function(actual, expected, message) {
357 | QUnit.push(expected === actual, actual, expected, message);
358 | },
359 |
360 | notStrictEqual: function(actual, expected, message) {
361 | QUnit.push(expected !== actual, actual, expected, message);
362 | },
363 |
364 | raises: function(block, expected, message) {
365 | var actual, ok = false;
366 |
367 | if (typeof expected === 'string') {
368 | message = expected;
369 | expected = null;
370 | }
371 |
372 | try {
373 | block();
374 | } catch (e) {
375 | actual = e;
376 | }
377 |
378 | if (actual) {
379 | // we don't want to validate thrown error
380 | if (!expected) {
381 | ok = true;
382 | // expected is a regexp
383 | } else if (QUnit.objectType(expected) === "regexp") {
384 | ok = expected.test(actual);
385 | // expected is a constructor
386 | } else if (actual instanceof expected) {
387 | ok = true;
388 | // expected is a validation function which returns true is validation passed
389 | } else if (expected.call({}, actual) === true) {
390 | ok = true;
391 | }
392 | }
393 |
394 | QUnit.ok(ok, message);
395 | },
396 |
397 | start: function(count) {
398 | config.semaphore -= count || 1;
399 | if (config.semaphore > 0) {
400 | // don't start until equal number of stop-calls
401 | return;
402 | }
403 | if (config.semaphore < 0) {
404 | // ignore if start is called more often then stop
405 | config.semaphore = 0;
406 | }
407 | // A slight delay, to avoid any current callbacks
408 | if ( defined.setTimeout ) {
409 | window.setTimeout(function() {
410 | if (config.semaphore > 0) {
411 | return;
412 | }
413 | if ( config.timeout ) {
414 | clearTimeout(config.timeout);
415 | }
416 |
417 | config.blocking = false;
418 | process(true);
419 | }, 13);
420 | } else {
421 | config.blocking = false;
422 | process(true);
423 | }
424 | },
425 |
426 | stop: function(count) {
427 | config.semaphore += count || 1;
428 | config.blocking = true;
429 |
430 | if ( config.testTimeout && defined.setTimeout ) {
431 | clearTimeout(config.timeout);
432 | config.timeout = window.setTimeout(function() {
433 | QUnit.ok( false, "Test timed out" );
434 | config.semaphore = 1;
435 | QUnit.start();
436 | }, config.testTimeout);
437 | }
438 | }
439 | };
440 |
441 | //We want access to the constructor's prototype
442 | (function() {
443 | function F(){};
444 | F.prototype = QUnit;
445 | QUnit = new F();
446 | //Make F QUnit's constructor so that we can add to the prototype later
447 | QUnit.constructor = F;
448 | })();
449 |
450 | // Backwards compatibility, deprecated
451 | QUnit.equals = QUnit.equal;
452 | QUnit.same = QUnit.deepEqual;
453 |
454 | // Maintain internal state
455 | var config = {
456 | // The queue of tests to run
457 | queue: [],
458 |
459 | // block until document ready
460 | blocking: true,
461 |
462 | // when enabled, show only failing tests
463 | // gets persisted through sessionStorage and can be changed in UI via checkbox
464 | hidepassed: false,
465 |
466 | // by default, run previously failed tests first
467 | // very useful in combination with "Hide passed tests" checked
468 | reorder: true,
469 |
470 | // by default, modify document.title when suite is done
471 | altertitle: true,
472 |
473 | urlConfig: ['noglobals', 'notrycatch'],
474 |
475 | //logging callback queues
476 | begin: [],
477 | done: [],
478 | log: [],
479 | testStart: [],
480 | testDone: [],
481 | moduleStart: [],
482 | moduleDone: []
483 | };
484 |
485 | // Load paramaters
486 | (function() {
487 | var location = window.location || { search: "", protocol: "file:" },
488 | params = location.search.slice( 1 ).split( "&" ),
489 | length = params.length,
490 | urlParams = {},
491 | current;
492 |
493 | if ( params[ 0 ] ) {
494 | for ( var i = 0; i < length; i++ ) {
495 | current = params[ i ].split( "=" );
496 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
497 | // allow just a key to turn on a flag, e.g., test.html?noglobals
498 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
499 | urlParams[ current[ 0 ] ] = current[ 1 ];
500 | }
501 | }
502 |
503 | QUnit.urlParams = urlParams;
504 | config.filter = urlParams.filter;
505 |
506 | // Figure out if we're running the tests from a server or not
507 | QUnit.isLocal = !!(location.protocol === 'file:');
508 | })();
509 |
510 | // Expose the API as global variables, unless an 'exports'
511 | // object exists, in that case we assume we're in CommonJS
512 | if ( typeof exports === "undefined" || typeof require === "undefined" ) {
513 | extend(window, QUnit);
514 | window.QUnit = QUnit;
515 | } else {
516 | extend(exports, QUnit);
517 | exports.QUnit = QUnit;
518 | }
519 |
520 | // define these after exposing globals to keep them in these QUnit namespace only
521 | extend(QUnit, {
522 | config: config,
523 |
524 | // Initialize the configuration options
525 | init: function() {
526 | extend(config, {
527 | stats: { all: 0, bad: 0 },
528 | moduleStats: { all: 0, bad: 0 },
529 | started: +new Date,
530 | updateRate: 1000,
531 | blocking: false,
532 | autostart: true,
533 | autorun: false,
534 | filter: "",
535 | queue: [],
536 | semaphore: 0
537 | });
538 |
539 | var tests = id( "qunit-tests" ),
540 | banner = id( "qunit-banner" ),
541 | result = id( "qunit-testresult" );
542 |
543 | if ( tests ) {
544 | tests.innerHTML = "";
545 | }
546 |
547 | if ( banner ) {
548 | banner.className = "";
549 | }
550 |
551 | if ( result ) {
552 | result.parentNode.removeChild( result );
553 | }
554 |
555 | if ( tests ) {
556 | result = document.createElement( "p" );
557 | result.id = "qunit-testresult";
558 | result.className = "result";
559 | tests.parentNode.insertBefore( result, tests );
560 | result.innerHTML = 'Running... ';
561 | }
562 | },
563 |
564 | /**
565 | * Resets the test setup. Useful for tests that modify the DOM.
566 | *
567 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
568 | */
569 | reset: function() {
570 | if ( window.jQuery ) {
571 | jQuery( "#qunit-fixture" ).html( config.fixture );
572 | } else {
573 | var main = id( 'qunit-fixture' );
574 | if ( main ) {
575 | main.innerHTML = config.fixture;
576 | }
577 | }
578 | },
579 |
580 | /**
581 | * Trigger an event on an element.
582 | *
583 | * @example triggerEvent( document.body, "click" );
584 | *
585 | * @param DOMElement elem
586 | * @param String type
587 | */
588 | triggerEvent: function( elem, type, event ) {
589 | if ( document.createEvent ) {
590 | event = document.createEvent("MouseEvents");
591 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
592 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
593 | elem.dispatchEvent( event );
594 |
595 | } else if ( elem.fireEvent ) {
596 | elem.fireEvent("on"+type);
597 | }
598 | },
599 |
600 | // Safe object type checking
601 | is: function( type, obj ) {
602 | return QUnit.objectType( obj ) == type;
603 | },
604 |
605 | objectType: function( obj ) {
606 | if (typeof obj === "undefined") {
607 | return "undefined";
608 |
609 | // consider: typeof null === object
610 | }
611 | if (obj === null) {
612 | return "null";
613 | }
614 |
615 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || '';
616 |
617 | switch (type) {
618 | case 'Number':
619 | if (isNaN(obj)) {
620 | return "nan";
621 | } else {
622 | return "number";
623 | }
624 | case 'String':
625 | case 'Boolean':
626 | case 'Array':
627 | case 'Date':
628 | case 'RegExp':
629 | case 'Function':
630 | return type.toLowerCase();
631 | }
632 | if (typeof obj === "object") {
633 | return "object";
634 | }
635 | return undefined;
636 | },
637 |
638 | push: function(result, actual, expected, message) {
639 | var details = {
640 | result: result,
641 | message: message,
642 | actual: actual,
643 | expected: expected
644 | };
645 |
646 | message = escapeInnerText(message) || (result ? "okay" : "failed");
647 | message = '' + message + " ";
648 | expected = escapeInnerText(QUnit.jsDump.parse(expected));
649 | actual = escapeInnerText(QUnit.jsDump.parse(actual));
650 | var output = message + 'Expected: ' + expected + ' ';
651 | if (actual != expected) {
652 | output += 'Result: ' + actual + ' ';
653 | output += 'Diff: ' + QUnit.diff(expected, actual) +' ';
654 | }
655 | if (!result) {
656 | var source = sourceFromStacktrace();
657 | if (source) {
658 | details.source = source;
659 | output += 'Source: ' + escapeInnerText(source) + ' ';
660 | }
661 | }
662 | output += "
";
663 |
664 | runLoggingCallbacks( 'log', QUnit, details );
665 |
666 | config.current.assertions.push({
667 | result: !!result,
668 | message: output
669 | });
670 | },
671 |
672 | url: function( params ) {
673 | params = extend( extend( {}, QUnit.urlParams ), params );
674 | var querystring = "?",
675 | key;
676 | for ( key in params ) {
677 | if ( !hasOwn.call( params, key ) ) {
678 | continue;
679 | }
680 | querystring += encodeURIComponent( key ) + "=" +
681 | encodeURIComponent( params[ key ] ) + "&";
682 | }
683 | return window.location.pathname + querystring.slice( 0, -1 );
684 | },
685 |
686 | extend: extend,
687 | id: id,
688 | addEvent: addEvent
689 | });
690 |
691 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later
692 | //Doing this allows us to tell if the following methods have been overwritten on the actual
693 | //QUnit object, which is a deprecated way of using the callbacks.
694 | extend(QUnit.constructor.prototype, {
695 | // Logging callbacks; all receive a single argument with the listed properties
696 | // run test/logs.html for any related changes
697 | begin: registerLoggingCallback('begin'),
698 | // done: { failed, passed, total, runtime }
699 | done: registerLoggingCallback('done'),
700 | // log: { result, actual, expected, message }
701 | log: registerLoggingCallback('log'),
702 | // testStart: { name }
703 | testStart: registerLoggingCallback('testStart'),
704 | // testDone: { name, failed, passed, total }
705 | testDone: registerLoggingCallback('testDone'),
706 | // moduleStart: { name }
707 | moduleStart: registerLoggingCallback('moduleStart'),
708 | // moduleDone: { name, failed, passed, total }
709 | moduleDone: registerLoggingCallback('moduleDone')
710 | });
711 |
712 | if ( typeof document === "undefined" || document.readyState === "complete" ) {
713 | config.autorun = true;
714 | }
715 |
716 | QUnit.load = function() {
717 | runLoggingCallbacks( 'begin', QUnit, {} );
718 |
719 | // Initialize the config, saving the execution queue
720 | var oldconfig = extend({}, config);
721 | QUnit.init();
722 | extend(config, oldconfig);
723 |
724 | config.blocking = false;
725 |
726 | var urlConfigHtml = '', len = config.urlConfig.length;
727 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) {
728 | config[val] = QUnit.urlParams[val];
729 | urlConfigHtml += ' ' + val + ' ';
730 | }
731 |
732 | var userAgent = id("qunit-userAgent");
733 | if ( userAgent ) {
734 | userAgent.innerHTML = navigator.userAgent;
735 | }
736 | var banner = id("qunit-header");
737 | if ( banner ) {
738 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml;
739 | addEvent( banner, "change", function( event ) {
740 | var params = {};
741 | params[ event.target.name ] = event.target.checked ? true : undefined;
742 | window.location = QUnit.url( params );
743 | });
744 | }
745 |
746 | var toolbar = id("qunit-testrunner-toolbar");
747 | if ( toolbar ) {
748 | var filter = document.createElement("input");
749 | filter.type = "checkbox";
750 | filter.id = "qunit-filter-pass";
751 | addEvent( filter, "click", function() {
752 | var ol = document.getElementById("qunit-tests");
753 | if ( filter.checked ) {
754 | ol.className = ol.className + " hidepass";
755 | } else {
756 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
757 | ol.className = tmp.replace(/ hidepass /, " ");
758 | }
759 | if ( defined.sessionStorage ) {
760 | if (filter.checked) {
761 | sessionStorage.setItem("qunit-filter-passed-tests", "true");
762 | } else {
763 | sessionStorage.removeItem("qunit-filter-passed-tests");
764 | }
765 | }
766 | });
767 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) {
768 | filter.checked = true;
769 | var ol = document.getElementById("qunit-tests");
770 | ol.className = ol.className + " hidepass";
771 | }
772 | toolbar.appendChild( filter );
773 |
774 | var label = document.createElement("label");
775 | label.setAttribute("for", "qunit-filter-pass");
776 | label.innerHTML = "Hide passed tests";
777 | toolbar.appendChild( label );
778 | }
779 |
780 | var main = id('qunit-fixture');
781 | if ( main ) {
782 | config.fixture = main.innerHTML;
783 | }
784 |
785 | if (config.autostart) {
786 | QUnit.start();
787 | }
788 | };
789 |
790 | addEvent(window, "load", QUnit.load);
791 |
792 | // addEvent(window, "error") gives us a useless event object
793 | window.onerror = function( message, file, line ) {
794 | if ( QUnit.config.current ) {
795 | ok( false, message + ", " + file + ":" + line );
796 | } else {
797 | test( "global failure", function() {
798 | ok( false, message + ", " + file + ":" + line );
799 | });
800 | }
801 | };
802 |
803 | function done() {
804 | config.autorun = true;
805 |
806 | // Log the last module results
807 | if ( config.currentModule ) {
808 | runLoggingCallbacks( 'moduleDone', QUnit, {
809 | name: config.currentModule,
810 | failed: config.moduleStats.bad,
811 | passed: config.moduleStats.all - config.moduleStats.bad,
812 | total: config.moduleStats.all
813 | } );
814 | }
815 |
816 | var banner = id("qunit-banner"),
817 | tests = id("qunit-tests"),
818 | runtime = +new Date - config.started,
819 | passed = config.stats.all - config.stats.bad,
820 | html = [
821 | 'Tests completed in ',
822 | runtime,
823 | ' milliseconds. ',
824 | '',
825 | passed,
826 | ' tests of ',
827 | config.stats.all,
828 | ' passed, ',
829 | config.stats.bad,
830 | ' failed.'
831 | ].join('');
832 |
833 | if ( banner ) {
834 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
835 | }
836 |
837 | if ( tests ) {
838 | id( "qunit-testresult" ).innerHTML = html;
839 | }
840 |
841 | if ( config.altertitle && typeof document !== "undefined" && document.title ) {
842 | // show ✖ for good, ✔ for bad suite result in title
843 | // use escape sequences in case file gets loaded with non-utf-8-charset
844 | document.title = [
845 | (config.stats.bad ? "\u2716" : "\u2714"),
846 | document.title.replace(/^[\u2714\u2716] /i, "")
847 | ].join(" ");
848 | }
849 |
850 | runLoggingCallbacks( 'done', QUnit, {
851 | failed: config.stats.bad,
852 | passed: passed,
853 | total: config.stats.all,
854 | runtime: runtime
855 | } );
856 | }
857 |
858 | function validTest( name ) {
859 | var filter = config.filter,
860 | run = false;
861 |
862 | if ( !filter ) {
863 | return true;
864 | }
865 |
866 | var not = filter.charAt( 0 ) === "!";
867 | if ( not ) {
868 | filter = filter.slice( 1 );
869 | }
870 |
871 | if ( name.indexOf( filter ) !== -1 ) {
872 | return !not;
873 | }
874 |
875 | if ( not ) {
876 | run = true;
877 | }
878 |
879 | return run;
880 | }
881 |
882 | // so far supports only Firefox, Chrome and Opera (buggy)
883 | // could be extended in the future to use something like https://github.com/csnover/TraceKit
884 | function sourceFromStacktrace() {
885 | try {
886 | throw new Error();
887 | } catch ( e ) {
888 | if (e.stacktrace) {
889 | // Opera
890 | return e.stacktrace.split("\n")[6];
891 | } else if (e.stack) {
892 | // Firefox, Chrome
893 | return e.stack.split("\n")[4];
894 | } else if (e.sourceURL) {
895 | // Safari, PhantomJS
896 | // TODO sourceURL points at the 'throw new Error' line above, useless
897 | //return e.sourceURL + ":" + e.line;
898 | }
899 | }
900 | }
901 |
902 | function escapeInnerText(s) {
903 | if (!s) {
904 | return "";
905 | }
906 | s = s + "";
907 | return s.replace(/[\&<>]/g, function(s) {
908 | switch(s) {
909 | case "&": return "&";
910 | case "<": return "<";
911 | case ">": return ">";
912 | default: return s;
913 | }
914 | });
915 | }
916 |
917 | function synchronize( callback, last ) {
918 | config.queue.push( callback );
919 |
920 | if ( config.autorun && !config.blocking ) {
921 | process(last);
922 | }
923 | }
924 |
925 | function process( last ) {
926 | var start = new Date().getTime();
927 | config.depth = config.depth ? config.depth + 1 : 1;
928 |
929 | while ( config.queue.length && !config.blocking ) {
930 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
931 | config.queue.shift()();
932 | } else {
933 | window.setTimeout( function(){
934 | process( last );
935 | }, 13 );
936 | break;
937 | }
938 | }
939 | config.depth--;
940 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
941 | done();
942 | }
943 | }
944 |
945 | function saveGlobal() {
946 | config.pollution = [];
947 |
948 | if ( config.noglobals ) {
949 | for ( var key in window ) {
950 | if ( !hasOwn.call( window, key ) ) {
951 | continue;
952 | }
953 | config.pollution.push( key );
954 | }
955 | }
956 | }
957 |
958 | function checkPollution( name ) {
959 | var old = config.pollution;
960 | saveGlobal();
961 |
962 | var newGlobals = diff( config.pollution, old );
963 | if ( newGlobals.length > 0 ) {
964 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
965 | }
966 |
967 | var deletedGlobals = diff( old, config.pollution );
968 | if ( deletedGlobals.length > 0 ) {
969 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
970 | }
971 | }
972 |
973 | // returns a new Array with the elements that are in a but not in b
974 | function diff( a, b ) {
975 | var result = a.slice();
976 | for ( var i = 0; i < result.length; i++ ) {
977 | for ( var j = 0; j < b.length; j++ ) {
978 | if ( result[i] === b[j] ) {
979 | result.splice(i, 1);
980 | i--;
981 | break;
982 | }
983 | }
984 | }
985 | return result;
986 | }
987 |
988 | function fail(message, exception, callback) {
989 | if ( typeof console !== "undefined" && console.error && console.warn ) {
990 | console.error(message);
991 | console.error(exception);
992 | console.warn(callback.toString());
993 |
994 | } else if ( window.opera && opera.postError ) {
995 | opera.postError(message, exception, callback.toString);
996 | }
997 | }
998 |
999 | function extend(a, b) {
1000 | for ( var prop in b ) {
1001 | if ( b[prop] === undefined ) {
1002 | delete a[prop];
1003 |
1004 | // Avoid "Member not found" error in IE8 caused by setting window.constructor
1005 | } else if ( prop !== "constructor" || a !== window ) {
1006 | a[prop] = b[prop];
1007 | }
1008 | }
1009 |
1010 | return a;
1011 | }
1012 |
1013 | function addEvent(elem, type, fn) {
1014 | if ( elem.addEventListener ) {
1015 | elem.addEventListener( type, fn, false );
1016 | } else if ( elem.attachEvent ) {
1017 | elem.attachEvent( "on" + type, fn );
1018 | } else {
1019 | fn();
1020 | }
1021 | }
1022 |
1023 | function id(name) {
1024 | return !!(typeof document !== "undefined" && document && document.getElementById) &&
1025 | document.getElementById( name );
1026 | }
1027 |
1028 | function registerLoggingCallback(key){
1029 | return function(callback){
1030 | config[key].push( callback );
1031 | };
1032 | }
1033 |
1034 | // Supports deprecated method of completely overwriting logging callbacks
1035 | function runLoggingCallbacks(key, scope, args) {
1036 | //debugger;
1037 | var callbacks;
1038 | if ( QUnit.hasOwnProperty(key) ) {
1039 | QUnit[key].call(scope, args);
1040 | } else {
1041 | callbacks = config[key];
1042 | for( var i = 0; i < callbacks.length; i++ ) {
1043 | callbacks[i].call( scope, args );
1044 | }
1045 | }
1046 | }
1047 |
1048 | // Test for equality any JavaScript type.
1049 | // Author: Philippe Rathé
1050 | QUnit.equiv = function () {
1051 |
1052 | var innerEquiv; // the real equiv function
1053 | var callers = []; // stack to decide between skip/abort functions
1054 | var parents = []; // stack to avoiding loops from circular referencing
1055 |
1056 | // Call the o related callback with the given arguments.
1057 | function bindCallbacks(o, callbacks, args) {
1058 | var prop = QUnit.objectType(o);
1059 | if (prop) {
1060 | if (QUnit.objectType(callbacks[prop]) === "function") {
1061 | return callbacks[prop].apply(callbacks, args);
1062 | } else {
1063 | return callbacks[prop]; // or undefined
1064 | }
1065 | }
1066 | }
1067 |
1068 | var callbacks = function () {
1069 |
1070 | // for string, boolean, number and null
1071 | function useStrictEquality(b, a) {
1072 | if (b instanceof a.constructor || a instanceof b.constructor) {
1073 | // to catch short annotaion VS 'new' annotation of a
1074 | // declaration
1075 | // e.g. var i = 1;
1076 | // var j = new Number(1);
1077 | return a == b;
1078 | } else {
1079 | return a === b;
1080 | }
1081 | }
1082 |
1083 | return {
1084 | "string" : useStrictEquality,
1085 | "boolean" : useStrictEquality,
1086 | "number" : useStrictEquality,
1087 | "null" : useStrictEquality,
1088 | "undefined" : useStrictEquality,
1089 |
1090 | "nan" : function(b) {
1091 | return isNaN(b);
1092 | },
1093 |
1094 | "date" : function(b, a) {
1095 | return QUnit.objectType(b) === "date"
1096 | && a.valueOf() === b.valueOf();
1097 | },
1098 |
1099 | "regexp" : function(b, a) {
1100 | return QUnit.objectType(b) === "regexp"
1101 | && a.source === b.source && // the regex itself
1102 | a.global === b.global && // and its modifers
1103 | // (gmi) ...
1104 | a.ignoreCase === b.ignoreCase
1105 | && a.multiline === b.multiline;
1106 | },
1107 |
1108 | // - skip when the property is a method of an instance (OOP)
1109 | // - abort otherwise,
1110 | // initial === would have catch identical references anyway
1111 | "function" : function() {
1112 | var caller = callers[callers.length - 1];
1113 | return caller !== Object && typeof caller !== "undefined";
1114 | },
1115 |
1116 | "array" : function(b, a) {
1117 | var i, j, loop;
1118 | var len;
1119 |
1120 | // b could be an object literal here
1121 | if (!(QUnit.objectType(b) === "array")) {
1122 | return false;
1123 | }
1124 |
1125 | len = a.length;
1126 | if (len !== b.length) { // safe and faster
1127 | return false;
1128 | }
1129 |
1130 | // track reference to avoid circular references
1131 | parents.push(a);
1132 | for (i = 0; i < len; i++) {
1133 | loop = false;
1134 | for (j = 0; j < parents.length; j++) {
1135 | if (parents[j] === a[i]) {
1136 | loop = true;// dont rewalk array
1137 | }
1138 | }
1139 | if (!loop && !innerEquiv(a[i], b[i])) {
1140 | parents.pop();
1141 | return false;
1142 | }
1143 | }
1144 | parents.pop();
1145 | return true;
1146 | },
1147 |
1148 | "object" : function(b, a) {
1149 | var i, j, loop;
1150 | var eq = true; // unless we can proove it
1151 | var aProperties = [], bProperties = []; // collection of
1152 | // strings
1153 |
1154 | // comparing constructors is more strict than using
1155 | // instanceof
1156 | if (a.constructor !== b.constructor) {
1157 | return false;
1158 | }
1159 |
1160 | // stack constructor before traversing properties
1161 | callers.push(a.constructor);
1162 | // track reference to avoid circular references
1163 | parents.push(a);
1164 |
1165 | for (i in a) { // be strict: don't ensures hasOwnProperty
1166 | // and go deep
1167 | loop = false;
1168 | for (j = 0; j < parents.length; j++) {
1169 | if (parents[j] === a[i])
1170 | loop = true; // don't go down the same path
1171 | // twice
1172 | }
1173 | aProperties.push(i); // collect a's properties
1174 |
1175 | if (!loop && !innerEquiv(a[i], b[i])) {
1176 | eq = false;
1177 | break;
1178 | }
1179 | }
1180 |
1181 | callers.pop(); // unstack, we are done
1182 | parents.pop();
1183 |
1184 | for (i in b) {
1185 | bProperties.push(i); // collect b's properties
1186 | }
1187 |
1188 | // Ensures identical properties name
1189 | return eq
1190 | && innerEquiv(aProperties.sort(), bProperties
1191 | .sort());
1192 | }
1193 | };
1194 | }();
1195 |
1196 | innerEquiv = function() { // can take multiple arguments
1197 | var args = Array.prototype.slice.apply(arguments);
1198 | if (args.length < 2) {
1199 | return true; // end transition
1200 | }
1201 |
1202 | return (function(a, b) {
1203 | if (a === b) {
1204 | return true; // catch the most you can
1205 | } else if (a === null || b === null || typeof a === "undefined"
1206 | || typeof b === "undefined"
1207 | || QUnit.objectType(a) !== QUnit.objectType(b)) {
1208 | return false; // don't lose time with error prone cases
1209 | } else {
1210 | return bindCallbacks(a, callbacks, [ b, a ]);
1211 | }
1212 |
1213 | // apply transition with (1..n) arguments
1214 | })(args[0], args[1])
1215 | && arguments.callee.apply(this, args.splice(1,
1216 | args.length - 1));
1217 | };
1218 |
1219 | return innerEquiv;
1220 |
1221 | }();
1222 |
1223 | /**
1224 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1225 | * http://flesler.blogspot.com Licensed under BSD
1226 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1227 | *
1228 | * @projectDescription Advanced and extensible data dumping for Javascript.
1229 | * @version 1.0.0
1230 | * @author Ariel Flesler
1231 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1232 | */
1233 | QUnit.jsDump = (function() {
1234 | function quote( str ) {
1235 | return '"' + str.toString().replace(/"/g, '\\"') + '"';
1236 | };
1237 | function literal( o ) {
1238 | return o + '';
1239 | };
1240 | function join( pre, arr, post ) {
1241 | var s = jsDump.separator(),
1242 | base = jsDump.indent(),
1243 | inner = jsDump.indent(1);
1244 | if ( arr.join )
1245 | arr = arr.join( ',' + s + inner );
1246 | if ( !arr )
1247 | return pre + post;
1248 | return [ pre, inner + arr, base + post ].join(s);
1249 | };
1250 | function array( arr, stack ) {
1251 | var i = arr.length, ret = Array(i);
1252 | this.up();
1253 | while ( i-- )
1254 | ret[i] = this.parse( arr[i] , undefined , stack);
1255 | this.down();
1256 | return join( '[', ret, ']' );
1257 | };
1258 |
1259 | var reName = /^function (\w+)/;
1260 |
1261 | var jsDump = {
1262 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance
1263 | stack = stack || [ ];
1264 | var parser = this.parsers[ type || this.typeOf(obj) ];
1265 | type = typeof parser;
1266 | var inStack = inArray(obj, stack);
1267 | if (inStack != -1) {
1268 | return 'recursion('+(inStack - stack.length)+')';
1269 | }
1270 | //else
1271 | if (type == 'function') {
1272 | stack.push(obj);
1273 | var res = parser.call( this, obj, stack );
1274 | stack.pop();
1275 | return res;
1276 | }
1277 | // else
1278 | return (type == 'string') ? parser : this.parsers.error;
1279 | },
1280 | typeOf:function( obj ) {
1281 | var type;
1282 | if ( obj === null ) {
1283 | type = "null";
1284 | } else if (typeof obj === "undefined") {
1285 | type = "undefined";
1286 | } else if (QUnit.is("RegExp", obj)) {
1287 | type = "regexp";
1288 | } else if (QUnit.is("Date", obj)) {
1289 | type = "date";
1290 | } else if (QUnit.is("Function", obj)) {
1291 | type = "function";
1292 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") {
1293 | type = "window";
1294 | } else if (obj.nodeType === 9) {
1295 | type = "document";
1296 | } else if (obj.nodeType) {
1297 | type = "node";
1298 | } else if (
1299 | // native arrays
1300 | toString.call( obj ) === "[object Array]" ||
1301 | // NodeList objects
1302 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1303 | ) {
1304 | type = "array";
1305 | } else {
1306 | type = typeof obj;
1307 | }
1308 | return type;
1309 | },
1310 | separator:function() {
1311 | return this.multiline ? this.HTML ? ' ' : '\n' : this.HTML ? ' ' : ' ';
1312 | },
1313 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
1314 | if ( !this.multiline )
1315 | return '';
1316 | var chr = this.indentChar;
1317 | if ( this.HTML )
1318 | chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
1319 | return Array( this._depth_ + (extra||0) ).join(chr);
1320 | },
1321 | up:function( a ) {
1322 | this._depth_ += a || 1;
1323 | },
1324 | down:function( a ) {
1325 | this._depth_ -= a || 1;
1326 | },
1327 | setParser:function( name, parser ) {
1328 | this.parsers[name] = parser;
1329 | },
1330 | // The next 3 are exposed so you can use them
1331 | quote:quote,
1332 | literal:literal,
1333 | join:join,
1334 | //
1335 | _depth_: 1,
1336 | // This is the list of parsers, to modify them, use jsDump.setParser
1337 | parsers:{
1338 | window: '[Window]',
1339 | document: '[Document]',
1340 | error:'[ERROR]', //when no parser is found, shouldn't happen
1341 | unknown: '[Unknown]',
1342 | 'null':'null',
1343 | 'undefined':'undefined',
1344 | 'function':function( fn ) {
1345 | var ret = 'function',
1346 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
1347 | if ( name )
1348 | ret += ' ' + name;
1349 | ret += '(';
1350 |
1351 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join('');
1352 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' );
1353 | },
1354 | array: array,
1355 | nodelist: array,
1356 | arguments: array,
1357 | object:function( map, stack ) {
1358 | var ret = [ ];
1359 | QUnit.jsDump.up();
1360 | for ( var key in map ) {
1361 | var val = map[key];
1362 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack));
1363 | }
1364 | QUnit.jsDump.down();
1365 | return join( '{', ret, '}' );
1366 | },
1367 | node:function( node ) {
1368 | var open = QUnit.jsDump.HTML ? '<' : '<',
1369 | close = QUnit.jsDump.HTML ? '>' : '>';
1370 |
1371 | var tag = node.nodeName.toLowerCase(),
1372 | ret = open + tag;
1373 |
1374 | for ( var a in QUnit.jsDump.DOMAttrs ) {
1375 | var val = node[QUnit.jsDump.DOMAttrs[a]];
1376 | if ( val )
1377 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' );
1378 | }
1379 | return ret + close + open + '/' + tag + close;
1380 | },
1381 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
1382 | var l = fn.length;
1383 | if ( !l ) return '';
1384 |
1385 | var args = Array(l);
1386 | while ( l-- )
1387 | args[l] = String.fromCharCode(97+l);//97 is 'a'
1388 | return ' ' + args.join(', ') + ' ';
1389 | },
1390 | key:quote, //object calls it internally, the key part of an item in a map
1391 | functionCode:'[code]', //function calls it internally, it's the content of the function
1392 | attribute:quote, //node calls it internally, it's an html attribute value
1393 | string:quote,
1394 | date:quote,
1395 | regexp:literal, //regex
1396 | number:literal,
1397 | 'boolean':literal
1398 | },
1399 | DOMAttrs:{//attributes to dump from nodes, name=>realName
1400 | id:'id',
1401 | name:'name',
1402 | 'class':'className'
1403 | },
1404 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
1405 | indentChar:' ',//indentation unit
1406 | multiline:true //if true, items in a collection, are separated by a \n, else just a space.
1407 | };
1408 |
1409 | return jsDump;
1410 | })();
1411 |
1412 | // from Sizzle.js
1413 | function getText( elems ) {
1414 | var ret = "", elem;
1415 |
1416 | for ( var i = 0; elems[i]; i++ ) {
1417 | elem = elems[i];
1418 |
1419 | // Get the text from text nodes and CDATA nodes
1420 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
1421 | ret += elem.nodeValue;
1422 |
1423 | // Traverse everything else, except comment nodes
1424 | } else if ( elem.nodeType !== 8 ) {
1425 | ret += getText( elem.childNodes );
1426 | }
1427 | }
1428 |
1429 | return ret;
1430 | };
1431 |
1432 | //from jquery.js
1433 | function inArray( elem, array ) {
1434 | if ( array.indexOf ) {
1435 | return array.indexOf( elem );
1436 | }
1437 |
1438 | for ( var i = 0, length = array.length; i < length; i++ ) {
1439 | if ( array[ i ] === elem ) {
1440 | return i;
1441 | }
1442 | }
1443 |
1444 | return -1;
1445 | }
1446 |
1447 | /*
1448 | * Javascript Diff Algorithm
1449 | * By John Resig (http://ejohn.org/)
1450 | * Modified by Chu Alan "sprite"
1451 | *
1452 | * Released under the MIT license.
1453 | *
1454 | * More Info:
1455 | * http://ejohn.org/projects/javascript-diff-algorithm/
1456 | *
1457 | * Usage: QUnit.diff(expected, actual)
1458 | *
1459 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over"
1460 | */
1461 | QUnit.diff = (function() {
1462 | function diff(o, n) {
1463 | var ns = {};
1464 | var os = {};
1465 |
1466 | for (var i = 0; i < n.length; i++) {
1467 | if (ns[n[i]] == null)
1468 | ns[n[i]] = {
1469 | rows: [],
1470 | o: null
1471 | };
1472 | ns[n[i]].rows.push(i);
1473 | }
1474 |
1475 | for (var i = 0; i < o.length; i++) {
1476 | if (os[o[i]] == null)
1477 | os[o[i]] = {
1478 | rows: [],
1479 | n: null
1480 | };
1481 | os[o[i]].rows.push(i);
1482 | }
1483 |
1484 | for (var i in ns) {
1485 | if ( !hasOwn.call( ns, i ) ) {
1486 | continue;
1487 | }
1488 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
1489 | n[ns[i].rows[0]] = {
1490 | text: n[ns[i].rows[0]],
1491 | row: os[i].rows[0]
1492 | };
1493 | o[os[i].rows[0]] = {
1494 | text: o[os[i].rows[0]],
1495 | row: ns[i].rows[0]
1496 | };
1497 | }
1498 | }
1499 |
1500 | for (var i = 0; i < n.length - 1; i++) {
1501 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
1502 | n[i + 1] == o[n[i].row + 1]) {
1503 | n[i + 1] = {
1504 | text: n[i + 1],
1505 | row: n[i].row + 1
1506 | };
1507 | o[n[i].row + 1] = {
1508 | text: o[n[i].row + 1],
1509 | row: i + 1
1510 | };
1511 | }
1512 | }
1513 |
1514 | for (var i = n.length - 1; i > 0; i--) {
1515 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
1516 | n[i - 1] == o[n[i].row - 1]) {
1517 | n[i - 1] = {
1518 | text: n[i - 1],
1519 | row: n[i].row - 1
1520 | };
1521 | o[n[i].row - 1] = {
1522 | text: o[n[i].row - 1],
1523 | row: i - 1
1524 | };
1525 | }
1526 | }
1527 |
1528 | return {
1529 | o: o,
1530 | n: n
1531 | };
1532 | }
1533 |
1534 | return function(o, n) {
1535 | o = o.replace(/\s+$/, '');
1536 | n = n.replace(/\s+$/, '');
1537 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/));
1538 |
1539 | var str = "";
1540 |
1541 | var oSpace = o.match(/\s+/g);
1542 | if (oSpace == null) {
1543 | oSpace = [" "];
1544 | }
1545 | else {
1546 | oSpace.push(" ");
1547 | }
1548 | var nSpace = n.match(/\s+/g);
1549 | if (nSpace == null) {
1550 | nSpace = [" "];
1551 | }
1552 | else {
1553 | nSpace.push(" ");
1554 | }
1555 |
1556 | if (out.n.length == 0) {
1557 | for (var i = 0; i < out.o.length; i++) {
1558 | str += '' + out.o[i] + oSpace[i] + "";
1559 | }
1560 | }
1561 | else {
1562 | if (out.n[0].text == null) {
1563 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
1564 | str += '' + out.o[n] + oSpace[n] + "";
1565 | }
1566 | }
1567 |
1568 | for (var i = 0; i < out.n.length; i++) {
1569 | if (out.n[i].text == null) {
1570 | str += '' + out.n[i] + nSpace[i] + " ";
1571 | }
1572 | else {
1573 | var pre = "";
1574 |
1575 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
1576 | pre += '' + out.o[n] + oSpace[n] + "";
1577 | }
1578 | str += " " + out.n[i].text + nSpace[i] + pre;
1579 | }
1580 | }
1581 | }
1582 |
1583 | return str;
1584 | };
1585 | })();
1586 |
1587 | })(this);
1588 |
--------------------------------------------------------------------------------