├── .gitignore ├── .npmignore ├── CNAME ├── LICENSE.txt ├── README.md ├── backbone-relational.js ├── bower.json ├── index.html ├── package.json ├── static ├── css │ └── style.css ├── fonts │ ├── Museo_Slab_500.eot │ ├── Museo_Slab_500.otf │ ├── Museo_Slab_500.woff │ └── PT_Sans.woff └── js │ └── prism.js └── test ├── backbone-relational.js ├── index.html ├── lib ├── backbone.js ├── jquery-1.9.1.js ├── qunit.css ├── qunit.js ├── require.js └── underscore.js ├── server.js ├── test_require.html └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | node_modules 3 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | backbonerelational.org -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2014 Paul Uithol, http://progressivecompany.nl/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone-relational 2 | 3 | Backbone-relational provides one-to-one, one-to-many and many-to-one relations between models for [Backbone](https://github.com/documentcloud/backbone). 4 | 5 | Documentation: http://backbonerelational.org 6 | 7 | Backbone-relational is released under the [MIT license](https://github.com/PaulUithol/Backbone-relational/blob/master/LICENSE.txt). 8 | Contributors: https://github.com/PaulUithol/Backbone-relational/contributors 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-relational", 3 | "main": "backbone-relational.js", 4 | "dependencies": { 5 | "underscore": ">=1.5.0", 6 | "backbone": ">=1.1.2" 7 | }, 8 | "ignore": ["static", "test", ".html"], 9 | "version": "0.9.0" 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-relational", 3 | "main": "backbone-relational.js", 4 | "description": "Get and set relations (one-to-one, one-to-many, many-to-one) for Backbone models", 5 | "homepage": "http://backbonerelational.org", 6 | "keywords": ["backbone", "relation", "nested", "model"], 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/PaulUithol/Backbone-relational.git" 10 | }, 11 | "author": "Paul Uithol ", 12 | "contributors": "Listed at ", 13 | "dependencies": { 14 | "underscore": ">=1.5.0", 15 | "backbone": ">=1.1.2" 16 | }, 17 | "license": "MIT", 18 | "version": "0.9.0" 19 | } -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | header, nav, section, article, aside, footer, hgroup { 2 | display: block; 3 | } 4 | 5 | @font-face { 6 | font-family: 'MuseoSlab500'; 7 | font-weight: normal; 8 | font-style: normal; 9 | src: url('../fonts/Museo_Slab_500.eot'); 10 | src: local('Museo Slab 500'), local('MuseoSlab-500'), url('../fonts/Museo_Slab_500.woff') format('woff'), url('../fonts/Museo_Slab_500.otf') format('opentype'); 11 | } 12 | 13 | @font-face { 14 | font-family: 'PT Sans'; 15 | font-style: normal; 16 | font-weight: 400; 17 | src: local('PT Sans'), local('PTSans-Regular'), url('../fonts/PT_Sans.woff') format('woff'); 18 | } 19 | 20 | body { 21 | font-size: 16px; 22 | line-height: 1.5em; 23 | font-family: "PT Sans", "Helvetica Neue", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif; 24 | background: #f4f4f4; 25 | } 26 | 27 | a, a:visited { 28 | color: #444; 29 | text-decoration: underline; 30 | } 31 | a:active, a:hover { 32 | color: #000; 33 | } 34 | 35 | h1, h2, h3, h4, h5, h6, .toc_title { 36 | font-family: MuseoSlab500, "Lucida Grande", "Lucida Sans Unicode", "Helvetica Neue", "Bitstream Vera Sans", Helvetica, Arial, sans-serif; 37 | } 38 | h1, h2 { 39 | text-shadow: 0 1px 0 #FFFFFF; 40 | } 41 | h1 { 42 | font-size: 2em; 43 | line-height: 1.5; 44 | margin: 0.75em 0 0.75em 0; 45 | } 46 | h2 { 47 | font-size: 1.5em; 48 | line-height: 1.25; 49 | margin: 1.5833em 0 0.6667em; 50 | } 51 | h3 { 52 | font-size: 1.25em; 53 | line-height: 1.2; 54 | margin: 1.8em 0 0.6em; 55 | } 56 | h4 { 57 | font-size: 1.125em; 58 | line-height: 1.3333; 59 | margin: 2em 0 0.66667em; 60 | } 61 | h5, h6 { 62 | font-size: 1em; 63 | line-height: 1.5; 64 | margin: 1.5em 0 0; 65 | } 66 | h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { 67 | font-size: 12px; 68 | font-family: "Helvetica Neue", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif; 69 | font-weight: normal; 70 | } 71 | 72 | p, ol, ul, dl, pre, figure, footer { 73 | margin: 0 0 1.5em 0; 74 | } 75 | 76 | ul { 77 | padding-left: 0; 78 | list-style-type: none; 79 | } 80 | ul li { 81 | margin: 0; 82 | } 83 | ul li:before { 84 | content: "\2013"; 85 | position: relative; 86 | left: -1ex; 87 | } 88 | 89 | dl { 90 | clear: both; 91 | } 92 | dl dt { 93 | clear: left; 94 | float: left; 95 | margin-bottom: .5em; 96 | width: 5em; 97 | } 98 | dl dd { 99 | margin-left: 5em; 100 | margin-bottom: .5em; 101 | } 102 | 103 | .button { 104 | display: inline-block; 105 | outline: none; 106 | cursor: pointer; 107 | text-align: center; 108 | text-decoration: none; 109 | font: 14px/100% Arial, Helvetica, sans-serif; 110 | padding: .5em 2em .55em; 111 | text-shadow: 0 1px 1px rgba(0,0,0,.3); 112 | -webkit-border-radius: .4em; 113 | -moz-border-radius: .4em; 114 | border-radius: .4em; 115 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2); 116 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.2); 117 | box-shadow: 0 1px 2px rgba(0,0,0,.2); 118 | } 119 | .button:hover { 120 | text-decoration: none; 121 | } 122 | .button:active { 123 | position: relative; 124 | top: 1px; 125 | } 126 | 127 | /* white */ 128 | .white { 129 | color: #606060; 130 | border: solid 1px #b7b7b7; 131 | background: #fff; 132 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ededed)); 133 | background: -moz-linear-gradient(top, #fff, #ededed); 134 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#ededed'); 135 | } 136 | .white:hover { 137 | background: #ededed; 138 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#dcdcdc)); 139 | background: -moz-linear-gradient(top, #fff, #dcdcdc); 140 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dcdcdc'); 141 | } 142 | .white:active { 143 | color: #999; 144 | background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#fff)); 145 | background: -moz-linear-gradient(top, #ededed, #fff); 146 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#ffffff'); 147 | } 148 | 149 | /* orange */ 150 | .orange { 151 | color: #fef4e9; 152 | border: solid 1px #da7c0c; 153 | background: #f78d1d; 154 | background: -webkit-gradient(linear, left top, left bottom, from(#faa51a), to(#f47a20)); 155 | background: -moz-linear-gradient(top, #faa51a, #f47a20); 156 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#faa51a', endColorstr='#f47a20'); 157 | } 158 | .orange:hover { 159 | color: #fef4e9; 160 | background: #f47c20; 161 | background: -webkit-gradient(linear, left top, left bottom, from(#f88e11), to(#f06015)); 162 | background: -moz-linear-gradient(top, #f88e11, #f06015); 163 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f88e11', endColorstr='#f06015'); 164 | } 165 | .orange:active { 166 | color: #fcd3a5; 167 | background: -webkit-gradient(linear, left top, left bottom, from(#f47a20), to(#faa51a)); 168 | background: -moz-linear-gradient(top, #f47a20, #faa51a); 169 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f47a20', endColorstr='#faa51a'); 170 | } 171 | 172 | div#sidebar { 173 | background: #fff; 174 | position: fixed; 175 | z-index: 10; 176 | top: 0; 177 | left: 0; 178 | bottom: 0; 179 | width: 240px; 180 | overflow-y: auto; 181 | overflow-x: hidden; 182 | -webkit-overflow-scrolling: touch; 183 | padding: 15px 15px 30px 30px; 184 | border-right: 1px solid #bbb; 185 | -webkit-box-shadow: 0 0 20px #ccc; 186 | -moz-box-shadow: 0 0 20px #ccc; 187 | box-shadow: 0 0 20px #ccc; 188 | } 189 | div#sidebar a, a:visited { 190 | color: black; 191 | text-decoration: none; 192 | } 193 | div#sidebar a:hover { 194 | text-decoration: underline; 195 | } 196 | a.toc_title, a.toc_title:visited { 197 | display: block; 198 | font-weight: bold; 199 | margin: 1em 0 .2em; 200 | } 201 | div#sidebar .version { 202 | font-size: 10px; 203 | font-weight: normal; 204 | } 205 | div#sidebar ul { 206 | font-size: 12px; 207 | line-height: 20px; 208 | margin-bottom: .5em; 209 | } 210 | div#sidebar ul ul { 211 | margin: 0 0 0 1em; 212 | } 213 | div#sidebar li.link_out:before { 214 | content: "\00BB"; 215 | } 216 | 217 | div.container { 218 | position: relative; 219 | width: 45em; 220 | margin: 40px 0 50px 320px; 221 | } 222 | div.container ul li { 223 | margin-bottom: .5em; 224 | text-indent: -1.5ex; 225 | } 226 | div.container ul.small { 227 | font-size: 14px; 228 | } 229 | 230 | div.run { 231 | cursor: pointer; 232 | font-family: Arial, Courier, Times New Roman, serif; 233 | font-size: 16px; 234 | position: absolute; 235 | bottom: 15px; 236 | right: 15px; 237 | } 238 | div.run a { 239 | border-radius: .2em; 240 | padding: .2em .5em .2em .6em; 241 | } 242 | 243 | p.warning { 244 | font-size: 13px; 245 | font-style: italic; 246 | } 247 | 248 | p.notice { 249 | background-color: #ffeeaa; 250 | border: 0 solid #eedd88; 251 | border-left-width: 5px; 252 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); 253 | color: #443; 254 | font-size: 12px; 255 | padding: 2px 6px 2px 15px; 256 | margin: 1.5em 0 1.5em -1em; 257 | } 258 | 259 | a.punch { 260 | display: inline-block; 261 | background: #4162a8; 262 | border-top: 1px solid #38538c; 263 | border-right: 1px solid #1f2d4d; 264 | border-bottom: 1px solid #151e33; 265 | border-left: 1px solid #1f2d4d; 266 | border-radius: .4em; 267 | -webkit-box-shadow: inset 0 1px 10px 1px #5c8bee, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 268 | -moz-box-shadow: inset 0 1px 10px 1px #5c8bee, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 269 | box-shadow: inset 0 1px 10px 1px #5c8bee, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 270 | color: #fff; 271 | font-weight: bold; 272 | line-height: 1; 273 | margin-bottom: 15px; 274 | padding: 10px 0; 275 | text-align: center; 276 | text-shadow: 0px -1px 1px #1e2d4d; 277 | text-decoration: none; 278 | width: 225px; 279 | -webkit-background-clip: padding-box; 280 | } 281 | a.punch:hover { 282 | -webkit-box-shadow: inset 0 0px 20px 1px #87adff, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 283 | -moz-box-shadow: inset 0 0px 20px 1px #87adff, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 284 | box-shadow: inset 0 0px 20px 1px #87adff, 0px 1px 0 #1d2c4d, 0 6px 0px #1f3053, 0 8px 4px 1px #333; 285 | cursor: pointer; 286 | } 287 | a.punch:active { 288 | -webkit-box-shadow: inset 0 1px 10px 1px #5c8bee, 0 1px 0 #1d2c4d, 0 2px 0 #1f3053, 0 4px 3px 0 #333; 289 | -moz-box-shadow: inset 0 1px 10px 1px #5c8bee, 0 1px 0 #1d2c4d, 0 2px 0 #1f3053, 0 4px 3px 0 #333; 290 | box-shadow: inset 0 1px 10px 1px #5c8bee, 0 1px 0 #1d2c4d, 0 2px 0 #1f3053, 0 4px 3px 0 #333; 291 | margin-top: 5px; margin-bottom: 10px; 292 | } 293 | 294 | a img { 295 | border: 0; 296 | } 297 | 298 | img.example_image { 299 | margin: 0 auto; 300 | } 301 | img.example_retina { 302 | margin: 20px; 303 | box-shadow: 0 8px 15px rgba(0,0,0,0.4); 304 | } 305 | 306 | span.alias { 307 | font-size: 14px; 308 | font-style: italic; 309 | margin-left: 20px; 310 | } 311 | 312 | table { 313 | margin: 15px 0 0; padding: 0; 314 | } 315 | table .rule { 316 | height: 1px; 317 | background: #ccc; 318 | margin: 5px 0; 319 | } 320 | tr, td { 321 | margin: 0; padding: 0; 322 | } 323 | td { 324 | padding: 0px 15px 5px 0; 325 | } 326 | 327 | h3.code, h4.code, h5.code, code, pre, q { 328 | font-family: Consolas, Monaco, 'Andale Mono', 'Liberation Mono', 'Lucida Console', monospace; 329 | font-style: normal; 330 | } 331 | code, pre, q { 332 | font-size: 13px; 333 | font-weight: normal; 334 | line-height: 18px; 335 | 336 | direction: ltr; 337 | text-align: left; 338 | white-space: pre; 339 | word-spacing: normal; 340 | 341 | -moz-tab-size: 4; 342 | -o-tab-size: 4; 343 | tab-size: 4; 344 | 345 | -webkit-hyphens: none; 346 | -moz-hyphens: none; 347 | -ms-hyphens: none; 348 | hyphens: none; 349 | } 350 | q:before, q:after { 351 | content: ''; 352 | } 353 | q { 354 | border-radius: .2em; 355 | display: inline-block; /* Prevents the syndrome where the left border and part of the padding are rendered on the previous line */ 356 | padding: 0 3px; 357 | margin: 0; 358 | background: #fff; 359 | border: 1px solid #ddd; 360 | white-space: nowrap; 361 | } 362 | li q { 363 | display: inline; /* No idea why, but having them `inline-block` in a list gets the sizing wrong */ 364 | } 365 | a q { 366 | border-bottom: none; 367 | color: #111; 368 | text-decoration: underline; 369 | } 370 | a:hover q { 371 | color: #000; 372 | } 373 | code { 374 | margin-left: 20px; 375 | } 376 | h4 code { 377 | white-space: pre-wrap; 378 | } 379 | pre { 380 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); 381 | font-size: 12px; 382 | padding: 2px 6px 2px 15px; 383 | position: relative; 384 | border: 0 solid #aaa; 385 | border-left-width: 5px; 386 | margin: 1.5em 0 1.5em -1em; 387 | } 388 | pre code { 389 | margin: 0; 390 | } 391 | pre.nomargin { 392 | border-top: 1px solid #444; 393 | margin-top: -1.5em; 394 | } 395 | 396 | #change-log small { 397 | float: right; 398 | } 399 | #change-log small .date { 400 | color: #999; 401 | } 402 | 403 | 404 | @media only screen and (-webkit-max-device-pixel-ratio: 1) and (max-width: 600px), 405 | only screen and (max--moz-device-pixel-ratio: 1) and (max-width: 600px) { 406 | div#sidebar { 407 | display: none; 408 | } 409 | img#logo { 410 | max-width: 450px; 411 | width: 100%; 412 | height: auto; 413 | } 414 | div.container { 415 | width: auto; 416 | margin-left: 15px; 417 | margin-right: 15px; 418 | } 419 | p, div.container ul { 420 | width: auto; 421 | } 422 | } 423 | 424 | 425 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5) and (max-width: 640px), 426 | only screen and (-o-min-device-pixel-ratio: 3/2) and (max-width: 640px), 427 | only screen and (min-device-pixel-ratio: 1.5) and (max-width: 640px) { 428 | img { 429 | max-width: 100%; 430 | height: auto; 431 | } 432 | div#sidebar { 433 | -webkit-overflow-scrolling: initial; 434 | position: relative; 435 | width: 90%; 436 | height: 120px; 437 | left: 0; 438 | top: -7px; 439 | padding: 10px 0 10px 30px; 440 | border: 0; 441 | } 442 | img#logo { 443 | width: auto; 444 | height: auto; 445 | } 446 | div.container { 447 | margin: 0; 448 | width: 100%; 449 | } 450 | p, div.container ul { 451 | max-width: 98%; 452 | overflow-x: scroll; 453 | } 454 | table { 455 | position: relative; 456 | } 457 | tr:first-child td { 458 | padding-bottom: 25px; 459 | } 460 | td.text { 461 | padding: 0; 462 | position: absolute; 463 | left: 0; 464 | top: 48px; 465 | } 466 | tr:last-child td.text { 467 | top: 122px; 468 | } 469 | pre { 470 | overflow: scroll; 471 | } 472 | } 473 | 474 | /** 475 | * Prism 476 | */ 477 | 478 | code[class*="language-"], 479 | pre[class*="language-"] { 480 | color: #FFF; 481 | } 482 | 483 | /* Code blocks */ 484 | pre[class*="language-"] { 485 | padding: 1em; 486 | overflow: auto; 487 | } 488 | 489 | :not(pre) > code[class*="language-"], 490 | pre[class*="language-"] { 491 | background: #0C131C; 492 | } 493 | 494 | /* Inline code */ 495 | :not(pre) > code[class*="language-"] { 496 | padding: .1em; 497 | border-radius: .3em; 498 | } 499 | 500 | .token.comment, 501 | .token.prolog, 502 | .token.doctype, 503 | .token.cdata { 504 | color: #3FBF3F; 505 | } 506 | 507 | .token.punctuation { 508 | color: #FFF; 509 | } 510 | 511 | .namespace { 512 | opacity: .7; 513 | } 514 | 515 | .token.property, 516 | .token.boolean, 517 | .token.number { 518 | color: #FF8080; 519 | } 520 | 521 | .token.selector, 522 | .token.attr-value, 523 | .token.string { 524 | color: #A0FFA0; 525 | } 526 | 527 | .token.attr-name, 528 | .token.operator, 529 | .token.entity, 530 | .token.url, 531 | .language-css .token.string, 532 | .style .token.string { 533 | color: #FFF; 534 | } 535 | 536 | .token.tag, 537 | .token.atrule, 538 | .token.keyword { 539 | color: #0099FF; 540 | font-weight: bold; 541 | } 542 | .token.atrule, 543 | .token.keyword { 544 | font-style: italic; 545 | } 546 | 547 | .token.regex, 548 | .token.important { 549 | color: #e90; 550 | } 551 | 552 | .token.important { 553 | font-weight: bold; 554 | } 555 | 556 | .token.entity { 557 | cursor: help; 558 | } 559 | -------------------------------------------------------------------------------- /static/fonts/Museo_Slab_500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/Backbone-relational/3a6bef0c4c6570f121d02fb365d8ae8d81a5864c/static/fonts/Museo_Slab_500.eot -------------------------------------------------------------------------------- /static/fonts/Museo_Slab_500.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/Backbone-relational/3a6bef0c4c6570f121d02fb365d8ae8d81a5864c/static/fonts/Museo_Slab_500.otf -------------------------------------------------------------------------------- /static/fonts/Museo_Slab_500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/Backbone-relational/3a6bef0c4c6570f121d02fb365d8ae8d81a5864c/static/fonts/Museo_Slab_500.woff -------------------------------------------------------------------------------- /static/fonts/PT_Sans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/Backbone-relational/3a6bef0c4c6570f121d02fb365d8ae8d81a5864c/static/fonts/PT_Sans.woff -------------------------------------------------------------------------------- /static/js/prism.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(//g,">").replace(/\u00a0/g," ");var l={element:r,language:o,grammar:u,code:f};t.hooks.run("before-highlight",l);if(i&&self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){l.highlightedCode=n.stringify(JSON.parse(e.data));l.element.innerHTML=l.highlightedCode;s&&s.call(l.element);t.hooks.run("after-highlight",l)};c.postMessage(JSON.stringify({language:l.language,code:l.code}))}else{l.highlightedCode=t.highlight(l.code,l.grammar);l.element.innerHTML=l.highlightedCode;s&&s.call(r);t.hooks.run("after-highlight",l)}},highlight:function(e,r){return n.stringify(t.tokenize(e,r))},tokenize:function(e,n){var r=t.Token,i=[e],s=n.rest;if(s){for(var o in s)n[o]=s[o];delete n.rest}e:for(var o in n){if(!n.hasOwnProperty(o)||!n[o])continue;var u=n[o],a=u.inside,f=!!u.lookbehind||0;u=u.pattern||u;for(var l=0;le.length)break e;if(c instanceof r)continue;u.lastIndex=0;var h=u.exec(c);if(h){f&&(f=h[1].length);var p=h.index-1+f,h=h[0].slice(f),d=h.length,v=p+d,m=c.slice(0,p+1),g=c.slice(v+1),y=[l,1];m&&y.push(m);var b=new r(o,a?t.tokenize(h,a):h);y.push(b);g&&y.push(g);Array.prototype.splice.apply(i,y)}}}return i},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]"){for(var r=0;r"+i.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; 6 | Prism.languages.markup={comment:/<!--[\w\W]*?--(>|>)/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]+?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; 7 | Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,number:/\b-?(0x)?\d*\.?[\da-f]+\b/g,operator:/[-+]{1,2}|!|=?<|=?>|={1,2}|(&){1,2}|\|?\||\?|\*|\//g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g};; 8 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|catch|finally|null|break|continue)\b/g,number:/\b(-?(0x)?\d*\.?[\da-f]+|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; 9 | -------------------------------------------------------------------------------- /test/backbone-relational.js: -------------------------------------------------------------------------------- 1 | ../backbone-relational.js -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Backbone-relational Test Suite 5 | 6 | 7 | 8 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /test/lib/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.1.2 2 | 3 | // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(root, factory) { 9 | 10 | // Set up Backbone appropriately for the environment. Start with AMD. 11 | if (typeof define === 'function' && define.amd) { 12 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) { 13 | // Export global even in AMD case in case this script is loaded with 14 | // others that may still expect a global Backbone. 15 | root.Backbone = factory(root, exports, _, $); 16 | }); 17 | 18 | // Next for Node.js or CommonJS. jQuery may not be needed as a module. 19 | } else if (typeof exports !== 'undefined') { 20 | var _ = require('underscore'); 21 | factory(root, exports, _); 22 | 23 | // Finally, as a browser global. 24 | } else { 25 | root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); 26 | } 27 | 28 | }(this, function(root, Backbone, _, $) { 29 | 30 | // Initial Setup 31 | // ------------- 32 | 33 | // Save the previous value of the `Backbone` variable, so that it can be 34 | // restored later on, if `noConflict` is used. 35 | var previousBackbone = root.Backbone; 36 | 37 | // Create local references to array methods we'll want to use later. 38 | var array = []; 39 | var push = array.push; 40 | var slice = array.slice; 41 | var splice = array.splice; 42 | 43 | // Current version of the library. Keep in sync with `package.json`. 44 | Backbone.VERSION = '1.1.2'; 45 | 46 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 47 | // the `$` variable. 48 | Backbone.$ = $; 49 | 50 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 51 | // to its previous owner. Returns a reference to this Backbone object. 52 | Backbone.noConflict = function() { 53 | root.Backbone = previousBackbone; 54 | return this; 55 | }; 56 | 57 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 58 | // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and 59 | // set a `X-Http-Method-Override` header. 60 | Backbone.emulateHTTP = false; 61 | 62 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 63 | // `application/json` requests ... will encode the body as 64 | // `application/x-www-form-urlencoded` instead and will send the model in a 65 | // form param named `model`. 66 | Backbone.emulateJSON = false; 67 | 68 | // Backbone.Events 69 | // --------------- 70 | 71 | // A module that can be mixed in to *any object* in order to provide it with 72 | // custom events. You may bind with `on` or remove with `off` callback 73 | // functions to an event; `trigger`-ing an event fires all callbacks in 74 | // succession. 75 | // 76 | // var object = {}; 77 | // _.extend(object, Backbone.Events); 78 | // object.on('expand', function(){ alert('expanded'); }); 79 | // object.trigger('expand'); 80 | // 81 | var Events = Backbone.Events = { 82 | 83 | // Bind an event to a `callback` function. Passing `"all"` will bind 84 | // the callback to all events fired. 85 | on: function(name, callback, context) { 86 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 87 | this._events || (this._events = {}); 88 | var events = this._events[name] || (this._events[name] = []); 89 | events.push({callback: callback, context: context, ctx: context || this}); 90 | return this; 91 | }, 92 | 93 | // Bind an event to only be triggered a single time. After the first time 94 | // the callback is invoked, it will be removed. 95 | once: function(name, callback, context) { 96 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 97 | var self = this; 98 | var once = _.once(function() { 99 | self.off(name, once); 100 | callback.apply(this, arguments); 101 | }); 102 | once._callback = callback; 103 | return this.on(name, once, context); 104 | }, 105 | 106 | // Remove one or many callbacks. If `context` is null, removes all 107 | // callbacks with that function. If `callback` is null, removes all 108 | // callbacks for the event. If `name` is null, removes all bound 109 | // callbacks for all events. 110 | off: function(name, callback, context) { 111 | var retain, ev, events, names, i, l, j, k; 112 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 113 | if (!name && !callback && !context) { 114 | this._events = void 0; 115 | return this; 116 | } 117 | names = name ? [name] : _.keys(this._events); 118 | for (i = 0, l = names.length; i < l; i++) { 119 | name = names[i]; 120 | if (events = this._events[name]) { 121 | this._events[name] = retain = []; 122 | if (callback || context) { 123 | for (j = 0, k = events.length; j < k; j++) { 124 | ev = events[j]; 125 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 126 | (context && context !== ev.context)) { 127 | retain.push(ev); 128 | } 129 | } 130 | } 131 | if (!retain.length) delete this._events[name]; 132 | } 133 | } 134 | 135 | return this; 136 | }, 137 | 138 | // Trigger one or many events, firing all bound callbacks. Callbacks are 139 | // passed the same arguments as `trigger` is, apart from the event name 140 | // (unless you're listening on `"all"`, which will cause your callback to 141 | // receive the true name of the event as the first argument). 142 | trigger: function(name) { 143 | if (!this._events) return this; 144 | var args = slice.call(arguments, 1); 145 | if (!eventsApi(this, 'trigger', name, args)) return this; 146 | var events = this._events[name]; 147 | var allEvents = this._events.all; 148 | if (events) triggerEvents(events, args); 149 | if (allEvents) triggerEvents(allEvents, arguments); 150 | return this; 151 | }, 152 | 153 | // Tell this object to stop listening to either specific events ... or 154 | // to every object it's currently listening to. 155 | stopListening: function(obj, name, callback) { 156 | var listeningTo = this._listeningTo; 157 | if (!listeningTo) return this; 158 | var remove = !name && !callback; 159 | if (!callback && typeof name === 'object') callback = this; 160 | if (obj) (listeningTo = {})[obj._listenId] = obj; 161 | for (var id in listeningTo) { 162 | obj = listeningTo[id]; 163 | obj.off(name, callback, this); 164 | if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; 165 | } 166 | return this; 167 | } 168 | 169 | }; 170 | 171 | // Regular expression used to split event strings. 172 | var eventSplitter = /\s+/; 173 | 174 | // Implement fancy features of the Events API such as multiple event 175 | // names `"change blur"` and jQuery-style event maps `{change: action}` 176 | // in terms of the existing API. 177 | var eventsApi = function(obj, action, name, rest) { 178 | if (!name) return true; 179 | 180 | // Handle event maps. 181 | if (typeof name === 'object') { 182 | for (var key in name) { 183 | obj[action].apply(obj, [key, name[key]].concat(rest)); 184 | } 185 | return false; 186 | } 187 | 188 | // Handle space separated event names. 189 | if (eventSplitter.test(name)) { 190 | var names = name.split(eventSplitter); 191 | for (var i = 0, l = names.length; i < l; i++) { 192 | obj[action].apply(obj, [names[i]].concat(rest)); 193 | } 194 | return false; 195 | } 196 | 197 | return true; 198 | }; 199 | 200 | // A difficult-to-believe, but optimized internal dispatch function for 201 | // triggering events. Tries to keep the usual cases speedy (most internal 202 | // Backbone events have 3 arguments). 203 | var triggerEvents = function(events, args) { 204 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 205 | switch (args.length) { 206 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 207 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 208 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 209 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 210 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 211 | } 212 | }; 213 | 214 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 215 | 216 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 217 | // listen to an event in another object ... keeping track of what it's 218 | // listening to. 219 | _.each(listenMethods, function(implementation, method) { 220 | Events[method] = function(obj, name, callback) { 221 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 222 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 223 | listeningTo[id] = obj; 224 | if (!callback && typeof name === 'object') callback = this; 225 | obj[implementation](name, callback, this); 226 | return this; 227 | }; 228 | }); 229 | 230 | // Aliases for backwards compatibility. 231 | Events.bind = Events.on; 232 | Events.unbind = Events.off; 233 | 234 | // Allow the `Backbone` object to serve as a global event bus, for folks who 235 | // want global "pubsub" in a convenient place. 236 | _.extend(Backbone, Events); 237 | 238 | // Backbone.Model 239 | // -------------- 240 | 241 | // Backbone **Models** are the basic data object in the framework -- 242 | // frequently representing a row in a table in a database on your server. 243 | // A discrete chunk of data and a bunch of useful, related methods for 244 | // performing computations and transformations on that data. 245 | 246 | // Create a new model with the specified attributes. A client id (`cid`) 247 | // is automatically generated and assigned for you. 248 | var Model = Backbone.Model = function(attributes, options) { 249 | var attrs = attributes || {}; 250 | options || (options = {}); 251 | this.cid = _.uniqueId('c'); 252 | this.attributes = {}; 253 | if (options.collection) this.collection = options.collection; 254 | if (options.parse) attrs = this.parse(attrs, options) || {}; 255 | attrs = _.defaults({}, attrs, _.result(this, 'defaults')); 256 | this.set(attrs, options); 257 | this.changed = {}; 258 | this.initialize.apply(this, arguments); 259 | }; 260 | 261 | // Attach all inheritable methods to the Model prototype. 262 | _.extend(Model.prototype, Events, { 263 | 264 | // A hash of attributes whose current and previous value differ. 265 | changed: null, 266 | 267 | // The value returned during the last failed validation. 268 | validationError: null, 269 | 270 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 271 | // CouchDB users may want to set this to `"_id"`. 272 | idAttribute: 'id', 273 | 274 | // Initialize is an empty function by default. Override it with your own 275 | // initialization logic. 276 | initialize: function(){}, 277 | 278 | // Return a copy of the model's `attributes` object. 279 | toJSON: function(options) { 280 | return _.clone(this.attributes); 281 | }, 282 | 283 | // Proxy `Backbone.sync` by default -- but override this if you need 284 | // custom syncing semantics for *this* particular model. 285 | sync: function() { 286 | return Backbone.sync.apply(this, arguments); 287 | }, 288 | 289 | // Get the value of an attribute. 290 | get: function(attr) { 291 | return this.attributes[attr]; 292 | }, 293 | 294 | // Get the HTML-escaped value of an attribute. 295 | escape: function(attr) { 296 | return _.escape(this.get(attr)); 297 | }, 298 | 299 | // Returns `true` if the attribute contains a value that is not null 300 | // or undefined. 301 | has: function(attr) { 302 | return this.get(attr) != null; 303 | }, 304 | 305 | // Set a hash of model attributes on the object, firing `"change"`. This is 306 | // the core primitive operation of a model, updating the data and notifying 307 | // anyone who needs to know about the change in state. The heart of the beast. 308 | set: function(key, val, options) { 309 | var attr, attrs, unset, changes, silent, changing, prev, current; 310 | if (key == null) return this; 311 | 312 | // Handle both `"key", value` and `{key: value}` -style arguments. 313 | if (typeof key === 'object') { 314 | attrs = key; 315 | options = val; 316 | } else { 317 | (attrs = {})[key] = val; 318 | } 319 | 320 | options || (options = {}); 321 | 322 | // Run validation. 323 | if (!this._validate(attrs, options)) return false; 324 | 325 | // Extract attributes and options. 326 | unset = options.unset; 327 | silent = options.silent; 328 | changes = []; 329 | changing = this._changing; 330 | this._changing = true; 331 | 332 | if (!changing) { 333 | this._previousAttributes = _.clone(this.attributes); 334 | this.changed = {}; 335 | } 336 | current = this.attributes, prev = this._previousAttributes; 337 | 338 | // Check for changes of `id`. 339 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 340 | 341 | // For each `set` attribute, update or delete the current value. 342 | for (attr in attrs) { 343 | val = attrs[attr]; 344 | if (!_.isEqual(current[attr], val)) changes.push(attr); 345 | if (!_.isEqual(prev[attr], val)) { 346 | this.changed[attr] = val; 347 | } else { 348 | delete this.changed[attr]; 349 | } 350 | unset ? delete current[attr] : current[attr] = val; 351 | } 352 | 353 | // Trigger all relevant attribute changes. 354 | if (!silent) { 355 | if (changes.length) this._pending = options; 356 | for (var i = 0, l = changes.length; i < l; i++) { 357 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 358 | } 359 | } 360 | 361 | // You might be wondering why there's a `while` loop here. Changes can 362 | // be recursively nested within `"change"` events. 363 | if (changing) return this; 364 | if (!silent) { 365 | while (this._pending) { 366 | options = this._pending; 367 | this._pending = false; 368 | this.trigger('change', this, options); 369 | } 370 | } 371 | this._pending = false; 372 | this._changing = false; 373 | return this; 374 | }, 375 | 376 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 377 | // if the attribute doesn't exist. 378 | unset: function(attr, options) { 379 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 380 | }, 381 | 382 | // Clear all attributes on the model, firing `"change"`. 383 | clear: function(options) { 384 | var attrs = {}; 385 | for (var key in this.attributes) attrs[key] = void 0; 386 | return this.set(attrs, _.extend({}, options, {unset: true})); 387 | }, 388 | 389 | // Determine if the model has changed since the last `"change"` event. 390 | // If you specify an attribute name, determine if that attribute has changed. 391 | hasChanged: function(attr) { 392 | if (attr == null) return !_.isEmpty(this.changed); 393 | return _.has(this.changed, attr); 394 | }, 395 | 396 | // Return an object containing all the attributes that have changed, or 397 | // false if there are no changed attributes. Useful for determining what 398 | // parts of a view need to be updated and/or what attributes need to be 399 | // persisted to the server. Unset attributes will be set to undefined. 400 | // You can also pass an attributes object to diff against the model, 401 | // determining if there *would be* a change. 402 | changedAttributes: function(diff) { 403 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 404 | var val, changed = false; 405 | var old = this._changing ? this._previousAttributes : this.attributes; 406 | for (var attr in diff) { 407 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 408 | (changed || (changed = {}))[attr] = val; 409 | } 410 | return changed; 411 | }, 412 | 413 | // Get the previous value of an attribute, recorded at the time the last 414 | // `"change"` event was fired. 415 | previous: function(attr) { 416 | if (attr == null || !this._previousAttributes) return null; 417 | return this._previousAttributes[attr]; 418 | }, 419 | 420 | // Get all of the attributes of the model at the time of the previous 421 | // `"change"` event. 422 | previousAttributes: function() { 423 | return _.clone(this._previousAttributes); 424 | }, 425 | 426 | // Fetch the model from the server. If the server's representation of the 427 | // model differs from its current attributes, they will be overridden, 428 | // triggering a `"change"` event. 429 | fetch: function(options) { 430 | options = options ? _.clone(options) : {}; 431 | if (options.parse === void 0) options.parse = true; 432 | var model = this; 433 | var success = options.success; 434 | options.success = function(resp) { 435 | if (!model.set(model.parse(resp, options), options)) return false; 436 | if (success) success(model, resp, options); 437 | model.trigger('sync', model, resp, options); 438 | }; 439 | wrapError(this, options); 440 | return this.sync('read', this, options); 441 | }, 442 | 443 | // Set a hash of model attributes, and sync the model to the server. 444 | // If the server returns an attributes hash that differs, the model's 445 | // state will be `set` again. 446 | save: function(key, val, options) { 447 | var attrs, method, xhr, attributes = this.attributes; 448 | 449 | // Handle both `"key", value` and `{key: value}` -style arguments. 450 | if (key == null || typeof key === 'object') { 451 | attrs = key; 452 | options = val; 453 | } else { 454 | (attrs = {})[key] = val; 455 | } 456 | 457 | options = _.extend({validate: true}, options); 458 | 459 | // If we're not waiting and attributes exist, save acts as 460 | // `set(attr).save(null, opts)` with validation. Otherwise, check if 461 | // the model will be valid when the attributes, if any, are set. 462 | if (attrs && !options.wait) { 463 | if (!this.set(attrs, options)) return false; 464 | } else { 465 | if (!this._validate(attrs, options)) return false; 466 | } 467 | 468 | // Set temporary attributes if `{wait: true}`. 469 | if (attrs && options.wait) { 470 | this.attributes = _.extend({}, attributes, attrs); 471 | } 472 | 473 | // After a successful server-side save, the client is (optionally) 474 | // updated with the server-side state. 475 | if (options.parse === void 0) options.parse = true; 476 | var model = this; 477 | var success = options.success; 478 | options.success = function(resp) { 479 | // Ensure attributes are restored during synchronous saves. 480 | model.attributes = attributes; 481 | var serverAttrs = model.parse(resp, options); 482 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 483 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 484 | return false; 485 | } 486 | if (success) success(model, resp, options); 487 | model.trigger('sync', model, resp, options); 488 | }; 489 | wrapError(this, options); 490 | 491 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 492 | if (method === 'patch') options.attrs = attrs; 493 | xhr = this.sync(method, this, options); 494 | 495 | // Restore attributes. 496 | if (attrs && options.wait) this.attributes = attributes; 497 | 498 | return xhr; 499 | }, 500 | 501 | // Destroy this model on the server if it was already persisted. 502 | // Optimistically removes the model from its collection, if it has one. 503 | // If `wait: true` is passed, waits for the server to respond before removal. 504 | destroy: function(options) { 505 | options = options ? _.clone(options) : {}; 506 | var model = this; 507 | var success = options.success; 508 | 509 | var destroy = function() { 510 | model.trigger('destroy', model, model.collection, options); 511 | }; 512 | 513 | options.success = function(resp) { 514 | if (options.wait || model.isNew()) destroy(); 515 | if (success) success(model, resp, options); 516 | if (!model.isNew()) model.trigger('sync', model, resp, options); 517 | }; 518 | 519 | if (this.isNew()) { 520 | options.success(); 521 | return false; 522 | } 523 | wrapError(this, options); 524 | 525 | var xhr = this.sync('delete', this, options); 526 | if (!options.wait) destroy(); 527 | return xhr; 528 | }, 529 | 530 | // Default URL for the model's representation on the server -- if you're 531 | // using Backbone's restful methods, override this to change the endpoint 532 | // that will be called. 533 | url: function() { 534 | var base = 535 | _.result(this, 'urlRoot') || 536 | _.result(this.collection, 'url') || 537 | urlError(); 538 | if (this.isNew()) return base; 539 | return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); 540 | }, 541 | 542 | // **parse** converts a response into the hash of attributes to be `set` on 543 | // the model. The default implementation is just to pass the response along. 544 | parse: function(resp, options) { 545 | return resp; 546 | }, 547 | 548 | // Create a new model with identical attributes to this one. 549 | clone: function() { 550 | return new this.constructor(this.attributes); 551 | }, 552 | 553 | // A model is new if it has never been saved to the server, and lacks an id. 554 | isNew: function() { 555 | return !this.has(this.idAttribute); 556 | }, 557 | 558 | // Check if the model is currently in a valid state. 559 | isValid: function(options) { 560 | return this._validate({}, _.extend(options || {}, { validate: true })); 561 | }, 562 | 563 | // Run validation against the next complete set of model attributes, 564 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 565 | _validate: function(attrs, options) { 566 | if (!options.validate || !this.validate) return true; 567 | attrs = _.extend({}, this.attributes, attrs); 568 | var error = this.validationError = this.validate(attrs, options) || null; 569 | if (!error) return true; 570 | this.trigger('invalid', this, error, _.extend(options, {validationError: error})); 571 | return false; 572 | } 573 | 574 | }); 575 | 576 | // Underscore methods that we want to implement on the Model. 577 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 578 | 579 | // Mix in each Underscore method as a proxy to `Model#attributes`. 580 | _.each(modelMethods, function(method) { 581 | Model.prototype[method] = function() { 582 | var args = slice.call(arguments); 583 | args.unshift(this.attributes); 584 | return _[method].apply(_, args); 585 | }; 586 | }); 587 | 588 | // Backbone.Collection 589 | // ------------------- 590 | 591 | // If models tend to represent a single row of data, a Backbone Collection is 592 | // more analagous to a table full of data ... or a small slice or page of that 593 | // table, or a collection of rows that belong together for a particular reason 594 | // -- all of the messages in this particular folder, all of the documents 595 | // belonging to this particular author, and so on. Collections maintain 596 | // indexes of their models, both in order, and for lookup by `id`. 597 | 598 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 599 | // If a `comparator` is specified, the Collection will maintain 600 | // its models in sort order, as they're added and removed. 601 | var Collection = Backbone.Collection = function(models, options) { 602 | options || (options = {}); 603 | if (options.model) this.model = options.model; 604 | if (options.comparator !== void 0) this.comparator = options.comparator; 605 | this._reset(); 606 | this.initialize.apply(this, arguments); 607 | if (models) this.reset(models, _.extend({silent: true}, options)); 608 | }; 609 | 610 | // Default options for `Collection#set`. 611 | var setOptions = {add: true, remove: true, merge: true}; 612 | var addOptions = {add: true, remove: false}; 613 | 614 | // Define the Collection's inheritable methods. 615 | _.extend(Collection.prototype, Events, { 616 | 617 | // The default model for a collection is just a **Backbone.Model**. 618 | // This should be overridden in most cases. 619 | model: Model, 620 | 621 | // Initialize is an empty function by default. Override it with your own 622 | // initialization logic. 623 | initialize: function(){}, 624 | 625 | // The JSON representation of a Collection is an array of the 626 | // models' attributes. 627 | toJSON: function(options) { 628 | return this.map(function(model){ return model.toJSON(options); }); 629 | }, 630 | 631 | // Proxy `Backbone.sync` by default. 632 | sync: function() { 633 | return Backbone.sync.apply(this, arguments); 634 | }, 635 | 636 | // Add a model, or list of models to the set. 637 | add: function(models, options) { 638 | return this.set(models, _.extend({merge: false}, options, addOptions)); 639 | }, 640 | 641 | // Remove a model, or a list of models from the set. 642 | remove: function(models, options) { 643 | var singular = !_.isArray(models); 644 | models = singular ? [models] : _.clone(models); 645 | options || (options = {}); 646 | var i, l, index, model; 647 | for (i = 0, l = models.length; i < l; i++) { 648 | model = models[i] = this.get(models[i]); 649 | if (!model) continue; 650 | delete this._byId[model.id]; 651 | delete this._byId[model.cid]; 652 | index = this.indexOf(model); 653 | this.models.splice(index, 1); 654 | this.length--; 655 | if (!options.silent) { 656 | options.index = index; 657 | model.trigger('remove', model, this, options); 658 | } 659 | this._removeReference(model, options); 660 | } 661 | return singular ? models[0] : models; 662 | }, 663 | 664 | // Update a collection by `set`-ing a new list of models, adding new ones, 665 | // removing models that are no longer present, and merging models that 666 | // already exist in the collection, as necessary. Similar to **Model#set**, 667 | // the core operation for updating the data contained by the collection. 668 | set: function(models, options) { 669 | options = _.defaults({}, options, setOptions); 670 | if (options.parse) models = this.parse(models, options); 671 | var singular = !_.isArray(models); 672 | models = singular ? (models ? [models] : []) : _.clone(models); 673 | var i, l, id, model, attrs, existing, sort; 674 | var at = options.at; 675 | var targetModel = this.model; 676 | var sortable = this.comparator && (at == null) && options.sort !== false; 677 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 678 | var toAdd = [], toRemove = [], modelMap = {}; 679 | var add = options.add, merge = options.merge, remove = options.remove; 680 | var order = !sortable && add && remove ? [] : false; 681 | 682 | // Turn bare objects into model references, and prevent invalid models 683 | // from being added. 684 | for (i = 0, l = models.length; i < l; i++) { 685 | attrs = models[i] || {}; 686 | if (attrs instanceof Model) { 687 | id = model = attrs; 688 | } else { 689 | id = attrs[targetModel.prototype.idAttribute || 'id']; 690 | } 691 | 692 | // If a duplicate is found, prevent it from being added and 693 | // optionally merge it into the existing model. 694 | if (existing = this.get(id)) { 695 | if (remove) modelMap[existing.cid] = true; 696 | if (merge) { 697 | attrs = attrs === model ? model.attributes : attrs; 698 | if (options.parse) attrs = existing.parse(attrs, options); 699 | existing.set(attrs, options); 700 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 701 | } 702 | models[i] = existing; 703 | 704 | // If this is a new, valid model, push it to the `toAdd` list. 705 | } else if (add) { 706 | model = models[i] = this._prepareModel(attrs, options); 707 | if (!model) continue; 708 | toAdd.push(model); 709 | this._addReference(model, options); 710 | } 711 | 712 | // Do not add multiple models with the same `id`. 713 | model = existing || model; 714 | if (order && (model.isNew() || !modelMap[model.id])) order.push(model); 715 | modelMap[model.id] = true; 716 | } 717 | 718 | // Remove nonexistent models if appropriate. 719 | if (remove) { 720 | for (i = 0, l = this.length; i < l; ++i) { 721 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 722 | } 723 | if (toRemove.length) this.remove(toRemove, options); 724 | } 725 | 726 | // See if sorting is needed, update `length` and splice in new models. 727 | if (toAdd.length || (order && order.length)) { 728 | if (sortable) sort = true; 729 | this.length += toAdd.length; 730 | if (at != null) { 731 | for (i = 0, l = toAdd.length; i < l; i++) { 732 | this.models.splice(at + i, 0, toAdd[i]); 733 | } 734 | } else { 735 | if (order) this.models.length = 0; 736 | var orderedModels = order || toAdd; 737 | for (i = 0, l = orderedModels.length; i < l; i++) { 738 | this.models.push(orderedModels[i]); 739 | } 740 | } 741 | } 742 | 743 | // Silently sort the collection if appropriate. 744 | if (sort) this.sort({silent: true}); 745 | 746 | // Unless silenced, it's time to fire all appropriate add/sort events. 747 | if (!options.silent) { 748 | for (i = 0, l = toAdd.length; i < l; i++) { 749 | (model = toAdd[i]).trigger('add', model, this, options); 750 | } 751 | if (sort || (order && order.length)) this.trigger('sort', this, options); 752 | } 753 | 754 | // Return the added (or merged) model (or models). 755 | return singular ? models[0] : models; 756 | }, 757 | 758 | // When you have more items than you want to add or remove individually, 759 | // you can reset the entire set with a new list of models, without firing 760 | // any granular `add` or `remove` events. Fires `reset` when finished. 761 | // Useful for bulk operations and optimizations. 762 | reset: function(models, options) { 763 | options || (options = {}); 764 | for (var i = 0, l = this.models.length; i < l; i++) { 765 | this._removeReference(this.models[i], options); 766 | } 767 | options.previousModels = this.models; 768 | this._reset(); 769 | models = this.add(models, _.extend({silent: true}, options)); 770 | if (!options.silent) this.trigger('reset', this, options); 771 | return models; 772 | }, 773 | 774 | // Add a model to the end of the collection. 775 | push: function(model, options) { 776 | return this.add(model, _.extend({at: this.length}, options)); 777 | }, 778 | 779 | // Remove a model from the end of the collection. 780 | pop: function(options) { 781 | var model = this.at(this.length - 1); 782 | this.remove(model, options); 783 | return model; 784 | }, 785 | 786 | // Add a model to the beginning of the collection. 787 | unshift: function(model, options) { 788 | return this.add(model, _.extend({at: 0}, options)); 789 | }, 790 | 791 | // Remove a model from the beginning of the collection. 792 | shift: function(options) { 793 | var model = this.at(0); 794 | this.remove(model, options); 795 | return model; 796 | }, 797 | 798 | // Slice out a sub-array of models from the collection. 799 | slice: function() { 800 | return slice.apply(this.models, arguments); 801 | }, 802 | 803 | // Get a model from the set by id. 804 | get: function(obj) { 805 | if (obj == null) return void 0; 806 | return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; 807 | }, 808 | 809 | // Get the model at the given index. 810 | at: function(index) { 811 | return this.models[index]; 812 | }, 813 | 814 | // Return models with matching attributes. Useful for simple cases of 815 | // `filter`. 816 | where: function(attrs, first) { 817 | if (_.isEmpty(attrs)) return first ? void 0 : []; 818 | return this[first ? 'find' : 'filter'](function(model) { 819 | for (var key in attrs) { 820 | if (attrs[key] !== model.get(key)) return false; 821 | } 822 | return true; 823 | }); 824 | }, 825 | 826 | // Return the first model with matching attributes. Useful for simple cases 827 | // of `find`. 828 | findWhere: function(attrs) { 829 | return this.where(attrs, true); 830 | }, 831 | 832 | // Force the collection to re-sort itself. You don't need to call this under 833 | // normal circumstances, as the set will maintain sort order as each item 834 | // is added. 835 | sort: function(options) { 836 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 837 | options || (options = {}); 838 | 839 | // Run sort based on type of `comparator`. 840 | if (_.isString(this.comparator) || this.comparator.length === 1) { 841 | this.models = this.sortBy(this.comparator, this); 842 | } else { 843 | this.models.sort(_.bind(this.comparator, this)); 844 | } 845 | 846 | if (!options.silent) this.trigger('sort', this, options); 847 | return this; 848 | }, 849 | 850 | // Pluck an attribute from each model in the collection. 851 | pluck: function(attr) { 852 | return _.invoke(this.models, 'get', attr); 853 | }, 854 | 855 | // Fetch the default set of models for this collection, resetting the 856 | // collection when they arrive. If `reset: true` is passed, the response 857 | // data will be passed through the `reset` method instead of `set`. 858 | fetch: function(options) { 859 | options = options ? _.clone(options) : {}; 860 | if (options.parse === void 0) options.parse = true; 861 | var success = options.success; 862 | var collection = this; 863 | options.success = function(resp) { 864 | var method = options.reset ? 'reset' : 'set'; 865 | collection[method](resp, options); 866 | if (success) success(collection, resp, options); 867 | collection.trigger('sync', collection, resp, options); 868 | }; 869 | wrapError(this, options); 870 | return this.sync('read', this, options); 871 | }, 872 | 873 | // Create a new instance of a model in this collection. Add the model to the 874 | // collection immediately, unless `wait: true` is passed, in which case we 875 | // wait for the server to agree. 876 | create: function(model, options) { 877 | options = options ? _.clone(options) : {}; 878 | if (!(model = this._prepareModel(model, options))) return false; 879 | if (!options.wait) this.add(model, options); 880 | var collection = this; 881 | var success = options.success; 882 | options.success = function(model, resp) { 883 | if (options.wait) collection.add(model, options); 884 | if (success) success(model, resp, options); 885 | }; 886 | model.save(null, options); 887 | return model; 888 | }, 889 | 890 | // **parse** converts a response into a list of models to be added to the 891 | // collection. The default implementation is just to pass it through. 892 | parse: function(resp, options) { 893 | return resp; 894 | }, 895 | 896 | // Create a new collection with an identical list of models as this one. 897 | clone: function() { 898 | return new this.constructor(this.models); 899 | }, 900 | 901 | // Private method to reset all internal state. Called when the collection 902 | // is first initialized or reset. 903 | _reset: function() { 904 | this.length = 0; 905 | this.models = []; 906 | this._byId = {}; 907 | }, 908 | 909 | // Prepare a hash of attributes (or other model) to be added to this 910 | // collection. 911 | _prepareModel: function(attrs, options) { 912 | if (attrs instanceof Model) return attrs; 913 | options = options ? _.clone(options) : {}; 914 | options.collection = this; 915 | var model = new this.model(attrs, options); 916 | if (!model.validationError) return model; 917 | this.trigger('invalid', this, model.validationError, options); 918 | return false; 919 | }, 920 | 921 | // Internal method to create a model's ties to a collection. 922 | _addReference: function(model, options) { 923 | this._byId[model.cid] = model; 924 | if (model.id != null) this._byId[model.id] = model; 925 | if (!model.collection) model.collection = this; 926 | model.on('all', this._onModelEvent, this); 927 | }, 928 | 929 | // Internal method to sever a model's ties to a collection. 930 | _removeReference: function(model, options) { 931 | if (this === model.collection) delete model.collection; 932 | model.off('all', this._onModelEvent, this); 933 | }, 934 | 935 | // Internal method called every time a model in the set fires an event. 936 | // Sets need to update their indexes when models change ids. All other 937 | // events simply proxy through. "add" and "remove" events that originate 938 | // in other collections are ignored. 939 | _onModelEvent: function(event, model, collection, options) { 940 | if ((event === 'add' || event === 'remove') && collection !== this) return; 941 | if (event === 'destroy') this.remove(model, options); 942 | if (model && event === 'change:' + model.idAttribute) { 943 | delete this._byId[model.previous(model.idAttribute)]; 944 | if (model.id != null) this._byId[model.id] = model; 945 | } 946 | this.trigger.apply(this, arguments); 947 | } 948 | 949 | }); 950 | 951 | // Underscore methods that we want to implement on the Collection. 952 | // 90% of the core usefulness of Backbone Collections is actually implemented 953 | // right here: 954 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 955 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 956 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 957 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 958 | 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 959 | 'lastIndexOf', 'isEmpty', 'chain', 'sample']; 960 | 961 | // Mix in each Underscore method as a proxy to `Collection#models`. 962 | _.each(methods, function(method) { 963 | Collection.prototype[method] = function() { 964 | var args = slice.call(arguments); 965 | args.unshift(this.models); 966 | return _[method].apply(_, args); 967 | }; 968 | }); 969 | 970 | // Underscore methods that take a property name as an argument. 971 | var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; 972 | 973 | // Use attributes instead of properties. 974 | _.each(attributeMethods, function(method) { 975 | Collection.prototype[method] = function(value, context) { 976 | var iterator = _.isFunction(value) ? value : function(model) { 977 | return model.get(value); 978 | }; 979 | return _[method](this.models, iterator, context); 980 | }; 981 | }); 982 | 983 | // Backbone.View 984 | // ------------- 985 | 986 | // Backbone Views are almost more convention than they are actual code. A View 987 | // is simply a JavaScript object that represents a logical chunk of UI in the 988 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 989 | // even the surrounding frame which wraps your whole app. Defining a chunk of 990 | // UI as a **View** allows you to define your DOM events declaratively, without 991 | // having to worry about render order ... and makes it easy for the view to 992 | // react to specific changes in the state of your models. 993 | 994 | // Creating a Backbone.View creates its initial element outside of the DOM, 995 | // if an existing element is not provided... 996 | var View = Backbone.View = function(options) { 997 | this.cid = _.uniqueId('view'); 998 | options || (options = {}); 999 | _.extend(this, _.pick(options, viewOptions)); 1000 | this._ensureElement(); 1001 | this.initialize.apply(this, arguments); 1002 | this.delegateEvents(); 1003 | }; 1004 | 1005 | // Cached regex to split keys for `delegate`. 1006 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1007 | 1008 | // List of view options to be merged as properties. 1009 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1010 | 1011 | // Set up all inheritable **Backbone.View** properties and methods. 1012 | _.extend(View.prototype, Events, { 1013 | 1014 | // The default `tagName` of a View's element is `"div"`. 1015 | tagName: 'div', 1016 | 1017 | // jQuery delegate for element lookup, scoped to DOM elements within the 1018 | // current view. This should be preferred to global lookups where possible. 1019 | $: function(selector) { 1020 | return this.$el.find(selector); 1021 | }, 1022 | 1023 | // Initialize is an empty function by default. Override it with your own 1024 | // initialization logic. 1025 | initialize: function(){}, 1026 | 1027 | // **render** is the core function that your view should override, in order 1028 | // to populate its element (`this.el`), with the appropriate HTML. The 1029 | // convention is for **render** to always return `this`. 1030 | render: function() { 1031 | return this; 1032 | }, 1033 | 1034 | // Remove this view by taking the element out of the DOM, and removing any 1035 | // applicable Backbone.Events listeners. 1036 | remove: function() { 1037 | this.$el.remove(); 1038 | this.stopListening(); 1039 | return this; 1040 | }, 1041 | 1042 | // Change the view's element (`this.el` property), including event 1043 | // re-delegation. 1044 | setElement: function(element, delegate) { 1045 | if (this.$el) this.undelegateEvents(); 1046 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1047 | this.el = this.$el[0]; 1048 | if (delegate !== false) this.delegateEvents(); 1049 | return this; 1050 | }, 1051 | 1052 | // Set callbacks, where `this.events` is a hash of 1053 | // 1054 | // *{"event selector": "callback"}* 1055 | // 1056 | // { 1057 | // 'mousedown .title': 'edit', 1058 | // 'click .button': 'save', 1059 | // 'click .open': function(e) { ... } 1060 | // } 1061 | // 1062 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1063 | // Uses event delegation for efficiency. 1064 | // Omitting the selector binds the event to `this.el`. 1065 | // This only works for delegate-able events: not `focus`, `blur`, and 1066 | // not `change`, `submit`, and `reset` in Internet Explorer. 1067 | delegateEvents: function(events) { 1068 | if (!(events || (events = _.result(this, 'events')))) return this; 1069 | this.undelegateEvents(); 1070 | for (var key in events) { 1071 | var method = events[key]; 1072 | if (!_.isFunction(method)) method = this[events[key]]; 1073 | if (!method) continue; 1074 | 1075 | var match = key.match(delegateEventSplitter); 1076 | var eventName = match[1], selector = match[2]; 1077 | method = _.bind(method, this); 1078 | eventName += '.delegateEvents' + this.cid; 1079 | if (selector === '') { 1080 | this.$el.on(eventName, method); 1081 | } else { 1082 | this.$el.on(eventName, selector, method); 1083 | } 1084 | } 1085 | return this; 1086 | }, 1087 | 1088 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1089 | // You usually don't need to use this, but may wish to if you have multiple 1090 | // Backbone views attached to the same DOM element. 1091 | undelegateEvents: function() { 1092 | this.$el.off('.delegateEvents' + this.cid); 1093 | return this; 1094 | }, 1095 | 1096 | // Ensure that the View has a DOM element to render into. 1097 | // If `this.el` is a string, pass it through `$()`, take the first 1098 | // matching element, and re-assign it to `el`. Otherwise, create 1099 | // an element from the `id`, `className` and `tagName` properties. 1100 | _ensureElement: function() { 1101 | if (!this.el) { 1102 | var attrs = _.extend({}, _.result(this, 'attributes')); 1103 | if (this.id) attrs.id = _.result(this, 'id'); 1104 | if (this.className) attrs['class'] = _.result(this, 'className'); 1105 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1106 | this.setElement($el, false); 1107 | } else { 1108 | this.setElement(_.result(this, 'el'), false); 1109 | } 1110 | } 1111 | 1112 | }); 1113 | 1114 | // Backbone.sync 1115 | // ------------- 1116 | 1117 | // Override this function to change the manner in which Backbone persists 1118 | // models to the server. You will be passed the type of request, and the 1119 | // model in question. By default, makes a RESTful Ajax request 1120 | // to the model's `url()`. Some possible customizations could be: 1121 | // 1122 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1123 | // * Send up the models as XML instead of JSON. 1124 | // * Persist models via WebSockets instead of Ajax. 1125 | // 1126 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1127 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1128 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1129 | // instead of `application/json` with the model in a param named `model`. 1130 | // Useful when interfacing with server-side languages like **PHP** that make 1131 | // it difficult to read the body of `PUT` requests. 1132 | Backbone.sync = function(method, model, options) { 1133 | var type = methodMap[method]; 1134 | 1135 | // Default options, unless specified. 1136 | _.defaults(options || (options = {}), { 1137 | emulateHTTP: Backbone.emulateHTTP, 1138 | emulateJSON: Backbone.emulateJSON 1139 | }); 1140 | 1141 | // Default JSON-request options. 1142 | var params = {type: type, dataType: 'json'}; 1143 | 1144 | // Ensure that we have a URL. 1145 | if (!options.url) { 1146 | params.url = _.result(model, 'url') || urlError(); 1147 | } 1148 | 1149 | // Ensure that we have the appropriate request data. 1150 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1151 | params.contentType = 'application/json'; 1152 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1153 | } 1154 | 1155 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1156 | if (options.emulateJSON) { 1157 | params.contentType = 'application/x-www-form-urlencoded'; 1158 | params.data = params.data ? {model: params.data} : {}; 1159 | } 1160 | 1161 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1162 | // And an `X-HTTP-Method-Override` header. 1163 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1164 | params.type = 'POST'; 1165 | if (options.emulateJSON) params.data._method = type; 1166 | var beforeSend = options.beforeSend; 1167 | options.beforeSend = function(xhr) { 1168 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1169 | if (beforeSend) return beforeSend.apply(this, arguments); 1170 | }; 1171 | } 1172 | 1173 | // Don't process data on a non-GET request. 1174 | if (params.type !== 'GET' && !options.emulateJSON) { 1175 | params.processData = false; 1176 | } 1177 | 1178 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1179 | // that still has ActiveX enabled by default, override jQuery to use that 1180 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1181 | if (params.type === 'PATCH' && noXhrPatch) { 1182 | params.xhr = function() { 1183 | return new ActiveXObject("Microsoft.XMLHTTP"); 1184 | }; 1185 | } 1186 | 1187 | // Make the request, allowing the user to override any Ajax options. 1188 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1189 | model.trigger('request', model, xhr, options); 1190 | return xhr; 1191 | }; 1192 | 1193 | var noXhrPatch = 1194 | typeof window !== 'undefined' && !!window.ActiveXObject && 1195 | !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); 1196 | 1197 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1198 | var methodMap = { 1199 | 'create': 'POST', 1200 | 'update': 'PUT', 1201 | 'patch': 'PATCH', 1202 | 'delete': 'DELETE', 1203 | 'read': 'GET' 1204 | }; 1205 | 1206 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1207 | // Override this if you'd like to use a different library. 1208 | Backbone.ajax = function() { 1209 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1210 | }; 1211 | 1212 | // Backbone.Router 1213 | // --------------- 1214 | 1215 | // Routers map faux-URLs to actions, and fire events when routes are 1216 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1217 | var Router = Backbone.Router = function(options) { 1218 | options || (options = {}); 1219 | if (options.routes) this.routes = options.routes; 1220 | this._bindRoutes(); 1221 | this.initialize.apply(this, arguments); 1222 | }; 1223 | 1224 | // Cached regular expressions for matching named param parts and splatted 1225 | // parts of route strings. 1226 | var optionalParam = /\((.*?)\)/g; 1227 | var namedParam = /(\(\?)?:\w+/g; 1228 | var splatParam = /\*\w+/g; 1229 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1230 | 1231 | // Set up all inheritable **Backbone.Router** properties and methods. 1232 | _.extend(Router.prototype, Events, { 1233 | 1234 | // Initialize is an empty function by default. Override it with your own 1235 | // initialization logic. 1236 | initialize: function(){}, 1237 | 1238 | // Manually bind a single named route to a callback. For example: 1239 | // 1240 | // this.route('search/:query/p:num', 'search', function(query, num) { 1241 | // ... 1242 | // }); 1243 | // 1244 | route: function(route, name, callback) { 1245 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1246 | if (_.isFunction(name)) { 1247 | callback = name; 1248 | name = ''; 1249 | } 1250 | if (!callback) callback = this[name]; 1251 | var router = this; 1252 | Backbone.history.route(route, function(fragment) { 1253 | var args = router._extractParameters(route, fragment); 1254 | router.execute(callback, args); 1255 | router.trigger.apply(router, ['route:' + name].concat(args)); 1256 | router.trigger('route', name, args); 1257 | Backbone.history.trigger('route', router, name, args); 1258 | }); 1259 | return this; 1260 | }, 1261 | 1262 | // Execute a route handler with the provided parameters. This is an 1263 | // excellent place to do pre-route setup or post-route cleanup. 1264 | execute: function(callback, args) { 1265 | if (callback) callback.apply(this, args); 1266 | }, 1267 | 1268 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1269 | navigate: function(fragment, options) { 1270 | Backbone.history.navigate(fragment, options); 1271 | return this; 1272 | }, 1273 | 1274 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1275 | // order of the routes here to support behavior where the most general 1276 | // routes can be defined at the bottom of the route map. 1277 | _bindRoutes: function() { 1278 | if (!this.routes) return; 1279 | this.routes = _.result(this, 'routes'); 1280 | var route, routes = _.keys(this.routes); 1281 | while ((route = routes.pop()) != null) { 1282 | this.route(route, this.routes[route]); 1283 | } 1284 | }, 1285 | 1286 | // Convert a route string into a regular expression, suitable for matching 1287 | // against the current location hash. 1288 | _routeToRegExp: function(route) { 1289 | route = route.replace(escapeRegExp, '\\$&') 1290 | .replace(optionalParam, '(?:$1)?') 1291 | .replace(namedParam, function(match, optional) { 1292 | return optional ? match : '([^/?]+)'; 1293 | }) 1294 | .replace(splatParam, '([^?]*?)'); 1295 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); 1296 | }, 1297 | 1298 | // Given a route, and a URL fragment that it matches, return the array of 1299 | // extracted decoded parameters. Empty or unmatched parameters will be 1300 | // treated as `null` to normalize cross-browser behavior. 1301 | _extractParameters: function(route, fragment) { 1302 | var params = route.exec(fragment).slice(1); 1303 | return _.map(params, function(param, i) { 1304 | // Don't decode the search params. 1305 | if (i === params.length - 1) return param || null; 1306 | return param ? decodeURIComponent(param) : null; 1307 | }); 1308 | } 1309 | 1310 | }); 1311 | 1312 | // Backbone.History 1313 | // ---------------- 1314 | 1315 | // Handles cross-browser history management, based on either 1316 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1317 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1318 | // and URL fragments. If the browser supports neither (old IE, natch), 1319 | // falls back to polling. 1320 | var History = Backbone.History = function() { 1321 | this.handlers = []; 1322 | _.bindAll(this, 'checkUrl'); 1323 | 1324 | // Ensure that `History` can be used outside of the browser. 1325 | if (typeof window !== 'undefined') { 1326 | this.location = window.location; 1327 | this.history = window.history; 1328 | } 1329 | }; 1330 | 1331 | // Cached regex for stripping a leading hash/slash and trailing space. 1332 | var routeStripper = /^[#\/]|\s+$/g; 1333 | 1334 | // Cached regex for stripping leading and trailing slashes. 1335 | var rootStripper = /^\/+|\/+$/g; 1336 | 1337 | // Cached regex for detecting MSIE. 1338 | var isExplorer = /msie [\w.]+/; 1339 | 1340 | // Cached regex for removing a trailing slash. 1341 | var trailingSlash = /\/$/; 1342 | 1343 | // Cached regex for stripping urls of hash. 1344 | var pathStripper = /#.*$/; 1345 | 1346 | // Has the history handling already been started? 1347 | History.started = false; 1348 | 1349 | // Set up all inheritable **Backbone.History** properties and methods. 1350 | _.extend(History.prototype, Events, { 1351 | 1352 | // The default interval to poll for hash changes, if necessary, is 1353 | // twenty times a second. 1354 | interval: 50, 1355 | 1356 | // Are we at the app root? 1357 | atRoot: function() { 1358 | return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; 1359 | }, 1360 | 1361 | // Gets the true hash value. Cannot use location.hash directly due to bug 1362 | // in Firefox where location.hash will always be decoded. 1363 | getHash: function(window) { 1364 | var match = (window || this).location.href.match(/#(.*)$/); 1365 | return match ? match[1] : ''; 1366 | }, 1367 | 1368 | // Get the cross-browser normalized URL fragment, either from the URL, 1369 | // the hash, or the override. 1370 | getFragment: function(fragment, forcePushState) { 1371 | if (fragment == null) { 1372 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1373 | fragment = decodeURI(this.location.pathname + this.location.search); 1374 | var root = this.root.replace(trailingSlash, ''); 1375 | if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); 1376 | } else { 1377 | fragment = this.getHash(); 1378 | } 1379 | } 1380 | return fragment.replace(routeStripper, ''); 1381 | }, 1382 | 1383 | // Start the hash change handling, returning `true` if the current URL matches 1384 | // an existing route, and `false` otherwise. 1385 | start: function(options) { 1386 | if (History.started) throw new Error("Backbone.history has already been started"); 1387 | History.started = true; 1388 | 1389 | // Figure out the initial configuration. Do we need an iframe? 1390 | // Is pushState desired ... is it available? 1391 | this.options = _.extend({root: '/'}, this.options, options); 1392 | this.root = this.options.root; 1393 | this._wantsHashChange = this.options.hashChange !== false; 1394 | this._wantsPushState = !!this.options.pushState; 1395 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1396 | var fragment = this.getFragment(); 1397 | var docMode = document.documentMode; 1398 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1399 | 1400 | // Normalize root to always include a leading and trailing slash. 1401 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1402 | 1403 | if (oldIE && this._wantsHashChange) { 1404 | var frame = Backbone.$('