Repository: jashkenas/backbone Branch: master Commit: e8bc45acb0a8 Files: 58 Total size: 1.5 MB Directory structure: gitextract_aenff3om/ ├── .editorconfig ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bugs.yml │ │ ├── Documentation.yml │ │ ├── Features.yml │ │ └── config.yml │ └── workflows/ │ └── tests.yml ├── .gitignore ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── backbone-min.js ├── backbone.js ├── bower.json ├── debug-info.js ├── docs/ │ ├── backbone.html │ ├── backbone.localStorage.html │ ├── docco.css │ ├── examples/ │ │ ├── backbone.localStorage.html │ │ └── todos/ │ │ └── todos.html │ ├── js/ │ │ └── jquery.lazyload.js │ ├── public/ │ │ └── stylesheets/ │ │ └── normalize.css │ ├── search.js │ └── todos.html ├── examples/ │ ├── backbone.localStorage.js │ └── todos/ │ ├── index.html │ ├── todos.css │ └── todos.js ├── index.html ├── karma.conf-sauce.js ├── karma.conf.js ├── modules/ │ ├── .eslintrc │ ├── debug-info.js │ └── package.json ├── package.json └── test/ ├── .eslintrc ├── collection.js ├── debuginfo.js ├── events.js ├── index.html ├── model.coffee ├── model.js ├── noconflict.js ├── router.js ├── setup/ │ ├── dom-setup.js │ └── environment.js ├── sync.js ├── vendor/ │ ├── jquery.js │ ├── json2.js │ ├── qunit.css │ ├── qunit.js │ ├── require.js │ └── underscore.js └── view.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] indent_style = space indent_size = 2 charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true [**/package.json] indent_size = 2 ================================================ FILE: .eslintrc ================================================ { "env": { "browser": true, "node": true, "amd": true }, "globals": { "attachEvent": false, "detachEvent": false }, "rules": { "array-bracket-spacing": 2, "block-scoped-var": 2, "brace-style": [1, "1tbs", {"allowSingleLine": true}], "camelcase": 2, "comma-dangle": [2, "never"], "comma-spacing": 2, "computed-property-spacing": [2, "never"], "dot-notation": [2, { "allowKeywords": false }], "eol-last": 2, "eqeqeq": [2, "smart"], "indent": [2, 2, { "MemberExpression": 0, "SwitchCase": 1, "VariableDeclarator": 2 }], "key-spacing": 1, "keyword-spacing": [2, { "after": true }], "linebreak-style": 2, "max-depth": [1, 4], "max-params": [1, 5], "new-cap": [2, {"newIsCapExceptions": ["model"]}], "no-alert": 2, "no-caller": 2, "no-catch-shadow": 2, "no-console": 2, "no-debugger": 2, "no-delete-var": 2, "no-div-regex": 1, "no-dupe-args": 2, "no-dupe-keys": 2, "no-duplicate-case": 2, "no-else-return": 1, "no-empty-character-class": 2, "no-eval": 2, "no-ex-assign": 2, "no-extend-native": 2, "no-extra-boolean-cast": 2, "no-extra-parens": 1, "no-extra-semi": 2, "no-fallthrough": 2, "no-floating-decimal": 2, "no-func-assign": 2, "no-implied-eval": 2, "no-inner-declarations": 2, "no-irregular-whitespace": 2, "no-label-var": 2, "no-labels": 2, "no-lone-blocks": 2, "no-lonely-if": 2, "no-multi-str": 2, "no-native-reassign": 2, "no-negated-in-lhs": 1, "no-new-object": 2, "no-new-wrappers": 2, "no-obj-calls": 2, "no-octal": 2, "no-octal-escape": 2, "no-proto": 2, "no-redeclare": 2, "no-shadow": 2, "no-spaced-func": 2, "no-throw-literal": 2, "no-trailing-spaces": 2, "no-undef": 2, "no-undef-init": 2, "no-undefined": 2, "no-unneeded-ternary": 2, "no-unreachable": 2, "no-unused-expressions": [2, {"allowTernary": true, "allowShortCircuit": true}], "no-with": 2, "object-curly-spacing": [2, "never"], "quote-props": [1, "consistent-as-needed", {"keywords": true}], "quotes": [2, "single", "avoid-escape"], "radix": 2, "semi": 2, "space-before-function-paren": [2, {"anonymous": "never", "named": "never"}], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], "use-isnan": 2, "valid-typeof": 2, "wrap-iife": [2, "inside"] } } ================================================ FILE: .github/FUNDING.yml ================================================ tidelift: "npm/backbone" patreon: juliangonggrijp github: [jgonggrijp] ================================================ FILE: .github/ISSUE_TEMPLATE/Bugs.yml ================================================ name: Bug report description: | Report something that is not working correctly. Not intended for security issues! title: Foo.bar should bazoonite, but frobulates instead body: - type: markdown attributes: value: " Thank you for taking the effort to report a bug.\n\n Is your bug a security issue? In that case, **please do not use this form!** Instead, see the [security policy](https://github.com/jashkenas/backbone/security/policy) on how to report the issue.\n\n ## Identification\n\n To start, some quick questions to pinpoint the issue." - type: input id: component attributes: label: Affected component description: > Which part of Backbone is affected? Please be as specific as possible, for example “the silent option of Collection.reset” or “importing Backbone with require.js”. placeholder: the sync event triggered after Model.fetch validations: required: true - type: input id: expected attributes: label: Expected behavior description: | In one sentence, what *should* the affected component do? placeholder: | Forward all options passed to Model.fetch to the event handler validations: required: true - type: input id: actual attributes: label: Actual behavior description: | In one sentence, what does the affected component *actually* do? placeholder: | Forward options to the method called last, e.g. save. validations: required: true - type: markdown attributes: value: " After filling the above three fields, please review the issue title. It should be short, including elements of all three fields and not much else.\n\n For example: **After Model.fetch, sync event may include options of a later sync, save or destroy call**\n\n ## Context" - type: textarea id: docs attributes: label: Relevant documentation description: | Which documentation, if any, did you base your above expectation on? Provide one link per line. placeholder: | - https://backbonejs.org/#Model-fetch - https://backbonejs.org/#Events-catalog - type: textarea id: stack attributes: label: Software stack description: " With which version(s) of Backbone, Underscore/Lodash, jQuery/Zepto, other relevant libraries or tools, your browser, etcetera, did you experience this problem? Please list one per line, including name, version number(s) and variant(s) if applicable.\n\n **Tip:** if you are using the bleeding-edge version of Backbone, much of this information can be obtained by using [debugInfo](https://backbonejs.org/#Utility-Backbone-debugInfo) and copy-pasting its console output below." placeholder: " - Backbone 1.4.1 and latest `master` (commit fcf5df6)\n - Underscore 1.13.6\n - jQuery 3.6.3 (slim build)\n - Marionette 4.1.2\n - Firefox 100\n - Node.js 14.6\n OR (stretch form field to see example content):\n ```json\n Backbone debug info: {\n \ \ \"backbone\": \"1.5.0\",\n \ \ \"distribution\": \"MARK_DEVELOPMENT\",\n \ \ \"_\": \"lodash 4.17.21\",\n \ \ \"$\": \"3.6.0\",\n \ \ \"navigator\": {\n \ \ \ \ \"userAgent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/116.0\",\n \ \ \ \ \"platform\": \"MacIntel\",\n \ \ \ \ \"webdriver\": false\n \ \ }\n }\n ```\n - Backbone `master` checked out on August 10, 2023\n - Marionette 4.1.2" validations: required: true - type: textarea id: discourse attributes: label: Related issues, prior discussion and CCs description: > Please list any issue numbers, pull requests or links to discussions elsewhere on the internet that may be relevant. You can also attract the attention of other GitHub users by listing their `@handles` here. placeholder: " #4229, #3410\n a Stack Overflow or Matrix link\n @jgonggrijp" - type: markdown attributes: value: "## Bug details" - type: input id: error attributes: label: Error description: > If possible, name the error that you observed and that anyone trying to reproduce the bug should look for. placeholder: TypeError (options.success is not an object) - type: textarea id: repro attributes: label: Steps to reproduce description: > List the minimal steps needed to make the bug happen. Include code examples as needed. validations: required: true - type: textarea id: details attributes: label: Additional information description: >- This is a free-form field where you can add any further details that may help to understand the bug. For example, you might provide permalinks to the affected lines of code in your actual project, attach logs or screenshots, point out things you noticed while debugging, and explain why the bug is especially problematic for your use case. - type: markdown attributes: value: "## Closing" - type: textarea id: solution attributes: label: Suggested solution(s) description: > If you have any idea on how the problem could (or should) be solved, please feel welcome to describe it here. Of course, if your idea is very concrete, you may as well submit a pull request! - type: textarea id: remarks attributes: label: Other remarks description: >- If there is anything else you would like to say about the issue, you can do so here. ================================================ FILE: .github/ISSUE_TEMPLATE/Documentation.yml ================================================ name: Documentation issue description: >- Report information that is missing, incomplete, vague, misleading or plain wrong in online documentation such as the website, the README, the wiki, etcetera. body: - type: markdown attributes: value: >- Thank you for taking the time to help us improve the documentation. - type: input id: reference attributes: label: Reference to current documentation description: >- If you can identify an existing piece of documentation that is lacking, please provide a URL (or multiple) below. If your report is about missing information and there is no single obvious place where it should be added, you can leave this empty. placeholder: "https://backbonejs.org/#Model-changedAttributes" - type: textarea id: quote attributes: label: Quote of current documentation description: >- If you provided a URL in the previous field, please quote the problematic documentation below. This ensures that future readers understand what you were responding to, in case the referred page disappears or changes content. placeholder: " > Retrieve a hash of only the model's attributes that have changed since the last [set](https://backbonejs.org/#Model-set), or `false` if there are none. Optionally, an external **attributes** hash can be passed in, returning the attributes in that hash which differ from the model. This can be used to figure out which portions of a view should be updated, or what calls need to be made to sync the changes to the server." - type: textarea id: effect attributes: label: Effect of the problem description: >- What did you or someone else not know, misunderstand or falsely believe due to the current state of the documentation? How has this misguided your or someone else's behavior? placeholder: >- I did not know that I could ..., so I needlessly ... validations: required: true - type: textarea id: cause attributes: label: Cause of the problem description: >- What is it about the current documentation that caused your problem? What is missing, ambiguous or wrong? Pinpoint specific words or phrases if possible. placeholder: >- It says "...", which seems to suggest that ..., while actually, ... validations: required: true - type: textarea id: suggestion attributes: label: Suggestion description: >- If you have any idea on how the documentation could be improved, please share it here. Of course, if your idea is very concrete, you can also submit a pull request! - type: textarea id: remarks attributes: label: Other remarks description: >- If there is anything else you would like to say about the issue, you can do so here. ================================================ FILE: .github/ISSUE_TEMPLATE/Features.yml ================================================ name: Feature request description: >- Tell us about functionality that you miss in Backbone. body: - type: markdown attributes: value: | Thank you for proposing a new feature. Let us begin with the end in mind. - type: textarea id: goal attributes: label: Ultimate goal description: >- This first question is not about Backbone but about the mission that you hope to accomplish with Backbone. What work do you need to get done? placeholder: | GOOD: I work on an application that needs to ... BAD (later question): I think Backbone.Collection should have a method that ... validations: required: true - type: textarea id: shortcoming attributes: label: Shortcomings description: >- Working towards your end goal, what task is currently difficult to achieve with Backbone as it is? Which features in Backbone or other libraries are currently available to you, which do not quite do what you need? placeholder: | In my application, I need to ..., but when I define a Backbone.Collection subclass, there does not seem to be any way to ... - I could use `Collection.slice`, but ... - I could use , but ... validations: required: true - type: textarea id: justification attributes: label: Justification description: >- Why do you believe that the missing functionality belongs in Backbone proper? Why could or should it not be provided by another library or tool? validations: required: true - type: textarea id: proposal attributes: label: Proposal description: >- Go ahead, describe your ideal solution. Show what it should look like and explain how it should behave. validations: required: true - type: textarea id: alternatives attributes: label: Possible alternatives description: >- Can you think of other ways in which your desired functionality could be provided? If they appeal less to you, why is this the case? - type: textarea id: remarks attributes: label: Other remarks description: >- If there is anything else you would like to say about your feature request, you can do so here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Security issue url: https://github.com/jashkenas/backbone/security/policy about: Go here if you would like to report a security issue. - name: Stack Overflow url: https://stackoverflow.com/questions/tagged/backbone.js about: The best place to get help making your code work (be sure to include the `backbone.js` tag). - name: Matrix/Gitter url: https://matrix.to/#/#jashkenas_backbone:gitter.im about: Discussions about, and general help with, Backbone. ================================================ FILE: .github/workflows/tests.yml ================================================ name: Test on: push: branches: - master pull_request: branches: - master paths: - '.github/workflows/**' - 'test/**' - '*.js' - 'package.json' jobs: tests: env: NPM_CONFIG_PROGRESS: "false" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 16 cache: 'npm' - name: Install dependencies run: | npm ci npm install --no-audit karma-cli karma-sauce-launcher - name: Test in Sauce Labs run: BUILD_NUMBER="$GITHUB_RUN_NUMBER" BUILD_ID="$GITHUB_RUN_ID" JOB_NUMBER="$GITHUB_JOB" ./node_modules/.bin/karma start karma.conf-sauce.js env: SAUCE_USERNAME: ${{ secrets.SauceUsername }} SAUCE_ACCESS_KEY: ${{ secrets.SauceAccessKey }} ================================================ FILE: .gitignore ================================================ raw *.sw? .DS_Store node_modules bower_components ================================================ FILE: CNAME ================================================ backbonejs.org ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at jashkenas@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Moderation Edits of another user's comment must be clearly marked with "**edit**", the moderator's username, and a timestamp for each occurrence. The only acceptable reasons for editing another user's comment are: 1. to edit out violations of Our Pledge. These edits must include a rationale. 2. to direct future readers to a relevant point later in the conversation (usually the resolution). These edits must be append-only. Deletion of another user's comment is only acceptable when the comment includes no original value, such as "+1", ":+1:", or "me too". ## Self-Moderation Edits of your own comment after someone has responded must be append-only and clearly marked with "**edit**". Typographical and formatting fixes to your own comment which do not affect its meaning are exempt from this requirement. Deletion of your own comment is only acceptable before any later comments have been posted. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ ## How to Open a Backbone.js Ticket * Do not use tickets to ask for help with (debugging) your application. Ask on the [mailing list](https://groups.google.com/forum/#!forum/backbonejs), in the IRC channel (`#documentcloud` on Freenode), or if you understand your specific problem, on [StackOverflow](http://stackoverflow.com/questions/tagged/backbone.js). * Before you open a ticket or send a pull request, [search](https://github.com/jashkenas/backbone/issues) for previous discussions about the same feature or issue. Add to the earlier ticket if you find one. * Before sending a pull request for a feature or bug fix, be sure to have [tests](http://backbonejs.org/test/) and to document any new functionality in the `index.html`. * Use the same coding style as the rest of the [codebase](https://github.com/jashkenas/backbone/blob/master/backbone.js). * In your pull request, do not regenerate the annotated sources or rebuild the minified `backbone-min.js` file. We'll do that before cutting a new release. * All pull requests should be made to the `master` branch. ================================================ FILE: LICENSE ================================================ Copyright (c) 2010-2024 Jeremy Ashkenas, DocumentCloud Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ____ __ __ /\ _`\ /\ \ /\ \ __ \ \ \ \ \ __ ___\ \ \/'\\ \ \____ ___ ___ __ /\_\ ____ \ \ _ <' /'__`\ /'___\ \ , < \ \ '__`\ / __`\ /' _ `\ /'__`\ \/\ \ /',__\ \ \ \ \ \/\ \ \.\_/\ \__/\ \ \\`\\ \ \ \ \/\ \ \ \/\ \/\ \/\ __/ __ \ \ \/\__, `\ \ \____/\ \__/.\_\ \____\\ \_\ \_\ \_,__/\ \____/\ \_\ \_\ \____\/\_\_\ \ \/\____/ \/___/ \/__/\/_/\/____/ \/_/\/_/\/___/ \/___/ \/_/\/_/\/____/\/_/\ \_\ \/___/ \ \____/ \/___/ (_'_______________________________________________________________________________'_) (_.———————————————————————————————————————————————————————————————————————————————._) Backbone supplies structure to JavaScript-heavy applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface. For Docs, License, Tests, pre-packed downloads, and everything else, really, see: https://backbonejs.org To suggest a feature or report a bug: https://github.com/jashkenas/backbone/issues For questions on working with Backbone or general discussions: [security policy](SECURITY.md), https://stackoverflow.com/questions/tagged/backbone.js, https://matrix.to/#/#jashkenas_backbone:gitter.im or https://groups.google.com/g/backbonejs Backbone is an open-sourced component of DocumentCloud: https://github.com/documentcloud Testing powered by SauceLabs: https://saucelabs.com Many thanks to our contributors: https://github.com/jashkenas/backbone/graphs/contributors Special thanks to Robert Kieffer for the original philosophy behind Backbone. https://github.com/broofa This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions We currently support the following versions of Backbone with security updates: - the latest commit on the `master` branch (published as "edge" on the [project website][website]); - the 1.x release tagged as [latest][npm-latest] on npm; - any release tagged as [preview][npm-preview] on npm, if present. [website]: https://backbonejs.org [npm-latest]: https://www.npmjs.com/package/backbone/v/latest [npm-preview]: https://www.npmjs.com/package/backbone/v/preview ## Reporting a Vulnerability Please report security issues by sending an email to dev@juliangonggrijp.com and jashkenas@gmail.com. Do __not__ submit an issue ticket or pull request or otherwise publicly disclose the issue. After receiving your email, we will respond as soon as possible and indicate what we plan to do. ## Disclosure policy After confirming a vulnerability, we will generally release a security update as soon as possible, including the minimum amount of information required for software maintainers and system administrators to assess the urgency of the update for their particular situation. We postpone the publication of any further details such as code comments, tests, commit history and diffs, in order to enable a substantial share of the users to install the security fix before this time. Upon publication of full details, we will credit the reporter if the reporter wishes to be publicly identified. ================================================ FILE: backbone-min.js ================================================ (function(r){var n=typeof self=="object"&&self.self===self&&self||typeof global=="object"&&global.global===global&&global;if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(t,e,i){n.Backbone=r(n,i,t,e)})}else if(typeof exports!=="undefined"){var t=require("underscore"),e;try{e=require("jquery")}catch(t){}r(n,exports,t,e)}else{n.Backbone=r(n,{},n._,n.jQuery||n.Zepto||n.ender||n.$)}})(function(t,h,x,e){var i=t.Backbone;var a=Array.prototype.slice;h.VERSION="1.6.1";h.$=e;h.noConflict=function(){t.Backbone=i;return this};h.emulateHTTP=false;h.emulateJSON=false;var r=h.Events={};var o=/\s+/;var l;var u=function(t,e,i,r,n){var s=0,a;if(i&&typeof i==="object"){if(r!==void 0&&"context"in n&&n.context===void 0)n.context=r;for(a=x.keys(i);sthis.length)r=this.length;if(r<0)r+=this.length+1;var n=[];var s=[];var a=[];var o=[];var h={};var l=e.add;var u=e.merge;var c=e.remove;var f=false;var d=this.comparator&&r==null&&e.sort!==false;var v=x.isString(this.comparator)?this.comparator:null;var p,g;for(g=0;g0&&!e.silent)delete e.index;return i},_isModel:function(t){return t instanceof g},_addReference:function(t,e){this._byId[t.cid]=t;var i=this.modelId(t.attributes,t.idAttribute);if(i!=null)this._byId[i]=t;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){delete this._byId[t.cid];var i=this.modelId(t.attributes,t.idAttribute);if(i!=null)delete this._byId[i];if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if(e){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(t==="changeId"){var n=this.modelId(e.previousAttributes(),e.idAttribute);var s=this.modelId(e.attributes,e.idAttribute);if(n!=null)delete this._byId[n];if(s!=null)this._byId[s]=e}}this.trigger.apply(this,arguments)},_forwardPristineError:function(t,e,i){if(this.has(t))return;this._onModelEvent("error",t,e,i)}});var y=typeof Symbol==="function"&&Symbol.iterator;if(y){m.prototype[y]=m.prototype.values}var b=function(t,e){this._collection=t;this._kind=e;this._index=0};var S=1;var I=2;var k=3;if(y){b.prototype[y]=function(){return this}}b.prototype.next=function(){if(this._collection){if(this._index7);this._useHashChange=this._wantsHashChange&&this._hasHashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.history&&this.history.pushState);this._usePushState=this._wantsPushState&&this._hasPushState;this.fragment=this.getFragment();this.root=("/"+this.root+"/").replace(L,"/");if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";this.location.replace(e+"#"+this.getPath());return true}else if(this._hasPushState&&this.atRoot()){this.navigate(this.getHash(),{replace:true})}}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe");this.iframe.src="javascript:0";this.iframe.style.display="none";this.iframe.tabIndex=-1;var i=document.body;var r=i.insertBefore(this.iframe,i.firstChild).contentWindow;r.document.open();r.document.close();r.location.hash="#"+this.fragment}var n=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState){n("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){n("hashchange",this.checkUrl,false)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}if(!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};if(this._usePushState){t("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){t("hashchange",this.checkUrl,false)}if(this.iframe){document.body.removeChild(this.iframe);this.iframe=null}if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);B.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getHash(this.iframe.contentWindow)}if(e===this.fragment){if(!this.matchRoot())return this.notfound();return false}if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(e){if(!this.matchRoot())return this.notfound();e=this.fragment=this.getFragment(e);return x.some(this.handlers,function(t){if(t.route.test(e)){t.callback(e);return true}})||this.notfound()},notfound:function(){this.trigger("notfound");return false},navigate:function(t,e){if(!B.started)return false;if(!e||e===true)e={trigger:!!e};t=this.getFragment(t||"");var i=this.root;if(!this._trailingSlash&&(t===""||t.charAt(0)==="?")){i=i.slice(0,-1)||"/"}var r=i+t;t=t.replace(W,"");var n=this.decodeFragment(t);if(this.fragment===n)return;this.fragment=n;if(this._usePushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,r)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var s=this.iframe.contentWindow;if(!e.replace){s.document.open();s.document.close()}this._updateHash(s.location,t,e.replace)}}else{return this.location.assign(r)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});h.history=new B;var D=function(t,e){var i=this;var r;if(t&&x.has(t,"constructor")){r=t.constructor}else{r=function(){return i.apply(this,arguments)}}x.extend(r,i,e);r.prototype=x.create(i.prototype,t);r.prototype.constructor=r;r.__super__=i.prototype;return r};g.extend=m.extend=O.extend=A.extend=B.extend=D;var V=function(){throw new Error('A "url" property or function must be specified')};var G=function(e,i){var r=i.error;i.error=function(t){if(r)r.call(i.context,e,t,i);e.trigger("error",e,t,i)}};h._debug=function(){return{root:t,_:x}};return h}); //# sourceMappingURL=backbone-min.js.map ================================================ FILE: backbone.js ================================================ // Backbone.js 1.6.1 // (c) 2010-2024 Jeremy Ashkenas and DocumentCloud // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org (function(factory) { // Establish the root object, `window` (`self`) in the browser, or `global` on the server. // We use `self` instead of `window` for `WebWorker` support. var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global; // Set up Backbone appropriately for the environment. Start with AMD. if (typeof define === 'function' && define.amd) { define(['underscore', 'jquery', 'exports'], function(_, $, exports) { // Export global even in AMD case in case this script is loaded with // others that may still expect a global Backbone. root.Backbone = factory(root, exports, _, $); }); // Next for Node.js or CommonJS. jQuery may not be needed as a module. } else if (typeof exports !== 'undefined') { var _ = require('underscore'), $; try { $ = require('jquery'); } catch (e) {} factory(root, exports, _, $); // Finally, as a browser global. } else { root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$); } })(function(root, Backbone, _, $) { // Initial Setup // ------------- // Save the previous value of the `Backbone` variable, so that it can be // restored later on, if `noConflict` is used. var previousBackbone = root.Backbone; // Create a local reference to a common array method we'll want to use later. var slice = Array.prototype.slice; // Current version of the library. Keep in sync with `package.json`. Backbone.VERSION = '1.6.1'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. Backbone.$ = $; // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // to its previous owner. Returns a reference to this Backbone object. Backbone.noConflict = function() { root.Backbone = previousBackbone; return this; }; // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and // set a `X-Http-Method-Override` header. Backbone.emulateHTTP = false; // Turn on `emulateJSON` to support legacy servers that can't deal with direct // `application/json` requests ... this will encode the body as // `application/x-www-form-urlencoded` instead and will send the model in a // form param named `model`. Backbone.emulateJSON = false; // Backbone.Events // --------------- // A module that can be mixed in to *any object* in order to provide it with // a custom event channel. You may bind a callback to an event with `on` or // remove with `off`; `trigger`-ing an event fires all callbacks in // succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.on('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // var Events = Backbone.Events = {}; // Regular expression used to split event strings. var eventSplitter = /\s+/; // A private global variable to share between listeners and listenees. var _listening; // Iterates over the standard `event, callback` (as well as the fancy multiple // space-separated events `"change blur", callback` and jQuery-style event // maps `{event: callback}`). var eventsApi = function(iteratee, events, name, callback, opts) { var i = 0, names; if (name && typeof name === 'object') { // Handle event maps. if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; for (names = _.keys(name); i < names.length ; i++) { events = eventsApi(iteratee, events, names[i], name[names[i]], opts); } } else if (name && eventSplitter.test(name)) { // Handle space-separated event names by delegating them individually. for (names = name.split(eventSplitter); i < names.length; i++) { events = iteratee(events, names[i], callback, opts); } } else { // Finally, standard events. events = iteratee(events, name, callback, opts); } return events; }; // Bind an event to a `callback` function. Passing `"all"` will bind // the callback to all events fired. Events.on = function(name, callback, context) { this._events = eventsApi(onApi, this._events || {}, name, callback, { context: context, ctx: this, listening: _listening }); if (_listening) { var listeners = this._listeners || (this._listeners = {}); listeners[_listening.id] = _listening; // Allow the listening to use a counter, instead of tracking // callbacks for library interop _listening.interop = false; } return this; }; // Inversion-of-control versions of `on`. Tell *this* object to listen to // an event in another object... keeping track of what it's listening to // for easier unbinding later. Events.listenTo = function(obj, name, callback) { if (!obj) return this; var id = obj._listenId || (obj._listenId = _.uniqueId('l')); var listeningTo = this._listeningTo || (this._listeningTo = {}); var listening = _listening = listeningTo[id]; // This object is not listening to any other events on `obj` yet. // Setup the necessary references to track the listening callbacks. if (!listening) { this._listenId || (this._listenId = _.uniqueId('l')); listening = _listening = listeningTo[id] = new Listening(this, obj); } // Bind callbacks on obj. var error = tryCatchOn(obj, name, callback, this); _listening = void 0; if (error) throw error; // If the target obj is not Backbone.Events, track events manually. if (listening.interop) listening.on(name, callback); return this; }; // The reducing API that adds a callback to the `events` object. var onApi = function(events, name, callback, options) { if (callback) { var handlers = events[name] || (events[name] = []); var context = options.context, ctx = options.ctx, listening = options.listening; if (listening) listening.count++; handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); } return events; }; // An try-catch guarded #on function, to prevent poisoning the global // `_listening` variable. var tryCatchOn = function(obj, name, callback, context) { try { obj.on(name, callback, context); } catch (e) { return e; } }; // Remove one or many callbacks. If `context` is null, removes all // callbacks with that function. If `callback` is null, removes all // callbacks for the event. If `name` is null, removes all bound // callbacks for all events. Events.off = function(name, callback, context) { if (!this._events) return this; this._events = eventsApi(offApi, this._events, name, callback, { context: context, listeners: this._listeners }); return this; }; // Tell this object to stop listening to either specific events ... or // to every object it's currently listening to. Events.stopListening = function(obj, name, callback) { var listeningTo = this._listeningTo; if (!listeningTo) return this; var ids = obj ? [obj._listenId] : _.keys(listeningTo); for (var i = 0; i < ids.length; i++) { var listening = listeningTo[ids[i]]; // If listening doesn't exist, this object is not currently // listening to obj. Break out early. if (!listening) break; listening.obj.off(name, callback, this); if (listening.interop) listening.off(name, callback); } if (_.isEmpty(listeningTo)) this._listeningTo = void 0; return this; }; // The reducing API that removes a callback from the `events` object. var offApi = function(events, name, callback, options) { if (!events) return; var context = options.context, listeners = options.listeners; var i = 0, names; // Delete all event listeners and "drop" events. if (!name && !context && !callback) { for (names = _.keys(listeners); i < names.length; i++) { listeners[names[i]].cleanup(); } return; } names = name ? [name] : _.keys(events); for (; i < names.length; i++) { name = names[i]; var handlers = events[name]; // Bail out if there are no events stored. if (!handlers) break; // Find any remaining events. var remaining = []; for (var j = 0; j < handlers.length; j++) { var handler = handlers[j]; if ( callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context ) { remaining.push(handler); } else { var listening = handler.listening; if (listening) listening.off(name, callback); } } // Replace events if there are any remaining. Otherwise, clean up. if (remaining.length) { events[name] = remaining; } else { delete events[name]; } } return events; }; // Bind an event to only be triggered a single time. After the first time // the callback is invoked, its listener will be removed. If multiple events // are passed in using the space-separated syntax, the handler will fire // once for each event, not once for a combination of all events. Events.once = function(name, callback, context) { // Map the event into a `{event: once}` object. var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); if (typeof name === 'string' && context == null) callback = void 0; return this.on(events, callback, context); }; // Inversion-of-control versions of `once`. Events.listenToOnce = function(obj, name, callback) { // Map the event into a `{event: once}` object. var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); return this.listenTo(obj, events); }; // Reduces the event callbacks into a map of `{event: onceWrapper}`. // `offer` unbinds the `onceWrapper` after it has been called. var onceMap = function(map, name, callback, offer) { if (callback) { var once = map[name] = _.once(function() { offer(name, once); callback.apply(this, arguments); }); once._callback = callback; } return map; }; // Trigger one or many events, firing all bound callbacks. Callbacks are // passed the same arguments as `trigger` is, apart from the event name // (unless you're listening on `"all"`, which will cause your callback to // receive the true name of the event as the first argument). Events.trigger = function(name) { if (!this._events) return this; var length = Math.max(0, arguments.length - 1); var args = Array(length); for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; eventsApi(triggerApi, this._events, name, void 0, args); return this; }; // Handles triggering the appropriate event callbacks. var triggerApi = function(objEvents, name, callback, args) { if (objEvents) { var events = objEvents[name]; var allEvents = objEvents.all; if (events && allEvents) allEvents = allEvents.slice(); if (events) triggerEvents(events, args); if (allEvents) triggerEvents(allEvents, [name].concat(args)); } return objEvents; }; // A difficult-to-believe, but optimized internal dispatch function for // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). var triggerEvents = function(events, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; } }; // A listening class that tracks and cleans up memory bindings // when all callbacks have been offed. var Listening = function(listener, obj) { this.id = listener._listenId; this.listener = listener; this.obj = obj; this.interop = true; this.count = 0; this._events = void 0; }; Listening.prototype.on = Events.on; // Offs a callback (or several). // Uses an optimized counter if the listenee uses Backbone.Events. // Otherwise, falls back to manual tracking to support events // library interop. Listening.prototype.off = function(name, callback) { var cleanup; if (this.interop) { this._events = eventsApi(offApi, this._events, name, callback, { context: void 0, listeners: void 0 }); cleanup = !this._events; } else { this.count--; cleanup = this.count === 0; } if (cleanup) this.cleanup(); }; // Cleans up memory bindings between the listener and the listenee. Listening.prototype.cleanup = function() { delete this.listener._listeningTo[this.obj._listenId]; if (!this.interop) delete this.obj._listeners[this.id]; }; // Aliases for backwards compatibility. Events.bind = Events.on; Events.unbind = Events.off; // Allow the `Backbone` object to serve as a global event bus, for folks who // want global "pubsub" in a convenient place. _.extend(Backbone, Events); // Backbone.Model // -------------- // Backbone **Models** are the basic data object in the framework -- // frequently representing a row in a table in a database on your server. // A discrete chunk of data and a bunch of useful, related methods for // performing computations and transformations on that data. // Create a new model with the specified attributes. A client id (`cid`) // is automatically generated and assigned for you. var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); this.preinitialize.apply(this, arguments); this.cid = _.uniqueId(this.cidPrefix); this.attributes = {}; if (options.collection) this.collection = options.collection; if (options.parse) attrs = this.parse(attrs, options) || {}; var defaults = _.result(this, 'defaults'); // Just _.defaults would work fine, but the additional _.extends // is in there for historical reasons. See #3843. attrs = _.defaults(_.extend({}, defaults, attrs), defaults); this.set(attrs, options); this.changed = {}; this.initialize.apply(this, arguments); }; // Attach all inheritable methods to the Model prototype. _.extend(Model.prototype, Events, { // A hash of attributes whose current and previous value differ. changed: null, // The value returned during the last failed validation. validationError: null, // The default name for the JSON `id` attribute is `"id"`. MongoDB and // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', // The prefix is used to create the client id which is used to identify models locally. // You may want to override this if you're experiencing name clashes with model ids. cidPrefix: 'c', // preinitialize is an empty function by default. You can override it with a function // or object. preinitialize will run before any instantiation logic is run in the Model. preinitialize: function(){}, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // Return a copy of the model's `attributes` object. toJSON: function(options) { return _.clone(this.attributes); }, // Proxy `Backbone.sync` by default -- but override this if you need // custom syncing semantics for *this* particular model. sync: function() { return Backbone.sync.apply(this, arguments); }, // Get the value of an attribute. get: function(attr) { return this.attributes[attr]; }, // Get the HTML-escaped value of an attribute. escape: function(attr) { return _.escape(this.get(attr)); }, // Returns `true` if the attribute contains a value that is not null // or undefined. has: function(attr) { return this.get(attr) != null; }, // Special-cased proxy to underscore's `_.matches` method. matches: function(attrs) { return !!_.iteratee(attrs, this)(this.attributes); }, // Set a hash of model attributes on the object, firing `"change"`. This is // the core primitive operation of a model, updating the data and notifying // anyone who needs to know about the change in state. The heart of the beast. set: function(key, val, options) { if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. var attrs; if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options || (options = {}); // Run validation. if (!this._validate(attrs, options)) return false; // Extract attributes and options. var unset = options.unset; var silent = options.silent; var changes = []; var changing = this._changing; this._changing = true; if (!changing) { this._previousAttributes = _.clone(this.attributes); this.changed = {}; } var current = this.attributes; var changed = this.changed; var prev = this._previousAttributes; // For each `set` attribute, update or delete the current value. for (var attr in attrs) { val = attrs[attr]; if (!_.isEqual(current[attr], val)) changes.push(attr); if (!_.isEqual(prev[attr], val)) { changed[attr] = val; } else { delete changed[attr]; } unset ? delete current[attr] : current[attr] = val; } // Update the `id`. if (this.idAttribute in attrs) { var prevId = this.id; this.id = this.get(this.idAttribute); if (this.id !== prevId) { this.trigger('changeId', this, prevId, options); } } // Trigger all relevant attribute changes. if (!silent) { if (changes.length) this._pending = options; for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. if (changing) return this; if (!silent) { while (this._pending) { options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; this._changing = false; return this; }, // Remove an attribute from the model, firing `"change"`. `unset` is a noop // if the attribute doesn't exist. unset: function(attr, options) { return this.set(attr, void 0, _.extend({}, options, {unset: true})); }, // Clear all attributes on the model, firing `"change"`. clear: function(options) { var attrs = {}; for (var key in this.attributes) attrs[key] = void 0; return this.set(attrs, _.extend({}, options, {unset: true})); }, // Determine if the model has changed since the last `"change"` event. // If you specify an attribute name, determine if that attribute has changed. hasChanged: function(attr) { if (attr == null) return !_.isEmpty(this.changed); return _.has(this.changed, attr); }, // Return an object containing all the attributes that have changed, or // false if there are no changed attributes. Useful for determining what // parts of a view need to be updated and/or what attributes need to be // persisted to the server. Unset attributes will be set to undefined. // You can also pass an attributes object to diff against the model, // determining if there *would be* a change. changedAttributes: function(diff) { if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; var old = this._changing ? this._previousAttributes : this.attributes; var changed = {}; var hasChanged; for (var attr in diff) { var val = diff[attr]; if (_.isEqual(old[attr], val)) continue; changed[attr] = val; hasChanged = true; } return hasChanged ? changed : false; }, // Get the previous value of an attribute, recorded at the time the last // `"change"` event was fired. previous: function(attr) { if (attr == null || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous // `"change"` event. previousAttributes: function() { return _.clone(this._previousAttributes); }, // Fetch the model from the server, merging the response with the model's // local attributes. Any changed attributes will trigger a "change" event. fetch: function(options) { options = _.extend({parse: true}, options); var model = this; var success = options.success; options.success = function(resp) { var serverAttrs = options.parse ? model.parse(resp, options) : resp; if (!model.set(serverAttrs, options)) return false; if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); return this.sync('read', this, options); }, // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. save: function(key, val, options) { // Handle both `"key", value` and `{key: value}` -style arguments. var attrs; if (key == null || typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options = _.extend({validate: true, parse: true}, options); var wait = options.wait; // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. if (attrs && !wait) { if (!this.set(attrs, options)) return false; } else if (!this._validate(attrs, options)) { return false; } // After a successful server-side save, the client is (optionally) // updated with the server-side state. var model = this; var success = options.success; var attributes = this.attributes; options.success = function(resp) { // Ensure attributes are restored during synchronous saves. model.attributes = attributes; var serverAttrs = options.parse ? model.parse(resp, options) : resp; if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); if (serverAttrs && !model.set(serverAttrs, options)) return false; if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); // Set temporary attributes if `{wait: true}` to properly find new ids. if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update'; if (method === 'patch' && !options.attrs) options.attrs = attrs; var xhr = this.sync(method, this, options); // Restore attributes. this.attributes = attributes; return xhr; }, // Destroy this model on the server if it was already persisted. // Optimistically removes the model from its collection, if it has one. // If `wait: true` is passed, waits for the server to respond before removal. destroy: function(options) { options = options ? _.clone(options) : {}; var model = this; var success = options.success; var wait = options.wait; var destroy = function() { model.stopListening(); model.trigger('destroy', model, model.collection, options); }; options.success = function(resp) { if (wait) destroy(); if (success) success.call(options.context, model, resp, options); if (!model.isNew()) model.trigger('sync', model, resp, options); }; var xhr = false; if (this.isNew()) { _.defer(options.success); } else { wrapError(this, options); xhr = this.sync('delete', this, options); } if (!wait) destroy(); return xhr; }, // Default URL for the model's representation on the server -- if you're // using Backbone's restful methods, override this to change the endpoint // that will be called. url: function() { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; var id = this.get(this.idAttribute); return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); }, // **parse** converts a response into the hash of attributes to be `set` on // the model. The default implementation is just to pass the response along. parse: function(resp, options) { return resp; }, // Create a new model with identical attributes to this one. clone: function() { return new this.constructor(this.attributes); }, // A model is new if it has never been saved to the server, and lacks an id. isNew: function() { return !this.has(this.idAttribute); }, // Check if the model is currently in a valid state. isValid: function(options) { return this._validate({}, _.extend({}, options, {validate: true})); }, // Run validation against the next complete set of model attributes, // returning `true` if all is well. Otherwise, fire an `"invalid"` event. _validate: function(attrs, options) { if (!options.validate || !this.validate) return true; attrs = _.extend({}, this.attributes, attrs); var error = this.validationError = this.validate(attrs, options) || null; if (!error) return true; this.trigger('invalid', this, error, _.extend(options, {validationError: error})); return false; } }); // Backbone.Collection // ------------------- // If models tend to represent a single row of data, a Backbone Collection is // more analogous to a table full of data ... or a small slice or page of that // table, or a collection of rows that belong together for a particular reason // -- all of the messages in this particular folder, all of the documents // belonging to this particular author, and so on. Collections maintain // indexes of their models, both in order, and for lookup by `id`. // Create a new **Collection**, perhaps to contain a specific type of `model`. // If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. var Collection = Backbone.Collection = function(models, options) { options || (options = {}); this.preinitialize.apply(this, arguments); if (options.model) this.model = options.model; if (options.comparator !== void 0) this.comparator = options.comparator; this._reset(); this.initialize.apply(this, arguments); if (models) this.reset(models, _.extend({silent: true}, options)); }; // Default options for `Collection#set`. var setOptions = {add: true, remove: true, merge: true}; var addOptions = {add: true, remove: false}; // Splices `insert` into `array` at index `at`. var splice = function(array, insert, at) { at = Math.min(Math.max(at, 0), array.length); var tail = Array(array.length - at); var length = insert.length; var i; for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; for (i = 0; i < length; i++) array[i + at] = insert[i]; for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; }; // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { // The default model for a collection is just a **Backbone.Model**. // This should be overridden in most cases. model: Model, // preinitialize is an empty function by default. You can override it with a function // or object. preinitialize will run before any instantiation logic is run in the Collection. preinitialize: function(){}, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // The JSON representation of a Collection is an array of the // models' attributes. toJSON: function(options) { return this.map(function(model) { return model.toJSON(options); }); }, // Proxy `Backbone.sync` by default. sync: function() { return Backbone.sync.apply(this, arguments); }, // Add a model, or list of models to the set. `models` may be Backbone // Models or raw JavaScript objects to be converted to Models, or any // combination of the two. add: function(models, options) { return this.set(models, _.extend({merge: false}, options, addOptions)); }, // Remove a model, or a list of models from the set. remove: function(models, options) { options = _.extend({}, options); var singular = !_.isArray(models); models = singular ? [models] : models.slice(); var removed = this._removeModels(models, options); if (!options.silent && removed.length) { options.changes = {added: [], merged: [], removed: removed}; this.trigger('update', this, options); } return singular ? removed[0] : removed; }, // Update a collection by `set`-ing a new list of models, adding new ones, // removing models that are no longer present, and merging models that // already exist in the collection, as necessary. Similar to **Model#set**, // the core operation for updating the data contained by the collection. set: function(models, options) { if (models == null) return; options = _.extend({}, setOptions, options); if (options.parse && !this._isModel(models)) { models = this.parse(models, options) || []; } var singular = !_.isArray(models); models = singular ? [models] : models.slice(); var at = options.at; if (at != null) at = +at; if (at > this.length) at = this.length; if (at < 0) at += this.length + 1; var set = []; var toAdd = []; var toMerge = []; var toRemove = []; var modelMap = {}; var add = options.add; var merge = options.merge; var remove = options.remove; var sort = false; var sortable = this.comparator && at == null && options.sort !== false; var sortAttr = _.isString(this.comparator) ? this.comparator : null; // Turn bare objects into model references, and prevent invalid models // from being added. var model, i; for (i = 0; i < models.length; i++) { model = models[i]; // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. var existing = this.get(model); if (existing) { if (merge && model !== existing) { var attrs = this._isModel(model) ? model.attributes : model; if (options.parse) attrs = existing.parse(attrs, options); existing.set(attrs, options); toMerge.push(existing); if (sortable && !sort) sort = existing.hasChanged(sortAttr); } if (!modelMap[existing.cid]) { modelMap[existing.cid] = true; set.push(existing); } models[i] = existing; // If this is a new, valid model, push it to the `toAdd` list. } else if (add) { model = models[i] = this._prepareModel(model, options); if (model) { toAdd.push(model); this._addReference(model, options); modelMap[model.cid] = true; set.push(model); } } } // Remove stale models. if (remove) { for (i = 0; i < this.length; i++) { model = this.models[i]; if (!modelMap[model.cid]) toRemove.push(model); } if (toRemove.length) this._removeModels(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. var orderChanged = false; var replace = !sortable && add && remove; if (set.length && replace) { orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { return m !== set[index]; }); this.models.length = 0; splice(this.models, set, 0); this.length = this.models.length; } else if (toAdd.length) { if (sortable) sort = true; splice(this.models, toAdd, at == null ? this.length : at); this.length = this.models.length; } // Silently sort the collection if appropriate. if (sort) this.sort({silent: true}); // Unless silenced, it's time to fire all appropriate add/sort/update events. if (!options.silent) { for (i = 0; i < toAdd.length; i++) { if (at != null) options.index = at + i; model = toAdd[i]; model.trigger('add', model, this, options); } if (sort || orderChanged) this.trigger('sort', this, options); if (toAdd.length || toRemove.length || toMerge.length) { options.changes = { added: toAdd, removed: toRemove, merged: toMerge }; this.trigger('update', this, options); } } // Return the added (or merged) model (or models). return singular ? models[0] : models; }, // When you have more items than you want to add or remove individually, // you can reset the entire set with a new list of models, without firing // any granular `add` or `remove` events. Fires `reset` when finished. // Useful for bulk operations and optimizations. reset: function(models, options) { options = options ? _.clone(options) : {}; for (var i = 0; i < this.models.length; i++) { this._removeReference(this.models[i], options); } options.previousModels = this.models; this._reset(); models = this.add(models, _.extend({silent: true}, options)); if (!options.silent) this.trigger('reset', this, options); return models; }, // Add a model to the end of the collection. push: function(model, options) { return this.add(model, _.extend({at: this.length}, options)); }, // Remove a model from the end of the collection. pop: function(options) { var model = this.at(this.length - 1); return this.remove(model, options); }, // Add a model to the beginning of the collection. unshift: function(model, options) { return this.add(model, _.extend({at: 0}, options)); }, // Remove a model from the beginning of the collection. shift: function(options) { var model = this.at(0); return this.remove(model, options); }, // Slice out a sub-array of models from the collection. slice: function() { return slice.apply(this.models, arguments); }, // Get a model from the set by id, cid, model object with id or cid // properties, or an attributes object that is transformed through modelId. get: function(obj) { if (obj == null) return void 0; return this._byId[obj] || this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj, obj.idAttribute)] || obj.cid && this._byId[obj.cid]; }, // Returns `true` if the model is in the collection. has: function(obj) { return this.get(obj) != null; }, // Get the model at the given index. at: function(index) { if (index < 0) index += this.length; return this.models[index]; }, // Return models with matching attributes. Useful for simple cases of // `filter`. where: function(attrs, first) { return this[first ? 'find' : 'filter'](attrs); }, // Return the first model with matching attributes. Useful for simple cases // of `find`. findWhere: function(attrs) { return this.where(attrs, true); }, // Force the collection to re-sort itself. You don't need to call this under // normal circumstances, as the set will maintain sort order as each item // is added. sort: function(options) { var comparator = this.comparator; if (!comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); var length = comparator.length; if (_.isFunction(comparator)) comparator = comparator.bind(this); // Run sort based on type of `comparator`. if (length === 1 || _.isString(comparator)) { this.models = this.sortBy(comparator); } else { this.models.sort(comparator); } if (!options.silent) this.trigger('sort', this, options); return this; }, // Pluck an attribute from each model in the collection. pluck: function(attr) { return this.map(attr + ''); }, // Fetch the default set of models for this collection, resetting the // collection when they arrive. If `reset: true` is passed, the response // data will be passed through the `reset` method instead of `set`. fetch: function(options) { options = _.extend({parse: true}, options); var success = options.success; var collection = this; options.success = function(resp) { var method = options.reset ? 'reset' : 'set'; collection[method](resp, options); if (success) success.call(options.context, collection, resp, options); collection.trigger('sync', collection, resp, options); }; wrapError(this, options); return this.sync('read', this, options); }, // Create a new instance of a model in this collection. Add the model to the // collection immediately, unless `wait: true` is passed, in which case we // wait for the server to agree. create: function(model, options) { options = options ? _.clone(options) : {}; var wait = options.wait; model = this._prepareModel(model, options); if (!model) return false; if (!wait) this.add(model, options); var collection = this; var success = options.success; options.success = function(m, resp, callbackOpts) { if (wait) { m.off('error', collection._forwardPristineError, collection); collection.add(m, callbackOpts); } if (success) success.call(callbackOpts.context, m, resp, callbackOpts); }; // In case of wait:true, our collection is not listening to any // of the model's events yet, so it will not forward the error // event. In this special case, we need to listen for it // separately and handle the event just once. // (The reason we don't need to do this for the sync event is // in the success handler above: we add the model first, which // causes the collection to listen, and then invoke the callback // that triggers the event.) if (wait) { model.once('error', this._forwardPristineError, this); } model.save(null, options); return model; }, // **parse** converts a response into a list of models to be added to the // collection. The default implementation is just to pass it through. parse: function(resp, options) { return resp; }, // Create a new collection with an identical list of models as this one. clone: function() { return new this.constructor(this.models, { model: this.model, comparator: this.comparator }); }, // Define how to uniquely identify models in the collection. modelId: function(attrs, idAttribute) { return attrs[idAttribute || this.model.prototype.idAttribute || 'id']; }, // Get an iterator of all models in this collection. values: function() { return new CollectionIterator(this, ITERATOR_VALUES); }, // Get an iterator of all model IDs in this collection. keys: function() { return new CollectionIterator(this, ITERATOR_KEYS); }, // Get an iterator of all [ID, model] tuples in this collection. entries: function() { return new CollectionIterator(this, ITERATOR_KEYSVALUES); }, // Private method to reset all internal state. Called when the collection // is first initialized or reset. _reset: function() { this.length = 0; this.models = []; this._byId = {}; }, // Prepare a hash of attributes (or other model) to be added to this // collection. _prepareModel: function(attrs, options) { if (this._isModel(attrs)) { if (!attrs.collection) attrs.collection = this; return attrs; } options = options ? _.clone(options) : {}; options.collection = this; var model; if (this.model.prototype) { model = new this.model(attrs, options); } else { // ES class methods didn't have prototype model = this.model(attrs, options); } if (!model.validationError) return model; this.trigger('invalid', this, model.validationError, options); return false; }, // Internal method called by both remove and set. _removeModels: function(models, options) { var removed = []; for (var i = 0; i < models.length; i++) { var model = this.get(models[i]); if (!model) continue; var index = this.indexOf(model); this.models.splice(index, 1); this.length--; // Remove references before triggering 'remove' event to prevent an // infinite loop. #3693 delete this._byId[model.cid]; var id = this.modelId(model.attributes, model.idAttribute); if (id != null) delete this._byId[id]; if (!options.silent) { options.index = index; model.trigger('remove', model, this, options); } removed.push(model); this._removeReference(model, options); } if (models.length > 0 && !options.silent) delete options.index; return removed; }, // Method for checking whether an object should be considered a model for // the purposes of adding to the collection. _isModel: function(model) { return model instanceof Model; }, // Internal method to create a model's ties to a collection. _addReference: function(model, options) { this._byId[model.cid] = model; var id = this.modelId(model.attributes, model.idAttribute); if (id != null) this._byId[id] = model; model.on('all', this._onModelEvent, this); }, // Internal method to sever a model's ties to a collection. _removeReference: function(model, options) { delete this._byId[model.cid]; var id = this.modelId(model.attributes, model.idAttribute); if (id != null) delete this._byId[id]; if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); }, // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. All other // events simply proxy through. "add" and "remove" events that originate // in other collections are ignored. _onModelEvent: function(event, model, collection, options) { if (model) { if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'destroy') this.remove(model, options); if (event === 'changeId') { var prevId = this.modelId(model.previousAttributes(), model.idAttribute); var id = this.modelId(model.attributes, model.idAttribute); if (prevId != null) delete this._byId[prevId]; if (id != null) this._byId[id] = model; } } this.trigger.apply(this, arguments); }, // Internal callback method used in `create`. It serves as a // stand-in for the `_onModelEvent` method, which is not yet bound // during the `wait` period of the `create` call. We still want to // forward any `'error'` event at the end of the `wait` period, // hence a customized callback. _forwardPristineError: function(model, collection, options) { // Prevent double forward if the model was already in the // collection before the call to `create`. if (this.has(model)) return; this._onModelEvent('error', model, collection, options); } }); // Defining an @@iterator method implements JavaScript's Iterable protocol. // In modern ES2015 browsers, this value is found at Symbol.iterator. /* global Symbol */ var $$iterator = typeof Symbol === 'function' && Symbol.iterator; if ($$iterator) { Collection.prototype[$$iterator] = Collection.prototype.values; } // CollectionIterator // ------------------ // A CollectionIterator implements JavaScript's Iterator protocol, allowing the // use of `for of` loops in modern browsers and interoperation between // Backbone.Collection and other JavaScript functions and third-party libraries // which can operate on Iterables. var CollectionIterator = function(collection, kind) { this._collection = collection; this._kind = kind; this._index = 0; }; // This "enum" defines the three possible kinds of values which can be emitted // by a CollectionIterator that correspond to the values(), keys() and entries() // methods on Collection, respectively. var ITERATOR_VALUES = 1; var ITERATOR_KEYS = 2; var ITERATOR_KEYSVALUES = 3; // All Iterators should themselves be Iterable. if ($$iterator) { CollectionIterator.prototype[$$iterator] = function() { return this; }; } CollectionIterator.prototype.next = function() { if (this._collection) { // Only continue iterating if the iterated collection is long enough. if (this._index < this._collection.length) { var model = this._collection.at(this._index); this._index++; // Construct a value depending on what kind of values should be iterated. var value; if (this._kind === ITERATOR_VALUES) { value = model; } else { var id = this._collection.modelId(model.attributes, model.idAttribute); if (this._kind === ITERATOR_KEYS) { value = id; } else { // ITERATOR_KEYSVALUES value = [id, model]; } } return {value: value, done: false}; } // Once exhausted, remove the reference to the collection so future // calls to the next method always return done. this._collection = void 0; } return {value: void 0, done: true}; }; // Backbone.View // ------------- // Backbone Views are almost more convention than they are actual code. A View // is simply a JavaScript object that represents a logical chunk of UI in the // DOM. This might be a single item, an entire list, a sidebar or panel, or // even the surrounding frame which wraps your whole app. Defining a chunk of // UI as a **View** allows you to define your DOM events declaratively, without // having to worry about render order ... and makes it easy for the view to // react to specific changes in the state of your models. // Creating a Backbone.View creates its initial element outside of the DOM, // if an existing element is not provided... var View = Backbone.View = function(options) { this.cid = _.uniqueId('view'); this.preinitialize.apply(this, arguments); _.extend(this, _.pick(options, viewOptions)); this._ensureElement(); this.initialize.apply(this, arguments); }; // Cached regex to split keys for `delegate`. var delegateEventSplitter = /^(\S+)\s*(.*)$/; // List of view options to be set as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; // Set up all inheritable **Backbone.View** properties and methods. _.extend(View.prototype, Events, { // The default `tagName` of a View's element is `"div"`. tagName: 'div', // jQuery delegate for element lookup, scoped to DOM elements within the // current view. This should be preferred to global lookups where possible. $: function(selector) { return this.$el.find(selector); }, // preinitialize is an empty function by default. You can override it with a function // or object. preinitialize will run before any instantiation logic is run in the View preinitialize: function(){}, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // **render** is the core function that your view should override, in order // to populate its element (`this.el`), with the appropriate HTML. The // convention is for **render** to always return `this`. render: function() { return this; }, // Remove this view by taking the element out of the DOM, and removing any // applicable Backbone.Events listeners. remove: function() { this._removeElement(); this.stopListening(); return this; }, // Remove this view's element from the document and all event listeners // attached to it. Exposed for subclasses using an alternative DOM // manipulation API. _removeElement: function() { this.$el.remove(); }, // Change the view's element (`this.el` property) and re-delegate the // view's events on the new element. setElement: function(element) { this.undelegateEvents(); this._setElement(element); this.delegateEvents(); return this; }, // Creates the `this.el` and `this.$el` references for this view using the // given `el`. `el` can be a CSS selector or an HTML string, a jQuery // context or an element. Subclasses can override this to utilize an // alternative DOM manipulation API and are only required to set the // `this.el` property. _setElement: function(el) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); this.el = this.$el[0]; }, // Set callbacks, where `this.events` is a hash of // // *{"event selector": "callback"}* // // { // 'mousedown .title': 'edit', // 'click .button': 'save', // 'click .open': function(e) { ... } // } // // pairs. Callbacks will be bound to the view, with `this` set properly. // Uses event delegation for efficiency. // Omitting the selector binds the event to `this.el`. delegateEvents: function(events) { events || (events = _.result(this, 'events')); if (!events) return this; this.undelegateEvents(); for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[method]; if (!method) continue; var match = key.match(delegateEventSplitter); this.delegate(match[1], match[2], method.bind(this)); } return this; }, // Add a single event listener to the view's element (or a child element // using `selector`). This only works for delegate-able events: not `focus`, // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. delegate: function(eventName, selector, listener) { this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. // You usually don't need to use this, but may wish to if you have multiple // Backbone views attached to the same DOM element. undelegateEvents: function() { if (this.$el) this.$el.off('.delegateEvents' + this.cid); return this; }, // A finer-grained `undelegateEvents` for removing a single delegated event. // `selector` and `listener` are both optional. undelegate: function(eventName, selector, listener) { this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); return this; }, // Produces a DOM element to be assigned to your view. Exposed for // subclasses using an alternative DOM manipulation API. _createElement: function(tagName) { return document.createElement(tagName); }, // Ensure that the View has a DOM element to render into. // If `this.el` is a string, pass it through `$()`, take the first // matching element, and re-assign it to `el`. Otherwise, create // an element from the `id`, `className` and `tagName` properties. _ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); this.setElement(this._createElement(_.result(this, 'tagName'))); this._setAttributes(attrs); } else { this.setElement(_.result(this, 'el')); } }, // Set attributes from a hash on this view's element. Exposed for // subclasses using an alternative DOM manipulation API. _setAttributes: function(attributes) { this.$el.attr(attributes); } }); // Proxy Backbone class methods to Underscore functions, wrapping the model's // `attributes` object or collection's `models` array behind the scenes. // // collection.filter(function(model) { return model.get('age') > 10 }); // collection.each(this.addView); // // `Function#apply` can be slow so we use the method's arg count, if we know it. var addMethod = function(base, length, method, attribute) { switch (length) { case 1: return function() { return base[method](this[attribute]); }; case 2: return function(value) { return base[method](this[attribute], value); }; case 3: return function(iteratee, context) { return base[method](this[attribute], cb(iteratee, this), context); }; case 4: return function(iteratee, defaultVal, context) { return base[method](this[attribute], cb(iteratee, this), defaultVal, context); }; default: return function() { var args = slice.call(arguments); args.unshift(this[attribute]); return base[method].apply(base, args); }; } }; var addUnderscoreMethods = function(Class, base, methods, attribute) { _.each(methods, function(length, method) { if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute); }); }; // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. var cb = function(iteratee, instance) { if (_.isFunction(iteratee)) return iteratee; if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; return iteratee; }; var modelMatcher = function(attrs) { var matcher = _.matches(attrs); return function(model) { return matcher(model.attributes); }; }; // Underscore methods that we want to implement on the Collection. // 90% of the core usefulness of Backbone Collections is actually implemented // right here: var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; // Underscore methods that we want to implement on the Model, mapped to the // number of arguments they take. var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, omit: 0, chain: 1, isEmpty: 1}; // Mix in each Underscore method as a proxy to `Collection#models`. _.each([ [Collection, collectionMethods, 'models'], [Model, modelMethods, 'attributes'] ], function(config) { var Base = config[0], methods = config[1], attribute = config[2]; Base.mixin = function(obj) { var mappings = _.reduce(_.functions(obj), function(memo, name) { memo[name] = 0; return memo; }, {}); addUnderscoreMethods(Base, obj, mappings, attribute); }; addUnderscoreMethods(Base, _, methods, attribute); }); // Backbone.sync // ------------- // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with a `_method` parameter containing the true HTTP method, // as well as all requests with the body as `application/x-www-form-urlencoded` // instead of `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make // it difficult to read the body of `PUT` requests. Backbone.sync = function(method, model, options) { var type = methodMap[method]; // Default options, unless specified. _.defaults(options || (options = {}), { emulateHTTP: Backbone.emulateHTTP, emulateJSON: Backbone.emulateJSON }); // Default JSON-request options. var params = {type: type, dataType: 'json'}; // Ensure that we have a URL. if (!options.url) { params.url = _.result(model, 'url') || urlError(); } // Ensure that we have the appropriate request data. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { params.contentType = 'application/json'; params.data = JSON.stringify(options.attrs || model.toJSON(options)); } // For older servers, emulate JSON by encoding the request into an HTML-form. if (options.emulateJSON) { params.contentType = 'application/x-www-form-urlencoded'; params.data = params.data ? {model: params.data} : {}; } // For older servers, emulate HTTP by mimicking the HTTP method with `_method` // And an `X-HTTP-Method-Override` header. if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { params.type = 'POST'; if (options.emulateJSON) params.data._method = type; var beforeSend = options.beforeSend; options.beforeSend = function(xhr) { xhr.setRequestHeader('X-HTTP-Method-Override', type); if (beforeSend) return beforeSend.apply(this, arguments); }; } // Don't process data on a non-GET request. if (params.type !== 'GET' && !options.emulateJSON) { params.processData = false; } // Pass along `textStatus` and `errorThrown` from jQuery. var error = options.error; options.error = function(xhr, textStatus, errorThrown) { options.textStatus = textStatus; options.errorThrown = errorThrown; if (error) error.call(options.context, xhr, textStatus, errorThrown); }; // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); model.trigger('request', model, xhr, options); return xhr; }; // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { 'create': 'POST', 'update': 'PUT', 'patch': 'PATCH', 'delete': 'DELETE', 'read': 'GET' }; // Set the default implementation of `Backbone.ajax` to proxy through to `$`. // Override this if you'd like to use a different library. Backbone.ajax = function() { return Backbone.$.ajax.apply(Backbone.$, arguments); }; // Backbone.Router // --------------- // Routers map faux-URLs to actions, and fire events when routes are // matched. Creating a new one sets its `routes` hash, if not set statically. var Router = Backbone.Router = function(options) { options || (options = {}); this.preinitialize.apply(this, arguments); if (options.routes) this.routes = options.routes; this._bindRoutes(); this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. var optionalParam = /\((.*?)\)/g; var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Router.prototype, Events, { // preinitialize is an empty function by default. You can override it with a function // or object. preinitialize will run before any instantiation logic is run in the Router. preinitialize: function(){}, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // Manually bind a single named route to a callback. For example: // // this.route('search/:query/p:num', 'search', function(query, num) { // ... // }); // route: function(route, name, callback) { if (!_.isRegExp(route)) route = this._routeToRegExp(route); if (_.isFunction(name)) { callback = name; name = ''; } if (!callback) callback = this[name]; var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); if (router.execute(callback, args, name) !== false) { router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger('route', name, args); Backbone.history.trigger('route', router, name, args); } }); return this; }, // Execute a route handler with the provided parameters. This is an // excellent place to do pre-route setup or post-route cleanup. execute: function(callback, args, name) { if (callback) callback.apply(this, args); }, // Simple proxy to `Backbone.history` to save a fragment into the history. navigate: function(fragment, options) { Backbone.history.navigate(fragment, options); return this; }, // Bind all defined routes to `Backbone.history`. We have to reverse the // order of the routes here to support behavior where the most general // routes can be defined at the bottom of the route map. _bindRoutes: function() { if (!this.routes) return; this.routes = _.result(this, 'routes'); var route, routes = _.keys(this.routes); while ((route = routes.pop()) != null) { this.route(route, this.routes[route]); } }, // Convert a route string into a regular expression, suitable for matching // against the current location hash. _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { return optional ? match : '([^/?]+)'; }) .replace(splatParam, '([^?]*?)'); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }, // Given a route, and a URL fragment that it matches, return the array of // extracted decoded parameters. Empty or unmatched parameters will be // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); return _.map(params, function(param, i) { // Don't decode the search params. if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); } }); // Backbone.History // ---------------- // Handles cross-browser history management, based on either // [pushState](http://diveintohtml5.info/history.html) and real URLs, or // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) // and URL fragments. If the browser supports neither (old IE, natch), // falls back to polling. var History = Backbone.History = function() { this.handlers = []; this.checkUrl = this.checkUrl.bind(this); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { this.location = window.location; this.history = window.history; } }; // Cached regex for stripping a leading hash/slash and trailing space. var routeStripper = /^[#\/]|\s+$/g; // Cached regex for stripping leading and trailing slashes. var rootStripper = /^\/+|\/+$/g; // Cached regex for stripping urls of hash. var pathStripper = /#.*$/; // Has the history handling already been started? History.started = false; // Set up all inheritable **Backbone.History** properties and methods. _.extend(History.prototype, Events, { // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, // Are we at the app root? atRoot: function() { var path = this.location.pathname.replace(/[^\/]$/, '$&/'); return path === this.root && !this.getSearch(); }, // Does the pathname match the root? matchRoot: function() { var path = this.decodeFragment(this.location.pathname); var rootPath = path.slice(0, this.root.length - 1) + '/'; return rootPath === this.root; }, // Unicode characters in `location.pathname` are percent encoded so they're // decoded for comparison. `%25` should not be decoded since it may be part // of an encoded parameter. decodeFragment: function(fragment) { return decodeURI(fragment.replace(/%25/g, '%2525')); }, // In IE6, the hash fragment and search params are incorrect if the // fragment contains `?`. getSearch: function() { var match = this.location.href.replace(/#.*/, '').match(/\?.+/); return match ? match[0] : ''; }, // Gets the true hash value. Cannot use location.hash directly due to bug // in Firefox where location.hash will always be decoded. getHash: function(window) { var match = (window || this).location.href.match(/#(.*)$/); return match ? match[1] : ''; }, // Get the pathname and search params, without the root. getPath: function() { var path = this.decodeFragment( this.location.pathname + this.getSearch() ).slice(this.root.length - 1); return path.charAt(0) === '/' ? path.slice(1) : path; }, // Get the cross-browser normalized URL fragment from the path or hash. getFragment: function(fragment) { if (fragment == null) { if (this._usePushState || !this._wantsHashChange) { fragment = this.getPath(); } else { fragment = this.getHash(); } } return fragment.replace(routeStripper, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. start: function(options) { if (History.started) throw new Error('Backbone.history has already been started'); History.started = true; // Figure out the initial configuration. Do we need an iframe? // Is pushState desired ... is it available? this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._trailingSlash = this.options.trailingSlash; this._wantsHashChange = this.options.hashChange !== false; this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); this._useHashChange = this._wantsHashChange && this._hasHashChange; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.history && this.history.pushState); this._usePushState = this._wantsPushState && this._hasPushState; this.fragment = this.getFragment(); // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); // Transition from hashChange to pushState or vice versa if both are // requested. if (this._wantsHashChange && this._wantsPushState) { // If we've started off with a route from a `pushState`-enabled // browser, but we're currently in a browser that doesn't support it... if (!this._hasPushState && !this.atRoot()) { var rootPath = this.root.slice(0, -1) || '/'; this.location.replace(rootPath + '#' + this.getPath()); // Return immediately as browser will do redirect to new url return true; // Or if we've started out with a hash-based route, but we're currently // in a browser where it could be `pushState`-based instead... } else if (this._hasPushState && this.atRoot()) { this.navigate(this.getHash(), {replace: true}); } } // Proxy an iframe to handle location events if the browser doesn't // support the `hashchange` event, HTML5 history, or the user wants // `hashChange` but not `pushState`. if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { this.iframe = document.createElement('iframe'); this.iframe.src = 'javascript:0'; this.iframe.style.display = 'none'; this.iframe.tabIndex = -1; var body = document.body; // Using `appendChild` will throw on IE < 9 if the document is not ready. var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; iWindow.document.open(); iWindow.document.close(); iWindow.location.hash = '#' + this.fragment; } // Add a cross-platform `addEventListener` shim for older browsers. var addEventListener = window.addEventListener || function(eventName, listener) { return attachEvent('on' + eventName, listener); }; // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._usePushState) { addEventListener('popstate', this.checkUrl, false); } else if (this._useHashChange && !this.iframe) { addEventListener('hashchange', this.checkUrl, false); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } if (!this.options.silent) return this.loadUrl(); }, // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function() { // Add a cross-platform `removeEventListener` shim for older browsers. var removeEventListener = window.removeEventListener || function(eventName, listener) { return detachEvent('on' + eventName, listener); }; // Remove window listeners. if (this._usePushState) { removeEventListener('popstate', this.checkUrl, false); } else if (this._useHashChange && !this.iframe) { removeEventListener('hashchange', this.checkUrl, false); } // Clean up the iframe if necessary. if (this.iframe) { document.body.removeChild(this.iframe); this.iframe = null; } // Some environments will throw when clearing an undefined interval. if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); History.started = false; }, // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. route: function(route, callback) { this.handlers.unshift({route: route, callback: callback}); }, // Checks the current URL to see if it has changed, and if it has, // calls `loadUrl`, normalizing across the hidden iframe. checkUrl: function(e) { var current = this.getFragment(); // If the user pressed the back button, the iframe's hash will have // changed and we should use that for comparison. if (current === this.fragment && this.iframe) { current = this.getHash(this.iframe.contentWindow); } if (current === this.fragment) { if (!this.matchRoot()) return this.notfound(); return false; } if (this.iframe) this.navigate(current); this.loadUrl(); }, // Attempt to load the current URL fragment. If a route succeeds with a // match, returns `true`. If no defined routes matches the fragment, // returns `false`. loadUrl: function(fragment) { // If the root doesn't match, no routes can match either. if (!this.matchRoot()) return this.notfound(); fragment = this.fragment = this.getFragment(fragment); return _.some(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }) || this.notfound(); }, // When no route could be matched, this method is called internally to // trigger the `'notfound'` event. It returns `false` so that it can be used // in tail position. notfound: function() { this.trigger('notfound'); return false; }, // Save a fragment into the hash history, or replace the URL state if the // 'replace' option is passed. You are responsible for properly URL-encoding // the fragment in advance. // // The options object can contain `trigger: true` if you wish to have the // route callback be fired (not usually desirable), or `replace: true`, if // you wish to modify the current URL without adding an entry to the history. navigate: function(fragment, options) { if (!History.started) return false; if (!options || options === true) options = {trigger: !!options}; // Normalize the fragment. fragment = this.getFragment(fragment || ''); // Strip trailing slash on the root unless _trailingSlash is true var rootPath = this.root; if (!this._trailingSlash && (fragment === '' || fragment.charAt(0) === '?')) { rootPath = rootPath.slice(0, -1) || '/'; } var url = rootPath + fragment; // Strip the fragment of the query and hash for matching. fragment = fragment.replace(pathStripper, ''); // Decode for matching. var decodedFragment = this.decodeFragment(fragment); if (this.fragment === decodedFragment) return; this.fragment = decodedFragment; // If pushState is available, we use it to set the fragment as a real URL. if (this._usePushState) { this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); // If hash changes haven't been explicitly disabled, update the hash // fragment to store history. } else if (this._wantsHashChange) { this._updateHash(this.location, fragment, options.replace); if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { var iWindow = this.iframe.contentWindow; // Opening and closing the iframe tricks IE7 and earlier to push a // history entry on hash-tag change. When replace is true, we don't // want this. if (!options.replace) { iWindow.document.open(); iWindow.document.close(); } this._updateHash(iWindow.location, fragment, options.replace); } // If you've told us that you explicitly don't want fallback hashchange- // based history, then `navigate` becomes a page refresh. } else { return this.location.assign(url); } if (options.trigger) return this.loadUrl(fragment); }, // Update the hash location, either replacing the current entry, or adding // a new one to the browser history. _updateHash: function(location, fragment, replace) { if (replace) { var href = location.href.replace(/(javascript:|#).*$/, ''); location.replace(href + '#' + fragment); } else { // Some browsers require that `hash` contains a leading #. location.hash = '#' + fragment; } } }); // Create the default Backbone.history. Backbone.history = new History; // Helpers // ------- // Helper function to correctly set up the prototype chain for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. var extend = function(protoProps, staticProps) { var parent = this; var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent constructor. if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // Add static properties to the constructor function, if supplied. _.extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function and add the prototype properties. child.prototype = _.create(parent.prototype, protoProps); child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed // later. child.__super__ = parent.prototype; return child; }; // Set up inheritance for the model, collection, router, view and history. Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; // Throw an error when a URL is needed, and none is supplied. var urlError = function() { throw new Error('A "url" property or function must be specified'); }; // Wrap an optional error callback with a fallback error event. var wrapError = function(model, options) { var error = options.error; options.error = function(resp) { if (error) error.call(options.context, model, resp, options); model.trigger('error', model, resp, options); }; }; // Provide useful information when things go wrong. This method is not meant // to be used directly; it merely provides the necessary introspection for the // external `debugInfo` function. Backbone._debug = function() { return {root: root, _: _}; }; return Backbone; }); ================================================ FILE: bower.json ================================================ { "name" : "backbone", "main" : "backbone.js", "dependencies" : { "underscore" : ">=1.8.3" }, "ignore" : ["docs", "examples", "test", "*.yml", "*.html", "*.ico", "*.md", "CNAME", ".*", "karma.*", "package.json"] } ================================================ FILE: debug-info.js ================================================ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('backbone')) : typeof define === 'function' && define.amd ? define(['backbone'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (global.Backbone = global.Backbone || {}, global.Backbone.debugInfo = factory(global.Backbone))); })(this, (function (Backbone) { // Provide useful information when things go wrong. function debugInfo() { // Introspect Backbone. var $ = Backbone.$, _b = Backbone._debug(), _ = _b._, root = _b.root; // Use the `partialRight` function as a Lodash indicator. It was never in // Underscore, has been in Lodash at least since version 1.3.1, and is // unlikely to be mixed into Underscore since nobody needs it. var lodash = !!_.partialRight; var info = { backbone: Backbone.VERSION, // Is this the exact released version, or a later development version? /* This is automatically temporarily replaced when publishing a release, so please don't edit this. */ distribution: 'MARK_DEVELOPMENT', _: (lodash ? 'lodash ' : '') + _.VERSION, $: !$ ? false : $.fn && $.fn.jquery ? $.fn.jquery : $.zepto ? 'zepto' : $.ender ? 'ender' : true }; if (typeof root.Deno !== 'undefined') { info.deno = _.pick(root.Deno, 'version', 'build'); } else if (typeof root.process !== 'undefined') { info.process = _.pick(root.process, 'version', 'platform', 'arch'); } else if (typeof root.navigator !== 'undefined') { info.navigator = _.pick(root.navigator, 'userAgent', 'platform', 'webdriver'); } /* eslint-disable-next-line no-console */ console.debug('Backbone debug info: ', JSON.stringify(info, null, 4)); return info; } return debugInfo; })); ================================================ FILE: docs/backbone.html ================================================ backbone.js
  • backbone.js

  • §
    Backbone.js 1.6.1
    
  • §
    (c) 2010-2024 Jeremy Ashkenas and DocumentCloud
    Backbone may be freely distributed under the MIT license.
    For all details and documentation:
    http://backbonejs.org
    
    (function(factory) {
  • §

    Establish the root object, window (self) in the browser, or global on the server. We use self instead of window for WebWorker support.

      var root = typeof self == 'object' && self.self === self && self ||
                typeof global == 'object' && global.global === global && global;
  • §

    Set up Backbone appropriately for the environment. Start with AMD.

      if (typeof define === 'function' && define.amd) {
        define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
  • §

    Export global even in AMD case in case this script is loaded with others that may still expect a global Backbone.

          root.Backbone = factory(root, exports, _, $);
        });
  • §

    Next for Node.js or CommonJS. jQuery may not be needed as a module.

      } else if (typeof exports !== 'undefined') {
        var _ = require('underscore'), $;
        try { $ = require('jquery'); } catch (e) {}
        factory(root, exports, _, $);
  • §

    Finally, as a browser global.

      } else {
        root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
      }
    
    })(function(root, Backbone, _, $) {
  • §

    Initial Setup

  • §
  • §

    Save the previous value of the Backbone variable, so that it can be restored later on, if noConflict is used.

      var previousBackbone = root.Backbone;
  • §

    Create a local reference to a common array method we’ll want to use later.

      var slice = Array.prototype.slice;
  • §

    Current version of the library. Keep in sync with package.json.

      Backbone.VERSION = '1.6.1';
  • §

    For Backbone’s purposes, jQuery, Zepto, Ender, or My Library (kidding) owns the $ variable.

      Backbone.$ = $;
  • §

    Runs Backbone.js in noConflict mode, returning the Backbone variable to its previous owner. Returns a reference to this Backbone object.

      Backbone.noConflict = function() {
        root.Backbone = previousBackbone;
        return this;
      };
  • §

    Turn on emulateHTTP to support legacy HTTP servers. Setting this option will fake "PATCH", "PUT" and "DELETE" requests via the _method parameter and set a X-Http-Method-Override header.

      Backbone.emulateHTTP = false;
  • §

    Turn on emulateJSON to support legacy servers that can’t deal with direct application/json requests … this will encode the body as application/x-www-form-urlencoded instead and will send the model in a form param named model.

      Backbone.emulateJSON = false;
  • §

    Backbone.Events

  • §
  • §

    A module that can be mixed in to any object in order to provide it with a custom event channel. You may bind a callback to an event with on or remove with off; trigger-ing an event fires all callbacks in succession.

    var object = {};
    _.extend(object, Backbone.Events);
    object.on('expand', function(){ alert('expanded'); });
    object.trigger('expand');
    
      var Events = Backbone.Events = {};
  • §

    Regular expression used to split event strings.

      var eventSplitter = /\s+/;
  • §

    A private global variable to share between listeners and listenees.

      var _listening;
  • §

    Iterates over the standard event, callback (as well as the fancy multiple space-separated events "change blur", callback and jQuery-style event maps {event: callback}).

      var eventsApi = function(iteratee, events, name, callback, opts) {
        var i = 0, names;
        if (name && typeof name === 'object') {
  • §

    Handle event maps.

          if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
          for (names = _.keys(name); i < names.length ; i++) {
            events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
          }
        } else if (name && eventSplitter.test(name)) {
  • §

    Handle space-separated event names by delegating them individually.

          for (names = name.split(eventSplitter); i < names.length; i++) {
            events = iteratee(events, names[i], callback, opts);
          }
        } else {
  • §

    Finally, standard events.

          events = iteratee(events, name, callback, opts);
        }
        return events;
      };
  • §

    Bind an event to a callback function. Passing "all" will bind the callback to all events fired.

      Events.on = function(name, callback, context) {
        this._events = eventsApi(onApi, this._events || {}, name, callback, {
          context: context,
          ctx: this,
          listening: _listening
        });
    
        if (_listening) {
          var listeners = this._listeners || (this._listeners = {});
          listeners[_listening.id] = _listening;
  • §

    Allow the listening to use a counter, instead of tracking callbacks for library interop

          _listening.interop = false;
        }
    
        return this;
      };
  • §

    Inversion-of-control versions of on. Tell this object to listen to an event in another object… keeping track of what it’s listening to for easier unbinding later.

      Events.listenTo = function(obj, name, callback) {
        if (!obj) return this;
        var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
        var listeningTo = this._listeningTo || (this._listeningTo = {});
        var listening = _listening = listeningTo[id];
  • §

    This object is not listening to any other events on obj yet. Setup the necessary references to track the listening callbacks.

        if (!listening) {
          this._listenId || (this._listenId = _.uniqueId('l'));
          listening = _listening = listeningTo[id] = new Listening(this, obj);
        }
  • §

    Bind callbacks on obj.

        var error = tryCatchOn(obj, name, callback, this);
        _listening = void 0;
    
        if (error) throw error;
  • §

    If the target obj is not Backbone.Events, track events manually.

        if (listening.interop) listening.on(name, callback);
    
        return this;
      };
  • §

    The reducing API that adds a callback to the events object.

      var onApi = function(events, name, callback, options) {
        if (callback) {
          var handlers = events[name] || (events[name] = []);
          var context = options.context, ctx = options.ctx, listening = options.listening;
          if (listening) listening.count++;
    
          handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
        }
        return events;
      };
  • §

    An try-catch guarded #on function, to prevent poisoning the global _listening variable.

      var tryCatchOn = function(obj, name, callback, context) {
        try {
          obj.on(name, callback, context);
        } catch (e) {
          return e;
        }
      };
  • §

    Remove one or many callbacks. If context is null, removes all callbacks with that function. If callback is null, removes all callbacks for the event. If name is null, removes all bound callbacks for all events.

      Events.off = function(name, callback, context) {
        if (!this._events) return this;
        this._events = eventsApi(offApi, this._events, name, callback, {
          context: context,
          listeners: this._listeners
        });
    
        return this;
      };
  • §

    Tell this object to stop listening to either specific events … or to every object it’s currently listening to.

      Events.stopListening = function(obj, name, callback) {
        var listeningTo = this._listeningTo;
        if (!listeningTo) return this;
    
        var ids = obj ? [obj._listenId] : _.keys(listeningTo);
        for (var i = 0; i < ids.length; i++) {
          var listening = listeningTo[ids[i]];
  • §

    If listening doesn’t exist, this object is not currently listening to obj. Break out early.

          if (!listening) break;
    
          listening.obj.off(name, callback, this);
          if (listening.interop) listening.off(name, callback);
        }
        if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
    
        return this;
      };
  • §

    The reducing API that removes a callback from the events object.

      var offApi = function(events, name, callback, options) {
        if (!events) return;
    
        var context = options.context, listeners = options.listeners;
        var i = 0, names;
  • §

    Delete all event listeners and “drop” events.

        if (!name && !context && !callback) {
          for (names = _.keys(listeners); i < names.length; i++) {
            listeners[names[i]].cleanup();
          }
          return;
        }
    
        names = name ? [name] : _.keys(events);
        for (; i < names.length; i++) {
          name = names[i];
          var handlers = events[name];
  • §

    Bail out if there are no events stored.

          if (!handlers) break;
  • §

    Find any remaining events.

          var remaining = [];
          for (var j = 0; j < handlers.length; j++) {
            var handler = handlers[j];
            if (
              callback && callback !== handler.callback &&
                callback !== handler.callback._callback ||
                  context && context !== handler.context
            ) {
              remaining.push(handler);
            } else {
              var listening = handler.listening;
              if (listening) listening.off(name, callback);
            }
          }
  • §

    Replace events if there are any remaining. Otherwise, clean up.

          if (remaining.length) {
            events[name] = remaining;
          } else {
            delete events[name];
          }
        }
    
        return events;
      };
  • §

    Bind an event to only be triggered a single time. After the first time the callback is invoked, its listener will be removed. If multiple events are passed in using the space-separated syntax, the handler will fire once for each event, not once for a combination of all events.

      Events.once = function(name, callback, context) {
  • §

    Map the event into a {event: once} object.

        var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
        if (typeof name === 'string' && context == null) callback = void 0;
        return this.on(events, callback, context);
      };
  • §

    Inversion-of-control versions of once.

      Events.listenToOnce = function(obj, name, callback) {
  • §

    Map the event into a {event: once} object.

        var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
        return this.listenTo(obj, events);
      };
  • §

    Reduces the event callbacks into a map of {event: onceWrapper}. offer unbinds the onceWrapper after it has been called.

      var onceMap = function(map, name, callback, offer) {
        if (callback) {
          var once = map[name] = _.once(function() {
            offer(name, once);
            callback.apply(this, arguments);
          });
          once._callback = callback;
        }
        return map;
      };
  • §

    Trigger one or many events, firing all bound callbacks. Callbacks are passed the same arguments as trigger is, apart from the event name (unless you’re listening on "all", which will cause your callback to receive the true name of the event as the first argument).

      Events.trigger = function(name) {
        if (!this._events) return this;
    
        var length = Math.max(0, arguments.length - 1);
        var args = Array(length);
        for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
    
        eventsApi(triggerApi, this._events, name, void 0, args);
        return this;
      };
  • §

    Handles triggering the appropriate event callbacks.

      var triggerApi = function(objEvents, name, callback, args) {
        if (objEvents) {
          var events = objEvents[name];
          var allEvents = objEvents.all;
          if (events && allEvents) allEvents = allEvents.slice();
          if (events) triggerEvents(events, args);
          if (allEvents) triggerEvents(allEvents, [name].concat(args));
        }
        return objEvents;
      };
  • §

    A difficult-to-believe, but optimized internal dispatch function for triggering events. Tries to keep the usual cases speedy (most internal Backbone events have 3 arguments).

      var triggerEvents = function(events, args) {
        var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
        switch (args.length) {
          case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
          case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
          case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
          case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
          default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
        }
      };
  • §

    A listening class that tracks and cleans up memory bindings when all callbacks have been offed.

      var Listening = function(listener, obj) {
        this.id = listener._listenId;
        this.listener = listener;
        this.obj = obj;
        this.interop = true;
        this.count = 0;
        this._events = void 0;
      };
    
      Listening.prototype.on = Events.on;
  • §

    Offs a callback (or several). Uses an optimized counter if the listenee uses Backbone.Events. Otherwise, falls back to manual tracking to support events library interop.

      Listening.prototype.off = function(name, callback) {
        var cleanup;
        if (this.interop) {
          this._events = eventsApi(offApi, this._events, name, callback, {
            context: void 0,
            listeners: void 0
          });
          cleanup = !this._events;
        } else {
          this.count--;
          cleanup = this.count === 0;
        }
        if (cleanup) this.cleanup();
      };
  • §

    Cleans up memory bindings between the listener and the listenee.

      Listening.prototype.cleanup = function() {
        delete this.listener._listeningTo[this.obj._listenId];
        if (!this.interop) delete this.obj._listeners[this.id];
      };
  • §

    Aliases for backwards compatibility.

      Events.bind   = Events.on;
      Events.unbind = Events.off;
  • §

    Allow the Backbone object to serve as a global event bus, for folks who want global “pubsub” in a convenient place.

      _.extend(Backbone, Events);
  • §

    Backbone.Model

  • §
  • §

    Backbone Models are the basic data object in the framework – frequently representing a row in a table in a database on your server. A discrete chunk of data and a bunch of useful, related methods for performing computations and transformations on that data.

  • §

    Create a new model with the specified attributes. A client id (cid) is automatically generated and assigned for you.

      var Model = Backbone.Model = function(attributes, options) {
        var attrs = attributes || {};
        options || (options = {});
        this.preinitialize.apply(this, arguments);
        this.cid = _.uniqueId(this.cidPrefix);
        this.attributes = {};
        if (options.collection) this.collection = options.collection;
        if (options.parse) attrs = this.parse(attrs, options) || {};
        var defaults = _.result(this, 'defaults');
  • §

    Just _.defaults would work fine, but the additional _.extends is in there for historical reasons. See #3843.

        attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
    
        this.set(attrs, options);
        this.changed = {};
        this.initialize.apply(this, arguments);
      };
  • §

    Attach all inheritable methods to the Model prototype.

      _.extend(Model.prototype, Events, {
  • §

    A hash of attributes whose current and previous value differ.

        changed: null,
  • §

    The value returned during the last failed validation.

        validationError: null,
  • §

    The default name for the JSON id attribute is "id". MongoDB and CouchDB users may want to set this to "_id".

        idAttribute: 'id',
  • §

    The prefix is used to create the client id which is used to identify models locally. You may want to override this if you’re experiencing name clashes with model ids.

        cidPrefix: 'c',
  • §

    preinitialize is an empty function by default. You can override it with a function or object. preinitialize will run before any instantiation logic is run in the Model.

        preinitialize: function(){},
  • §

    Initialize is an empty function by default. Override it with your own initialization logic.

        initialize: function(){},
  • §

    Return a copy of the model’s attributes object.

        toJSON: function(options) {
          return _.clone(this.attributes);
        },
  • §

    Proxy Backbone.sync by default – but override this if you need custom syncing semantics for this particular model.

        sync: function() {
          return Backbone.sync.apply(this, arguments);
        },
  • §

    Get the value of an attribute.

        get: function(attr) {
          return this.attributes[attr];
        },
  • §

    Get the HTML-escaped value of an attribute.

        escape: function(attr) {
          return _.escape(this.get(attr));
        },
  • §

    Returns true if the attribute contains a value that is not null or undefined.

        has: function(attr) {
          return this.get(attr) != null;
        },
  • §

    Special-cased proxy to underscore’s _.matches method.

        matches: function(attrs) {
          return !!_.iteratee(attrs, this)(this.attributes);
        },
  • §

    Set a hash of model attributes on the object, firing "change". This is the core primitive operation of a model, updating the data and notifying anyone who needs to know about the change in state. The heart of the beast.

        set: function(key, val, options) {
          if (key == null) return this;
  • §

    Handle both "key", value and {key: value} -style arguments.

          var attrs;
          if (typeof key === 'object') {
            attrs = key;
            options = val;
          } else {
            (attrs = {})[key] = val;
          }
    
          options || (options = {});
  • §

    Run validation.

          if (!this._validate(attrs, options)) return false;
  • §

    Extract attributes and options.

          var unset      = options.unset;
          var silent     = options.silent;
          var changes    = [];
          var changing   = this._changing;
          this._changing = true;
    
          if (!changing) {
            this._previousAttributes = _.clone(this.attributes);
            this.changed = {};
          }
    
          var current = this.attributes;
          var changed = this.changed;
          var prev    = this._previousAttributes;
  • §

    For each set attribute, update or delete the current value.

          for (var attr in attrs) {
            val = attrs[attr];
            if (!_.isEqual(current[attr], val)) changes.push(attr);
            if (!_.isEqual(prev[attr], val)) {
              changed[attr] = val;
            } else {
              delete changed[attr];
            }
            unset ? delete current[attr] : current[attr] = val;
          }
  • §

    Update the id.

          if (this.idAttribute in attrs) {
            var prevId = this.id;
            this.id = this.get(this.idAttribute);
            if (this.id !== prevId) {
              this.trigger('changeId', this, prevId, options);
            }
          }
  • §

    Trigger all relevant attribute changes.

          if (!silent) {
            if (changes.length) this._pending = options;
            for (var i = 0; i < changes.length; i++) {
              this.trigger('change:' + changes[i], this, current[changes[i]], options);
            }
          }
  • §

    You might be wondering why there’s a while loop here. Changes can be recursively nested within "change" events.

          if (changing) return this;
          if (!silent) {
            while (this._pending) {
              options = this._pending;
              this._pending = false;
              this.trigger('change', this, options);
            }
          }
          this._pending = false;
          this._changing = false;
          return this;
        },
  • §

    Remove an attribute from the model, firing "change". unset is a noop if the attribute doesn’t exist.

        unset: function(attr, options) {
          return this.set(attr, void 0, _.extend({}, options, {unset: true}));
        },
  • §

    Clear all attributes on the model, firing "change".

        clear: function(options) {
          var attrs = {};
          for (var key in this.attributes) attrs[key] = void 0;
          return this.set(attrs, _.extend({}, options, {unset: true}));
        },
  • §

    Determine if the model has changed since the last "change" event. If you specify an attribute name, determine if that attribute has changed.

        hasChanged: function(attr) {
          if (attr == null) return !_.isEmpty(this.changed);
          return _.has(this.changed, attr);
        },
  • §

    Return an object containing all the attributes that have changed, or false if there are no changed attributes. Useful for determining what parts of a view need to be updated and/or what attributes need to be persisted to the server. Unset attributes will be set to undefined. You can also pass an attributes object to diff against the model, determining if there would be a change.

        changedAttributes: function(diff) {
          if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
          var old = this._changing ? this._previousAttributes : this.attributes;
          var changed = {};
          var hasChanged;
          for (var attr in diff) {
            var val = diff[attr];
            if (_.isEqual(old[attr], val)) continue;
            changed[attr] = val;
            hasChanged = true;
          }
          return hasChanged ? changed : false;
        },
  • §

    Get the previous value of an attribute, recorded at the time the last "change" event was fired.

        previous: function(attr) {
          if (attr == null || !this._previousAttributes) return null;
          return this._previousAttributes[attr];
        },
  • §

    Get all of the attributes of the model at the time of the previous "change" event.

        previousAttributes: function() {
          return _.clone(this._previousAttributes);
        },
  • §

    Fetch the model from the server, merging the response with the model’s local attributes. Any changed attributes will trigger a “change” event.

        fetch: function(options) {
          options = _.extend({parse: true}, options);
          var model = this;
          var success = options.success;
          options.success = function(resp) {
            var serverAttrs = options.parse ? model.parse(resp, options) : resp;
            if (!model.set(serverAttrs, options)) return false;
            if (success) success.call(options.context, model, resp, options);
            model.trigger('sync', model, resp, options);
          };
          wrapError(this, options);
          return this.sync('read', this, options);
        },
  • §

    Set a hash of model attributes, and sync the model to the server. If the server returns an attributes hash that differs, the model’s state will be set again.

        save: function(key, val, options) {
  • §

    Handle both "key", value and {key: value} -style arguments.

          var attrs;
          if (key == null || typeof key === 'object') {
            attrs = key;
            options = val;
          } else {
            (attrs = {})[key] = val;
          }
    
          options = _.extend({validate: true, parse: true}, options);
          var wait = options.wait;
  • §

    If we’re not waiting and attributes exist, save acts as set(attr).save(null, opts) with validation. Otherwise, check if the model will be valid when the attributes, if any, are set.

          if (attrs && !wait) {
            if (!this.set(attrs, options)) return false;
          } else if (!this._validate(attrs, options)) {
            return false;
          }
  • §

    After a successful server-side save, the client is (optionally) updated with the server-side state.

          var model = this;
          var success = options.success;
          var attributes = this.attributes;
          options.success = function(resp) {
  • §

    Ensure attributes are restored during synchronous saves.

            model.attributes = attributes;
            var serverAttrs = options.parse ? model.parse(resp, options) : resp;
            if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
            if (serverAttrs && !model.set(serverAttrs, options)) return false;
            if (success) success.call(options.context, model, resp, options);
            model.trigger('sync', model, resp, options);
          };
          wrapError(this, options);
  • §

    Set temporary attributes if {wait: true} to properly find new ids.

          if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
    
          var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
          if (method === 'patch' && !options.attrs) options.attrs = attrs;
          var xhr = this.sync(method, this, options);
  • §

    Restore attributes.

          this.attributes = attributes;
    
          return xhr;
        },
  • §

    Destroy this model on the server if it was already persisted. Optimistically removes the model from its collection, if it has one. If wait: true is passed, waits for the server to respond before removal.

        destroy: function(options) {
          options = options ? _.clone(options) : {};
          var model = this;
          var success = options.success;
          var wait = options.wait;
    
          var destroy = function() {
            model.stopListening();
            model.trigger('destroy', model, model.collection, options);
          };
    
          options.success = function(resp) {
            if (wait) destroy();
            if (success) success.call(options.context, model, resp, options);
            if (!model.isNew()) model.trigger('sync', model, resp, options);
          };
    
          var xhr = false;
          if (this.isNew()) {
            _.defer(options.success);
          } else {
            wrapError(this, options);
            xhr = this.sync('delete', this, options);
          }
          if (!wait) destroy();
          return xhr;
        },
  • §

    Default URL for the model’s representation on the server – if you’re using Backbone’s restful methods, override this to change the endpoint that will be called.

        url: function() {
          var base =
            _.result(this, 'urlRoot') ||
            _.result(this.collection, 'url') ||
            urlError();
          if (this.isNew()) return base;
          var id = this.get(this.idAttribute);
          return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
        },
  • §

    parse converts a response into the hash of attributes to be set on the model. The default implementation is just to pass the response along.

        parse: function(resp, options) {
          return resp;
        },
  • §

    Create a new model with identical attributes to this one.

        clone: function() {
          return new this.constructor(this.attributes);
        },
  • §

    A model is new if it has never been saved to the server, and lacks an id.

        isNew: function() {
          return !this.has(this.idAttribute);
        },
  • §

    Check if the model is currently in a valid state.

        isValid: function(options) {
          return this._validate({}, _.extend({}, options, {validate: true}));
        },
  • §

    Run validation against the next complete set of model attributes, returning true if all is well. Otherwise, fire an "invalid" event.

        _validate: function(attrs, options) {
          if (!options.validate || !this.validate) return true;
          attrs = _.extend({}, this.attributes, attrs);
          var error = this.validationError = this.validate(attrs, options) || null;
          if (!error) return true;
          this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
          return false;
        }
    
      });
  • §

    Backbone.Collection

  • §
  • §

    If models tend to represent a single row of data, a Backbone Collection is more analogous to a table full of data … or a small slice or page of that table, or a collection of rows that belong together for a particular reason – all of the messages in this particular folder, all of the documents belonging to this particular author, and so on. Collections maintain indexes of their models, both in order, and for lookup by id.

  • §

    Create a new Collection, perhaps to contain a specific type of model. If a comparator is specified, the Collection will maintain its models in sort order, as they’re added and removed.

      var Collection = Backbone.Collection = function(models, options) {
        options || (options = {});
        this.preinitialize.apply(this, arguments);
        if (options.model) this.model = options.model;
        if (options.comparator !== void 0) this.comparator = options.comparator;
        this._reset();
        this.initialize.apply(this, arguments);
        if (models) this.reset(models, _.extend({silent: true}, options));
      };
  • §

    Default options for Collection#set.

      var setOptions = {add: true, remove: true, merge: true};
      var addOptions = {add: true, remove: false};
  • §

    Splices insert into array at index at.

      var splice = function(array, insert, at) {
        at = Math.min(Math.max(at, 0), array.length);
        var tail = Array(array.length - at);
        var length = insert.length;
        var i;
        for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
        for (i = 0; i < length; i++) array[i + at] = insert[i];
        for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
      };
  • §

    Define the Collection’s inheritable methods.

      _.extend(Collection.prototype, Events, {
  • §

    The default model for a collection is just a Backbone.Model. This should be overridden in most cases.

        model: Model,
  • §

    preinitialize is an empty function by default. You can override it with a function or object. preinitialize will run before any instantiation logic is run in the Collection.

        preinitialize: function(){},
  • §

    Initialize is an empty function by default. Override it with your own initialization logic.

        initialize: function(){},
  • §

    The JSON representation of a Collection is an array of the models’ attributes.

        toJSON: function(options) {
          return this.map(function(model) { return model.toJSON(options); });
        },
  • §

    Proxy Backbone.sync by default.

        sync: function() {
          return Backbone.sync.apply(this, arguments);
        },
  • §

    Add a model, or list of models to the set. models may be Backbone Models or raw JavaScript objects to be converted to Models, or any combination of the two.

        add: function(models, options) {
          return this.set(models, _.extend({merge: false}, options, addOptions));
        },
  • §

    Remove a model, or a list of models from the set.

        remove: function(models, options) {
          options = _.extend({}, options);
          var singular = !_.isArray(models);
          models = singular ? [models] : models.slice();
          var removed = this._removeModels(models, options);
          if (!options.silent && removed.length) {
            options.changes = {added: [], merged: [], removed: removed};
            this.trigger('update', this, options);
          }
          return singular ? removed[0] : removed;
        },
  • §

    Update a collection by set-ing a new list of models, adding new ones, removing models that are no longer present, and merging models that already exist in the collection, as necessary. Similar to Model#set, the core operation for updating the data contained by the collection.

        set: function(models, options) {
          if (models == null) return;
    
          options = _.extend({}, setOptions, options);
          if (options.parse && !this._isModel(models)) {
            models = this.parse(models, options) || [];
          }
    
          var singular = !_.isArray(models);
          models = singular ? [models] : models.slice();
    
          var at = options.at;
          if (at != null) at = +at;
          if (at > this.length) at = this.length;
          if (at < 0) at += this.length + 1;
    
          var set = [];
          var toAdd = [];
          var toMerge = [];
          var toRemove = [];
          var modelMap = {};
    
          var add = options.add;
          var merge = options.merge;
          var remove = options.remove;
    
          var sort = false;
          var sortable = this.comparator && at == null && options.sort !== false;
          var sortAttr = _.isString(this.comparator) ? this.comparator : null;
  • §

    Turn bare objects into model references, and prevent invalid models from being added.

          var model, i;
          for (i = 0; i < models.length; i++) {
            model = models[i];
  • §

    If a duplicate is found, prevent it from being added and optionally merge it into the existing model.

            var existing = this.get(model);
            if (existing) {
              if (merge && model !== existing) {
                var attrs = this._isModel(model) ? model.attributes : model;
                if (options.parse) attrs = existing.parse(attrs, options);
                existing.set(attrs, options);
                toMerge.push(existing);
                if (sortable && !sort) sort = existing.hasChanged(sortAttr);
              }
              if (!modelMap[existing.cid]) {
                modelMap[existing.cid] = true;
                set.push(existing);
              }
              models[i] = existing;
  • §

    If this is a new, valid model, push it to the toAdd list.

            } else if (add) {
              model = models[i] = this._prepareModel(model, options);
              if (model) {
                toAdd.push(model);
                this._addReference(model, options);
                modelMap[model.cid] = true;
                set.push(model);
              }
            }
          }
  • §

    Remove stale models.

          if (remove) {
            for (i = 0; i < this.length; i++) {
              model = this.models[i];
              if (!modelMap[model.cid]) toRemove.push(model);
            }
            if (toRemove.length) this._removeModels(toRemove, options);
          }
  • §

    See if sorting is needed, update length and splice in new models.

          var orderChanged = false;
          var replace = !sortable && add && remove;
          if (set.length && replace) {
            orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
              return m !== set[index];
            });
            this.models.length = 0;
            splice(this.models, set, 0);
            this.length = this.models.length;
          } else if (toAdd.length) {
            if (sortable) sort = true;
            splice(this.models, toAdd, at == null ? this.length : at);
            this.length = this.models.length;
          }
  • §

    Silently sort the collection if appropriate.

          if (sort) this.sort({silent: true});
  • §

    Unless silenced, it’s time to fire all appropriate add/sort/update events.

          if (!options.silent) {
            for (i = 0; i < toAdd.length; i++) {
              if (at != null) options.index = at + i;
              model = toAdd[i];
              model.trigger('add', model, this, options);
            }
            if (sort || orderChanged) this.trigger('sort', this, options);
            if (toAdd.length || toRemove.length || toMerge.length) {
              options.changes = {
                added: toAdd,
                removed: toRemove,
                merged: toMerge
              };
              this.trigger('update', this, options);
            }
          }
  • §

    Return the added (or merged) model (or models).

          return singular ? models[0] : models;
        },
  • §

    When you have more items than you want to add or remove individually, you can reset the entire set with a new list of models, without firing any granular add or remove events. Fires reset when finished. Useful for bulk operations and optimizations.

        reset: function(models, options) {
          options = options ? _.clone(options) : {};
          for (var i = 0; i < this.models.length; i++) {
            this._removeReference(this.models[i], options);
          }
          options.previousModels = this.models;
          this._reset();
          models = this.add(models, _.extend({silent: true}, options));
          if (!options.silent) this.trigger('reset', this, options);
          return models;
        },
  • §

    Add a model to the end of the collection.

        push: function(model, options) {
          return this.add(model, _.extend({at: this.length}, options));
        },
  • §

    Remove a model from the end of the collection.

        pop: function(options) {
          var model = this.at(this.length - 1);
          return this.remove(model, options);
        },
  • §

    Add a model to the beginning of the collection.

        unshift: function(model, options) {
          return this.add(model, _.extend({at: 0}, options));
        },
  • §

    Remove a model from the beginning of the collection.

        shift: function(options) {
          var model = this.at(0);
          return this.remove(model, options);
        },
  • §

    Slice out a sub-array of models from the collection.

        slice: function() {
          return slice.apply(this.models, arguments);
        },
  • §

    Get a model from the set by id, cid, model object with id or cid properties, or an attributes object that is transformed through modelId.

        get: function(obj) {
          if (obj == null) return void 0;
          return this._byId[obj] ||
            this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj, obj.idAttribute)] ||
            obj.cid && this._byId[obj.cid];
        },
  • §

    Returns true if the model is in the collection.

        has: function(obj) {
          return this.get(obj) != null;
        },
  • §

    Get the model at the given index.

        at: function(index) {
          if (index < 0) index += this.length;
          return this.models[index];
        },
  • §

    Return models with matching attributes. Useful for simple cases of filter.

        where: function(attrs, first) {
          return this[first ? 'find' : 'filter'](attrs);
        },
  • §

    Return the first model with matching attributes. Useful for simple cases of find.

        findWhere: function(attrs) {
          return this.where(attrs, true);
        },
  • §

    Force the collection to re-sort itself. You don’t need to call this under normal circumstances, as the set will maintain sort order as each item is added.

        sort: function(options) {
          var comparator = this.comparator;
          if (!comparator) throw new Error('Cannot sort a set without a comparator');
          options || (options = {});
    
          var length = comparator.length;
          if (_.isFunction(comparator)) comparator = comparator.bind(this);
  • §

    Run sort based on type of comparator.

          if (length === 1 || _.isString(comparator)) {
            this.models = this.sortBy(comparator);
          } else {
            this.models.sort(comparator);
          }
          if (!options.silent) this.trigger('sort', this, options);
          return this;
        },
  • §

    Pluck an attribute from each model in the collection.

        pluck: function(attr) {
          return this.map(attr + '');
        },
  • §

    Fetch the default set of models for this collection, resetting the collection when they arrive. If reset: true is passed, the response data will be passed through the reset method instead of set.

        fetch: function(options) {
          options = _.extend({parse: true}, options);
          var success = options.success;
          var collection = this;
          options.success = function(resp) {
            var method = options.reset ? 'reset' : 'set';
            collection[method](resp, options);
            if (success) success.call(options.context, collection, resp, options);
            collection.trigger('sync', collection, resp, options);
          };
          wrapError(this, options);
          return this.sync('read', this, options);
        },
  • §

    Create a new instance of a model in this collection. Add the model to the collection immediately, unless wait: true is passed, in which case we wait for the server to agree.

        create: function(model, options) {
          options = options ? _.clone(options) : {};
          var wait = options.wait;
          model = this._prepareModel(model, options);
          if (!model) return false;
          if (!wait) this.add(model, options);
          var collection = this;
          var success = options.success;
          options.success = function(m, resp, callbackOpts) {
            if (wait) {
              m.off('error', collection._forwardPristineError, collection);
              collection.add(m, callbackOpts);
            }
            if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
          };
  • §

    In case of wait:true, our collection is not listening to any of the model’s events yet, so it will not forward the error event. In this special case, we need to listen for it separately and handle the event just once. (The reason we don’t need to do this for the sync event is in the success handler above: we add the model first, which causes the collection to listen, and then invoke the callback that triggers the event.)

          if (wait) {
            model.once('error', this._forwardPristineError, this);
          }
          model.save(null, options);
          return model;
        },
  • §

    parse converts a response into a list of models to be added to the collection. The default implementation is just to pass it through.

        parse: function(resp, options) {
          return resp;
        },
  • §

    Create a new collection with an identical list of models as this one.

        clone: function() {
          return new this.constructor(this.models, {
            model: this.model,
            comparator: this.comparator
          });
        },
  • §

    Define how to uniquely identify models in the collection.

        modelId: function(attrs, idAttribute) {
          return attrs[idAttribute || this.model.prototype.idAttribute || 'id'];
        },
  • §

    Get an iterator of all models in this collection.

        values: function() {
          return new CollectionIterator(this, ITERATOR_VALUES);
        },
  • §

    Get an iterator of all model IDs in this collection.

        keys: function() {
          return new CollectionIterator(this, ITERATOR_KEYS);
        },
  • §

    Get an iterator of all [ID, model] tuples in this collection.

        entries: function() {
          return new CollectionIterator(this, ITERATOR_KEYSVALUES);
        },
  • §

    Private method to reset all internal state. Called when the collection is first initialized or reset.

        _reset: function() {
          this.length = 0;
          this.models = [];
          this._byId  = {};
        },
  • §

    Prepare a hash of attributes (or other model) to be added to this collection.

        _prepareModel: function(attrs, options) {
          if (this._isModel(attrs)) {
            if (!attrs.collection) attrs.collection = this;
            return attrs;
          }
          options = options ? _.clone(options) : {};
          options.collection = this;
    
          var model;
          if (this.model.prototype) {
            model = new this.model(attrs, options);
          } else {
  • §

    ES class methods didn’t have prototype

            model = this.model(attrs, options);
          }
    
          if (!model.validationError) return model;
          this.trigger('invalid', this, model.validationError, options);
          return false;
        },
  • §

    Internal method called by both remove and set.

        _removeModels: function(models, options) {
          var removed = [];
          for (var i = 0; i < models.length; i++) {
            var model = this.get(models[i]);
            if (!model) continue;
    
            var index = this.indexOf(model);
            this.models.splice(index, 1);
            this.length--;
  • §

    Remove references before triggering ‘remove’ event to prevent an infinite loop. #3693

            delete this._byId[model.cid];
            var id = this.modelId(model.attributes, model.idAttribute);
            if (id != null) delete this._byId[id];
    
            if (!options.silent) {
              options.index = index;
              model.trigger('remove', model, this, options);
            }
    
            removed.push(model);
            this._removeReference(model, options);
          }
          if (models.length > 0 && !options.silent) delete options.index;
          return removed;
        },
  • §

    Method for checking whether an object should be considered a model for the purposes of adding to the collection.

        _isModel: function(model) {
          return model instanceof Model;
        },
  • §

    Internal method to create a model’s ties to a collection.

        _addReference: function(model, options) {
          this._byId[model.cid] = model;
          var id = this.modelId(model.attributes, model.idAttribute);
          if (id != null) this._byId[id] = model;
          model.on('all', this._onModelEvent, this);
        },
  • §

    Internal method to sever a model’s ties to a collection.

        _removeReference: function(model, options) {
          delete this._byId[model.cid];
          var id = this.modelId(model.attributes, model.idAttribute);
          if (id != null) delete this._byId[id];
          if (this === model.collection) delete model.collection;
          model.off('all', this._onModelEvent, this);
        },
  • §

    Internal method called every time a model in the set fires an event. Sets need to update their indexes when models change ids. All other events simply proxy through. “add” and “remove” events that originate in other collections are ignored.

        _onModelEvent: function(event, model, collection, options) {
          if (model) {
            if ((event === 'add' || event === 'remove') && collection !== this) return;
            if (event === 'destroy') this.remove(model, options);
            if (event === 'changeId') {
              var prevId = this.modelId(model.previousAttributes(), model.idAttribute);
              var id = this.modelId(model.attributes, model.idAttribute);
              if (prevId != null) delete this._byId[prevId];
              if (id != null) this._byId[id] = model;
            }
          }
          this.trigger.apply(this, arguments);
        },
  • §

    Internal callback method used in create. It serves as a stand-in for the _onModelEvent method, which is not yet bound during the wait period of the create call. We still want to forward any 'error' event at the end of the wait period, hence a customized callback.

        _forwardPristineError: function(model, collection, options) {
  • §

    Prevent double forward if the model was already in the collection before the call to create.

          if (this.has(model)) return;
          this._onModelEvent('error', model, collection, options);
        }
      });
  • §

    Defining an @@iterator method implements JavaScript’s Iterable protocol. In modern ES2015 browsers, this value is found at Symbol.iterator.

      /* global Symbol */
      var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
      if ($$iterator) {
        Collection.prototype[$$iterator] = Collection.prototype.values;
      }
  • §

    CollectionIterator

  • §
  • §

    A CollectionIterator implements JavaScript’s Iterator protocol, allowing the use of for of loops in modern browsers and interoperation between Backbone.Collection and other JavaScript functions and third-party libraries which can operate on Iterables.

      var CollectionIterator = function(collection, kind) {
        this._collection = collection;
        this._kind = kind;
        this._index = 0;
      };
  • §

    This “enum” defines the three possible kinds of values which can be emitted by a CollectionIterator that correspond to the values(), keys() and entries() methods on Collection, respectively.

      var ITERATOR_VALUES = 1;
      var ITERATOR_KEYS = 2;
      var ITERATOR_KEYSVALUES = 3;
  • §

    All Iterators should themselves be Iterable.

      if ($$iterator) {
        CollectionIterator.prototype[$$iterator] = function() {
          return this;
        };
      }
    
      CollectionIterator.prototype.next = function() {
        if (this._collection) {
  • §

    Only continue iterating if the iterated collection is long enough.

          if (this._index < this._collection.length) {
            var model = this._collection.at(this._index);
            this._index++;
  • §

    Construct a value depending on what kind of values should be iterated.

            var value;
            if (this._kind === ITERATOR_VALUES) {
              value = model;
            } else {
              var id = this._collection.modelId(model.attributes, model.idAttribute);
              if (this._kind === ITERATOR_KEYS) {
                value = id;
              } else { // ITERATOR_KEYSVALUES
                value = [id, model];
              }
            }
            return {value: value, done: false};
          }
  • §

    Once exhausted, remove the reference to the collection so future calls to the next method always return done.

          this._collection = void 0;
        }
    
        return {value: void 0, done: true};
      };
  • §

    Backbone.View

  • §
  • §

    Backbone Views are almost more convention than they are actual code. A View is simply a JavaScript object that represents a logical chunk of UI in the DOM. This might be a single item, an entire list, a sidebar or panel, or even the surrounding frame which wraps your whole app. Defining a chunk of UI as a View allows you to define your DOM events declaratively, without having to worry about render order … and makes it easy for the view to react to specific changes in the state of your models.

  • §

    Creating a Backbone.View creates its initial element outside of the DOM, if an existing element is not provided…

      var View = Backbone.View = function(options) {
        this.cid = _.uniqueId('view');
        this.preinitialize.apply(this, arguments);
        _.extend(this, _.pick(options, viewOptions));
        this._ensureElement();
        this.initialize.apply(this, arguments);
      };
  • §

    Cached regex to split keys for delegate.

      var delegateEventSplitter = /^(\S+)\s*(.*)$/;
  • §

    List of view options to be set as properties.

      var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
  • §

    Set up all inheritable Backbone.View properties and methods.

      _.extend(View.prototype, Events, {
  • §

    The default tagName of a View’s element is "div".

        tagName: 'div',
  • §

    jQuery delegate for element lookup, scoped to DOM elements within the current view. This should be preferred to global lookups where possible.

        $: function(selector) {
          return this.$el.find(selector);
        },
  • §

    preinitialize is an empty function by default. You can override it with a function or object. preinitialize will run before any instantiation logic is run in the View

        preinitialize: function(){},
  • §

    Initialize is an empty function by default. Override it with your own initialization logic.

        initialize: function(){},
  • §

    render is the core function that your view should override, in order to populate its element (this.el), with the appropriate HTML. The convention is for render to always return this.

        render: function() {
          return this;
        },
  • §

    Remove this view by taking the element out of the DOM, and removing any applicable Backbone.Events listeners.

        remove: function() {
          this._removeElement();
          this.stopListening();
          return this;
        },
  • §

    Remove this view’s element from the document and all event listeners attached to it. Exposed for subclasses using an alternative DOM manipulation API.

        _removeElement: function() {
          this.$el.remove();
        },
  • §

    Change the view’s element (this.el property) and re-delegate the view’s events on the new element.

        setElement: function(element) {
          this.undelegateEvents();
          this._setElement(element);
          this.delegateEvents();
          return this;
        },
  • §

    Creates the this.el and this.$el references for this view using the given el. el can be a CSS selector or an HTML string, a jQuery context or an element. Subclasses can override this to utilize an alternative DOM manipulation API and are only required to set the this.el property.

        _setElement: function(el) {
          this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
          this.el = this.$el[0];
        },
  • §

    Set callbacks, where this.events is a hash of

    {“event selector”: “callback”}

    {
      'mousedown .title':  'edit',
      'click .button':     'save',
      'click .open':       function(e) { ... }
    }
    

    pairs. Callbacks will be bound to the view, with this set properly. Uses event delegation for efficiency. Omitting the selector binds the event to this.el.

        delegateEvents: function(events) {
          events || (events = _.result(this, 'events'));
          if (!events) return this;
          this.undelegateEvents();
          for (var key in events) {
            var method = events[key];
            if (!_.isFunction(method)) method = this[method];
            if (!method) continue;
            var match = key.match(delegateEventSplitter);
            this.delegate(match[1], match[2], method.bind(this));
          }
          return this;
        },
  • §

    Add a single event listener to the view’s element (or a child element using selector). This only works for delegate-able events: not focus, blur, and not change, submit, and reset in Internet Explorer.

        delegate: function(eventName, selector, listener) {
          this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
          return this;
        },
  • §

    Clears all callbacks previously bound to the view by delegateEvents. You usually don’t need to use this, but may wish to if you have multiple Backbone views attached to the same DOM element.

        undelegateEvents: function() {
          if (this.$el) this.$el.off('.delegateEvents' + this.cid);
          return this;
        },
  • §

    A finer-grained undelegateEvents for removing a single delegated event. selector and listener are both optional.

        undelegate: function(eventName, selector, listener) {
          this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
          return this;
        },
  • §

    Produces a DOM element to be assigned to your view. Exposed for subclasses using an alternative DOM manipulation API.

        _createElement: function(tagName) {
          return document.createElement(tagName);
        },
  • §

    Ensure that the View has a DOM element to render into. If this.el is a string, pass it through $(), take the first matching element, and re-assign it to el. Otherwise, create an element from the id, className and tagName properties.

        _ensureElement: function() {
          if (!this.el) {
            var attrs = _.extend({}, _.result(this, 'attributes'));
            if (this.id) attrs.id = _.result(this, 'id');
            if (this.className) attrs['class'] = _.result(this, 'className');
            this.setElement(this._createElement(_.result(this, 'tagName')));
            this._setAttributes(attrs);
          } else {
            this.setElement(_.result(this, 'el'));
          }
        },
  • §

    Set attributes from a hash on this view’s element. Exposed for subclasses using an alternative DOM manipulation API.

        _setAttributes: function(attributes) {
          this.$el.attr(attributes);
        }
    
      });
  • §

    Proxy Backbone class methods to Underscore functions, wrapping the model’s attributes object or collection’s models array behind the scenes.

    collection.filter(function(model) { return model.get(‘age’) > 10 }); collection.each(this.addView);

    Function#apply can be slow so we use the method’s arg count, if we know it.

      var addMethod = function(base, length, method, attribute) {
        switch (length) {
          case 1: return function() {
            return base[method](this[attribute]);
          };
          case 2: return function(value) {
            return base[method](this[attribute], value);
          };
          case 3: return function(iteratee, context) {
            return base[method](this[attribute], cb(iteratee, this), context);
          };
          case 4: return function(iteratee, defaultVal, context) {
            return base[method](this[attribute], cb(iteratee, this), defaultVal, context);
          };
          default: return function() {
            var args = slice.call(arguments);
            args.unshift(this[attribute]);
            return base[method].apply(base, args);
          };
        }
      };
    
      var addUnderscoreMethods = function(Class, base, methods, attribute) {
        _.each(methods, function(length, method) {
          if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute);
        });
      };
  • §

    Support collection.sortBy('attr') and collection.findWhere({id: 1}).

      var cb = function(iteratee, instance) {
        if (_.isFunction(iteratee)) return iteratee;
        if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
        if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
        return iteratee;
      };
      var modelMatcher = function(attrs) {
        var matcher = _.matches(attrs);
        return function(model) {
          return matcher(model.attributes);
        };
      };
  • §

    Underscore methods that we want to implement on the Collection. 90% of the core usefulness of Backbone Collections is actually implemented right here:

      var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
        foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
        select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
        contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
        head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
        without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
        isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
        sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
  • §

    Underscore methods that we want to implement on the Model, mapped to the number of arguments they take.

      var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
        omit: 0, chain: 1, isEmpty: 1};
  • §

    Mix in each Underscore method as a proxy to Collection#models.

      _.each([
        [Collection, collectionMethods, 'models'],
        [Model, modelMethods, 'attributes']
      ], function(config) {
        var Base = config[0],
            methods = config[1],
            attribute = config[2];
    
        Base.mixin = function(obj) {
          var mappings = _.reduce(_.functions(obj), function(memo, name) {
            memo[name] = 0;
            return memo;
          }, {});
          addUnderscoreMethods(Base, obj, mappings, attribute);
        };
    
        addUnderscoreMethods(Base, _, methods, attribute);
      });
  • §

    Backbone.sync

  • §
  • §

    Override this function to change the manner in which Backbone persists models to the server. You will be passed the type of request, and the model in question. By default, makes a RESTful Ajax request to the model’s url(). Some possible customizations could be:

    • Use setTimeout to batch rapid-fire updates into a single request.
    • Send up the models as XML instead of JSON.
    • Persist models via WebSockets instead of Ajax.

    Turn on Backbone.emulateHTTP in order to send PUT and DELETE requests as POST, with a _method parameter containing the true HTTP method, as well as all requests with the body as application/x-www-form-urlencoded instead of application/json with the model in a param named model. Useful when interfacing with server-side languages like PHP that make it difficult to read the body of PUT requests.

      Backbone.sync = function(method, model, options) {
        var type = methodMap[method];
  • §

    Default options, unless specified.

        _.defaults(options || (options = {}), {
          emulateHTTP: Backbone.emulateHTTP,
          emulateJSON: Backbone.emulateJSON
        });
  • §

    Default JSON-request options.

        var params = {type: type, dataType: 'json'};
  • §

    Ensure that we have a URL.

        if (!options.url) {
          params.url = _.result(model, 'url') || urlError();
        }
  • §

    Ensure that we have the appropriate request data.

        if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
          params.contentType = 'application/json';
          params.data = JSON.stringify(options.attrs || model.toJSON(options));
        }
  • §

    For older servers, emulate JSON by encoding the request into an HTML-form.

        if (options.emulateJSON) {
          params.contentType = 'application/x-www-form-urlencoded';
          params.data = params.data ? {model: params.data} : {};
        }
  • §

    For older servers, emulate HTTP by mimicking the HTTP method with _method And an X-HTTP-Method-Override header.

        if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
          params.type = 'POST';
          if (options.emulateJSON) params.data._method = type;
          var beforeSend = options.beforeSend;
          options.beforeSend = function(xhr) {
            xhr.setRequestHeader('X-HTTP-Method-Override', type);
            if (beforeSend) return beforeSend.apply(this, arguments);
          };
        }
  • §

    Don’t process data on a non-GET request.

        if (params.type !== 'GET' && !options.emulateJSON) {
          params.processData = false;
        }
  • §

    Pass along textStatus and errorThrown from jQuery.

        var error = options.error;
        options.error = function(xhr, textStatus, errorThrown) {
          options.textStatus = textStatus;
          options.errorThrown = errorThrown;
          if (error) error.call(options.context, xhr, textStatus, errorThrown);
        };
  • §

    Make the request, allowing the user to override any Ajax options.

        var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
        model.trigger('request', model, xhr, options);
        return xhr;
      };
  • §

    Map from CRUD to HTTP for our default Backbone.sync implementation.

      var methodMap = {
        'create': 'POST',
        'update': 'PUT',
        'patch': 'PATCH',
        'delete': 'DELETE',
        'read': 'GET'
      };
  • §

    Set the default implementation of Backbone.ajax to proxy through to $. Override this if you’d like to use a different library.

      Backbone.ajax = function() {
        return Backbone.$.ajax.apply(Backbone.$, arguments);
      };
  • §

    Backbone.Router

  • §
  • §

    Routers map faux-URLs to actions, and fire events when routes are matched. Creating a new one sets its routes hash, if not set statically.

      var Router = Backbone.Router = function(options) {
        options || (options = {});
        this.preinitialize.apply(this, arguments);
        if (options.routes) this.routes = options.routes;
        this._bindRoutes();
        this.initialize.apply(this, arguments);
      };
  • §

    Cached regular expressions for matching named param parts and splatted parts of route strings.

      var optionalParam = /\((.*?)\)/g;
      var namedParam    = /(\(\?)?:\w+/g;
      var splatParam    = /\*\w+/g;
      var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;
  • §

    Set up all inheritable Backbone.Router properties and methods.

      _.extend(Router.prototype, Events, {
  • §

    preinitialize is an empty function by default. You can override it with a function or object. preinitialize will run before any instantiation logic is run in the Router.

        preinitialize: function(){},
  • §

    Initialize is an empty function by default. Override it with your own initialization logic.

        initialize: function(){},
  • §

    Manually bind a single named route to a callback. For example:

    this.route('search/:query/p:num', 'search', function(query, num) {
      ...
    });
    
        route: function(route, name, callback) {
          if (!_.isRegExp(route)) route = this._routeToRegExp(route);
          if (_.isFunction(name)) {
            callback = name;
            name = '';
          }
          if (!callback) callback = this[name];
          var router = this;
          Backbone.history.route(route, function(fragment) {
            var args = router._extractParameters(route, fragment);
            if (router.execute(callback, args, name) !== false) {
              router.trigger.apply(router, ['route:' + name].concat(args));
              router.trigger('route', name, args);
              Backbone.history.trigger('route', router, name, args);
            }
          });
          return this;
        },
  • §

    Execute a route handler with the provided parameters. This is an excellent place to do pre-route setup or post-route cleanup.

        execute: function(callback, args, name) {
          if (callback) callback.apply(this, args);
        },
  • §

    Simple proxy to Backbone.history to save a fragment into the history.

        navigate: function(fragment, options) {
          Backbone.history.navigate(fragment, options);
          return this;
        },
  • §

    Bind all defined routes to Backbone.history. We have to reverse the order of the routes here to support behavior where the most general routes can be defined at the bottom of the route map.

        _bindRoutes: function() {
          if (!this.routes) return;
          this.routes = _.result(this, 'routes');
          var route, routes = _.keys(this.routes);
          while ((route = routes.pop()) != null) {
            this.route(route, this.routes[route]);
          }
        },
  • §

    Convert a route string into a regular expression, suitable for matching against the current location hash.

        _routeToRegExp: function(route) {
          route = route.replace(escapeRegExp, '\\$&')
          .replace(optionalParam, '(?:$1)?')
          .replace(namedParam, function(match, optional) {
            return optional ? match : '([^/?]+)';
          })
          .replace(splatParam, '([^?]*?)');
          return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
        },
  • §

    Given a route, and a URL fragment that it matches, return the array of extracted decoded parameters. Empty or unmatched parameters will be treated as null to normalize cross-browser behavior.

        _extractParameters: function(route, fragment) {
          var params = route.exec(fragment).slice(1);
          return _.map(params, function(param, i) {
  • §

    Don’t decode the search params.

            if (i === params.length - 1) return param || null;
            return param ? decodeURIComponent(param) : null;
          });
        }
    
      });
  • §

    Backbone.History

  • §
  • §

    Handles cross-browser history management, based on either pushState and real URLs, or onhashchange and URL fragments. If the browser supports neither (old IE, natch), falls back to polling.

      var History = Backbone.History = function() {
        this.handlers = [];
        this.checkUrl = this.checkUrl.bind(this);
  • §

    Ensure that History can be used outside of the browser.

        if (typeof window !== 'undefined') {
          this.location = window.location;
          this.history = window.history;
        }
      };
  • §

    Cached regex for stripping a leading hash/slash and trailing space.

      var routeStripper = /^[#\/]|\s+$/g;
  • §

    Cached regex for stripping leading and trailing slashes.

      var rootStripper = /^\/+|\/+$/g;
  • §

    Cached regex for stripping urls of hash.

      var pathStripper = /#.*$/;
  • §

    Has the history handling already been started?

      History.started = false;
  • §

    Set up all inheritable Backbone.History properties and methods.

      _.extend(History.prototype, Events, {
  • §

    The default interval to poll for hash changes, if necessary, is twenty times a second.

        interval: 50,
  • §

    Are we at the app root?

        atRoot: function() {
          var path = this.location.pathname.replace(/[^\/]$/, '$&/');
          return path === this.root && !this.getSearch();
        },
  • §

    Does the pathname match the root?

        matchRoot: function() {
          var path = this.decodeFragment(this.location.pathname);
          var rootPath = path.slice(0, this.root.length - 1) + '/';
          return rootPath === this.root;
        },
  • §

    Unicode characters in location.pathname are percent encoded so they’re decoded for comparison. %25 should not be decoded since it may be part of an encoded parameter.

        decodeFragment: function(fragment) {
          return decodeURI(fragment.replace(/%25/g, '%2525'));
        },
  • §

    In IE6, the hash fragment and search params are incorrect if the fragment contains ?.

        getSearch: function() {
          var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
          return match ? match[0] : '';
        },
  • §

    Gets the true hash value. Cannot use location.hash directly due to bug in Firefox where location.hash will always be decoded.

        getHash: function(window) {
          var match = (window || this).location.href.match(/#(.*)$/);
          return match ? match[1] : '';
        },
  • §

    Get the pathname and search params, without the root.

        getPath: function() {
          var path = this.decodeFragment(
            this.location.pathname + this.getSearch()
          ).slice(this.root.length - 1);
          return path.charAt(0) === '/' ? path.slice(1) : path;
        },
  • §

    Get the cross-browser normalized URL fragment from the path or hash.

        getFragment: function(fragment) {
          if (fragment == null) {
            if (this._usePushState || !this._wantsHashChange) {
              fragment = this.getPath();
            } else {
              fragment = this.getHash();
            }
          }
          return fragment.replace(routeStripper, '');
        },
  • §

    Start the hash change handling, returning true if the current URL matches an existing route, and false otherwise.

        start: function(options) {
          if (History.started) throw new Error('Backbone.history has already been started');
          History.started = true;
  • §

    Figure out the initial configuration. Do we need an iframe? Is pushState desired … is it available?

          this.options          = _.extend({root: '/'}, this.options, options);
          this.root             = this.options.root;
          this._trailingSlash   = this.options.trailingSlash;
          this._wantsHashChange = this.options.hashChange !== false;
          this._hasHashChange   = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
          this._useHashChange   = this._wantsHashChange && this._hasHashChange;
          this._wantsPushState  = !!this.options.pushState;
          this._hasPushState    = !!(this.history && this.history.pushState);
          this._usePushState    = this._wantsPushState && this._hasPushState;
          this.fragment         = this.getFragment();
  • §

    Normalize root to always include a leading and trailing slash.

          this.root = ('/' + this.root + '/').replace(rootStripper, '/');
  • §

    Transition from hashChange to pushState or vice versa if both are requested.

          if (this._wantsHashChange && this._wantsPushState) {
  • §

    If we’ve started off with a route from a pushState-enabled browser, but we’re currently in a browser that doesn’t support it…

            if (!this._hasPushState && !this.atRoot()) {
              var rootPath = this.root.slice(0, -1) || '/';
              this.location.replace(rootPath + '#' + this.getPath());
  • §

    Return immediately as browser will do redirect to new url

              return true;
  • §

    Or if we’ve started out with a hash-based route, but we’re currently in a browser where it could be pushState-based instead…

            } else if (this._hasPushState && this.atRoot()) {
              this.navigate(this.getHash(), {replace: true});
            }
    
          }
  • §

    Proxy an iframe to handle location events if the browser doesn’t support the hashchange event, HTML5 history, or the user wants hashChange but not pushState.

          if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
            this.iframe = document.createElement('iframe');
            this.iframe.src = 'javascript:0';
            this.iframe.style.display = 'none';
            this.iframe.tabIndex = -1;
            var body = document.body;
  • §

    Using appendChild will throw on IE < 9 if the document is not ready.

            var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
            iWindow.document.open();
            iWindow.document.close();
            iWindow.location.hash = '#' + this.fragment;
          }
  • §

    Add a cross-platform addEventListener shim for older browsers.

          var addEventListener = window.addEventListener || function(eventName, listener) {
            return attachEvent('on' + eventName, listener);
          };
  • §

    Depending on whether we’re using pushState or hashes, and whether ‘onhashchange’ is supported, determine how we check the URL state.

          if (this._usePushState) {
            addEventListener('popstate', this.checkUrl, false);
          } else if (this._useHashChange && !this.iframe) {
            addEventListener('hashchange', this.checkUrl, false);
          } else if (this._wantsHashChange) {
            this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
          }
    
          if (!this.options.silent) return this.loadUrl();
        },
  • §

    Disable Backbone.history, perhaps temporarily. Not useful in a real app, but possibly useful for unit testing Routers.

        stop: function() {
  • §

    Add a cross-platform removeEventListener shim for older browsers.

          var removeEventListener = window.removeEventListener || function(eventName, listener) {
            return detachEvent('on' + eventName, listener);
          };
  • §

    Remove window listeners.

          if (this._usePushState) {
            removeEventListener('popstate', this.checkUrl, false);
          } else if (this._useHashChange && !this.iframe) {
            removeEventListener('hashchange', this.checkUrl, false);
          }
  • §

    Clean up the iframe if necessary.

          if (this.iframe) {
            document.body.removeChild(this.iframe);
            this.iframe = null;
          }
  • §

    Some environments will throw when clearing an undefined interval.

          if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
          History.started = false;
        },
  • §

    Add a route to be tested when the fragment changes. Routes added later may override previous routes.

        route: function(route, callback) {
          this.handlers.unshift({route: route, callback: callback});
        },
  • §

    Checks the current URL to see if it has changed, and if it has, calls loadUrl, normalizing across the hidden iframe.

        checkUrl: function(e) {
          var current = this.getFragment();
  • §

    If the user pressed the back button, the iframe’s hash will have changed and we should use that for comparison.

          if (current === this.fragment && this.iframe) {
            current = this.getHash(this.iframe.contentWindow);
          }
    
          if (current === this.fragment) {
            if (!this.matchRoot()) return this.notfound();
            return false;
          }
          if (this.iframe) this.navigate(current);
          this.loadUrl();
        },
  • §

    Attempt to load the current URL fragment. If a route succeeds with a match, returns true. If no defined routes matches the fragment, returns false.

        loadUrl: function(fragment) {
  • §

    If the root doesn’t match, no routes can match either.

          if (!this.matchRoot()) return this.notfound();
          fragment = this.fragment = this.getFragment(fragment);
          return _.some(this.handlers, function(handler) {
            if (handler.route.test(fragment)) {
              handler.callback(fragment);
              return true;
            }
          }) || this.notfound();
        },
  • §

    When no route could be matched, this method is called internally to trigger the 'notfound' event. It returns false so that it can be used in tail position.

        notfound: function() {
          this.trigger('notfound');
          return false;
        },
  • §

    Save a fragment into the hash history, or replace the URL state if the ‘replace’ option is passed. You are responsible for properly URL-encoding the fragment in advance.

    The options object can contain trigger: true if you wish to have the route callback be fired (not usually desirable), or replace: true, if you wish to modify the current URL without adding an entry to the history.

        navigate: function(fragment, options) {
          if (!History.started) return false;
          if (!options || options === true) options = {trigger: !!options};
  • §

    Normalize the fragment.

          fragment = this.getFragment(fragment || '');
  • §

    Strip trailing slash on the root unless _trailingSlash is true

          var rootPath = this.root;
          if (!this._trailingSlash && (fragment === '' || fragment.charAt(0) === '?')) {
            rootPath = rootPath.slice(0, -1) || '/';
          }
          var url = rootPath + fragment;
  • §

    Strip the fragment of the query and hash for matching.

          fragment = fragment.replace(pathStripper, '');
  • §

    Decode for matching.

          var decodedFragment = this.decodeFragment(fragment);
    
          if (this.fragment === decodedFragment) return;
          this.fragment = decodedFragment;
  • §

    If pushState is available, we use it to set the fragment as a real URL.

          if (this._usePushState) {
            this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
  • §

    If hash changes haven’t been explicitly disabled, update the hash fragment to store history.

          } else if (this._wantsHashChange) {
            this._updateHash(this.location, fragment, options.replace);
            if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
              var iWindow = this.iframe.contentWindow;
  • §

    Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change. When replace is true, we don’t want this.

              if (!options.replace) {
                iWindow.document.open();
                iWindow.document.close();
              }
    
              this._updateHash(iWindow.location, fragment, options.replace);
            }
  • §

    If you’ve told us that you explicitly don’t want fallback hashchange- based history, then navigate becomes a page refresh.

          } else {
            return this.location.assign(url);
          }
          if (options.trigger) return this.loadUrl(fragment);
        },
  • §

    Update the hash location, either replacing the current entry, or adding a new one to the browser history.

        _updateHash: function(location, fragment, replace) {
          if (replace) {
            var href = location.href.replace(/(javascript:|#).*$/, '');
            location.replace(href + '#' + fragment);
          } else {
  • §

    Some browsers require that hash contains a leading #.

            location.hash = '#' + fragment;
          }
        }
    
      });
  • §

    Create the default Backbone.history.

      Backbone.history = new History;
  • §

    Helpers

  • §
  • §

    Helper function to correctly set up the prototype chain for subclasses. Similar to goog.inherits, but uses a hash of prototype properties and class properties to be extended.

      var extend = function(protoProps, staticProps) {
        var parent = this;
        var child;
  • §

    The constructor function for the new subclass is either defined by you (the “constructor” property in your extend definition), or defaulted by us to simply call the parent constructor.

        if (protoProps && _.has(protoProps, 'constructor')) {
          child = protoProps.constructor;
        } else {
          child = function(){ return parent.apply(this, arguments); };
        }
  • §

    Add static properties to the constructor function, if supplied.

        _.extend(child, parent, staticProps);
  • §

    Set the prototype chain to inherit from parent, without calling parent‘s constructor function and add the prototype properties.

        child.prototype = _.create(parent.prototype, protoProps);
        child.prototype.constructor = child;
  • §

    Set a convenience property in case the parent’s prototype is needed later.

        child.__super__ = parent.prototype;
    
        return child;
      };
  • §

    Set up inheritance for the model, collection, router, view and history.

      Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
  • §

    Throw an error when a URL is needed, and none is supplied.

      var urlError = function() {
        throw new Error('A "url" property or function must be specified');
      };
  • §

    Wrap an optional error callback with a fallback error event.

      var wrapError = function(model, options) {
        var error = options.error;
        options.error = function(resp) {
          if (error) error.call(options.context, model, resp, options);
          model.trigger('error', model, resp, options);
        };
      };
  • §

    Provide useful information when things go wrong. This method is not meant to be used directly; it merely provides the necessary introspection for the external debugInfo function.

      Backbone._debug = function() {
        return {root: root, _: _};
      };
    
      return Backbone;
    });
================================================ FILE: docs/backbone.localStorage.html ================================================

This annotated source has moved to examples/backbone.localStorage.html. You will be automatically redirected in two seconds.

================================================ FILE: docs/docco.css ================================================ /*--------------------- Typography ----------------------------*/ @font-face { font-family: 'aller-light'; src: url('public/fonts/aller-light.eot'); src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), url('public/fonts/aller-light.woff') format('woff'), url('public/fonts/aller-light.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'aller-bold'; src: url('public/fonts/aller-bold.eot'); src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), url('public/fonts/aller-bold.woff') format('woff'), url('public/fonts/aller-bold.ttf') format('truetype'); font-weight: normal; font-style: normal; } @font-face { font-family: 'roboto-black'; src: url('public/fonts/roboto-black.eot'); src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'), url('public/fonts/roboto-black.woff') format('woff'), url('public/fonts/roboto-black.ttf') format('truetype'); font-weight: normal; font-style: normal; } /*--------------------- Layout ----------------------------*/ html { height: 100%; } body { font-family: "aller-light"; font-size: 14px; line-height: 18px; color: #30404f; margin: 0; padding: 0; height:100%; } #container { min-height: 100%; } a { color: #000; } b, strong { font-weight: normal; font-family: "aller-bold"; } p { margin: 15px 0 0px; } .annotation ul, .annotation ol { margin: 25px 0; } .annotation ul li, .annotation ol li { font-size: 14px; line-height: 18px; margin: 10px 0; } h1, h2, h3, h4, h5, h6 { color: #112233; line-height: 1em; font-weight: normal; font-family: "roboto-black"; text-transform: uppercase; margin: 30px 0 15px 0; } h1 { margin-top: 40px; } h2 { font-size: 1.26em; } hr { border: 0; background: 1px #ddd; height: 1px; margin: 20px 0; } pre, tt, code { font-size: 12px; line-height: 16px; font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; margin: 0; padding: 0; } .annotation pre { display: block; margin: 0; padding: 7px 10px; background: #fcfcfc; -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); box-shadow: inset 0 0 10px rgba(0,0,0,0.1); overflow-x: auto; } .annotation pre code { border: 0; padding: 0; background: transparent; } blockquote { border-left: 5px solid #ccc; margin: 0; padding: 1px 0 1px 1em; } .sections blockquote p { font-family: Menlo, Consolas, Monaco, monospace; font-size: 12px; line-height: 16px; color: #999; margin: 10px 0 0; white-space: pre-wrap; } ul.sections { list-style: none; padding:0 0 5px 0;; margin:0; } /* Force border-box so that % widths fit the parent container without overlap because of margin/padding. More Info : http://www.quirksmode.org/css/box.html */ ul.sections > li > div { -moz-box-sizing: border-box; /* firefox */ -ms-box-sizing: border-box; /* ie */ -webkit-box-sizing: border-box; /* webkit */ -khtml-box-sizing: border-box; /* konqueror */ box-sizing: border-box; /* css3 */ } /*---------------------- Jump Page -----------------------------*/ #jump_to, #jump_page { margin: 0; background: white; -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; font: 16px Arial; cursor: pointer; text-align: right; list-style: none; } #jump_to a { text-decoration: none; } #jump_to a.large { display: none; } #jump_to a.small { font-size: 22px; font-weight: bold; color: #676767; } #jump_to, #jump_wrapper { position: fixed; right: 0; top: 0; padding: 10px 15px; margin:0; } #jump_wrapper { display: none; padding:0; } #jump_to:hover #jump_wrapper { display: block; } #jump_page_wrapper{ position: fixed; right: 0; top: 0; bottom: 0; } #jump_page { padding: 5px 0 3px; margin: 0 0 25px 25px; max-height: 100%; overflow: auto; } #jump_page .source { display: block; padding: 15px; text-decoration: none; border-top: 1px solid #eee; } #jump_page .source:hover { background: #f5f5ff; } #jump_page .source:first-child { } /*---------------------- Low resolutions (> 320px) ---------------------*/ @media only screen and (min-width: 320px) { .sswrap { display: none; } ul.sections > li > div { display: block; padding:5px 10px 0 10px; } ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { padding-left: 30px; } ul.sections > li > div.content { overflow-x:auto; -webkit-box-shadow: inset 0 0 5px #e5e5ee; box-shadow: inset 0 0 5px #e5e5ee; border: 1px solid #dedede; margin:5px 10px 5px 10px; padding-bottom: 5px; } ul.sections > li > div.annotation pre { margin: 7px 0 7px; padding-left: 15px; } ul.sections > li > div.annotation p tt, .annotation code { background: #f8f8ff; border: 1px solid #dedede; font-size: 12px; padding: 0 0.2em; } } /*---------------------- (> 481px) ---------------------*/ @media only screen and (min-width: 481px) { #container { position: relative; } body { background-color: #F5F5FF; font-size: 15px; line-height: 21px; } pre, tt, code { line-height: 18px; } p, ul, ol { margin: 0 0 15px; } #jump_to { padding: 5px 10px; } #jump_wrapper { padding: 0; } #jump_to, #jump_page { font: 10px Arial; text-transform: uppercase; } #jump_page .source { padding: 5px 10px; } #jump_to a.large { display: inline-block; } #jump_to a.small { display: none; } #background { position: absolute; top: 0; bottom: 0; width: 350px; background: #fff; border-right: 1px solid #e5e5ee; z-index: -1; } ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { padding-left: 40px; } ul.sections > li { white-space: nowrap; } ul.sections > li > div { display: inline-block; } ul.sections > li > div.annotation { max-width: 350px; min-width: 350px; min-height: 5px; padding: 13px; overflow-x: hidden; white-space: normal; vertical-align: top; text-align: left; } ul.sections > li > div.annotation pre { margin: 15px 0 15px; padding-left: 15px; } ul.sections > li > div.content { padding: 13px; vertical-align: top; border: none; -webkit-box-shadow: none; box-shadow: none; } .sswrap { position: relative; display: inline; } .ss { font: 12px Arial; text-decoration: none; color: #454545; position: absolute; top: 3px; left: -20px; padding: 1px 2px; opacity: 0; -webkit-transition: opacity 0.2s linear; } .for-h1 .ss { top: 47px; } .for-h2 .ss, .for-h3 .ss, .for-h4 .ss { top: 35px; } ul.sections > li > div.annotation:hover .ss { opacity: 1; } } /*---------------------- (> 1025px) ---------------------*/ @media only screen and (min-width: 1025px) { body { font-size: 16px; line-height: 24px; } #background { width: 525px; } ul.sections > li > div.annotation { max-width: 525px; min-width: 525px; padding: 10px 25px 1px 50px; } ul.sections > li > div.content { padding: 9px 15px 16px 25px; } } /*---------------------- Syntax Highlighting -----------------------------*/ td.linenos { background-color: #f0f0f0; padding-right: 10px; } span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } /* github.com style (c) Vasily Polovnyov */ pre code { display: block; padding: 0.5em; color: #000; background: #f8f8ff } pre .hljs-comment, pre .hljs-template_comment, pre .hljs-diff .hljs-header, pre .hljs-javadoc { color: #408080; font-style: italic } pre .hljs-keyword, pre .hljs-assignment, pre .hljs-literal, pre .hljs-css .hljs-rule .hljs-keyword, pre .hljs-winutils, pre .hljs-javascript .hljs-title, pre .hljs-lisp .hljs-title, pre .hljs-subst { color: #954121; /*font-weight: bold*/ } pre .hljs-number, pre .hljs-hexcolor { color: #40a070 } pre .hljs-string, pre .hljs-tag .hljs-value, pre .hljs-phpdoc, pre .hljs-tex .hljs-formula { color: #219161; } pre .hljs-title, pre .hljs-id { color: #19469D; } pre .hljs-params { color: #00F; } pre .hljs-javascript .hljs-title, pre .hljs-lisp .hljs-title, pre .hljs-subst { font-weight: normal } pre .hljs-class .hljs-title, pre .hljs-haskell .hljs-label, pre .hljs-tex .hljs-command { color: #458; font-weight: bold } pre .hljs-tag, pre .hljs-tag .hljs-title, pre .hljs-rules .hljs-property, pre .hljs-django .hljs-tag .hljs-keyword { color: #000080; font-weight: normal } pre .hljs-attribute, pre .hljs-variable, pre .hljs-instancevar, pre .hljs-lisp .hljs-body { color: #008080 } pre .hljs-regexp { color: #B68 } pre .hljs-class { color: #458; font-weight: bold } pre .hljs-symbol, pre .hljs-ruby .hljs-symbol .hljs-string, pre .hljs-ruby .hljs-symbol .hljs-keyword, pre .hljs-ruby .hljs-symbol .hljs-keymethods, pre .hljs-lisp .hljs-keyword, pre .hljs-tex .hljs-special, pre .hljs-input_number { color: #990073 } pre .hljs-builtin, pre .hljs-constructor, pre .hljs-built_in, pre .hljs-lisp .hljs-title { color: #0086b3 } pre .hljs-preprocessor, pre .hljs-pi, pre .hljs-doctype, pre .hljs-shebang, pre .hljs-cdata { color: #999; font-weight: bold } pre .hljs-deletion { background: #fdd } pre .hljs-addition { background: #dfd } pre .hljs-diff .hljs-change { background: #0086b3 } pre .hljs-chunk { color: #aaa } pre .hljs-tex .hljs-formula { opacity: 0.5; } ================================================ FILE: docs/examples/backbone.localStorage.html ================================================ backbone.localStorage.js
  • backbone.localStorage.js

  • §
    /**
     * Backbone localStorage Adapter
     * Version 1.1.0
     *
     * https://github.com/jeromegn/Backbone.localStorage
     */
    (function (root, factory) {
       if (typeof define === "function" && define.amd) {
  • §

    AMD. Register as an anonymous module.

          define(["underscore","backbone"], function(_, Backbone) {
  • §

    Use global variables if the locals are undefined.

            return factory(_ || root._, Backbone || root.Backbone);
          });
       } else {
  • §

    RequireJS isn’t being used. Assume underscore and backbone are loaded in script tags

          factory(_, Backbone);
       }
    }(this, function(_, Backbone) {
  • §

    A simple module to replace Backbone.sync with localStorage-based persistence. Models are given GUIDS, and saved into a JSON object. Simple as that.

  • §

    Hold reference to Underscore.js and Backbone.js in the closure in order to make things work even if they are removed from the global namespace

  • §

    Generate four random hex digits.

    function S4() {
       return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    };
  • §

    Generate a pseudo-GUID by concatenating random hexadecimal.

    function guid() {
       return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
    };
  • §

    Our Store is represented by a single JS object in localStorage. Create it with a meaningful name, like the name you’d give a table. window.Store is deprecated, use Backbone.LocalStorage instead

    Backbone.LocalStorage = window.Store = function(name) {
      this.name = name;
      var store = this.localStorage().getItem(this.name);
      this.records = (store && store.split(",")) || [];
    };
    
    _.extend(Backbone.LocalStorage.prototype, {
  • §

    Save the current state of the Store to localStorage.

      save: function() {
        this.localStorage().setItem(this.name, this.records.join(","));
      },
  • §

    Add a model, giving it a (hopefully)-unique GUID, if it doesn’t already have an id of it’s own.

      create: function(model) {
        if (!model.id) {
          model.id = guid();
          model.set(model.idAttribute, model.id);
        }
        this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
        this.records.push(model.id.toString());
        this.save();
        return this.find(model);
      },
  • §

    Update a model by replacing its copy in this.data.

      update: function(model) {
        this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
        if (!_.include(this.records, model.id.toString()))
          this.records.push(model.id.toString()); this.save();
        return this.find(model);
      },
  • §

    Retrieve a model from this.data by id.

      find: function(model) {
        return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id));
      },
  • §

    Return the array of all models currently in storage.

      findAll: function() {
        return _(this.records).chain()
          .map(function(id){
            return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
          }, this)
          .compact()
          .value();
      },
  • §

    Delete a model from this.data, returning it.

      destroy: function(model) {
        if (model.isNew())
          return false
        this.localStorage().removeItem(this.name+"-"+model.id);
        this.records = _.reject(this.records, function(id){
          return id === model.id.toString();
        });
        this.save();
        return model;
      },
    
      localStorage: function() {
        return localStorage;
      },
  • §

    fix for “illegal access” error on Android when JSON.parse is passed null

      jsonData: function (data) {
          return data && JSON.parse(data);
      }
    
    });
  • §

    localSync delegate to the model or collection’s localStorage property, which should be an instance of Store. window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead

    Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
      var store = model.localStorage || model.collection.localStorage;
    
      var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
    
      try {
    
        switch (method) {
          case "read":
            resp = model.id != undefined ? store.find(model) : store.findAll();
            break;
          case "create":
            resp = store.create(model);
            break;
          case "update":
            resp = store.update(model);
            break;
          case "delete":
            resp = store.destroy(model);
            break;
        }
    
      } catch(error) {
        if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
          errorMessage = "Private browsing is unsupported";
        else
          errorMessage = error.message;
      }
    
      if (resp) {
        model.trigger("sync", model, resp, options);
        if (options && options.success)
          options.success(resp);
        if (syncDfd)
          syncDfd.resolve(resp);
    
      } else {
        errorMessage = errorMessage ? errorMessage
                                    : "Record Not Found";
    
        if (options && options.error)
          options.error(errorMessage);
        if (syncDfd)
          syncDfd.reject(errorMessage);
      }
  • §

    add compatibility with $.ajax always execute callback for success and error

      if (options && options.complete) options.complete(resp);
    
      return syncDfd && syncDfd.promise();
    };
    
    Backbone.ajaxSync = Backbone.sync;
    
    Backbone.getSyncMethod = function(model) {
      if(model.localStorage || (model.collection && model.collection.localStorage)) {
        return Backbone.localSync;
      }
    
      return Backbone.ajaxSync;
    };
  • §

    Override ‘Backbone.sync’ to default to localSync, the original ‘Backbone.sync’ is still available in ‘Backbone.ajaxSync’

    Backbone.sync = function(method, model, options) {
      return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
    };
    
    return Backbone.LocalStorage;
    }));
================================================ FILE: docs/examples/todos/todos.html ================================================ todos.js
  • todos.js

  • §

    An example Backbone application contributed by Jérôme Gravel-Niquet. This demo uses a simple LocalStorage adapter to persist Backbone models within your browser.

  • §

    Load the application once the DOM is ready, using jQuery.ready:

    $(function(){
  • §

    Todo Model

  • §
  • §

    Our basic Todo model has title, order, and done attributes.

      var Todo = Backbone.Model.extend({
  • §

    Default attributes for the todo item.

        defaults: function() {
          return {
            title: "empty todo...",
            order: Todos.nextOrder(),
            done: false
          };
        },
  • §

    Toggle the done state of this todo item.

        toggle: function() {
          this.save({done: !this.get("done")});
        }
    
      });
  • §

    Todo Collection

  • §
  • §

    The collection of todos is backed by localStorage instead of a remote server.

      var TodoList = Backbone.Collection.extend({
  • §

    Reference to this collection’s model.

        model: Todo,
  • §

    Save all of the todo items under the "todos-backbone" namespace.

        localStorage: new Backbone.LocalStorage("todos-backbone"),
  • §

    Filter down the list of all todo items that are finished.

        done: function() {
          return this.where({done: true});
        },
  • §

    Filter down the list to only todo items that are still not finished.

        remaining: function() {
          return this.where({done: false});
        },
  • §

    We keep the Todos in sequential order, despite being saved by unordered GUID in the database. This generates the next order number for new items.

        nextOrder: function() {
          if (!this.length) return 1;
          return this.last().get('order') + 1;
        },
  • §

    Todos are sorted by their original insertion order.

        comparator: 'order'
    
      });
  • §

    Create our global collection of Todos.

      var Todos = new TodoList;
  • §

    Todo Item View

  • §
  • §

    The DOM element for a todo item…

      var TodoView = Backbone.View.extend({
  • §

    … is a list tag.

        tagName:  "li",
  • §

    Cache the template function for a single item.

        template: _.template($('#item-template').html()),
  • §

    The DOM events specific to an item.

        events: {
          "click .toggle"   : "toggleDone",
          "dblclick .view"  : "edit",
          "click a.destroy" : "clear",
          "keypress .edit"  : "updateOnEnter",
          "blur .edit"      : "close"
        },
  • §

    The TodoView listens for changes to its model, re-rendering. Since there’s a one-to-one correspondence between a Todo and a TodoView in this app, we set a direct reference on the model for convenience.

        initialize: function() {
          this.listenTo(this.model, 'change', this.render);
          this.listenTo(this.model, 'destroy', this.remove);
        },
  • §

    Re-render the titles of the todo item.

        render: function() {
          this.$el.html(this.template(this.model.toJSON()));
          this.$el.toggleClass('done', this.model.get('done'));
          this.input = this.$('.edit');
          return this;
        },
  • §

    Toggle the "done" state of the model.

        toggleDone: function() {
          this.model.toggle();
        },
  • §

    Switch this view into "editing" mode, displaying the input field.

        edit: function() {
          this.$el.addClass("editing");
          this.input.focus();
        },
  • §

    Close the "editing" mode, saving changes to the todo.

        close: function() {
          var value = this.input.val();
          if (!value) {
            this.clear();
          } else {
            this.model.save({title: value});
            this.$el.removeClass("editing");
          }
        },
  • §

    If you hit enter, we’re through editing the item.

        updateOnEnter: function(e) {
          if (e.keyCode == 13) this.close();
        },
  • §

    Remove the item, destroy the model.

        clear: function() {
          this.model.destroy();
        }
    
      });
  • §

    The Application

  • §
  • §

    Our overall AppView is the top-level piece of UI.

      var AppView = Backbone.View.extend({
  • §

    Instead of generating a new element, bind to the existing skeleton of the App already present in the HTML.

        el: $("#todoapp"),
  • §

    Our template for the line of statistics at the bottom of the app.

        statsTemplate: _.template($('#stats-template').html()),
  • §

    Delegated events for creating new items, and clearing completed ones.

        events: {
          "keypress #new-todo":  "createOnEnter",
          "click #clear-completed": "clearCompleted",
          "click #toggle-all": "toggleAllComplete"
        },
  • §

    At initialization we bind to the relevant events on the Todos collection, when items are added or changed. Kick things off by loading any preexisting todos that might be saved in localStorage.

        initialize: function() {
    
          this.input = this.$("#new-todo");
          this.allCheckbox = this.$("#toggle-all")[0];
    
          this.listenTo(Todos, 'add', this.addOne);
          this.listenTo(Todos, 'reset', this.addAll);
          this.listenTo(Todos, 'all', this.render);
    
          this.footer = this.$('footer');
          this.main = $('#main');
    
          Todos.fetch();
        },
  • §

    Re-rendering the App just means refreshing the statistics – the rest of the app doesn’t change.

        render: function() {
          var done = Todos.done().length;
          var remaining = Todos.remaining().length;
    
          if (Todos.length) {
            this.main.show();
            this.footer.show();
            this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
          } else {
            this.main.hide();
            this.footer.hide();
          }
    
          this.allCheckbox.checked = !remaining;
        },
  • §

    Add a single todo item to the list by creating a view for it, and appending its element to the <ul>.

        addOne: function(todo) {
          var view = new TodoView({model: todo});
          this.$("#todo-list").append(view.render().el);
        },
  • §

    Add all items in the Todos collection at once.

        addAll: function() {
          Todos.each(this.addOne, this);
        },
  • §

    If you hit return in the main input field, create new Todo model, persisting it to localStorage.

        createOnEnter: function(e) {
          if (e.keyCode != 13) return;
          if (!this.input.val()) return;
    
          Todos.create({title: this.input.val()});
          this.input.val('');
        },
  • §

    Clear all done todo items, destroying their models.

        clearCompleted: function() {
          _.invoke(Todos.done(), 'destroy');
          return false;
        },
    
        toggleAllComplete: function () {
          var done = this.allCheckbox.checked;
          Todos.each(function (todo) { todo.save({'done': done}); });
        }
    
      });
  • §

    Finally, we kick things off by creating the App.

      var App = new AppView;
    
    });
================================================ FILE: docs/js/jquery.lazyload.js ================================================ /* * Lazy Load - jQuery plugin for lazy loading images * * Copyright (c) 2007-2012 Mika Tuupola * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * Project home: * http://www.appelsiini.net/projects/lazyload * * Version: 1.7.2 * */ (function($, window) { $window = $(window); $.fn.lazyload = function(options) { var elements = this; var settings = { threshold : 0, failure_limit : 0, event : "scroll", effect : "show", container : window, data_attribute : "original", skip_invisible : true, appear : null, load : null }; function update() { var counter = 0; elements.each(function() { var $this = $(this); if (settings.skip_invisible && !$this.is(":visible")) { return; } if ($.abovethetop(this, settings) || $.leftofbegin(this, settings)) { /* Nothing. */ } else if (!$.belowthefold(this, settings) && !$.rightoffold(this, settings)) { $this.trigger("appear"); } else { if (++counter > settings.failure_limit) { return false; } } }); } if(options) { /* Maintain BC for a couple of versions. */ if (undefined !== options.failurelimit) { options.failure_limit = options.failurelimit; delete options.failurelimit; } if (undefined !== options.effectspeed) { options.effect_speed = options.effectspeed; delete options.effectspeed; } $.extend(settings, options); } /* Cache container as jQuery as object. */ $container = (settings.container === undefined || settings.container === window) ? $window : $(settings.container); /* Fire one scroll event per scroll. Not one scroll event per image. */ if (0 === settings.event.indexOf("scroll")) { $container.bind(settings.event, function(event) { return update(); }); } this.each(function() { var self = this; var $self = $(self); self.loaded = false; /* When appear is triggered load original image. */ $self.one("appear", function() { if (!this.loaded) { if (settings.appear) { var elements_left = elements.length; settings.appear.call(self, elements_left, settings); } $("") .bind("load", function() { $self .hide() .attr("src", $self.data(settings.data_attribute)) [settings.effect](settings.effect_speed); self.loaded = true; /* Remove image from array so it is not looped next time. */ var temp = $.grep(elements, function(element) { return !element.loaded; }); elements = $(temp); if (settings.load) { var elements_left = elements.length; settings.load.call(self, elements_left, settings); } }) .attr("src", $self.data(settings.data_attribute)); } }); /* When wanted event is triggered load original image */ /* by triggering appear. */ if (0 !== settings.event.indexOf("scroll")) { $self.bind(settings.event, function(event) { if (!self.loaded) { $self.trigger("appear"); } }); } }); /* Check if something appears when window is resized. */ $window.bind("resize", function(event) { update(); }); /* Force initial check if images should appear. */ update(); return this; }; /* Convenience methods in jQuery namespace. */ /* Use as $.belowthefold(element, {threshold : 100, container : window}) */ $.belowthefold = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.height() + $window.scrollTop(); } else { fold = $container.offset().top + $container.height(); } return fold <= $(element).offset().top - settings.threshold; }; $.rightoffold = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.width() + $window.scrollLeft(); } else { fold = $container.offset().left + $container.width(); } return fold <= $(element).offset().left - settings.threshold; }; $.abovethetop = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.scrollTop(); } else { fold = $container.offset().top; } return fold >= $(element).offset().top + settings.threshold + $(element).height(); }; $.leftofbegin = function(element, settings) { var fold; if (settings.container === undefined || settings.container === window) { fold = $window.scrollLeft(); } else { fold = $container.offset().left; } return fold >= $(element).offset().left + settings.threshold + $(element).width(); }; $.inviewport = function(element, settings) { return !$.rightofscreen(element, settings) && !$.leftofscreen(element, settings) && !$.belowthefold(element, settings) && !$.abovethetop(element, settings); }; /* Custom selectors for your convenience. */ /* Use as $("img:below-the-fold").something() */ $.extend($.expr[':'], { "below-the-fold" : function(a) { return $.belowthefold(a, {threshold : 0, container: window}); }, "above-the-top" : function(a) { return !$.belowthefold(a, {threshold : 0, container: window}); }, "right-of-screen": function(a) { return $.rightoffold(a, {threshold : 0, container: window}); }, "left-of-screen" : function(a) { return !$.rightoffold(a, {threshold : 0, container: window}); }, "in-viewport" : function(a) { return !$.inviewport(a, {threshold : 0, container: window}); }, /* Maintain BC for couple of versions. */ "above-the-fold" : function(a) { return !$.belowthefold(a, {threshold : 0, container: window}); }, "right-of-fold" : function(a) { return $.rightoffold(a, {threshold : 0, container: window}); }, "left-of-fold" : function(a) { return !$.rightoffold(a, {threshold : 0, container: window}); } }); })(jQuery, window); ================================================ FILE: docs/public/stylesheets/normalize.css ================================================ /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ /* ========================================================================== HTML5 display definitions ========================================================================== */ /* * Corrects `block` display not defined in IE 8/9. */ article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary { display: block; } /* * Corrects `inline-block` display not defined in IE 8/9. */ audio, canvas, video { display: inline-block; } /* * Prevents modern browsers from displaying `audio` without controls. * Remove excess height in iOS 5 devices. */ audio:not([controls]) { display: none; height: 0; } /* * Addresses styling for `hidden` attribute not present in IE 8/9. */ [hidden] { display: none; } /* ========================================================================== Base ========================================================================== */ /* * 1. Sets default font family to sans-serif. * 2. Prevents iOS text size adjust after orientation change, without disabling * user zoom. */ html { font-family: sans-serif; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ -ms-text-size-adjust: 100%; /* 2 */ } /* * Removes default margin. */ body { margin: 0; } /* ========================================================================== Links ========================================================================== */ /* * Addresses `outline` inconsistency between Chrome and other browsers. */ a:focus { outline: thin dotted; } /* * Improves readability when focused and also mouse hovered in all browsers. */ a:active, a:hover { outline: 0; } /* ========================================================================== Typography ========================================================================== */ /* * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, * Safari 5, and Chrome. */ h1 { font-size: 2em; } /* * Addresses styling not present in IE 8/9, Safari 5, and Chrome. */ abbr[title] { border-bottom: 1px dotted; } /* * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ b, strong { font-weight: bold; } /* * Addresses styling not present in Safari 5 and Chrome. */ dfn { font-style: italic; } /* * Addresses styling not present in IE 8/9. */ mark { background: #ff0; color: #000; } /* * Corrects font family set oddly in Safari 5 and Chrome. */ code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } /* * Improves readability of pre-formatted text in all browsers. */ pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } /* * Sets consistent quote types. */ q { quotes: "\201C" "\201D" "\2018" "\2019"; } /* * Addresses inconsistent and variable font size in all browsers. */ small { font-size: 80%; } /* * Prevents `sub` and `sup` affecting `line-height` in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } /* ========================================================================== Embedded content ========================================================================== */ /* * Removes border when inside `a` element in IE 8/9. */ img { border: 0; } /* * Corrects overflow displayed oddly in IE 9. */ svg:not(:root) { overflow: hidden; } /* ========================================================================== Figures ========================================================================== */ /* * Addresses margin not present in IE 8/9 and Safari 5. */ figure { margin: 0; } /* ========================================================================== Forms ========================================================================== */ /* * Define consistent border, margin, and padding. */ fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } /* * 1. Corrects color not being inherited in IE 8/9. * 2. Remove padding so people aren't caught out if they zero out fieldsets. */ legend { border: 0; /* 1 */ padding: 0; /* 2 */ } /* * 1. Corrects font family not being inherited in all browsers. * 2. Corrects font size not being inherited in all browsers. * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome */ button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } /* * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in * the UA stylesheet. */ button, input { line-height: normal; } /* * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * and `video` controls. * 2. Corrects inability to style clickable `input` types in iOS. * 3. Improves usability and consistency of cursor style between image-type * `input` and others. */ button, html input[type="button"], /* 1 */ input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } /* * Re-set default cursor for disabled elements. */ button[disabled], input[disabled] { cursor: default; } /* * 1. Addresses box sizing set to `content-box` in IE 8/9. * 2. Removes excess padding in IE 8/9. */ input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /* * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome * (include `-moz` to future-proof). */ input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } /* * Removes inner padding and search cancel button in Safari 5 and Chrome * on OS X. */ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /* * Removes inner padding and border in Firefox 4+. */ button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } /* * 1. Removes default vertical scrollbar in IE 8/9. * 2. Improves readability and alignment in all browsers. */ textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } /* ========================================================================== Tables ========================================================================== */ /* * Remove most spacing between table cells. */ table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: docs/search.js ================================================ (function() { var functions = document.querySelectorAll('[data-name]'); var sections = document.querySelectorAll('.searchable_section'); var searchInput = document.getElementById('function_filter'); function strIn(a, b) { a = a.toLowerCase(); b = b.toLowerCase(); return b.indexOf(a) >= 0; } function doesMatch(element) { var name = element.getAttribute('data-name'); var aliases = element.getAttribute('data-aliases') || ''; return strIn(searchInput.value, name) || strIn(searchInput.value, aliases); } function filterElement(element) { element.style.display = doesMatch(element) ? '' : 'none'; } function filterToc() { _.each(functions, filterElement); var emptySearch = searchInput.value === ''; // Hide the titles of empty sections _.each(sections, function(section) { var sectionFunctions = section.querySelectorAll('[data-name]'); var showSection = emptySearch || _.some(sectionFunctions, doesMatch); section.style.display = showSection ? '' : 'none'; }); } function gotoFirst() { var firstFunction = _.find(functions, doesMatch); if(firstFunction) { window.location.hash = firstFunction.lastChild.getAttribute('href'); searchInput.focus(); } } searchInput.addEventListener('input', filterToc, false); // Press "Enter" to jump to the first matching function searchInput.addEventListener('keypress', function(e) { if (e.which === 13) { gotoFirst(); } }); // Press "/" to search document.body.addEventListener('keyup', function(event) { if (191 === event.which) { searchInput.focus(); } }); }()); ================================================ FILE: docs/todos.html ================================================

This annotated source has moved to examples/todos/todos.html. You will be automatically redirected in two seconds.

================================================ FILE: examples/backbone.localStorage.js ================================================ /** * Backbone localStorage Adapter * Version 1.1.0 * * https://github.com/jeromegn/Backbone.localStorage */ (function (root, factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define(["underscore","backbone"], function(_, Backbone) { // Use global variables if the locals are undefined. return factory(_ || root._, Backbone || root.Backbone); }); } else { // RequireJS isn't being used. Assume underscore and backbone are loaded in script tags factory(_, Backbone); } }(this, function(_, Backbone) { // A simple module to replace `Backbone.sync` with *localStorage*-based // persistence. Models are given GUIDS, and saved into a JSON object. Simple // as that. // Hold reference to Underscore.js and Backbone.js in the closure in order // to make things work even if they are removed from the global namespace // Generate four random hex digits. function S4() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); }; // Generate a pseudo-GUID by concatenating random hexadecimal. function guid() { return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); }; // Our Store is represented by a single JS object in *localStorage*. Create it // with a meaningful name, like the name you'd give a table. // window.Store is deprecated, use Backbone.LocalStorage instead Backbone.LocalStorage = window.Store = function(name) { this.name = name; var store = this.localStorage().getItem(this.name); this.records = (store && store.split(",")) || []; }; _.extend(Backbone.LocalStorage.prototype, { // Save the current state of the **Store** to *localStorage*. save: function() { this.localStorage().setItem(this.name, this.records.join(",")); }, // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already // have an id of it's own. create: function(model) { if (!model.id) { model.id = guid(); model.set(model.idAttribute, model.id); } this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); this.records.push(model.id.toString()); this.save(); return this.find(model); }, // Update a model by replacing its copy in `this.data`. update: function(model) { this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save(); return this.find(model); }, // Retrieve a model from `this.data` by id. find: function(model) { return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id)); }, // Return the array of all models currently in storage. findAll: function() { return _(this.records).chain() .map(function(id){ return this.jsonData(this.localStorage().getItem(this.name+"-"+id)); }, this) .compact() .value(); }, // Delete a model from `this.data`, returning it. destroy: function(model) { if (model.isNew()) return false this.localStorage().removeItem(this.name+"-"+model.id); this.records = _.reject(this.records, function(id){ return id === model.id.toString(); }); this.save(); return model; }, localStorage: function() { return localStorage; }, // fix for "illegal access" error on Android when JSON.parse is passed null jsonData: function (data) { return data && JSON.parse(data); } }); // localSync delegate to the model or collection's // *localStorage* property, which should be an instance of `Store`. // window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) { var store = model.localStorage || model.collection.localStorage; var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it. try { switch (method) { case "read": resp = model.id != undefined ? store.find(model) : store.findAll(); break; case "create": resp = store.create(model); break; case "update": resp = store.update(model); break; case "delete": resp = store.destroy(model); break; } } catch(error) { if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0) errorMessage = "Private browsing is unsupported"; else errorMessage = error.message; } if (resp) { model.trigger("sync", model, resp, options); if (options && options.success) options.success(resp); if (syncDfd) syncDfd.resolve(resp); } else { errorMessage = errorMessage ? errorMessage : "Record Not Found"; if (options && options.error) options.error(errorMessage); if (syncDfd) syncDfd.reject(errorMessage); } // add compatibility with $.ajax // always execute callback for success and error if (options && options.complete) options.complete(resp); return syncDfd && syncDfd.promise(); }; Backbone.ajaxSync = Backbone.sync; Backbone.getSyncMethod = function(model) { if(model.localStorage || (model.collection && model.collection.localStorage)) { return Backbone.localSync; } return Backbone.ajaxSync; }; // Override 'Backbone.sync' to default to localSync, // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' Backbone.sync = function(method, model, options) { return Backbone.getSyncMethod(model).apply(this, [method, model, options]); }; return Backbone.LocalStorage; })); ================================================ FILE: examples/todos/index.html ================================================ Backbone.js Todos

Todos

    Double-click to edit a todo.
    Created by
    Jérôme Gravel-Niquet.
    Rewritten by: TodoMVC.
    ================================================ FILE: examples/todos/todos.css ================================================ html, body { margin: 0; padding: 0; } body { font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.4em; background: #eeeeee; color: #333333; width: 520px; margin: 0 auto; -webkit-font-smoothing: antialiased; } #todoapp { background: #fff; padding: 20px; margin-bottom: 40px; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; -webkit-border-radius: 0 0 5px 5px; -moz-border-radius: 0 0 5px 5px; -ms-border-radius: 0 0 5px 5px; -o-border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px; } #todoapp h1 { font-size: 36px; font-weight: bold; text-align: center; padding: 0 0 10px 0; } #todoapp input[type="text"] { width: 466px; font-size: 24px; font-family: inherit; line-height: 1.4em; border: 0; outline: none; padding: 6px; border: 1px solid #999999; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; } #todoapp input::-webkit-input-placeholder { font-style: italic; } #main { display: none; } #todo-list { margin: 10px 0; padding: 0; list-style: none; } #todo-list li { padding: 18px 20px 18px 0; position: relative; font-size: 24px; border-bottom: 1px solid #cccccc; } #todo-list li:last-child { border-bottom: none; } #todo-list li.done label { color: #777777; text-decoration: line-through; } #todo-list .destroy { position: absolute; right: 5px; top: 20px; display: none; cursor: pointer; width: 20px; height: 20px; background: url(destroy.png) no-repeat; } #todo-list li:hover .destroy { display: block; } #todo-list .destroy:hover { background-position: 0 -20px; } #todo-list li.editing { border-bottom: none; margin-top: -1px; padding: 0; } #todo-list li.editing:last-child { margin-bottom: -1px; } #todo-list li.editing .edit { display: block; width: 444px; padding: 13px 15px 14px 20px; margin: 0; } #todo-list li.editing .view { display: none; } #todo-list li .view label { word-break: break-word; } #todo-list li .edit { display: none; } #todoapp footer { display: none; margin: 0 -20px -20px -20px; overflow: hidden; color: #555555; background: #f4fce8; border-top: 1px solid #ededed; padding: 0 20px; line-height: 37px; -webkit-border-radius: 0 0 5px 5px; -moz-border-radius: 0 0 5px 5px; -ms-border-radius: 0 0 5px 5px; -o-border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px; } #clear-completed { float: right; line-height: 20px; text-decoration: none; background: rgba(0, 0, 0, 0.1); color: #555555; font-size: 11px; margin-top: 8px; margin-bottom: 8px; padding: 0 10px 1px; cursor: pointer; -webkit-border-radius: 12px; -moz-border-radius: 12px; -ms-border-radius: 12px; -o-border-radius: 12px; border-radius: 12px; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; } #clear-completed:hover { background: rgba(0, 0, 0, 0.15); -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; -ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; } #clear-completed:active { position: relative; top: 1px; } #todo-count span { font-weight: bold; } #instructions { margin: 10px auto; color: #777777; text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; text-align: center; } #instructions a { color: #336699; } #credits { margin: 30px auto; color: #999; text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; text-align: center; } #credits a { color: #888; } ================================================ FILE: examples/todos/todos.js ================================================ // An example Backbone application contributed by // [Jérôme Gravel-Niquet](http://jgn.me/). This demo uses a simple // [LocalStorage adapter](backbone.localStorage.html) // to persist Backbone models within your browser. // Load the application once the DOM is ready, using `jQuery.ready`: $(function(){ // Todo Model // ---------- // Our basic **Todo** model has `title`, `order`, and `done` attributes. var Todo = Backbone.Model.extend({ // Default attributes for the todo item. defaults: function() { return { title: "empty todo...", order: Todos.nextOrder(), done: false }; }, // Toggle the `done` state of this todo item. toggle: function() { this.save({done: !this.get("done")}); } }); // Todo Collection // --------------- // The collection of todos is backed by *localStorage* instead of a remote // server. var TodoList = Backbone.Collection.extend({ // Reference to this collection's model. model: Todo, // Save all of the todo items under the `"todos-backbone"` namespace. localStorage: new Backbone.LocalStorage("todos-backbone"), // Filter down the list of all todo items that are finished. done: function() { return this.where({done: true}); }, // Filter down the list to only todo items that are still not finished. remaining: function() { return this.where({done: false}); }, // We keep the Todos in sequential order, despite being saved by unordered // GUID in the database. This generates the next order number for new items. nextOrder: function() { if (!this.length) return 1; return this.last().get('order') + 1; }, // Todos are sorted by their original insertion order. comparator: 'order' }); // Create our global collection of **Todos**. var Todos = new TodoList; // Todo Item View // -------------- // The DOM element for a todo item... var TodoView = Backbone.View.extend({ //... is a list tag. tagName: "li", // Cache the template function for a single item. template: _.template($('#item-template').html()), // The DOM events specific to an item. events: { "click .toggle" : "toggleDone", "dblclick .view" : "edit", "click a.destroy" : "clear", "keypress .edit" : "updateOnEnter", "blur .edit" : "close" }, // The TodoView listens for changes to its model, re-rendering. Since there's // a one-to-one correspondence between a **Todo** and a **TodoView** in this // app, we set a direct reference on the model for convenience. initialize: function() { this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); }, // Re-render the titles of the todo item. render: function() { this.$el.html(this.template(this.model.toJSON())); this.$el.toggleClass('done', this.model.get('done')); this.input = this.$('.edit'); return this; }, // Toggle the `"done"` state of the model. toggleDone: function() { this.model.toggle(); }, // Switch this view into `"editing"` mode, displaying the input field. edit: function() { this.$el.addClass("editing"); this.input.focus(); }, // Close the `"editing"` mode, saving changes to the todo. close: function() { var value = this.input.val(); if (!value) { this.clear(); } else { this.model.save({title: value}); this.$el.removeClass("editing"); } }, // If you hit `enter`, we're through editing the item. updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); }, // Remove the item, destroy the model. clear: function() { this.model.destroy(); } }); // The Application // --------------- // Our overall **AppView** is the top-level piece of UI. var AppView = Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. el: $("#todoapp"), // Our template for the line of statistics at the bottom of the app. statsTemplate: _.template($('#stats-template').html()), // Delegated events for creating new items, and clearing completed ones. events: { "keypress #new-todo": "createOnEnter", "click #clear-completed": "clearCompleted", "click #toggle-all": "toggleAllComplete" }, // At initialization we bind to the relevant events on the `Todos` // collection, when items are added or changed. Kick things off by // loading any preexisting todos that might be saved in *localStorage*. initialize: function() { this.input = this.$("#new-todo"); this.allCheckbox = this.$("#toggle-all")[0]; this.listenTo(Todos, 'add', this.addOne); this.listenTo(Todos, 'reset', this.addAll); this.listenTo(Todos, 'all', this.render); this.footer = this.$('footer'); this.main = $('#main'); Todos.fetch(); }, // Re-rendering the App just means refreshing the statistics -- the rest // of the app doesn't change. render: function() { var done = Todos.done().length; var remaining = Todos.remaining().length; if (Todos.length) { this.main.show(); this.footer.show(); this.footer.html(this.statsTemplate({done: done, remaining: remaining})); } else { this.main.hide(); this.footer.hide(); } this.allCheckbox.checked = !remaining; }, // Add a single todo item to the list by creating a view for it, and // appending its element to the `
      `. addOne: function(todo) { var view = new TodoView({model: todo}); this.$("#todo-list").append(view.render().el); }, // Add all items in the **Todos** collection at once. addAll: function() { Todos.each(this.addOne, this); }, // If you hit return in the main input field, create new **Todo** model, // persisting it to *localStorage*. createOnEnter: function(e) { if (e.keyCode != 13) return; if (!this.input.val()) return; Todos.create({title: this.input.val()}); this.input.val(''); }, // Clear all done todo items, destroying their models. clearCompleted: function() { _.invoke(Todos.done(), 'destroy'); return false; }, toggleAllComplete: function () { var done = this.allCheckbox.checked; Todos.each(function (todo) { todo.save({'done': done}); }); } }); // Finally, we kick things off by creating the **App**. var App = new AppView; }); ================================================ FILE: index.html ================================================ Backbone.js

      Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

      The project is hosted on GitHub, and the annotated source code is available, as well as an online test suite, an example application, a list of tutorials and a long list of real-world projects that use Backbone. Backbone is available for use under the MIT software license.

      You can report bugs and discuss features on the GitHub issues page, or add pages to the wiki.

      Backbone is an open-source component of DocumentCloud.

      Downloads & Dependencies (Right-click, and use "Save As")

      Development Version (1.6.1) 72kb, Full source, tons of comments
      Production Version (1.6.1) 7.9kb, Packed and gzipped
      (Source Map)
      Edge Version (master) Unreleased, use at your own risk

      Backbone's only hard dependency is Underscore.js ( >= 1.8.3). For RESTful persistence and DOM manipulation with Backbone.View, include jQuery ( >= 1.11.0). (Mimics of the Underscore and jQuery APIs, such as Lodash and Zepto, will also tend to work, with varying degrees of compatibility.)

      Getting Started

      When working on a web application that involves a lot of JavaScript, one of the first things you learn is to stop tying your data to the DOM. It's all too easy to create JavaScript applications that end up as tangled piles of jQuery selectors and callbacks, all trying frantically to keep data in sync between the HTML UI, your JavaScript logic, and the database on your server. For rich client-side applications, a more structured approach is often helpful.

      With Backbone, you represent your data as Models, which can be created, validated, destroyed, and saved to the server. Whenever a UI action causes an attribute of a model to change, the model triggers a "change" event; all the Views that display the model's state can be notified of the change, so that they are able to respond accordingly, re-rendering themselves with the new information. In a finished Backbone app, you don't have to write the glue code that looks into the DOM to find an element with a specific id, and update the HTML manually — when the model changes, the views simply update themselves.

      Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript. In an ecosystem where overarching, decides-everything-for-you frameworks are commonplace, and many libraries require your site to be reorganized to suit their look, feel, and default behavior — Backbone should continue to be a tool that gives you the freedom to design the full experience of your web application.

      If you're new here, and aren't yet quite sure what Backbone is for, start by browsing the list of Backbone-based projects.

      Many of the code examples in this documentation are runnable, because Backbone is included on this page. Click the play button to execute them.

      Models and Views

      Model-View Separation.

      The single most important thing that Backbone can help you with is keeping your business logic separate from your user interface. When the two are entangled, change is hard; when logic doesn't depend on UI, your interface becomes easier to work with.

      Model
      • Orchestrates data and business logic.
      • Loads and saves data from the server.
      • Emits events when data changes.
      View
      • Listens for changes and renders UI.
      • Handles user input and interactivity.
      • Sends captured input to the model.

      A Model manages an internal table of data attributes, and triggers "change" events when any of its data is modified. Models handle syncing data with a persistence layer — usually a REST API with a backing database. Design your models as the atomic reusable objects containing all of the helpful functions for manipulating their particular bit of data. Models should be able to be passed around throughout your app, and used anywhere that bit of data is needed.

      A View is an atomic chunk of user interface. It often renders the data from a specific model, or number of models — but views can also be data-less chunks of UI that stand alone. Models should be generally unaware of views. Instead, views listen to the model "change" events, and react or re-render themselves appropriately.

      Collections

      Model Collections.

      A Collection helps you deal with a group of related models, handling the loading and saving of new models to the server and providing helper functions for performing aggregations or computations against a list of models. Aside from their own events, collections also proxy through all of the events that occur to models within them, allowing you to listen in one place for any change that might happen to any model in the collection.

      API Integration

      Backbone is pre-configured to sync with a RESTful API. Simply create a new Collection with the url of your resource endpoint:

      var Books = Backbone.Collection.extend({
        url: '/books'
      });
      

      The Collection and Model components together form a direct mapping of REST resources using the following methods:

      GET  /books/ .... collection.fetch();
      POST /books/ .... collection.create();
      GET  /books/1 ... model.fetch();
      PUT  /books/1 ... model.save();
      DEL  /books/1 ... model.destroy();
      

      When fetching raw JSON data from an API, a Collection will automatically populate itself with data formatted as an array, while a Model will automatically populate itself with data formatted as an object:

      [{"id": 1}] ..... populates a Collection with one model.
      {"id": 1} ....... populates a Model with one attribute.
      

      However, it's fairly common to encounter APIs that return data in a different format than what Backbone expects. For example, consider fetching a Collection from an API that returns the real data array wrapped in metadata:

      {
        "page": 1,
        "limit": 10,
        "total": 2,
        "books": [
          {"id": 1, "title": "Pride and Prejudice"},
          {"id": 4, "title": "The Great Gatsby"}
        ]
      }
      

      In the above example data, a Collection should populate using the "books" array rather than the root object structure. This difference is easily reconciled using a parse method that returns (or transforms) the desired portion of API data:

      var Books = Backbone.Collection.extend({
        url: '/books',
        parse: function(data) {
          return data.books;
        }
      });
      

      View Rendering

      View rendering.

      Each View manages the rendering and user interaction within its own DOM element. If you're strict about not allowing views to reach outside of themselves, it helps keep your interface flexible — allowing views to be rendered in isolation in any place where they might be needed.

      Backbone remains unopinionated about the process used to render View objects and their subviews into UI: you define how your models get translated into HTML (or SVG, or Canvas, or something even more exotic). It could be as prosaic as a simple Underscore template, or as fancy as the React virtual DOM. Some basic approaches to rendering views can be found in the Backbone primer.

      Routing with URLs

      Routing

      In rich web applications, we still want to provide linkable, bookmarkable, and shareable URLs to meaningful locations within an app. Use the Router to update the browser URL whenever the user reaches a new "place" in your app that they might want to bookmark or share. Conversely, the Router detects changes to the URL — say, pressing the "Back" button — and can tell your application exactly where you are now.

      Backbone.Events

      Events is a module that can be mixed in to any object, giving the object the ability to bind and trigger custom named events. Events do not have to be declared before they are bound, and may take passed arguments. For example:

      var object = {};
      
      _.extend(object, Backbone.Events);
      
      object.on("alert", function(msg) {
        alert("Triggered " + msg);
      });
      
      object.trigger("alert", "an event");
      

      For example, to make a handy event dispatcher that can coordinate events among different areas of your application: var dispatcher = _.clone(Backbone.Events)

      onobject.on(event, callback, [context])Alias: bind
      Bind a callback function to an object. The callback will be invoked whenever the event is fired. If you have a large number of different events on a page, the convention is to use colons to namespace them: "poll:start", or "change:selection". The event string may also be a space-delimited list of several events...

      book.on("change:title change:author", ...);
      

      Callbacks bound to the special "all" event will be triggered when any event occurs, and are passed the name of the event as the first argument. For example, to proxy all events from one object to another:

      proxy.on("all", function(eventName) {
        object.trigger(eventName);
      });
      

      All Backbone event methods also support an event map syntax, as an alternative to positional arguments:

      book.on({
        "change:author": authorPane.update,
        "change:title change:subtitle": titleView.update,
        "destroy": bookView.remove
      });
      

      To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).

      offobject.off([event], [callback], [context])Alias: unbind
      Remove a previously-bound callback function from an object. If no context is specified, all of the versions of the callback with different contexts will be removed. If no callback is specified, all callbacks for the event will be removed. If no event is specified, callbacks for all events will be removed.

      // Removes just the `onChange` callback.
      object.off("change", onChange);
      
      // Removes all "change" callbacks.
      object.off("change");
      
      // Removes the `onChange` callback for all events.
      object.off(null, onChange);
      
      // Removes all callbacks for `context` for all events.
      object.off(null, null, context);
      
      // Removes all callbacks on `object`.
      object.off();
      

      Note that calling model.off(), for example, will indeed remove all events on the model — including events that Backbone uses for internal bookkeeping.

      triggerobject.trigger(event, [*args])
      Trigger callbacks for the given event, or space-delimited list of events. Subsequent arguments to trigger will be passed along to the event callbacks.

      onceobject.once(event, callback, [context])
      Just like on, but causes the bound callback to fire only once before being removed. Handy for saying "the next time that X happens, do this". When multiple events are passed in using the space separated syntax, the event will fire once for every event you passed in, not once for a combination of all events

      listenToobject.listenTo(other, event, callback)
      Tell an object to listen to a particular event on an other object. The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.

      view.listenTo(model, 'change', view.render);
      

      stopListeningobject.stopListening([other], [event], [callback])
      Tell an object to stop listening to events. Either call stopListening with no arguments to have the object remove all of its registered callbacks ... or be more precise by telling it to remove just the events it's listening to on a specific object, or a specific event, or just a specific callback.

      view.stopListening();
      
      view.stopListening(model);
      

      listenToOnceobject.listenToOnce(other, event, callback)
      Just like listenTo, but causes the bound callback to fire only once before being removed.

      Catalog of Events
      Here's the complete list of built-in Backbone events, with arguments. You're also free to trigger your own events on Models, Collections and Views as you see fit. The Backbone object itself mixes in Events, and can be used to emit any global events that your application needs.

      • "add" (model, collection, options) — when a model is added to a collection.
      • "remove" (model, collection, options) — when a model is removed from a collection.
      • "update" (collection, options) — single event triggered after any number of models have been added, removed or changed in a collection.
      • "reset" (collection, options) — when the collection's entire contents have been reset.
      • "sort" (collection, options) — when the collection has been re-sorted.
      • "change" (model, options) — when a model's attributes have changed.
      • "changeId" (model, previousId, options) — when the model's id has been updated.
      • "change:[attribute]" (model, value, options) — when a specific attribute has been updated.
      • "destroy" (model, collection, options) — when a model is destroyed.
      • "request" (model_or_collection, xhr, options) — when a model or collection has started a request to the server.
      • "sync" (model_or_collection, response, options) — when a model or collection has been successfully synced with the server.
      • "error" (model_or_collection, xhr, options) — when a model's or collection's request to the server has failed.
      • "invalid" (model, error, options) — when a model's validation fails on the client.
      • "route:[name]" (params) — Fired by the router when a specific route is matched.
      • "route" (route, params) — Fired by the router when any route has been matched.
      • "route" (router, route, params) — Fired by history when any route has been matched.
      • "notfound" () — Fired by history when no route could be matched.
      • "all" — this special event fires for any triggered event, passing the event name as the first argument followed by all trigger arguments.

      Generally speaking, when calling a function that emits an event (model.set, collection.add, and so on...), if you'd like to prevent the event from being triggered, you may pass {silent: true} as an option. Note that this is rarely, perhaps even never, a good idea. Passing through a specific flag in the options for your event callback to look at, and choose to ignore, will usually work out better.

      Backbone.Model

      Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control. You extend Backbone.Model with your domain-specific methods, and Model provides a basic set of functionality for managing changes.

      The following is a contrived example, but it demonstrates defining a model with a custom method, setting an attribute, and firing an event keyed to changes in that specific attribute. After running this code once, sidebar will be available in your browser's console, so you can play around with it.

      var Sidebar = Backbone.Model.extend({
        promptColor: function() {
          var cssColor = prompt("Please enter a CSS color:");
          this.set({color: cssColor});
        }
      });
      
      window.sidebar = new Sidebar;
      
      sidebar.on('change:color', function(model, color) {
        $('#sidebar').css({background: color});
      });
      
      sidebar.set({color: 'white'});
      
      sidebar.promptColor();
      

      extendBackbone.Model.extend(properties, [classProperties])
      To create a Model class of your own, you extend Backbone.Model and provide instance properties, as well as optional classProperties to be attached directly to the constructor function.

      extend correctly sets up the prototype chain, so subclasses created with extend can be further extended and subclassed as far as you like.

      var Note = Backbone.Model.extend({
      
        initialize: function() { ... },
      
        author: function() { ... },
      
        coordinates: function() { ... },
      
        allowedToEdit: function(account) {
          return true;
        }
      
      });
      
      var PrivateNote = Note.extend({
      
        allowedToEdit: function(account) {
          return account.owns(this);
        }
      
      });
      

      Brief aside on super: JavaScript does not provide a simple way to call super — the function of the same name defined higher on the prototype chain. If you override a core function like set, or save, and you want to invoke the parent object's implementation, you'll have to explicitly call it, along these lines:

      var Note = Backbone.Model.extend({
        set: function(attributes, options) {
          Backbone.Model.prototype.set.apply(this, arguments);
          ...
        }
      });
      

      preinitializenew Model([attributes], [options])
      For use with models as ES classes. If you define a preinitialize method, it will be invoked when the Model is first created, before any instantiation logic is run for the Model.

      class Country extends Backbone.Model {
          preinitialize({countryCode}) {
            this.name = COUNTRY_NAMES[countryCode];
          }
      
          initialize() { ... }
      }
      

      constructor / initializenew Model([attributes], [options])
      When creating an instance of a model, you can pass in the initial values of the attributes, which will be set on the model. If you define an initialize function, it will be invoked when the model is created.

      new Book({
        title: "One Thousand and One Nights",
        author: "Scheherazade"
      });
      

      In rare cases, if you're looking to get fancy, you may want to override constructor, which allows you to replace the actual constructor function for your model.

      var Library = Backbone.Model.extend({
        constructor: function() {
          this.books = new Books();
          Backbone.Model.apply(this, arguments);
        },
        parse: function(data, options) {
          this.books.reset(data.books);
          return data.library;
        }
      });
      

      If you pass a {collection: ...} as the options, the model gains a collection property that will be used to indicate which collection the model belongs to, and is used to help compute the model's url. The model.collection property is normally created automatically when you first add a model to a collection. Note that the reverse is not true, as passing this option to the constructor will not automatically add the model to the collection. Useful, sometimes.

      If {parse: true} is passed as an option, the attributes will first be converted by parse before being set on the model.

      getmodel.get(attribute)
      Get the current value of an attribute from the model. For example: note.get("title")

      setmodel.set(attributes, [options])
      Set a hash of attributes (one or many) on the model. If any of the attributes change the model's state, a "change" event will be triggered on the model. Change events for specific attributes are also triggered, and you can bind to those as well, for example: change:title, and change:content. You may also pass individual keys and values.

      note.set({title: "March 20", content: "In his eyes she eclipses..."});
      
      book.set("title", "A Scandal in Bohemia");
      

      escapemodel.escape(attribute)
      Similar to get, but returns the HTML-escaped version of a model's attribute. If you're interpolating data from the model into HTML, using escape to retrieve attributes will prevent XSS attacks.

      var hacker = new Backbone.Model({
        name: "<script>alert('xss')</script>"
      });
      
      alert(hacker.escape('name'));
      

      hasmodel.has(attribute)
      Returns true if the attribute is set to a non-null or non-undefined value.

      if (note.has("title")) {
        ...
      }
      

      unsetmodel.unset(attribute, [options])
      Remove an attribute by deleting it from the internal attributes hash. Fires a "change" event unless silent is passed as an option.

      clearmodel.clear([options])
      Removes all attributes from the model, including the id attribute. Fires a "change" event unless silent is passed as an option.

      idmodel.id
      A special property of models, the id is an arbitrary string (integer id or UUID). If you set the id in the attributes hash, it will be copied onto the model as a direct property. model.id should not be manipulated directly, it should be modified only via model.set('id', …). Models can be retrieved by id from collections, and the id is used to generate model URLs by default.

      idAttributemodel.idAttribute
      A model's unique identifier is stored under the id attribute. If you're directly communicating with a backend (CouchDB, MongoDB) that uses a different unique key, you may set a Model's idAttribute to transparently map from that key to id. If you set idAttribute, you may also want to override cidPrefix.

      var Meal = Backbone.Model.extend({
        idAttribute: "_id"
      });
      
      var cake = new Meal({ _id: 1, name: "Cake" });
      alert("Cake id: " + cake.id);
      

      cidmodel.cid
      A special property of models, the cid or client id is a unique identifier automatically assigned to all models when they're first created. Client ids are handy when the model has not yet been saved to the server, and does not yet have its eventual true id, but already needs to be visible in the UI.

      cidPrefixmodel.cidPrefix
      If your model has an id that is anything other than an integer or a UUID, there is the possibility that it might collide with its cid. To prevent this, you can override the prefix that cids start with.

      // If both lengths are 2, refresh the page before running this example.
      var clashy = new Backbone.Collection([
        {id: 'c2'},
        {id: 'c1'},
      ]);
      alert('clashy length: ' + clashy.length);
      
      var ClashFree = Backbone.Model.extend({cidPrefix: 'm'});
      var clashless = new Backbone.Collection([
        {id: 'c3'},
        {id: 'c2'},
      ], {model: ClashFree});
      alert('clashless length: ' + clashless.length);
      

      attributesmodel.attributes
      The attributes property is the internal hash containing the model's state — usually (but not necessarily) a form of the JSON object representing the model data on the server. It's often a straightforward serialization of a row from the database, but it could also be client-side computed state.

      Please use set to update the attributes instead of modifying them directly. If you'd like to retrieve and munge a copy of the model's attributes, use _.clone(model.attributes) instead.

      Due to the fact that Events accepts space separated lists of events, attribute names should not include spaces.

      changedmodel.changed
      The changed property is the internal hash containing all the attributes that have changed since its last set. Please do not update changed directly since its state is internally maintained by set. A copy of changed can be acquired from changedAttributes.

      defaultsmodel.defaults or model.defaults()
      The defaults hash (or function) can be used to specify the default attributes for your model. When creating an instance of the model, any unspecified attributes will be set to their default value.

      var Meal = Backbone.Model.extend({
        defaults: {
          "appetizer":  "caesar salad",
          "entree":     "ravioli",
          "dessert":    "cheesecake"
        }
      });
      
      alert("Dessert will be " + (new Meal).get('dessert'));
      

      Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.

      If you set a value for the model’s idAttribute, you should define the defaults as a function that returns a different, universally unique id on every invocation. Not doing so would likely prevent an instance of Backbone.Collection from correctly identifying model hashes and is almost certainly a mistake, unless you never add instances of the model class to a collection.

      toJSONmodel.toJSON([options])
      Return a shallow copy of the model's attributes for JSON stringification. This can be used for persistence, serialization, or for augmentation before being sent to the server. The name of this method is a bit confusing, as it doesn't actually return a JSON string — but I'm afraid that it's the way that the JavaScript API for JSON.stringify works.

      var artist = new Backbone.Model({
        firstName: "Wassily",
        lastName: "Kandinsky"
      });
      
      artist.set({birthday: "December 16, 1866"});
      
      alert(JSON.stringify(artist));
      

      syncmodel.sync(method, model, [options])
      Uses Backbone.sync to persist the state of a model to the server. Can be overridden for custom behavior.

      fetchmodel.fetch([options])
      Merges the model's state with attributes fetched from the server by delegating to Backbone.sync. Returns a jqXHR. Useful if the model has never been populated with data, or if you'd like to ensure that you have the latest server state. Triggers a "change" event if the server's state differs from the current attributes. fetch accepts success and error callbacks in the options hash, which are both passed (model, response, options) as arguments.

      // Poll every 10 seconds to keep the channel model up-to-date.
      setInterval(function() {
        channel.fetch();
      }, 10000);
      

      savemodel.save([attributes], [options])
      Save a model to your database (or alternative persistence layer), by delegating to Backbone.sync. Returns a jqXHR if validation is successful and false otherwise. The attributes hash (as in set) should contain the attributes you'd like to change — keys that aren't mentioned won't be altered — but, a complete representation of the resource will be sent to the server. As with set, you may pass individual keys and values instead of a hash. If the model has a validate method, and validation fails, the model will not be saved. If the model isNew, the save will be a "create" (HTTP POST), if the model already exists on the server, the save will be an "update" (HTTP PUT).

      If instead, you'd only like the changed attributes to be sent to the server, call model.save(attrs, {patch: true}). You'll get an HTTP PATCH request to the server with just the passed-in attributes.

      Calling save with new attributes will cause a "change" event immediately, a "request" event as the Ajax request begins to go to the server, and a "sync" event after the server has acknowledged the successful change. Pass {wait: true} if you'd like to wait for the server before setting the new attributes on the model.

      In the following example, notice how our overridden version of Backbone.sync receives a "create" request the first time the model is saved and an "update" request the second time.

      Backbone.sync = function(method, model) {
        alert(method + ": " + JSON.stringify(model));
        model.set('id', 1);
      };
      
      var book = new Backbone.Model({
        title: "The Rough Riders",
        author: "Theodore Roosevelt"
      });
      
      book.save();
      
      book.save({author: "Teddy"});
      

      save accepts success and error callbacks in the options hash, which will be passed the arguments (model, response, options). If a server-side validation fails, return a non-200 HTTP response code, along with an error response in text or JSON.

      book.save("author", "F.D.R.", {error: function(){ ... }});
      

      destroymodel.destroy([options])
      Destroys the model on the server by delegating an HTTP DELETE request to Backbone.sync. Returns a jqXHR object, or false if the model isNew. Accepts success and error callbacks in the options hash, which will be passed (model, response, options). Triggers a "destroy" event on the model, which will bubble up through any collections that contain it, a "request" event as it begins the Ajax request to the server, and a "sync" event, after the server has successfully acknowledged the model's deletion. Pass {wait: true} if you'd like to wait for the server to respond before removing the model from the collection.

      book.destroy({success: function(model, response) {
        ...
      }});
      

      Underscore Methods (9)
      Backbone proxies to Underscore.js to provide 9 object functions on Backbone.Model. They aren't all documented here, but you can take a look at the Underscore documentation for the full details…

      user.pick('first_name', 'last_name', 'email');
      
      chapters.keys().join(', ');
      

      validatemodel.validate(attributes, options)
      This method is left undefined and you're encouraged to override it with any custom validation logic you have that can be performed in JavaScript. If the attributes are valid, don't return anything from validate; if they are invalid return an error of your choosing. It can be as simple as a string error message to be displayed, or a complete error object that describes the error programmatically.

      By default save checks validate before setting any attributes but you may also tell set to validate the new attributes by passing {validate: true} as an option. The validate method receives the model attributes as well as any options passed to set or save, if validate returns an error, save does not continue, the model attributes are not modified on the server, an "invalid" event is triggered, and the validationError property is set on the model with the value returned by this method.

      var Chapter = Backbone.Model.extend({
        validate: function(attrs, options) {
          if (attrs.end < attrs.start) {
            return "can't end before it starts";
          }
        }
      });
      
      var one = new Chapter({
        title : "Chapter One: The Beginning"
      });
      
      one.on("invalid", function(model, error) {
        alert(model.get("title") + " " + error);
      });
      
      one.save({
        start: 15,
        end:   10
      });
      

      "invalid" events are useful for providing coarse-grained error messages at the model or collection level.

      validationErrormodel.validationError
      The value returned by validate during the last failed validation.

      isValidmodel.isValid(options)
      Run validate to check the model state.

      The validate method receives the model attributes as well as any options passed to isValid, if validate returns an error an "invalid" event is triggered, and the error is set on the model in the validationError property.

      var Chapter = Backbone.Model.extend({
        validate: function(attrs, options) {
          if (attrs.end < attrs.start) {
            return "can't end before it starts";
          }
        }
      });
      
      var one = new Chapter({
        title : "Chapter One: The Beginning"
      });
      
      one.set({
        start: 15,
        end:   10
      });
      
      if (!one.isValid()) {
        alert(one.get("title") + " " + one.validationError);
      }
      

      urlmodel.url()
      Returns the relative URL where the model's resource would be located on the server. If your models are located somewhere else, override this method with the correct logic. Generates URLs of the form: "[collection.url]/[id]" by default, but you may override by specifying an explicit urlRoot if the model's collection shouldn't be taken into account.

      Delegates to Collection#url to generate the URL, so make sure that you have it defined, or a urlRoot property, if all models of this class share a common root URL. A model with an id of 101, stored in a Backbone.Collection with a url of "/documents/7/notes", would have this URL: "/documents/7/notes/101"

      urlRootmodel.urlRoot or model.urlRoot()
      Specify a urlRoot if you're using a model outside of a collection, to enable the default url function to generate URLs based on the model id. "[urlRoot]/id"
      Normally, you won't need to define this. Note that urlRoot may also be a function.

      var Book = Backbone.Model.extend({urlRoot : '/books'});
      
      var solaris = new Book({id: "1083-lem-solaris"});
      
      alert(solaris.url());
      

      parsemodel.parse(response, options)
      parse is called whenever a model's data is returned by the server, in fetch, and save. The function is passed the raw response object, and should return the attributes hash to be set on the model. The default implementation is a no-op, simply passing through the JSON response. Override this if you need to work with a preexisting API, or better namespace your responses.

      If you're working with a Rails backend that has a version prior to 3.1, you'll notice that its default to_json implementation includes a model's attributes under a namespace. To disable this behavior for seamless Backbone integration, set:

      ActiveRecord::Base.include_root_in_json = false
      

      clonemodel.clone()
      Returns a new instance of the model with identical attributes.

      isNewmodel.isNew()
      Has this model been saved to the server yet? If the model does not yet have an id, it is considered to be new.

      hasChangedmodel.hasChanged([attribute])
      Has the model changed since its last set? If an attribute is passed, returns true if that specific attribute has changed.

      Note that this method, and the following change-related ones, are only useful during the course of a "change" event.

      book.on("change", function() {
        if (book.hasChanged("title")) {
          ...
        }
      });
      

      changedAttributesmodel.changedAttributes([attributes])
      Retrieve a hash of only the model's attributes that have changed since the last set, or false if there are none. Optionally, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model. This can be used to figure out which portions of a view should be updated, or what calls need to be made to sync the changes to the server.

      previousmodel.previous(attribute)
      During a "change" event, this method can be used to get the previous value of a changed attribute.

      var bill = new Backbone.Model({
        name: "Bill Smith"
      });
      
      bill.on("change:name", function(model, name) {
        alert("Changed name from " + bill.previous("name") + " to " + name);
      });
      
      bill.set({name : "Bill Jones"});
      

      previousAttributesmodel.previousAttributes()
      Return a copy of the model's previous attributes. Useful for getting a diff between versions of a model, or getting back to a valid state after an error occurs.

      Backbone.Collection

      Collections are ordered sets of models. You can bind "change" events to be notified when any model in the collection has been modified, listen for "add" and "remove" events, fetch the collection from the server, and use a full suite of Underscore.js methods.

      Any event that is triggered on a model in a collection will also be triggered on the collection directly, for convenience. This allows you to listen for changes to specific attributes in any model in a collection, for example: documents.on("change:selected", ...)

      extendBackbone.Collection.extend(properties, [classProperties])
      To create a Collection class of your own, extend Backbone.Collection, providing instance properties, as well as optional classProperties to be attached directly to the collection's constructor function.

      modelcollection.model([attrs], [options])
      Override this property to specify the model class that the collection contains. If defined, you can pass raw attributes objects (and arrays) and options to add, create, and reset, and the attributes will be converted into a model of the proper type using the provided options, if any.

      var Library = Backbone.Collection.extend({
        model: Book
      });
      

      A collection can also contain polymorphic models by overriding this property with a constructor that returns a model.

      var Library = Backbone.Collection.extend({
      
        model: function(attrs, options) {
          if (condition) {
            return new PublicDocument(attrs, options);
          } else {
            return new PrivateDocument(attrs, options);
          }
        }
      
      });
      

      modelIdcollection.modelId(attrs, idAttribute)
      Override this method to return the value the collection will use to identify a model given its attributes. Useful for combining models from multiple tables with different idAttribute values into a single collection.

      By default returns the value of the given idAttribute within the attrs, or failing that, id. If your collection uses a model factory and the id ranges of those models might collide, you must override this method.

      var Library = Backbone.Collection.extend({
        modelId: function(attrs) {
          return attrs.type + attrs.id;
        }
      });
      
      var library = new Library([
        {type: 'dvd', id: 1},
        {type: 'vhs', id: 1}
      ]);
      
      var dvdId = library.get('dvd1').id;
      var vhsId = library.get('vhs1').id;
      alert('dvd: ' + dvdId + ', vhs: ' + vhsId);
      

      preinitializenew Backbone.Collection([models], [options])
      For use with collections as ES classes. If you define a preinitialize method, it will be invoked when the Collection is first created and before any instantiation logic is run for the Collection.

      class Library extends Backbone.Collection {
        preinitialize() {
          this.on("add", function() {
            console.log("Add model event got fired!");
          });
        }
      }
      

      constructor / initializenew Backbone.Collection([models], [options])
      When creating a Collection, you may choose to pass in the initial array of models. The collection's comparator may be included as an option. Passing false as the comparator option will prevent sorting. If you define an initialize function, it will be invoked when the collection is created. There are a couple of options that, if provided, are attached to the collection directly: model and comparator.
      Pass null for models to create an empty Collection with options.

      var tabs = new TabSet([tab1, tab2, tab3]);
      var spaces = new Backbone.Collection(null, {
        model: Space
      });
      

      If {parse: true} is passed as an option, the attributes will first be converted by parse before being set on the collection.

      modelscollection.models
      Raw access to the JavaScript array of models inside of the collection. Usually you'll want to use get, at, or the Underscore methods to access model objects, but occasionally a direct reference to the array is desired.

      toJSONcollection.toJSON([options])
      Return an array containing the attributes hash of each model (via toJSON) in the collection. This can be used to serialize and persist the collection as a whole. The name of this method is a bit confusing, because it conforms to JavaScript's JSON API.

      var collection = new Backbone.Collection([
        {name: "Tim", age: 5},
        {name: "Ida", age: 26},
        {name: "Rob", age: 55}
      ]);
      
      alert(JSON.stringify(collection));
      

      synccollection.sync(method, collection, [options])
      Uses Backbone.sync to persist the state of a collection to the server. Can be overridden for custom behavior.

      Underscore Methods (46)
      Backbone proxies to Underscore.js to provide 46 iteration functions on Backbone.Collection. They aren't all documented here, but you can take a look at the Underscore documentation for the full details…

      Most methods can take an object or string to support model-attribute-style predicates or a function that receives the model instance as an argument.

      books.each(function(book) {
        book.publish();
      });
      
      var titles = books.map("title");
      
      var publishedBooks = books.filter({published: true});
      
      var alphabetical = books.sortBy(function(book) {
        return book.author.get("name").toLowerCase();
      });
      
      var randomThree = books.sample(3);
      

      addcollection.add(models, [options])
      Add a model (or an array of models) to the collection, firing an "add" event for each model, and an "update" event afterwards. This is a variant of set with the same options and return value, but it always adds and never removes. If you're adding models to the collection that are already in the collection, they'll be ignored, unless you pass {merge: true}, in which case their attributes will be merged into the corresponding models, firing any appropriate "change" events.

      var ships = new Backbone.Collection;
      
      ships.on("add", function(ship) {
        alert("Ahoy " + ship.get("name") + "!");
      });
      
      ships.add([
        {name: "Flying Dutchman"},
        {name: "Black Pearl"}
      ]);
      

      Note that adding the same model (a model with the same id) to a collection more than once
      is a no-op.

      removecollection.remove(models, [options])
      Remove a model (or an array of models) from the collection, and return them. Each model can be a Model instance, an id string or a JS object, any value acceptable as the id argument of collection.get. Fires a "remove" event for each model, and a single "update" event afterwards, unless {silent: true} is passed. The model's index before removal is available to listeners as options.index.

      resetcollection.reset([models], [options])
      Adding and removing models one at a time is all well and good, but sometimes you have so many models to change that you'd rather just update the collection in bulk. Use reset to replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event on completion, and without triggering any add or remove events on any models. Returns the newly-set models. For convenience, within a "reset" event, the list of any previous models is available as options.previousModels.
      Pass null for models to empty your Collection with options.

      Here's an example using reset to bootstrap a collection during initial page load, in a Rails application:

      <script>
        var accounts = new Backbone.Collection;
        accounts.reset(<%= @accounts.to_json %>);
      </script>
      

      Calling collection.reset() without passing any models as arguments will empty the entire collection.

      setcollection.set(models, [options])
      The set method performs a "smart" update of the collection with the passed list of models. If a model in the list isn't yet in the collection it will be added; if the model is already in the collection its attributes will be merged; and if the collection contains any models that aren't present in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as this happens, with a single "update" event at the end. Returns the touched models in the collection. If you'd like to customize this behavior, you can change it with options: {add: false}, {remove: false}, or {merge: false}.

      If a model property is defined, you may also pass raw attributes objects and options, and have them be vivified as instances of the model using the provided options. If you set a comparator, the collection will automatically sort itself and trigger a "sort" event, unless you pass {sort: false} or use the {at: index} option. Pass {at: index} to splice the model(s) into the collection at the specified index.

      var vanHalen = new Backbone.Collection([eddie, alex, stone, roth]);
      
      vanHalen.set([eddie, alex, stone, hagar]);
      
      // Fires a "remove" event for roth, and an "add" event for "hagar".
      // Updates any of stone, alex, and eddie's attributes that may have
      // changed over the years.
      

      getcollection.get(id)
      Get a model from a collection, specified by an id, a cid, or by passing in a model.

      var book = library.get(110);
      

      atcollection.at(index)
      Get a model from a collection, specified by index. Useful if your collection is sorted, and if your collection isn't sorted, at will still retrieve models in insertion order. When passed a negative index, it will retrieve the model from the back of the collection.

      pushcollection.push(model, [options])
      Like add, but always adds a model at the end of the collection and never sorts.

      popcollection.pop([options])
      Remove and return the last model from a collection. Takes the same options as remove.

      unshiftcollection.unshift(model, [options])
      Like add, but always adds a model at the beginning of the collection and never sorts.

      shiftcollection.shift([options])
      Remove and return the first model from a collection. Takes the same options as remove.

      slicecollection.slice(begin, end)
      Return a shallow copy of this collection's models, using the same options as native Array#slice.

      lengthcollection.length
      Like an array, a Collection maintains a length property, counting the number of models it contains.

      comparatorcollection.comparator
      By default there is no comparator for a collection. If you define a comparator, it will be used to sort the collection any time a model is added. A comparator can be defined as a sortBy (pass a function that takes a single argument), as a sort (pass a comparator function that expects two arguments), or as a string indicating the attribute to sort by.

      "sortBy" comparator functions take a model and return a numeric or string value by which the model should be ordered relative to others. "sort" comparator functions take two models, and return -1 if the first model should come before the second, 0 if they are of the same rank and 1 if the first model should come after. Note that Backbone depends on the arity of your comparator function to determine between the two styles, so be careful if your comparator function is bound.

      Note how even though all of the chapters in this example are added backwards, they come out in the proper order:

      var Chapter  = Backbone.Model;
      var chapters = new Backbone.Collection;
      
      chapters.comparator = 'page';
      
      chapters.add(new Chapter({page: 9, title: "The End"}));
      chapters.add(new Chapter({page: 5, title: "The Middle"}));
      chapters.add(new Chapter({page: 1, title: "The Beginning"}));
      
      alert(chapters.pluck('title'));
      

      Collections with a comparator will not automatically re-sort if you later change model attributes, so you may wish to call sort after changing model attributes that would affect the order.

      sortcollection.sort([options])
      Force a collection to re-sort itself. Note that a collection with a comparator will sort itself automatically whenever a model is added. To disable sorting when adding a model, pass {sort: false} to add. Calling sort triggers a "sort" event on the collection.

      pluckcollection.pluck(attribute)
      Pluck an attribute from each model in the collection. Equivalent to calling map and returning a single attribute from the iterator.

      var stooges = new Backbone.Collection([
        {name: "Curly"},
        {name: "Larry"},
        {name: "Moe"}
      ]);
      
      var names = stooges.pluck("name");
      
      alert(JSON.stringify(names));
      

      wherecollection.where(attributes)
      Return an array of all the models in a collection that match the passed attributes. Useful for simple cases of filter.

      var friends = new Backbone.Collection([
        {name: "Athos",      job: "Musketeer"},
        {name: "Porthos",    job: "Musketeer"},
        {name: "Aramis",     job: "Musketeer"},
        {name: "d'Artagnan", job: "Guard"},
      ]);
      
      var musketeers = friends.where({job: "Musketeer"});
      
      alert(musketeers.length);
      

      findWherecollection.findWhere(attributes)
      Just like where, but directly returns only the first model in the collection that matches the passed attributes. If no model matches returns undefined.

      urlcollection.url or collection.url()
      Set the url property (or function) on a collection to reference its location on the server. Models within the collection will use url to construct URLs of their own.

      var Notes = Backbone.Collection.extend({
        url: '/notes'
      });
      
      // Or, something more sophisticated:
      
      var Notes = Backbone.Collection.extend({
        url: function() {
          return this.document.url() + '/notes';
        }
      });
      

      parsecollection.parse(response, options)
      parse is called by Backbone whenever a collection's models are returned by the server, in fetch. The function is passed the raw response object, and should return the array of model attributes to be added to the collection. The default implementation is a no-op, simply passing through the JSON response. Override this if you need to work with a preexisting API, or better namespace your responses.

      var Tweets = Backbone.Collection.extend({
        // The Twitter Search API returns tweets under "results".
        parse: function(response) {
          return response.results;
        }
      });
      

      clonecollection.clone()
      Returns a new instance of the collection with an identical list of models.

      fetchcollection.fetch([options])
      Fetch the default set of models for this collection from the server, setting them on the collection when they arrive. The options hash takes success and error callbacks which will both be passed (collection, response, options) as arguments. When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true}, in which case the collection will be (efficiently) reset. Delegates to Backbone.sync under the covers for custom persistence strategies and returns a jqXHR. The server handler for fetch requests should return a JSON array of models.

      Backbone.sync = function(method, model) {
        alert(method + ": " + model.url);
      };
      
      var accounts = new Backbone.Collection;
      accounts.url = '/accounts';
      
      accounts.fetch();
      

      The behavior of fetch can be customized by using the available set options. For example, to fetch a collection, getting an "add" event for every new model, and a "change" event for every changed existing model, without removing anything: collection.fetch({remove: false})

      jQuery.ajax options can also be passed directly as fetch options, so to fetch a specific page of a paginated collection: Documents.fetch({data: {page: 3}})

      Note that fetch should not be used to populate collections on page load — all models needed at load time should already be bootstrapped in to place. fetch is intended for lazily-loading models for interfaces that are not needed immediately: for example, documents with collections of notes that may be toggled open and closed.

      createcollection.create(attributes, [options])
      Convenience to create a new instance of a model within a collection. Equivalent to instantiating a model with a hash of attributes, saving the model to the server, and adding the model to the set after being successfully created. Returns the new model. If client-side validation failed, the model will be unsaved, with validation errors. In order for this to work, you should set the model property of the collection. The create method can accept either an attributes hash and options to be passed down during model instantiation or an existing, unsaved model object.

      Creating a model will cause an immediate "add" event to be triggered on the collection, a "request" event as the new model is sent to the server, as well as a "sync" event, once the server has responded with the successful creation of the model. Pass {wait: true} if you'd like to wait for the server before adding the new model to the collection.

      var Library = Backbone.Collection.extend({
        model: Book
      });
      
      var nypl = new Library;
      
      var othello = nypl.create({
        title: "Othello",
        author: "William Shakespeare"
      });
      

      mixinBackbone.Collection.mixin(properties)
      mixin provides a way to enhance the base Backbone.Collection and any collections which extend it. This can be used to add generic methods (e.g. additional Underscore Methods).

      Backbone.Collection.mixin({
        sum: function(models, iteratee) {
          return _.reduce(models, function(s, m) {
            return s + iteratee(m);
          }, 0);
        }
      });
      
      var cart = new Backbone.Collection([
        {price: 16, name: 'monopoly'},
        {price: 5, name: 'deck of cards'},
        {price: 20, name: 'chess'}
      ]);
      
      var cost = cart.sum('price');
      

      Backbone.Router

      Web applications often provide linkable, bookmarkable, shareable URLs for important locations in the app. Until recently, hash fragments (#page) were used to provide these permalinks, but with the arrival of the History API, it's now possible to use standard URLs (/page). Backbone.Router provides methods for routing client-side pages, and connecting them to actions and events. For browsers which don't yet support the History API, the Router handles graceful fallback and transparent translation to the fragment version of the URL.

      During page load, after your application has finished creating all of its routers, be sure to call Backbone.history.start() or Backbone.history.start({pushState: true}) to route the initial URL.

      extendBackbone.Router.extend(properties, [classProperties])
      Get started by creating a custom router class. Define action functions that are triggered when certain URL fragments are matched, and provide a routes hash that pairs routes to actions. Note that you'll want to avoid using a leading slash in your route definitions:

      var Workspace = Backbone.Router.extend({
      
        routes: {
          "help":                 "help",    // #help
          "search/:query":        "search",  // #search/kiwis
          "search/:query/p:page": "search"   // #search/kiwis/p7
        },
      
        help: function() {
          ...
        },
      
        search: function(query, page) {
          ...
        }
      
      });
      

      routesrouter.routes
      The routes hash maps URLs with parameters to functions on your router (or just direct function definitions, if you prefer), similar to the View's events hash. Routes can contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components. Part of a route can be made optional by surrounding it in parentheses (/:optional).

      For example, a route of "search/:query/p:page" will match a fragment of #search/obama/p2, passing "obama" and "2" to the action as positional arguments.

      A route of "file/*path" will match #file/folder/file.txt, passing "folder/file.txt" to the action.

      A route of "docs/:section(/:subsection)" will match #docs/faq and #docs/faq/installing, passing "faq" to the action in the first case, and passing "faq" and "installing" to the action in the second.

      A nested optional route of "docs(/:section)(/:subsection)" will match #docs, #docs/faq, and #docs/faq/installing, passing "faq" to the action in the second case, and passing "faq" and "installing" to the action in the third.

      Trailing slashes are treated as part of the URL, and (correctly) treated as a unique route when accessed. docs and docs/ will fire different callbacks. If you can't avoid generating both types of URLs, you can define a "docs(/)" matcher to capture both cases.

      When the visitor presses the back button, or enters a URL, and a particular route is matched, the name of the action will be fired as an event, so that other objects can listen to the router, and be notified. In the following example, visiting #help/uploading will fire a route:help event from the router.

      routes: {
        "help/:page":         "help",
        "download/*path":     "download",
        "folder/:name":       "openFolder",
        "folder/:name-:mode": "openFolder"
      }
      
      router.on("route:help", function(page) {
        ...
      });
      

      preinitializenew Backbone.Router([options])
      For use with routers as ES classes. If you define a preinitialize method, it will be invoked when the Router is first created and before any instantiation logic is run for the Router.

      class Router extends Backbone.Router {
        preinitialize() {
          // Override execute method
          this.execute = function(callback, args, name) {
            if (!loggedIn) {
              goToLogin();
              return false;
            }
            args.push(parseQueryString(args.pop()));
            if (callback) callback.apply(this, args);
          }
        }
      }
      

      constructor / initializenew Router([options])
      When creating a new router, you may pass its routes hash directly as an option, if you choose. All options will also be passed to your initialize function, if defined.

      routerouter.route(route, name, [callback])
      Manually create a route for the router, The route argument may be a routing string or regular expression. Each matching capture from the route or regular expression will be passed as an argument to the callback. The name argument will be triggered as a "route:name" event whenever the route is matched. If the callback argument is omitted router[name] will be used instead. Routes added later may override previously declared routes.

      initialize: function(options) {
      
        // Matches #page/10, passing "10"
        this.route("page/:number", "page", function(number){ ... });
      
        // Matches /117-a/b/c/open, passing "117-a/b/c" to this.open
        this.route(/^(.*?)\/open$/, "open");
      
      },
      
      open: function(id) { ... }
      

      navigaterouter.navigate(fragment, [options])
      Whenever you reach a point in your application that you'd like to save as a URL, call navigate in order to update the URL. If you also wish to call the route function, set the trigger option to true. To update the URL without creating an entry in the browser's history, set the replace option to true.

      openPage: function(pageNumber) {
        this.document.pages.at(pageNumber).open();
        this.navigate("page/" + pageNumber);
      }
      
      # Or ...
      
      app.navigate("help/troubleshooting", {trigger: true});
      
      # Or ...
      
      app.navigate("help/troubleshooting", {trigger: true, replace: true});
      

      executerouter.execute(callback, args, name)
      This method is called internally within the router, whenever a route matches and its corresponding callback is about to be executed. Return false from execute to cancel the current transition. Override it to perform custom parsing or wrapping of your routes, for example, to parse query strings before handing them to your route callback, like so:

      var Router = Backbone.Router.extend({
        execute: function(callback, args, name) {
          if (!loggedIn) {
            goToLogin();
            return false;
          }
          args.push(parseQueryString(args.pop()));
          if (callback) callback.apply(this, args);
        }
      });
      

      Backbone.history

      History serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. It forwards the "route" and "route[name]" events of the matching router, or "notfound" when no route in any router matches the current URL. You shouldn't ever have to create one of these yourself since Backbone.history already contains one.

      pushState support exists on a purely opt-in basis in Backbone. Older browsers that don't support pushState will continue to use hash-based URL fragments, and if a hash URL is visited by a pushState-capable browser, it will be transparently upgraded to the true URL. Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly. For full search-engine crawlability, it's best to have the server generate the complete HTML for the page ... but if it's a web application, just rendering the same content you would have for the root URL, and filling in the rest with Backbone Views and JavaScript works fine.

      startBackbone.history.start([options])
      When all of your Routers have been created, and all of the routes are set up properly, call Backbone.history.start() to begin monitoring hashchange events, and dispatching routes. Subsequent calls to Backbone.history.start() will throw an error, and Backbone.History.started is a boolean value indicating whether it has already been called.

      To indicate that you'd like to use HTML5 pushState support in your application, use Backbone.history.start({pushState: true}). If you'd like to use pushState, but have browsers that don't support it natively use full page refreshes instead, you can add {hashChange: false} to the options.

      If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: Backbone.history.start({pushState: true, root: "/public/search/"}).

      The value provided for root will be normalized to include a leading and trailing slash. When navigating to a route the default behavior is to exclude the trailing slash from the URL (e.g., /public/search?query=...). If you prefer to include the trailing slash (e.g., /public/search/?query=...) use Backbone.history.start({trailingSlash: true}). URLs will always contain a leading slash. When root is / URLs will look like /?query=... regardless of the value of trailingSlash.

      When called, if a route succeeds with a match for the current URL, Backbone.history.start() returns true and the "route" and "route[name]" events are triggered. If no defined route matches the current URL, it returns false and "notfound" is triggered instead.

      If the server has already rendered the entire page, and you don't want the initial route to trigger when starting History, pass silent: true.

      Because hash-based history in Internet Explorer relies on an <iframe>, be sure to call start() only after the DOM is ready.

      $(function(){
        new WorkspaceRouter();
        new HelpPaneRouter();
        Backbone.history.start({pushState: true});
      });
      

      Backbone.sync

      Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server. By default, it uses jQuery.ajax to make a RESTful JSON request and returns a jqXHR. You can override it in order to use a different persistence strategy, such as WebSockets, XML transport, or Local Storage.

      The method signature of Backbone.sync is sync(method, model, [options])

      • method – the CRUD method ("create", "read", "update", or "delete")
      • model – the model to be saved (or collection to be read)
      • options – success and error callbacks, and all other jQuery request options

      With the default implementation, when Backbone.sync sends up a request to save a model, its attributes will be passed, serialized as JSON, and sent in the HTTP body with content-type application/json. When returning a JSON response, send down the attributes of the model that have been changed by the server, and need to be updated on the client. When responding to a "read" request from a collection (Collection#fetch), send down an array of model attribute objects.

      Whenever a model or collection begins a sync with the server, a "request" event is emitted. If the request completes successfully you'll get a "sync" event, and an "error" event if not.

      The sync function may be overridden globally as Backbone.sync, or at a finer-grained level, by adding a sync function to a Backbone collection or to an individual model.

      The default sync handler maps CRUD to REST like so:

      • create → POST   /collection
      • read → GET   /collection[/id]
      • update → PUT   /collection/id
      • patch → PATCH   /collection/id
      • delete → DELETE   /collection/id

      As an example, a Rails 4 handler responding to an "update" call from Backbone might look like this:

      def update
        account = Account.find params[:id]
        permitted = params.require(:account).permit(:name, :otherparam)
        account.update_attributes permitted
        render :json => account
      end
      

      One more tip for integrating Rails versions prior to 3.1 is to disable the default namespacing for to_json calls on models by setting ActiveRecord::Base.include_root_in_json = false

      ajaxBackbone.ajax = function(request) { ... };
      If you want to use a custom AJAX function, or your endpoint doesn't support the jQuery.ajax API and you need to tweak things, you can do so by setting Backbone.ajax.

      emulateHTTPBackbone.emulateHTTP = true
      If you want to work with a legacy web server that doesn't support Backbone's default REST/HTTP approach, you may choose to turn on Backbone.emulateHTTP. Setting this option will fake PUT, PATCH and DELETE requests with a HTTP POST, setting the X-HTTP-Method-Override header with the true method. If emulateJSON is also on, the true method will be passed as an additional _method parameter.

      Backbone.emulateHTTP = true;
      
      model.save();  // POST to "/collection/id", with "_method=PUT" + header.
      

      emulateJSONBackbone.emulateJSON = true
      If you're working with a legacy web server that can't handle requests encoded as application/json, setting Backbone.emulateJSON = true; will cause the JSON to be serialized under a model parameter, and the request to be made with a application/x-www-form-urlencoded MIME type, as if from an HTML form.

      Backbone.View

      Backbone views are almost more convention than they are code — they don't determine anything about your HTML or CSS for you, and can be used with any JavaScript templating library. The general idea is to organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page. Instead of digging into a JSON object, looking up an element in the DOM, and updating the HTML by hand, you can bind your view's render function to the model's "change" event — and now everywhere that model data is displayed in the UI, it is always immediately up to date.

      extendBackbone.View.extend(properties, [classProperties])
      Get started with views by creating a custom view class. You'll want to override the render function, specify your declarative events, and perhaps the tagName, className, or id of the View's root element.

      var DocumentRow = Backbone.View.extend({
      
        tagName: "li",
      
        className: "document-row",
      
        events: {
          "click .icon":          "open",
          "click .button.edit":   "openEditDialog",
          "click .button.delete": "destroy"
        },
      
        initialize: function() {
          this.listenTo(this.model, "change", this.render);
        },
      
        render: function() {
          ...
        }
      
      });
      

      Properties like tagName, id, className, el, and events may also be defined as a function, if you want to wait to define them until runtime.

      preinitializenew View([options])
      For use with views as ES classes. If you define a preinitialize method, it will be invoked when the view is first created, before any instantiation logic is run.

      class Document extends Backbone.View {
        preinitialize({autoRender}) {
          this.autoRender = autoRender;
        }
      
        initialize() {
          if (this.autoRender) {
            this.listenTo(this.model, "change", this.render);
          }
        }
      }
      

      constructor / initializenew View([options])
      There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events. If the view defines an initialize function, it will be called when the view is first created. If you'd like to create a view that references an element already in the DOM, pass in the element as an option: new View({el: existingElement})

      var doc = documents.first();
      
      new DocumentRow({
        model: doc,
        id: "document-row-" + doc.id
      });
      

      elview.el
      All views have a DOM element at all times (the el property), whether they've already been inserted into the page or not. In this fashion, views can be rendered at any time, and inserted into the DOM all at once, in order to get high-performance UI rendering with as few reflows and repaints as possible.

      this.el can be resolved from a DOM selector string or an Element; otherwise it will be created from the view's tagName, className, id and attributes properties. If none are set, this.el is an empty div, which is often just fine. An el reference may also be passed in to the view's constructor.

      var ItemView = Backbone.View.extend({
        tagName: 'li'
      });
      
      var BodyView = Backbone.View.extend({
        el: 'body'
      });
      
      var item = new ItemView();
      var body = new BodyView();
      
      alert(item.el + ' ' + body.el);
      

      $elview.$el
      A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time.

      view.$el.show();
      
      listView.$el.append(itemView.el);
      

      setElementview.setElement(element)
      If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the cached $el reference and move the view's delegated events from the old element to the new one.

      attributesview.attributes
      A hash of attributes that will be set as HTML DOM element attributes on the view's el (id, class, data-properties, etc.), or a function that returns such a hash.

      $ (jQuery)view.$(selector)
      If jQuery is included on the page, each view has a $ function that runs queries scoped within the view's element. If you use this scoped jQuery function, you don't have to use model ids as part of your query to pull out specific elements in a list, and can rely much more on HTML class attributes. It's equivalent to running: view.$el.find(selector)

      ui.Chapter = Backbone.View.extend({
        serialize : function() {
          return {
            title: this.$(".title").text(),
            start: this.$(".start-page").text(),
            end:   this.$(".end-page").text()
          };
        }
      });
      

      templateview.template([data])
      While templating for a view isn't a function provided directly by Backbone, it's often a nice convention to define a template function on your views. In this way, when rendering your view, you have convenient access to instance data. For example, using Underscore templates:

      var LibraryView = Backbone.View.extend({
        template: _.template(...)
      });
      

      renderview.render()
      The default implementation of render is a no-op. Override this function with your code that renders the view template from model data, and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls.

      var Bookmark = Backbone.View.extend({
        template: _.template(...),
        render: function() {
          this.$el.html(this.template(this.model.attributes));
          return this;
        }
      });
      

      Backbone is agnostic with respect to your preferred method of HTML templating. Your render function could even munge together an HTML string, or use document.createElement to generate a DOM tree. However, we suggest choosing a nice JavaScript templating library. Mustache.js, Haml-js, and Eco are all fine alternatives. Because Underscore.js is already on the page, _.template is available, and is an excellent choice if you prefer simple interpolated-JavaScript style templates.

      Whatever templating strategy you end up with, it's nice if you never have to put strings of HTML in your JavaScript. At DocumentCloud, we use Jammit in order to package up JavaScript templates stored in /app/views as part of our main core.js asset package.

      removeview.remove()
      Removes a view and its el from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd.

      eventsview.events or view.events()
      The events hash (or method) can be used to specify a set of DOM events that will be bound to methods on your View through delegateEvents.

      Backbone will automatically attach the event listeners at instantiation time, right before invoking initialize.

      var ENTER_KEY = 13;
      var InputView = Backbone.View.extend({
      
        tagName: 'input',
      
        events: {
          "keydown" : "keyAction",
        },
      
        render: function() { ... },
      
        keyAction: function(e) {
          if (e.which === ENTER_KEY) {
            this.collection.add({text: this.$el.val()});
          }
        }
      });
      

      delegateEventsdelegateEvents([events])
      Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source. Events are written in the format {"event selector": "callback"}. The callback may be either the name of a method on the view, or a direct function body. Omitting the selector causes the event to be bound to the view's root element (this.el). By default, delegateEvents is called within the View's constructor for you, so if you have a simple events hash, all of your DOM events will always already be connected, and you will never have to call this function yourself.

      The events property may also be defined as a function that returns an events hash, to make it easier to programmatically define your events, as well as inherit them from parent views.

      Using delegateEvents provides a number of advantages over manually using jQuery to bind events to child elements during render. All attached callbacks are bound to the view before being handed off to jQuery, so when the callbacks are invoked, this continues to refer to the view object. When delegateEvents is run again, perhaps with a different events hash, all callbacks are removed and delegated afresh — useful for views which need to behave differently when in different modes.

      A single-event version of delegateEvents is available as delegate. In fact, delegateEvents is simply a multi-event wrapper around delegate. A counterpart to undelegateEvents is available as undelegate.

      A view that displays a document in a search result might look something like this:

      var DocumentView = Backbone.View.extend({
      
        events: {
          "dblclick"                : "open",
          "click .icon.doc"         : "select",
          "contextmenu .icon.doc"   : "showMenu",
          "click .show_notes"       : "toggleNotes",
          "click .title .lock"      : "editAccessLevel",
          "mouseover .title .date"  : "showTooltip"
        },
      
        render: function() {
          this.$el.html(this.template(this.model.attributes));
          return this;
        },
      
        open: function() {
          window.open(this.model.get("viewer_url"));
        },
      
        select: function() {
          this.model.set({selected: true});
        },
      
        ...
      
      });
      

      undelegateEventsundelegateEvents()
      Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.

      Utility

      Backbone.noConflictvar backbone = Backbone.noConflict();
      Returns the Backbone object back to its original value. You can use the return value of Backbone.noConflict() to keep a local reference to Backbone. Useful for embedding Backbone on third-party websites, where you don't want to clobber the existing Backbone.

      var localBackbone = Backbone.noConflict();
      var model = localBackbone.Model.extend(...);
      

      Backbone.$Backbone.$ = $;
      If you have multiple copies of jQuery on the page, or simply want to tell Backbone to use a particular object as its DOM / Ajax library, this is the property for you.

      Backbone.$ = require('jquery');
      

      debugInfodebugInfo();
      In the unfortunate event that you need to submit a bug report, this function makes it easier to provide detailed information about your setup. It prints a JSON object with version information about Backbone and its dependencies through console.debug. It also returns this object in case you want to inspect it in code.

      debugInfo comes in a separate module that ships with the edge version and releases later than 1.5.0. It is available in UMD format under the same prefix as backbone.js, but with debug-info.js as the file name. It is also experimentally available in ES module format under backbone/modules/.

        <!-- browser embeds -->
        <script src="some-path-or-url/backbone.js"></script>
        <script src="some-path-or-url/debug-info.js"></script>
        <script>
          Backbone.debugInfo();
        </script>
      
        // CommonJS
        require('backbone/debug-info.js')();
      
        // ESM
        import debugInfo from 'backbone/modules/debug-info.js';
        debugInfo();
      

      F.A.Q.

      Why use Backbone, not [other framework X]?
      If your eye hasn't already been caught by the adaptability and elan on display in the above list of examples, we can get more specific: Backbone.js aims to provide the common foundation that data-rich web applications with ambitious interfaces require — while very deliberately avoiding painting you into a corner by making any decisions that you're better equipped to make yourself.

      • The focus is on supplying you with helpful methods to manipulate and query your data, not on HTML widgets or reinventing the JavaScript object model.
      • Backbone does not force you to use a single template engine. Views can bind to HTML constructed in your favorite way.
      • It's smaller. There are fewer kilobytes for your browser or phone to download, and less conceptual surface area. You can read and understand the source in an afternoon.
      • It doesn't depend on stuffing application logic into your HTML. There's no embedded JavaScript, template logic, or binding hookup code in data- or ng- attributes, and no need to invent your own HTML tags.
      • Synchronous events are used as the fundamental building block, not a difficult-to-reason-about run loop, or by constantly polling and traversing your data structures to hunt for changes. And if you want a specific event to be asynchronous and aggregated, no problem.
      • Backbone scales well, from embedded widgets to massive apps.
      • Backbone is a library, not a framework, and plays well with others. You can embed Backbone widgets in Dojo apps without trouble, or use Backbone models as the data backing for D3 visualizations (to pick two entirely random examples).
      • "Two-way data-binding" is avoided. While it certainly makes for a nifty demo, and works for the most basic CRUD, it doesn't tend to be terribly useful in your real-world app. Sometimes you want to update on every keypress, sometimes on blur, sometimes when the panel is closed, and sometimes when the "save" button is clicked. In almost all cases, simply serializing the form to JSON is faster and easier. All that aside, if your heart is set, go for it.
      • There's no built-in performance penalty for choosing to structure your code with Backbone. And if you do want to optimize further, thin models and templates with flexible granularity make it easy to squeeze every last drop of potential performance out of, say, IE8.

      There's More Than One Way To Do It
      It's common for folks just getting started to treat the examples listed on this page as some sort of gospel truth. In fact, Backbone.js is intended to be fairly agnostic about many common patterns in client-side code. For example...

      References between Models and Views can be handled several ways. Some people like to have direct pointers, where views correspond 1:1 with models (model.view and view.model). Others prefer to have intermediate "controller" objects that orchestrate the creation and organization of views into a hierarchy. Others still prefer the evented approach, and always fire events instead of calling methods directly. All of these styles work well.

      Batch operations on Models are common, but often best handled differently depending on your server-side setup. Some folks don't mind making individual Ajax requests. Others create explicit resources for RESTful batch operations: /notes/batch/destroy?ids=1,2,3,4. Others tunnel REST over JSON, with the creation of "changeset" requests:

        {
          "create":  [array of models to create]
          "update":  [array of models to update]
          "destroy": [array of model ids to destroy]
        }
      

      Feel free to define your own events. Backbone.Events is designed so that you can mix it in to any JavaScript object or prototype. Since you can use any string as an event, it's often handy to bind and trigger your own custom events: model.on("selected:true") or model.on("editing")

      Render the UI as you see fit. Backbone is agnostic as to whether you use Underscore templates, Mustache.js, direct DOM manipulation, server-side rendered snippets of HTML, or jQuery UI in your render function. Sometimes you'll create a view for each model ... sometimes you'll have a view that renders thousands of models at once, in a tight loop. Both can be appropriate in the same app, depending on the quantity of data involved, and the complexity of the UI.

      Nested Models & Collections
      It's common to nest collections inside of models with Backbone. For example, consider a Mailbox model that contains many Message models. One nice pattern for handling this is have a this.messages collection for each mailbox, enabling the lazy-loading of messages, when the mailbox is first opened ... perhaps with MessageList views listening for "add" and "remove" events.

      var Mailbox = Backbone.Model.extend({
      
        initialize: function() {
          this.messages = new Messages;
          this.messages.url = '/mailbox/' + this.id + '/messages';
          this.messages.on("reset", this.updateCounts);
        },
      
        ...
      
      });
      
      var inbox = new Mailbox;
      
      // And then, when the Inbox is opened:
      
      inbox.messages.fetch({reset: true});
      

      If you're looking for something more opinionated, there are a number of Backbone plugins that add sophisticated associations among models, available on the wiki.

      Backbone doesn't include direct support for nested models and collections or "has many" associations because there are a number of good patterns for modeling structured data on the client side, and Backbone should provide the foundation for implementing any of them. You may want to…

      • Mirror an SQL database's structure, or the structure of a NoSQL database.
      • Use models with arrays of "foreign key" ids, and join to top level collections (a-la tables).
      • For associations that are numerous, use a range of ids instead of an explicit list.
      • Avoid ids, and use direct references, creating a partial object graph representing your data set.
      • Lazily load joined models from the server, or lazily deserialize nested models from JSON documents.

      Loading Bootstrapped Models
      When your app first loads, it's common to have a set of initial models that you know you're going to need, in order to render the page. Instead of firing an extra AJAX request to fetch them, a nicer pattern is to have their data already bootstrapped into the page. You can then use reset to populate your collections with the initial data. At DocumentCloud, in the ERB template for the workspace, we do something along these lines:

      <script>
        var accounts = new Backbone.Collection;
        accounts.reset(<%= @accounts.to_json %>);
        var projects = new Backbone.Collection;
        projects.reset(<%= @projects.to_json(:collaborators => true) %>);
      </script>
      

      You have to escape </ within the JSON string, to prevent JavaScript injection attacks.

      Extending Backbone
      Many JavaScript libraries are meant to be insular and self-enclosed, where you interact with them by calling their public API, but never peek inside at the guts. Backbone.js is not that kind of library.

      Because it serves as a foundation for your application, you're meant to extend and enhance it in the ways you see fit — the entire source code is annotated to make this easier for you. You'll find that there's very little there apart from core functions, and most of those can be overridden or augmented should you find the need. If you catch yourself adding methods to Backbone.Model.prototype, or creating your own base subclass, don't worry — that's how things are supposed to work.

      How does Backbone relate to "traditional" MVC?
      Different implementations of the Model-View-Controller pattern tend to disagree about the definition of a controller. If it helps any, in Backbone, the View class can also be thought of as a kind of controller, dispatching events that originate from the UI, with the HTML template serving as the true view. We call it a View because it represents a logical chunk of UI, responsible for the contents of a single DOM element.

      Comparing the overall structure of Backbone to a server-side MVC framework like Rails, the pieces line up like so:

      • Backbone.Model – Like a Rails model minus the class methods. Wraps a row of data in business logic.
      • Backbone.Collection – A group of models on the client-side, with sorting/filtering/aggregation logic.
      • Backbone.Router – Rails routes.rb + Rails controller actions. Maps URLs to functions.
      • Backbone.View – A logical, re-usable piece of UI. Often, but not always, associated with a model.
      • Client-side Templates – Rails .html.erb views, rendering a chunk of HTML.

      Binding "this"
      Perhaps the single most common JavaScript "gotcha" is the fact that when you pass a function as a callback, its value for this is lost. When dealing with events and callbacks in Backbone, you'll often find it useful to rely on listenTo or the optional context argument that many of Underscore and Backbone's methods use to specify the this that will be used when the callback is later invoked. (See _.each, _.map, and object.on, to name a few). View events are automatically bound to the view's context for you. You may also find it helpful to use _.bind and _.bindAll from Underscore.js.

      var MessageList = Backbone.View.extend({
      
        initialize: function() {
          var messages = this.collection;
          messages.on("reset", this.render, this);
          messages.on("add", this.addMessage, this);
          messages.on("remove", this.removeMessage, this);
      
          messsages.each(this.addMessage, this);
        }
      
      });
      
      // Later, in the app...
      
      Inbox.messages.add(newMessage);
      

      Working with Rails
      Backbone.js was originally extracted from a Rails application; getting your client-side (Backbone) Models to sync correctly with your server-side (Rails) Models is painless, but there are still a few things to be aware of.

      By default, Rails versions prior to 3.1 add an extra layer of wrapping around the JSON representation of models. You can disable this wrapping by setting:

      ActiveRecord::Base.include_root_in_json = false
      

      ... in your configuration. Otherwise, override parse to pull model attributes out of the wrapper. Similarly, Backbone PUTs and POSTs direct JSON representations of models, where by default Rails expects namespaced attributes. You can have your controllers filter attributes directly from params, or you can override toJSON in Backbone to add the extra wrapping Rails expects.

      Examples

      The list of examples that follows, while long, is not exhaustive — nor in any way current. If you've worked on an app that uses Backbone, please add it to the wiki page of Backbone apps.

      Jérôme Gravel-Niquet has contributed a Todo List application that is bundled in the repository as Backbone example. If you're wondering where to get started with Backbone in general, take a moment to read through the annotated source. The app uses a LocalStorage adapter to transparently save all of your todos within your browser, instead of sending them to a server. Jérôme also has a version hosted at localtodos.com.

      DocumentCloud

      The DocumentCloud workspace is built on Backbone.js, with Documents, Projects, Notes, and Accounts all as Backbone models and collections. If you're interested in history — both Underscore.js and Backbone.js were originally extracted from the DocumentCloud codebase, and packaged into standalone JS libraries.

      USA Today

      USA Today takes advantage of the modularity of Backbone's data/model lifecycle — which makes it simple to create, inherit, isolate, and link application objects — to keep the codebase both manageable and efficient. The new website also makes heavy use of the Backbone Router to control the page for both pushState-capable and legacy browsers. Finally, the team took advantage of Backbone's Event module to create a PubSub API that allows third parties and analytics packages to hook into the heart of the app.

      Rdio

      New Rdio was developed from the ground up with a component based framework based on Backbone.js. Every component on the screen is dynamically loaded and rendered, with data provided by the Rdio API. When changes are pushed, every component can update itself without reloading the page or interrupting the user's music. All of this relies on Backbone's views and models, and all URL routing is handled by Backbone's Router. When data changes are signaled in realtime, Backbone's Events notify the interested components in the data changes. Backbone forms the core of the new, dynamic, realtime Rdio web and desktop applications.

      Hulu

      Hulu used Backbone.js to build its next generation online video experience. With Backbone as a foundation, the web interface was rewritten from scratch so that all page content can be loaded dynamically with smooth transitions as you navigate. Backbone makes it easy to move through the app quickly without the reloading of scripts and embedded videos, while also offering models and collections for additional data manipulation support.

      Quartz

      Quartz sees itself as a digitally native news outlet for the new global economy. Because Quartz believes in the future of open, cross-platform web applications, they selected Backbone and Underscore to fetch, sort, store, and display content from a custom WordPress API. Although qz.com uses responsive design for phone, tablet, and desktop browsers, it also takes advantage of Backbone events and views to render device-specific templates in some cases.

      Earth

      Earth.nullschool.net displays real-time weather conditions on an interactive animated globe, and Backbone provides the foundation upon which all of the site's components are built. Despite the presence of several other JavaScript libraries, Backbone's non-opinionated design made it effortless to mix-in the Events functionality used for distributing state changes throughout the page. When the decision was made to switch to Backbone, large blocks of custom logic simply disappeared.

      Vox

      Vox Media, the publisher of SB Nation, The Verge, Polygon, Eater, Racked, Curbed, and Vox.com, uses Backbone throughout Chorus, its home-grown publishing platform. Backbone powers the liveblogging platform and commenting system used across all Vox Media properties; Coverage, an internal editorial coordination tool; SB Nation Live, a live event coverage and chat tool; and Vox Cards, Vox.com's highlighter-and-index-card inspired app for providing context about the news.

      Gawker Media

      Kinja is Gawker Media's publishing platform designed to create great stories by breaking down the lines between the traditional roles of content creators and consumers. Everyone — editors, readers, marketers — have access to the same tools to engage in passionate discussion and pursue the truth of the story. Sharing, recommending, and following within the Kinja ecosystem allows for improved information discovery across all the sites.

      Kinja is the platform behind Gawker, Gizmodo, Lifehacker, io9 and other Gawker Media blogs. Backbone.js underlies the front-end application code that powers everything from user authentication to post authoring, commenting, and even serving ads. The JavaScript stack includes Underscore.js and jQuery, with some plugins, all loaded with RequireJS. Closure templates are shared between the Play! Framework based Scala application and Backbone views, and the responsive layout is done with the Foundation framework using SASS.

      Flow

      MetaLab used Backbone.js to create Flow, a task management app for teams. The workspace relies on Backbone.js to construct task views, activities, accounts, folders, projects, and tags. You can see the internals under window.Flow.

      Gilt Groupe

      Gilt Groupe uses Backbone.js to build multiple applications across their family of sites. Gilt's mobile website uses Backbone and Zepto.js to create a blazing-fast shopping experience for users on-the-go, while Gilt Live combines Backbone with WebSockets to display the items that customers are buying in real-time. Gilt's search functionality also uses Backbone to filter and sort products efficiently by moving those actions to the client-side.

      Enigma

      Enigma is a portal amassing the largest collection of public data produced by governments, universities, companies, and organizations. Enigma uses Backbone Models and Collections to represent complex data structures; and Backbone's Router gives Enigma users unique URLs for application states, allowing them to navigate quickly through the site while maintaining the ability to bookmark pages and navigate forward and backward through their session.

      NewsBlur

      NewsBlur is an RSS feed reader and social news network with a fast and responsive UI that feels like a native desktop app. Backbone.js was selected for a major rewrite and transition from spaghetti code because of its powerful yet simple feature set, easy integration, and large community. If you want to poke around under the hood, NewsBlur is also entirely open-source.

      WordPress.com

      WordPress.com is the software-as-a-service version of WordPress. It uses Backbone.js Models, Collections, and Views in its Notifications system. Backbone.js was selected because it was easy to fit into the structure of the application, not the other way around. Automattic (the company behind WordPress.com) is integrating Backbone.js into the Stats tab and other features throughout the homepage.

      Foursquare

      Foursquare is a fun little startup that helps you meet up with friends, discover new places, and save money. Backbone Models are heavily used in the core JavaScript API layer and Views power many popular features like the homepage map and lists.

      Bitbucket

      Bitbucket is a free source code hosting service for Git and Mercurial. Through its models and collections, Backbone.js has proved valuable in supporting Bitbucket's REST API, as well as newer components such as in-line code comments and approvals for pull requests. Mustache templates provide server and client-side rendering, while a custom Google Closure inspired life-cycle for widgets allows Bitbucket to decorate existing DOM trees and insert new ones.

      Disqus

      Disqus chose Backbone.js to power the latest version of their commenting widget. Backbone’s small footprint and easy extensibility made it the right choice for Disqus’ distributed web application, which is hosted entirely inside an iframe and served on thousands of large web properties, including IGN, Wired, CNN, MLB, and more.

      Delicious

      Delicious is a social bookmarking platform making it easy to save, sort, and store bookmarks from across the web. Delicious uses Chaplin.js, Backbone.js and AppCache to build a full-featured MVC web app. The use of Backbone helped the website and mobile apps share a single API service, and the reuse of the model tier made it significantly easier to share code during the recent Delicious redesign.

      Khan Academy

      Khan Academy is on a mission to provide a free world-class education to anyone anywhere. With thousands of videos, hundreds of JavaScript-driven exercises, and big plans for the future, Khan Academy uses Backbone to keep frontend code modular and organized. User profiles and goal setting are implemented with Backbone, jQuery and Handlebars, and most new feature work is being pushed to the client side, greatly increasing the quality of the API.

      IRCCloud

      IRCCloud is an always-connected IRC client that you use in your browser — often leaving it open all day in a tab. The sleek web interface communicates with an Erlang backend via websockets and the IRCCloud API. It makes heavy use of Backbone.js events, models, views and routing to keep your IRC conversations flowing in real time.

      Pitchfork

      Pitchfork uses Backbone.js to power its site-wide audio player, Pitchfork.tv, location routing, a write-thru page fragment cache, and more. Backbone.js (and Underscore.js) helps the team create clean and modular components, move very quickly, and focus on the site, not the spaghetti.

      Spin

      Spin pulls in the latest news stories from their internal API onto their site using Backbone models and collections, and a custom sync method. Because the music should never stop playing, even as you click through to different "pages", Spin uses a Backbone router for navigation within the site.

      ZocDoc

      ZocDoc helps patients find local, in-network doctors and dentists, see their real-time availability, and instantly book appointments. On the public side, the webapp uses Backbone.js to handle client-side state and rendering in search pages and doctor profiles. In addition, the new version of the doctor-facing part of the website is a large single-page application that benefits from Backbone's structure and modularity. ZocDoc's Backbone classes are tested with Jasmine, and delivered to the end user with Cassette.

      Walmart Mobile

      Walmart used Backbone.js to create the new version of their mobile web application and created two new frameworks in the process. Thorax provides mixins, inheritable events, as well as model and collection view bindings that integrate directly with Handlebars templates. Lumbar allows the application to be split into modules which can be loaded on demand, and creates platform specific builds for the portions of the web application that are embedded in Walmart's native Android and iOS applications.

      Groupon Now!

      Groupon Now! helps you find local deals that you can buy and use right now. When first developing the product, the team decided it would be AJAX heavy with smooth transitions between sections instead of full refreshes, but still needed to be fully linkable and shareable. Despite never having used Backbone before, the learning curve was incredibly quick — a prototype was hacked out in an afternoon, and the team was able to ship the product in two weeks. Because the source is minimal and understandable, it was easy to add several Backbone extensions for Groupon Now!: changing the router to handle URLs with querystring parameters, and adding a simple in-memory store for caching repeated requests for the same data.

      Basecamp

      37Signals chose Backbone.js to create the calendar feature of its popular project management software Basecamp. The Basecamp Calendar uses Backbone.js models and views in conjunction with the Eco templating system to present a polished, highly interactive group scheduling interface.

      Slavery Footprint

      Slavery Footprint allows consumers to visualize how their consumption habits are connected to modern-day slavery and provides them with an opportunity to have a deeper conversation with the companies that manufacture the goods they purchased. Based in Oakland, California, the Slavery Footprint team works to engage individuals, groups, and businesses to build awareness for and create deployable action against forced labor, human trafficking, and modern-day slavery through online tools, as well as off-line community education and mobilization programs.

      Stripe

      Stripe provides an API for accepting credit cards on the web. Stripe's management interface was recently rewritten from scratch in CoffeeScript using Backbone.js as the primary framework, Eco for templates, Sass for stylesheets, and Stitch to package everything together as CommonJS modules. The new app uses Stripe's API directly for the majority of its actions; Backbone.js models made it simple to map client-side models to their corresponding RESTful resources.

      Airbnb

      Airbnb uses Backbone in many of its products. It started with Airbnb Mobile Web (built in six weeks by a team of three) and has since grown to Wish Lists, Match, Search, Communities, Payments, and Internal Tools.

      SoundCloud Mobile

      SoundCloud is the leading sound sharing platform on the internet, and Backbone.js provides the foundation for SoundCloud Mobile. The project uses the public SoundCloud API as a data source (channeled through a nginx proxy), jQuery templates for the rendering, Qunit and PhantomJS for the testing suite. The JS code, templates and CSS are built for the production deployment with various Node.js tools like ready.js, Jake, jsdom. The Backbone.History was modified to support the HTML5 history.pushState. Backbone.sync was extended with an additional SessionStorage based cache layer.

      Art.sy

      Art.sy is a place to discover art you'll love. Art.sy is built on Rails, using Grape to serve a robust JSON API. The main site is a single page app written in CoffeeScript and uses Backbone to provide structure around this API. An admin panel and partner CMS have also been extracted into their own API-consuming Backbone projects.

      Pandora

      When Pandora redesigned their site in HTML5, they chose Backbone.js to help manage the user interface and interactions. For example, there's a model that represents the "currently playing track", and multiple views that automatically update when the current track changes. The station list is a collection, so that when stations are added or changed, the UI stays up to date.

      Inkling

      Inkling is a cross-platform way to publish interactive learning content. Inkling for Web uses Backbone.js to make hundreds of complex books — from student textbooks to travel guides and programming manuals — engaging and accessible on the web. Inkling supports WebGL-enabled 3D graphics, interactive assessments, social sharing, and a system for running practice code right in the book, all within a single page Backbone-driven app. Early on, the team decided to keep the site lightweight by using only Backbone.js and raw JavaScript. The result? Complete source code weighing in at a mere 350kb with feature-parity across the iPad, iPhone and web clients. Give it a try with this excerpt from JavaScript: The Definitive Guide.

      Code School

      Code School courses teach people about various programming topics like CoffeeScript, CSS, Ruby on Rails, and more. The new Code School course challenge page is built from the ground up on Backbone.js, using everything it has to offer: the router, collections, models, and complex event handling. Before, the page was a mess of jQuery DOM manipulation and manual Ajax calls. Backbone.js helped introduce a new way to think about developing an organized front-end application in JavaScript.

      CloudApp

      CloudApp is simple file and link sharing for the Mac. Backbone.js powers the web tools which consume the documented API to manage Drops. Data is either pulled manually or pushed by Pusher and fed to Mustache templates for rendering. Check out the annotated source code to see the magic.

      SeatGeek

      SeatGeek's stadium ticket maps were originally developed with Prototype.js. Moving to Backbone.js and jQuery helped organize a lot of the UI code, and the increased structure has made adding features a lot easier. SeatGeek is also in the process of building a mobile interface that will be Backbone.js from top to bottom.

      Easel

      Easel is an in-browser, high fidelity web design tool that integrates with your design and development process. The Easel team uses CoffeeScript, Underscore.js and Backbone.js for their rich visual editor as well as other management functions throughout the site. The structure of Backbone allowed the team to break the complex problem of building a visual editor into manageable components and still move quickly.

      Jolicloud

      Jolicloud is an open and independent platform and operating system that provides music playback, video streaming, photo browsing and document editing — transforming low cost computers into beautiful cloud devices. The new Jolicloud HTML5 app was built from the ground up using Backbone and talks to the Jolicloud Platform, which is based on Node.js. Jolicloud works offline using the HTML5 AppCache, extends Backbone.sync to store data in IndexedDB or localStorage, and communicates with the Joli OS via WebSockets.

      Salon.io

      Salon.io provides a space where photographers, artists and designers freely arrange their visual art on virtual walls. Salon.io runs on Rails, but does not use much of the traditional stack, as the entire frontend is designed as a single page web app, using Backbone.js, Brunch and CoffeeScript.

      TileMill

      Our fellow Knight Foundation News Challenge winners, MapBox, created an open-source map design studio with Backbone.js: TileMill. TileMill lets you manage map layers based on shapefiles and rasters, and edit their appearance directly in the browser with the Carto styling language. Note that the gorgeous MapBox homepage is also a Backbone.js app.

      Blossom

      Blossom is a lightweight project management tool for lean teams. Backbone.js is heavily used in combination with CoffeeScript to provide a smooth interaction experience. The app is packaged with Brunch. The RESTful backend is built with Flask on Google App Engine.

      Trello

      Trello is a collaboration tool that organizes your projects into boards. A Trello board holds many lists of cards, which can contain checklists, files and conversations, and may be voted on and organized with labels. Updates on the board happen in real time. The site was built ground up using Backbone.js for all the models, views, and routes.

      Tzigla

      Cristi Balan and Irina Dumitrascu created Tzigla, a collaborative drawing application where artists make tiles that connect to each other to create surreal drawings. Backbone models help organize the code, routers provide bookmarkable deep links, and the views are rendered with haml.js and Zepto. Tzigla is written in Ruby (Rails) on the backend, and CoffeeScript on the frontend, with Jammit prepackaging the static assets.

      Change Log

      1.6.1Apr. 1, 2025DiffDocs
      • Fixed a bug where the changeId event would sometimes fire even though the id of a model did not change.
      • Several small changes in the test infrastructure.
      1.6.0Feb. 5, 2024DiffDocs
      • Added a notfound event to Backbone.history for when no router matches the current URL.
      • Added the debugInfo function to make bug reports easier.
      • Fixed a corner case where a collection would forward error events twice if the model was first added through the create method with wait: true.
      • Added issue templates and other documentation improvements.
      1.5.0Jul. 28, 2023DiffDocs
      • Added a trailingSlash option to the Backbone.history.start method. When this option is true, the trailing slash of the root is always retained in the route, even if the path segment of the current URL is empty.
      • Fixed a bug that caused collection add events to include an irrelevant options.index if other models were removed during the same call to Collection.set.
      • Fixed a corner case where a collection would not forward the error event if Collection.create was invoked with {wait: true}.
      • Adapted CoffeeScript Model test to CoffeeScript version 2.
      • Added a security policy and a code of conduct.
      • Added a .editorconfig to the project root in order to promote consistent whitespace handling across editors.
      • Many clarifications, corrections and refinements to the documentation, as well as some code comments.
      1.4.1Feb. 26, 2022DiffDocs
      • Improved support for polymorphic collections in which two or more model types might have different idAttributes. Collection.modelId() overrides can now exploit the fact that internal methods pass the idAttribute as a second argument to modelId.
      • Fixed a temporary inconsistency in a collection's internal administration during model change events. Models (and by extension, collections) now emit a specialized changeId event when the id changes.
      • Fixed an issue where an ES6 class or object method could not be used as the model for a collection due to the lack of a prototype.
      • Restored continuous integration using GitHub Actions and cross-browser testing using Sauce Labs.
      • Several improvements to the online documentation.
      • Due to upgraded development tools, the annotated sources of the example code as well as the sourcemap of the minified bundle have changed filenames. Aliases and redirects in the old locations are kept for backwards compatibility.
      1.4.0Feb. 19, 2019DiffDocs
      • Collections now support the Javascript Iterator Protocol!
      • listenTo uses the listened object's public on method. This helps maintain interoperability between Backbone and other event libraries (including Node.js).
      • Added support for setting instance properties before the constructor in ES2015 classes with a preinitialize method.
      • Collection.get now checks if obj is a Model to allow retrieving models with an `attributes` key.
      • Fixed several issues with Router's URL hashing and parsing.
      1.3.3Apr. 5, 2016DiffDocs
      • Added findIndex and findLastIndex Underscore methods to Collection.
      • Added options.changes to Collection "update" event which includes added, merged, and removed models.
      • Added support for Collection#mixin and Model#mixin.
      • Ensured Collection#reduce and Collection#reduceRight work without an initial accumulator value.
      • Ensured Collection#_removeModels always returns an array.
      • Fixed a bug where Events.once with object syntax failed to bind context.
      • Fixed Collection#_onModelEvent regression where triggering a change event without a model would error.
      • Fixed Collection#set regression when parse returns a falsy value.
      • Fixed Model#id regression where id would be unintentionally undefined.
      • Fixed _removeModels regression which could cause an infinite loop under certain conditions.
      • Removed component package support.
      • Note that 1.3.3 fixes several bugs in versions 1.3.0 to 1.3.2. Please upgrade immediately if you are on one of those versions.
      1.2.3Sept. 3, 2015DiffDocs
      • Fixed a minor regression in 1.2.2 that would cause an error when adding a model to a collection at an out of bounds index.
      1.2.2Aug. 19, 2015DiffDocs
      • Collection methods find, filter, reject, every, some, and partition can now take a model-attributes-style predicate: this.collection.reject({user: 'guybrush'}).
      • Backbone Events once again supports multiple-event maps (obj.on({'error change': action})). This was a previously undocumented feature inadvertently removed in 1.2.0.
      • Added Collection#includes as an alias of Collection#contains and as a replacement for Collection#include in Underscore.js >= 1.8.
      1.2.1Jun. 4, 2015DiffDocs
      • Collection#add now avoids trying to parse a model instance when passed parse: false.
      • Bug fix in Collection#remove. The removed models are now actually returned.
      • Model#fetch no longer parses the response when passing parse: false.
      • Bug fix for iframe-based History when used with JSDOM.
      • Bug fix where Collection#invoke was not taking additional arguments.
      • When using on with an event map, you can now pass the context as the second argument. This was a previously undocumented feature inadvertently removed in 1.2.0.
      1.2.0May 13, 2015DiffDocs
      • Added new hooks to Views to allow them to work without jQuery. See the wiki page for more info.
      • As a neat side effect, Backbone.History no longer uses jQuery's event methods for pushState and hashChange listeners. We're native all the way.
      • Also on the subject of jQuery, if you're using Backbone with CommonJS (node, browserify, webpack) Backbone will automatically try to load jQuery for you.
      • Views now always delegate their events in setElement. You can no longer modify the events hash or your view's el property in initialize.
      • Added an "update" event that triggers after any amount of models are added or removed from a collection. Handy to re-render lists of things without debouncing.
      • Collection#at can take a negative index.
      • Added modelId to Collection for generating unique ids on polymorphic collections. Handy for cases when your model ids would otherwise collide.
      • Added an overridable _isModel for more advanced control of what's considered a model by your Collection.
      • The success callback passed to Model#destroy is always called asynchronously now.
      • Router#execute passes back the route name as its third argument.
      • Cancel the current Router transition by returning false in Router#execute. Great for checking logged-in status or other prerequisites.
      • Added getSearch and getPath methods to Backbone.History as cross-browser and overridable ways of slicing up the URL.
      • Added delegate and undelegate as finer-grained versions of delegateEvents and undelegateEvents. Useful for plugin authors to use a consistent events interface in Backbone.
      • A collection will only fire a "sort" event if its order was actually updated, not on every set.
      • Any passed options.attrs are now respected when saving a model with patch: true.
      • Collection#clone now sets the model and comparator functions of the cloned collection to the new one.
      • Adding models to your Collection when specifying an at position now sends the actual position of your model in the add event, not just the one you've passed in.
      • Collection#remove will now only return a list of models that have actually been removed from the collection.
      • Fixed loading Backbone.js in strict ES2015 module loaders.
      1.1.2Feb. 20, 2014DiffDocs
      • Backbone no longer tries to require jQuery in Node/CommonJS environments, for better compatibility with folks using Browserify. If you'd like to have Backbone use jQuery from Node, assign it like so: Backbone.$ = require('jquery');
      • Bugfix for route parameters with newlines in them.
      1.1.1Feb. 13, 2014DiffDocs
      • Backbone now registers itself for AMD (Require.js), Bower and Component, as well as being a CommonJS module and a regular (Java)Script. Whew.
      • Added an execute hook to the Router, which allows you to hook in and custom-parse route arguments, like query strings, for example.
      • Performance fine-tuning for Backbone Events.
      • Better matching for Unicode in routes, in old browsers.
      • Backbone Routers now handle query params in route fragments, passing them into the handler as the last argument. Routes specified as strings should no longer include the query string ('foo?:query' should be 'foo').
      1.1.0Oct. 10, 2013DiffDocs
      • Made the return values of Collection's set, add, remove, and reset more useful. Instead of returning this, they now return the changed (added, removed or updated) model or list of models.
      • Backbone Views no longer automatically attach options passed to the constructor as this.options and Backbone Models no longer attach url and urlRoot options, but you can do it yourself if you prefer.
      • All "invalid" events now pass consistent arguments. First the model in question, then the error object, then options.
      • You are no longer permitted to change the id of your model during parse. Use idAttribute instead.
      • On the other hand, parse is now an excellent place to extract and vivify incoming nested JSON into associated submodels.
      • Many tweaks, optimizations and bugfixes relating to Backbone 1.0, including URL overrides, mutation of options, bulk ordering, trailing slashes, edge-case listener leaks, nested model parsing...
      1.0.0March 20, 2013DiffDocs
      • Renamed Collection's "update" to set, for parallelism with the similar model.set(), and contrast with reset. It's now the default updating mechanism after a fetch. If you'd like to continue using "reset", pass {reset: true}.
      • Your route handlers will now receive their URL parameters pre-decoded.
      • Added listenToOnce as the analogue of once.
      • Added the findWhere method to Collections, similar to where.
      • Added the keys, values, pairs, invert, pick, and omit Underscore.js methods to Backbone Models.
      • The routes in a Router's route map may now be function literals, instead of references to methods, if you like.
      • url and urlRoot properties may now be passed as options when instantiating a new Model.
      0.9.10Jan. 15, 2013DiffDocs
      • A "route" event is triggered on the router in addition to being fired on Backbone.history.
      • Model validation is now only enforced by default in Model#save and no longer enforced by default upon construction or in Model#set, unless the {validate:true} option is passed.
      • View#make has been removed. You'll need to use $ directly to construct DOM elements now.
      • Passing {silent:true} on change will no longer delay individual "change:attr" events, instead they are silenced entirely.
      • The Model#change method has been removed, as delayed attribute changes are no longer available.
      • Bug fix on change where attribute comparison uses !== instead of _.isEqual.
      • Bug fix where an empty response from the server on save would not call the success function.
      • parse now receives options as its second argument.
      • Model validation now fires invalid event instead of error.
      0.9.9Dec. 13, 2012DiffDocs
      • Added listenTo and stopListening to Events. They can be used as inversion-of-control flavors of on and off, for convenient unbinding of all events an object is currently listening to. view.remove() automatically calls view.stopListening().
      • When using add on a collection, passing {merge: true} will now cause duplicate models to have their attributes merged in to the existing models, instead of being ignored.
      • Added update (which is also available as an option to fetch) for "smart" updating of sets of models.
      • HTTP PATCH support in save by passing {patch: true}.
      • The Backbone object now extends Events so that you can use it as a global event bus, if you like.
      • Added a "request" event to Backbone.sync, which triggers whenever a request begins to be made to the server. The natural complement to the "sync" event.
      • Router URLs now support optional parts via parentheses, without having to use a regex.
      • Backbone events now supports once, similar to Node's once, or jQuery's one.
      • Backbone events now support jQuery-style event maps obj.on({click: action}).
      • While listening to a reset event, the list of previous models is now available in options.previousModels, for convenience.
      • Validation now occurs even during "silent" changes. This change means that the isValid method has been removed. Failed validations also trigger an error, even if an error callback is specified in the options.
      • Consolidated "sync" and "error" events within Backbone.sync. They are now triggered regardless of the existence of success or error callbacks.
      • For mixed-mode APIs, Backbone.sync now accepts emulateHTTP and emulateJSON as inline options.
      • Collections now also proxy Underscore method name aliases (collect, inject, foldl, foldr, head, tail, take, and so on...)
      • Removed getByCid from Collections. collection.get now supports lookup by both id and cid.
      • After fetching a model or a collection, all defined parse functions will now be run. So fetching a collection and getting back new models could cause both the collection to parse the list, and then each model to be parsed in turn, if you have both functions defined.
      • Bugfix for normalizing leading and trailing slashes in the Router definitions. Their presence (or absence) should not affect behavior.
      • When declaring a View, options, el, tagName, id and className may now be defined as functions, if you want their values to be determined at runtime.
      • Added a Backbone.ajax hook for more convenient overriding of the default use of $.ajax. If AJAX is too passé, set it to your preferred method for server communication.
      • Collection#sort now triggers a sort event, instead of a reset event.
      • Calling destroy on a Model will now return false if the model isNew.
      • To set what library Backbone uses for DOM manipulation and Ajax calls, use Backbone.$ = ... instead of setDomLibrary.
      • Removed the Backbone.wrapError helper method. Overriding sync should work better for those particular use cases.
      • To improve the performance of add, options.index will no longer be set in the add event callback. collection.indexOf(model) can be used to retrieve the index of a model as necessary.
      • For semantic and cross browser reasons, routes will now ignore search parameters. Routes like search?query=…&page=3 should become search/…/3.
      • Model#set no longer accepts another model as an argument. This leads to subtle problems and is easily replaced with model.set(other.attributes).
      0.9.2March 21, 2012DiffDocs
      • Instead of throwing an error when adding duplicate models to a collection, Backbone will now silently skip them instead.
      • Added push, pop, unshift, and shift to collections.
      • A model's changed hash is now exposed for easy reading of the changed attribute delta, since the model's last "change" event.
      • Added where to collections for simple filtering.
      • You can now use a single off call to remove all callbacks bound to a specific object.
      • Bug fixes for nested individual change events, some of which may be "silent".
      • Bug fixes for URL encoding in location.hash fragments.
      • Bug fix for client-side validation in advance of a save call with {wait: true}.
      • Updated / refreshed the example Todo List app.
      0.9.1Feb. 2, 2012DiffDocs
      • Reverted to 0.5.3-esque behavior for validating models. Silent changes no longer trigger validation (making it easier to work with forms). Added an isValid function that you can use to check if a model is currently in a valid state.
      • If you have multiple versions of jQuery on the page, you can now tell Backbone which one to use with Backbone.setDomLibrary.
      • Fixes regressions in 0.9.0 for routing with "root", saving with both "wait" and "validate", and the order of nested "change" events.
      0.9.0Jan. 30, 2012DiffDocs
      • Creating and destroying models with create and destroy are now optimistic by default. Pass {wait: true} as an option if you'd like them to wait for a successful server response to proceed.
      • Two new properties on views: $el — a cached jQuery (or Zepto) reference to the view's element, and setElement, which should be used instead of manually setting a view's el. It will both set view.el and view.$el correctly, as well as re-delegating events on the new DOM element.
      • You can now bind and trigger multiple spaced-delimited events at once. For example: model.on("change:name change:age", ...)
      • When you don't know the key in advance, you may now call model.set(key, value) as well as save.
      • Multiple models with the same id are no longer allowed in a single collection.
      • Added a "sync" event, which triggers whenever a model's state has been successfully synced with the server (create, save, destroy).
      • bind and unbind have been renamed to on and off for clarity, following jQuery's lead. The old names are also still supported.
      • A Backbone collection's comparator function may now behave either like a sortBy (pass a function that takes a single argument), or like a sort (pass a comparator function that expects two arguments). The comparator function is also now bound by default to the collection — so you can refer to this within it.
      • A view's events hash may now also contain direct function values as well as the string names of existing view methods.
      • Validation has gotten an overhaul — a model's validate function will now be run even for silent changes, and you can no longer create a model in an initially invalid state.
      • Added shuffle and initial to collections, proxied from Underscore.
      • Model#urlRoot may now be defined as a function as well as a value.
      • View#attributes may now be defined as a function as well as a value.
      • Calling fetch on a collection will now cause all fetched JSON to be run through the collection's model's parse function, if one is defined.
      • You may now tell a router to navigate(fragment, {replace: true}), which will either use history.replaceState or location.hash.replace, in order to change the URL without adding a history entry.
      • Within a collection's add and remove events, the index of the model being added or removed is now available as options.index.
      • Added an undelegateEvents to views, allowing you to manually remove all configured event delegations.
      • Although you shouldn't be writing your routes with them in any case — leading slashes (/) are now stripped from routes.
      • Calling clone on a model now only passes the attributes for duplication, not a reference to the model itself.
      • Calling clear on a model now removes the id attribute.

      0.5.3August 9, 2011DiffDocs
      A View's events property may now be defined as a function, as well as an object literal, making it easier to programmatically define and inherit events. groupBy is now proxied from Underscore as a method on Collections. If the server has already rendered everything on page load, pass Backbone.history.start({silent: true}) to prevent the initial route from triggering. Bugfix for pushState with encoded URLs.

      0.5.2July 26, 2011DiffDocs
      The bind function, can now take an optional third argument, to specify the this of the callback function. Multiple models with the same id are now allowed in a collection. Fixed a bug where calling .fetch(jQueryOptions) could cause an incorrect URL to be serialized. Fixed a brief extra route fire before redirect, when degrading from pushState.

      0.5.1July 5, 2011DiffDocs
      Cleanups from the 0.5.0 release, to wit: improved transparent upgrades from hash-based URLs to pushState, and vice-versa. Fixed inconsistency with non-modified attributes being passed to Model#initialize. Reverted a 0.5.0 change that would strip leading hashbangs from routes. Added contains as an alias for includes.

      0.5.0July 1, 2011DiffDocs
      A large number of tiny tweaks and micro bugfixes, best viewed by looking at the commit diff. HTML5 pushState support, enabled by opting-in with: Backbone.history.start({pushState: true}). Controller was renamed to Router, for clarity. Collection#refresh was renamed to Collection#reset to emphasize its ability to both reset the collection with new models, as well as empty out the collection when used with no parameters. saveLocation was replaced with navigate. RESTful persistence methods (save, fetch, etc.) now return the jQuery deferred object for further success/error chaining and general convenience. Improved XSS escaping for Model#escape. Added a urlRoot option to allow specifying RESTful urls without the use of a collection. An error is thrown if Backbone.history.start is called multiple times. Collection#create now validates before initializing the new model. view.el can now be a jQuery string lookup. Backbone Views can now also take an attributes parameter. Model#defaults can now be a function as well as a literal attributes object.

      0.3.3Dec 1, 2010DiffDocs
      Backbone.js now supports Zepto, alongside jQuery, as a framework for DOM manipulation and Ajax support. Implemented Model#escape, to efficiently handle attributes intended for HTML interpolation. When trying to persist a model, failed requests will now trigger an "error" event. The ubiquitous options argument is now passed as the final argument to all "change" events.

      0.3.2Nov 23, 2010DiffDocs
      Bugfix for IE7 + iframe-based "hashchange" events. sync may now be overridden on a per-model, or per-collection basis. Fixed recursion error when calling save with no changed attributes, within a "change" event.

      0.3.1Nov 15, 2010DiffDocs
      All "add" and "remove" events are now sent through the model, so that views can listen for them without having to know about the collection. Added a remove method to Backbone.View. toJSON is no longer called at all for 'read' and 'delete' requests. Backbone routes are now able to load empty URL fragments.

      0.3.0Nov 9, 2010DiffDocs
      Backbone now has Controllers and History, for doing client-side routing based on URL fragments. Added emulateHTTP to provide support for legacy servers that don't do PUT and DELETE. Added emulateJSON for servers that can't accept application/json encoded requests. Added Model#clear, which removes all attributes from a model. All Backbone classes may now be seamlessly inherited by CoffeeScript classes.

      0.2.0Oct 25, 2010DiffDocs
      Instead of requiring server responses to be namespaced under a model key, now you can define your own parse method to convert responses into attributes for Models and Collections. The old handleEvents function is now named delegateEvents, and is automatically called as part of the View's constructor. Added a toJSON function to Collections. Added Underscore's chain to Collections.

      0.1.2Oct 19, 2010DiffDocs
      Added a Model#fetch method for refreshing the attributes of single model from the server. An error callback may now be passed to set and save as an option, which will be invoked if validation fails, overriding the "error" event. You can now tell backbone to use the _method hack instead of HTTP methods by setting Backbone.emulateHTTP = true. Existing Model and Collection data is no longer sent up unnecessarily with GET and DELETE requests. Added a rake lint task. Backbone is now published as an NPM module.

      0.1.1Oct 14, 2010DiffDocs
      Added a convention for initialize functions to be called upon instance construction, if defined. Documentation tweaks.

      0.1.0Oct 13, 2010Docs
      Initial Backbone release.


      A DocumentCloud Project

      ================================================ FILE: karma.conf-sauce.js ================================================ var _ = require('underscore'); // Browsers to run on Sauce Labs platforms var sauceBrowsers = _.reduce([ ['firefox', 'latest'], ['firefox', '60'], ['firefox', '40'], // TODO: find a way to get testing on old Firefox to work. (#4253) // ['firefox', '11'], ['chrome', 'latest'], ['chrome', '60'], // TODO: these versions of Chrome fail with a mysterious // "_T_ is not defined" (#4253) // ['chrome', '40'], // ['chrome', '26'], // latest Edge as well as pre-Blink versions ['microsoftedge', 'latest', 'Windows 11'], ['microsoftedge', '18', 'Windows 10'], ['microsoftedge', '13', 'Windows 10'], ['internet explorer', 'latest', 'Windows 10'], // TODO: these versions of IE run 50 out of 425 tests, then hang for unknown // reasons. (#4253) // ['internet explorer', '10', 'Windows 8'], // ['internet explorer', '9', 'Windows 7'], // Older versions of IE no longer supported by Sauce Labs ['safari', 'latest', 'macOS 12'], ['safari', '12', 'macOS 10.14'], ['safari', '11', 'macOS 10.13'], ['safari', '8', 'OS X 10.10'], ], function(memo, platform) { // internet explorer -> ie var label = platform[0].split(' '); if (label.length > 1) { label = _.invoke(label, 'charAt', 0); } label = (label.join('') + '_v' + platform[1]).replace(' ', '_').toUpperCase(); memo[label] = _.pick({ base: 'SauceLabs', browserName: platform[0], version: platform[1], platform: platform[2] }, Boolean); return memo; }, {}); module.exports = function(config) { if ( !process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY ) { // eslint-disable-next-line no-console console.log('Sauce environments not set --- Skipping'); return process.exit(0); } config.set({ basePath: '', frameworks: ['qunit'], singleRun: true, browserDisconnectTimeout: 60000, browserDisconnectTolerance: 2, browserNoActivityTimeout: 60000, // list of files / patterns to load in the browser files: [ 'test/vendor/jquery.js', 'test/vendor/json2.js', 'test/vendor/underscore.js', 'backbone.js', 'debug-info.js', 'test/setup/*.js', 'test/*.js' ], // Number of sauce tests to start in parallel concurrency: 4, // test results reporter to use reporters: ['dots', 'saucelabs'], port: 9876, colors: true, logLevel: config.LOG_INFO, sauceLabs: { build: 'GH #' + process.env.BUILD_NUMBER + ' (' + process.env.BUILD_ID + ')', startConnect: true, tunnelIdentifier: process.env.JOB_NUMBER, region: 'eu' }, captureTimeout: 60000, customLaunchers: sauceBrowsers, // Browsers to launch, commented out to prevent karma from starting // too many concurrent browsers and timing sauce out. browsers: _.keys(sauceBrowsers) }); }; ================================================ FILE: karma.conf.js ================================================ // Note some browser launchers should be installed before using karma start. // For example: // npm install karma-firefox-launcher // karma start --browsers=Firefox module.exports = function(config) { config.set({ basePath: '', frameworks: ['qunit'], // list of files / patterns to load in the browser files: [ 'test/vendor/jquery.js', 'test/vendor/json2.js', 'test/vendor/underscore.js', 'backbone.js', 'debug-info.js', 'test/setup/*.js', 'test/*.js' ], // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['progress'], // web server port port: 9877, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: false, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: [], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true, // See http://stackoverflow.com/a/27873086/1517919 customLaunchers: { Chrome_sandbox: { base: 'Chrome', flags: ['--no-sandbox'] } } }); }; ================================================ FILE: modules/.eslintrc ================================================ { "parserOptions": { "ecmaVersion": 6, "sourceType": "module", }, "plugins": [ "import" ], "extends": [ "plugin:import/errors" ], "rules": { "import/no-unresolved": 0 } } ================================================ FILE: modules/debug-info.js ================================================ import Backbone from 'backbone'; // Provide useful information when things go wrong. export default function() { // Introspect Backbone. var $ = Backbone.$, _b = Backbone._debug(), _ = _b._, root = _b.root; // Use the `partialRight` function as a Lodash indicator. It was never in // Underscore, has been in Lodash at least since version 1.3.1, and is // unlikely to be mixed into Underscore since nobody needs it. var lodash = !!_.partialRight; var info = { backbone: Backbone.VERSION, // Is this the exact released version, or a later development version? /* This is automatically temporarily replaced when publishing a release, so please don't edit this. */ distribution: 'MARK_DEVELOPMENT', _: (lodash ? 'lodash ' : '') + _.VERSION, $: !$ ? false : $.fn && $.fn.jquery ? $.fn.jquery : $.zepto ? 'zepto' : $.ender ? 'ender' : true }; if (typeof root.Deno !== 'undefined') { info.deno = _.pick(root.Deno, 'version', 'build'); } else if (typeof root.process !== 'undefined') { info.process = _.pick(root.process, 'version', 'platform', 'arch'); } else if (typeof root.navigator !== 'undefined') { info.navigator = _.pick(root.navigator, 'userAgent', 'platform', 'webdriver'); } /* eslint-disable-next-line no-console */ console.debug('Backbone debug info: ', JSON.stringify(info, null, 4)); return info; } ================================================ FILE: modules/package.json ================================================ {"type":"module","version":"1.4.1"} ================================================ FILE: package.json ================================================ { "name": "backbone", "description": "Give your JS App some Backbone with Models, Views, Collections, and Events.", "url": "http://backbonejs.org", "keywords": [ "model", "view", "controller", "router", "server", "client", "browser" ], "author": "Jeremy Ashkenas", "dependencies": { "underscore": ">=1.8.3" }, "devDependencies": { "coffeescript": "^2.7.0", "cpy-cli": "^5.0.0", "docco": "^0.9.1", "eslint": "^8.7.0", "eslint-plugin-import": "^2.28.1", "karma": "^6.3.11", "karma-qunit": "^4.1.2", "qunit": "^2.17.2", "replace-in-file": "^7.0.1", "rollup": "^3.28.1", "uglify-js": "^3.14.5" }, "scripts": { "test": "karma start && coffee test/model.coffee && npm run lint", "build": "uglifyjs backbone.js --mangle --source-map url=backbone-min.js.map -o backbone-min.js", "build-debug": "npx rollup -e backbone -f umd -g backbone:Backbone -n Backbone.debugInfo -o debug-info.js --no-strict modules/debug-info.js", "alias-sourcemap": "cpy --rename=backbone-min.map backbone-min.js.map .", "doc": "docco backbone.js && docco examples/todos/todos.js examples/backbone.localStorage.js", "lint": "eslint backbone.js test/*.js modules/*.js", "mark-release": "replace-in-file MARK_DEVELOPMENT MARK_RELEASE modules/debug-info.js && npm run build-debug", "mark-develop": "replace-in-file MARK_RELEASE MARK_DEVELOPMENT modules/debug-info.js && npm run build-debug", "prepublishOnly": "npm run test && npm run mark-release && npm run build && npm run alias-sourcemap && npm run doc && git add -u && npm run mark-develop" }, "main": "backbone.js", "version": "1.6.1", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/jashkenas/backbone.git" }, "files": [ "backbone.js", "backbone-min.js", "backbone-min.js.map", "backbone-min.map", "debug-info.js", "LICENSE", "modules/debug-info.js", "modules/package.json" ] } ================================================ FILE: test/.eslintrc ================================================ { "env": { "browser": true }, "globals": { "QUnit": false, "Backbone": true, "_": true, "$": true }, "rules": { "no-throw-literal": 0, "no-undefined": 0 } } ================================================ FILE: test/collection.js ================================================ (function(QUnit) { var a, b, c, d, e, col, otherCol; QUnit.module('Backbone.Collection', { beforeEach: function(assert) { a = new Backbone.Model({id: 3, label: 'a'}); b = new Backbone.Model({id: 2, label: 'b'}); c = new Backbone.Model({id: 1, label: 'c'}); d = new Backbone.Model({id: 0, label: 'd'}); e = null; col = new Backbone.Collection([a, b, c, d]); otherCol = new Backbone.Collection(); } }); QUnit.test('new and sort', function(assert) { assert.expect(6); var counter = 0; col.on('sort', function(){ counter++; }); assert.deepEqual(col.pluck('label'), ['a', 'b', 'c', 'd']); col.comparator = function(m1, m2) { return m1.id > m2.id ? -1 : 1; }; col.sort(); assert.equal(counter, 1); assert.deepEqual(col.pluck('label'), ['a', 'b', 'c', 'd']); col.comparator = function(model) { return model.id; }; col.sort(); assert.equal(counter, 2); assert.deepEqual(col.pluck('label'), ['d', 'c', 'b', 'a']); assert.equal(col.length, 4); }); QUnit.test('String comparator.', function(assert) { assert.expect(1); var collection = new Backbone.Collection([ {id: 3}, {id: 1}, {id: 2} ], {comparator: 'id'}); assert.deepEqual(collection.pluck('id'), [1, 2, 3]); }); QUnit.test('new and parse', function(assert) { assert.expect(3); var Collection = Backbone.Collection.extend({ parse: function(data) { return _.filter(data, function(datum) { return datum.a % 2 === 0; }); } }); var models = [{a: 1}, {a: 2}, {a: 3}, {a: 4}]; var collection = new Collection(models, {parse: true}); assert.strictEqual(collection.length, 2); assert.strictEqual(collection.first().get('a'), 2); assert.strictEqual(collection.last().get('a'), 4); }); QUnit.test('clone preserves model and comparator', function(assert) { assert.expect(3); var Model = Backbone.Model.extend(); var comparator = function(model){ return model.id; }; var collection = new Backbone.Collection([{id: 1}], { model: Model, comparator: comparator }).clone(); collection.add({id: 2}); assert.ok(collection.at(0) instanceof Model); assert.ok(collection.at(1) instanceof Model); assert.strictEqual(collection.comparator, comparator); }); QUnit.test('get', function(assert) { assert.expect(6); assert.equal(col.get(0), d); assert.equal(col.get(d.clone()), d); assert.equal(col.get(2), b); assert.equal(col.get({id: 1}), c); assert.equal(col.get(c.clone()), c); assert.equal(col.get(col.first().cid), col.first()); }); QUnit.test('get with non-default ids', function(assert) { assert.expect(5); var MongoModel = Backbone.Model.extend({idAttribute: '_id'}); var model = new MongoModel({_id: 100}); var collection = new Backbone.Collection([model], {model: MongoModel}); assert.equal(collection.get(100), model); assert.equal(collection.get(model.cid), model); assert.equal(collection.get(model), model); assert.equal(collection.get(101), void 0); var collection2 = new Backbone.Collection(); collection2.model = MongoModel; collection2.add(model.attributes); assert.equal(collection2.get(model.clone()), collection2.first()); }); QUnit.test('has', function(assert) { assert.expect(15); assert.ok(col.has(a)); assert.ok(col.has(b)); assert.ok(col.has(c)); assert.ok(col.has(d)); assert.ok(col.has(a.id)); assert.ok(col.has(b.id)); assert.ok(col.has(c.id)); assert.ok(col.has(d.id)); assert.ok(col.has(a.cid)); assert.ok(col.has(b.cid)); assert.ok(col.has(c.cid)); assert.ok(col.has(d.cid)); var outsider = new Backbone.Model({id: 4}); assert.notOk(col.has(outsider)); assert.notOk(col.has(outsider.id)); assert.notOk(col.has(outsider.cid)); }); QUnit.test('update index when id changes', function(assert) { assert.expect(6); var collection = new Backbone.Collection(); collection.add([ {id: 0, name: 'one'}, {id: 1, name: 'two'} ]); var one = collection.get(0); assert.equal(one.get('name'), 'one'); collection.on('change:name', function(model) { assert.ok(this.get(model)); assert.equal(model, this.get(101)); assert.equal(this.get(0), null); }); one.set({name: 'dalmatians', id: 101}); assert.equal(collection.get(0), null); assert.equal(collection.get(101).get('name'), 'dalmatians'); }); QUnit.test('at', function(assert) { assert.expect(2); assert.equal(col.at(2), c); assert.equal(col.at(-2), c); }); QUnit.test('pluck', function(assert) { assert.expect(1); assert.equal(col.pluck('label').join(' '), 'a b c d'); }); QUnit.test('add', function(assert) { assert.expect(14); var added, opts, secondAdded; added = opts = secondAdded = null; e = new Backbone.Model({id: 10, label: 'e'}); otherCol.add(e); otherCol.on('add', function() { secondAdded = true; }); col.on('add', function(model, collection, options){ added = model.get('label'); opts = options; }); col.add(e, {amazing: true}); assert.equal(added, 'e'); assert.equal(col.length, 5); assert.equal(col.last(), e); assert.equal(otherCol.length, 1); assert.equal(secondAdded, null); assert.ok(opts.amazing); var f = new Backbone.Model({id: 20, label: 'f'}); var g = new Backbone.Model({id: 21, label: 'g'}); var h = new Backbone.Model({id: 22, label: 'h'}); var atCol = new Backbone.Collection([f, g, h]); assert.equal(atCol.length, 3); atCol.add(e, {at: 1}); assert.equal(atCol.length, 4); assert.equal(atCol.at(1), e); assert.equal(atCol.last(), h); var coll = new Backbone.Collection(new Array(2)); var addCount = 0; coll.on('add', function(){ addCount += 1; }); coll.add([undefined, f, g]); assert.equal(coll.length, 5); assert.equal(addCount, 3); coll.add(new Array(4)); assert.equal(coll.length, 9); assert.equal(addCount, 7); }); QUnit.test('add multiple models', function(assert) { assert.expect(6); var collection = new Backbone.Collection([{at: 0}, {at: 1}, {at: 9}]); collection.add([{at: 2}, {at: 3}, {at: 4}, {at: 5}, {at: 6}, {at: 7}, {at: 8}], {at: 2}); for (var i = 0; i <= 5; i++) { assert.equal(collection.at(i).get('at'), i); } }); QUnit.test('add; at should have preference over comparator', function(assert) { assert.expect(1); var Col = Backbone.Collection.extend({ comparator: function(m1, m2) { return m1.id > m2.id ? -1 : 1; } }); var collection = new Col([{id: 2}, {id: 3}]); collection.add(new Backbone.Model({id: 1}), {at: 1}); assert.equal(collection.pluck('id').join(' '), '3 1 2'); }); QUnit.test('add; at should add to the end if the index is out of bounds', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 2}, {id: 3}]); collection.add(new Backbone.Model({id: 1}), {at: 5}); assert.equal(collection.pluck('id').join(' '), '2 3 1'); }); QUnit.test("can't add model to collection twice", function(assert) { var collection = new Backbone.Collection([{id: 1}, {id: 2}, {id: 1}, {id: 2}, {id: 3}]); assert.equal(collection.pluck('id').join(' '), '1 2 3'); }); QUnit.test("can't add different model with same id to collection twice", function(assert) { assert.expect(1); var collection = new Backbone.Collection; collection.unshift({id: 101}); collection.add({id: 101}); assert.equal(collection.length, 1); }); QUnit.test('merge in duplicate models with {merge: true}', function(assert) { assert.expect(3); var collection = new Backbone.Collection; collection.add([{id: 1, name: 'Moe'}, {id: 2, name: 'Curly'}, {id: 3, name: 'Larry'}]); collection.add({id: 1, name: 'Moses'}); assert.equal(collection.first().get('name'), 'Moe'); collection.add({id: 1, name: 'Moses'}, {merge: true}); assert.equal(collection.first().get('name'), 'Moses'); collection.add({id: 1, name: 'Tim'}, {merge: true, silent: true}); assert.equal(collection.first().get('name'), 'Tim'); }); QUnit.test('add model to multiple collections', function(assert) { assert.expect(10); var counter = 0; var m = new Backbone.Model({id: 10, label: 'm'}); m.on('add', function(model, collection) { counter++; assert.equal(m, model); if (counter > 1) { assert.equal(collection, col2); } else { assert.equal(collection, col1); } }); var col1 = new Backbone.Collection([]); col1.on('add', function(model, collection) { assert.equal(m, model); assert.equal(col1, collection); }); var col2 = new Backbone.Collection([]); col2.on('add', function(model, collection) { assert.equal(m, model); assert.equal(col2, collection); }); col1.add(m); assert.equal(m.collection, col1); col2.add(m); assert.equal(m.collection, col1); }); QUnit.test('add model with parse', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ parse: function(obj) { obj.value += 1; return obj; } }); var Col = Backbone.Collection.extend({model: Model}); var collection = new Col; collection.add({value: 1}, {parse: true}); assert.equal(collection.at(0).get('value'), 2); }); QUnit.test('add with parse and merge', function(assert) { var collection = new Backbone.Collection(); collection.parse = function(attrs) { return _.map(attrs, function(model) { if (model.model) return model.model; return model; }); }; collection.add({id: 1}); collection.add({model: {id: 1, name: 'Alf'}}, {parse: true, merge: true}); assert.equal(collection.first().get('name'), 'Alf'); }); QUnit.test('add model to collection with sort()-style comparator', function(assert) { assert.expect(3); var collection = new Backbone.Collection; collection.comparator = function(m1, m2) { return m1.get('name') < m2.get('name') ? -1 : 1; }; var tom = new Backbone.Model({name: 'Tom'}); var rob = new Backbone.Model({name: 'Rob'}); var tim = new Backbone.Model({name: 'Tim'}); collection.add(tom); collection.add(rob); collection.add(tim); assert.equal(collection.indexOf(rob), 0); assert.equal(collection.indexOf(tim), 1); assert.equal(collection.indexOf(tom), 2); }); QUnit.test('comparator that depends on `this`', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.negative = function(num) { return -num; }; collection.comparator = function(model) { return this.negative(model.id); }; collection.add([{id: 1}, {id: 2}, {id: 3}]); assert.deepEqual(collection.pluck('id'), [3, 2, 1]); collection.comparator = function(m1, m2) { return this.negative(m2.id) - this.negative(m1.id); }; collection.sort(); assert.deepEqual(collection.pluck('id'), [1, 2, 3]); }); QUnit.test('remove', function(assert) { assert.expect(12); var removed = null; var result = null; col.on('remove', function(model, collection, options) { removed = model.get('label'); assert.equal(options.index, 3); assert.equal(collection.get(model), undefined, '#3693: model cannot be fetched from collection'); }); result = col.remove(d); assert.equal(removed, 'd'); assert.strictEqual(result, d); //if we try to remove d again, it's not going to actually get removed result = col.remove(d); assert.strictEqual(result, undefined); assert.equal(col.length, 3); assert.equal(col.first(), a); col.off(); result = col.remove([c, d]); assert.equal(result.length, 1, 'only returns removed models'); assert.equal(result[0], c, 'only returns removed models'); result = col.remove([c, b]); assert.equal(result.length, 1, 'only returns removed models'); assert.equal(result[0], b, 'only returns removed models'); result = col.remove([]); assert.deepEqual(result, [], 'returns empty array when nothing removed'); }); QUnit.test('add and remove return values', function(assert) { assert.expect(13); var Even = Backbone.Model.extend({ validate: function(attrs) { if (attrs.id % 2 !== 0) return 'odd'; } }); var collection = new Backbone.Collection; collection.model = Even; var list = collection.add([{id: 2}, {id: 4}], {validate: true}); assert.equal(list.length, 2); assert.ok(list[0] instanceof Backbone.Model); assert.equal(list[1], collection.last()); assert.equal(list[1].get('id'), 4); list = collection.add([{id: 3}, {id: 6}], {validate: true}); assert.equal(collection.length, 3); assert.equal(list[0], false); assert.equal(list[1].get('id'), 6); var result = collection.add({id: 6}); assert.equal(result.cid, list[1].cid); result = collection.remove({id: 6}); assert.equal(collection.length, 2); assert.equal(result.id, 6); list = collection.remove([{id: 2}, {id: 8}]); assert.equal(collection.length, 1); assert.equal(list[0].get('id'), 2); assert.equal(list[1], null); }); QUnit.test('shift and pop', function(assert) { assert.expect(2); var collection = new Backbone.Collection([{a: 'a'}, {b: 'b'}, {c: 'c'}]); assert.equal(collection.shift().get('a'), 'a'); assert.equal(collection.pop().get('c'), 'c'); }); QUnit.test('slice', function(assert) { assert.expect(2); var collection = new Backbone.Collection([{a: 'a'}, {b: 'b'}, {c: 'c'}]); var array = collection.slice(1, 3); assert.equal(array.length, 2); assert.equal(array[0].get('b'), 'b'); }); QUnit.test('events are unbound on remove', function(assert) { assert.expect(3); var counter = 0; var dj = new Backbone.Model(); var emcees = new Backbone.Collection([dj]); emcees.on('change', function(){ counter++; }); dj.set({name: 'Kool'}); assert.equal(counter, 1); emcees.reset([]); assert.equal(dj.collection, undefined); dj.set({name: 'Shadow'}); assert.equal(counter, 1); }); QUnit.test('remove in multiple collections', function(assert) { assert.expect(7); var modelData = { id: 5, title: 'Othello' }; var passed = false; var m1 = new Backbone.Model(modelData); var m2 = new Backbone.Model(modelData); m2.on('remove', function() { passed = true; }); var col1 = new Backbone.Collection([m1]); var col2 = new Backbone.Collection([m2]); assert.notEqual(m1, m2); assert.ok(col1.length === 1); assert.ok(col2.length === 1); col1.remove(m1); assert.equal(passed, false); assert.ok(col1.length === 0); col2.remove(m1); assert.ok(col2.length === 0); assert.equal(passed, true); }); QUnit.test('remove same model in multiple collection', function(assert) { assert.expect(16); var counter = 0; var m = new Backbone.Model({id: 5, title: 'Othello'}); m.on('remove', function(model, collection) { counter++; assert.equal(m, model); if (counter > 1) { assert.equal(collection, col1); } else { assert.equal(collection, col2); } }); var col1 = new Backbone.Collection([m]); col1.on('remove', function(model, collection) { assert.equal(m, model); assert.equal(col1, collection); }); var col2 = new Backbone.Collection([m]); col2.on('remove', function(model, collection) { assert.equal(m, model); assert.equal(col2, collection); }); assert.equal(col1, m.collection); col2.remove(m); assert.ok(col2.length === 0); assert.ok(col1.length === 1); assert.equal(counter, 1); assert.equal(col1, m.collection); col1.remove(m); assert.equal(null, m.collection); assert.ok(col1.length === 0); assert.equal(counter, 2); }); QUnit.test('model destroy removes from all collections', function(assert) { assert.expect(3); var m = new Backbone.Model({id: 5, title: 'Othello'}); m.sync = function(method, model, options) { options.success(); }; var col1 = new Backbone.Collection([m]); var col2 = new Backbone.Collection([m]); m.destroy(); assert.ok(col1.length === 0); assert.ok(col2.length === 0); assert.equal(undefined, m.collection); }); QUnit.test('Collection: non-persisted model destroy removes from all collections', function(assert) { assert.expect(3); var m = new Backbone.Model({title: 'Othello'}); m.sync = function(method, model, options) { throw 'should not be called'; }; var col1 = new Backbone.Collection([m]); var col2 = new Backbone.Collection([m]); m.destroy(); assert.ok(col1.length === 0); assert.ok(col2.length === 0); assert.equal(undefined, m.collection); }); QUnit.test('fetch', function(assert) { assert.expect(4); var collection = new Backbone.Collection; collection.url = '/test'; collection.fetch(); assert.equal(this.syncArgs.method, 'read'); assert.equal(this.syncArgs.model, collection); assert.equal(this.syncArgs.options.parse, true); collection.fetch({parse: false}); assert.equal(this.syncArgs.options.parse, false); }); QUnit.test('fetch with an error response triggers an error event', function(assert) { assert.expect(1); var collection = new Backbone.Collection(); collection.on('error', function() { assert.ok(true); }); collection.sync = function(method, model, options) { options.error(); }; collection.fetch(); }); QUnit.test('#3283 - fetch with an error response calls error with context', function(assert) { assert.expect(1); var collection = new Backbone.Collection(); var obj = {}; var options = { context: obj, error: function() { assert.equal(this, obj); } }; collection.sync = function(method, model, opts) { opts.error.call(opts.context); }; collection.fetch(options); }); QUnit.test('ensure fetch only parses once', function(assert) { assert.expect(1); var collection = new Backbone.Collection; var counter = 0; collection.parse = function(models) { counter++; return models; }; collection.url = '/test'; collection.fetch(); this.syncArgs.options.success([]); assert.equal(counter, 1); }); QUnit.test('create', function(assert) { assert.expect(4); var collection = new Backbone.Collection; collection.url = '/test'; var model = collection.create({label: 'f'}, {wait: true}); assert.equal(this.syncArgs.method, 'create'); assert.equal(this.syncArgs.model, model); assert.equal(model.get('label'), 'f'); assert.equal(model.collection, collection); }); QUnit.test('create with validate:true enforces validation', function(assert) { assert.expect(3); var ValidatingModel = Backbone.Model.extend({ validate: function(attrs) { return 'fail'; } }); var ValidatingCollection = Backbone.Collection.extend({ model: ValidatingModel }); var collection = new ValidatingCollection(); collection.on('invalid', function(coll, error, options) { assert.equal(error, 'fail'); assert.equal(options.validationError, 'fail'); }); assert.equal(collection.create({foo: 'bar'}, {validate: true}), false); }); QUnit.test('create will pass extra options to success callback', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ sync: function(method, model, options) { _.extend(options, {specialSync: true}); return Backbone.Model.prototype.sync.call(this, method, model, options); } }); var Collection = Backbone.Collection.extend({ model: Model, url: '/test' }); var collection = new Collection; var success = function(model, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; collection.create({}, {success: success}); this.ajaxSettings.success(); }); QUnit.test('create with wait:true should not call collection.parse', function(assert) { assert.expect(0); var Collection = Backbone.Collection.extend({ url: '/test', parse: function() { assert.ok(false); } }); var collection = new Collection; collection.create({}, {wait: true}); this.ajaxSettings.success(); }); QUnit.test('a failing create returns model with errors', function(assert) { var ValidatingModel = Backbone.Model.extend({ validate: function(attrs) { return 'fail'; } }); var ValidatingCollection = Backbone.Collection.extend({ model: ValidatingModel }); var collection = new ValidatingCollection(); var m = collection.create({foo: 'bar'}); assert.equal(m.validationError, 'fail'); assert.equal(collection.length, 1); }); QUnit.test('failing create with wait:true triggers error event (#4262)', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.url = '/test'; collection.on('error', function() { assert.ok(true); }); var model = collection.create({id: '1'}, {wait: true}); model.on('error', function() { assert.ok(true); }); this.ajaxSettings.error(); }); QUnit.test('successful create with wait:true triggers success event (#4262)', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.url = '/test'; collection.on('sync', function() { assert.ok(true); }); var model = collection.create({id: '1'}, {wait: true}); model.on('sync', function() { assert.ok(true); }); this.ajaxSettings.success(); }); QUnit.test('successful create with wait:true drops special error listener (#4284)', function(assert) { assert.expect(1); var collection = new Backbone.Collection; collection.url = '/test'; collection.on('error', function() { assert.ok(true); }); var model = collection.create({id: '1'}, {wait: true}); this.ajaxSettings.success(); model.trigger('error'); }); QUnit.test('failing create pre-existing with wait:true triggers once (#4262)', function(assert) { assert.expect(1); var model = new Backbone.Model; var collection = new Backbone.Collection([model]); collection.url = '/test'; collection.on('error', function() { assert.ok(true); }); collection.create(model, {wait: true}); this.ajaxSettings.error(); }); QUnit.test('successful create pre-existing with wait:true preserves other error bindings (#4262)', function(assert) { assert.expect(1); var model = new Backbone.Model; var collection = new Backbone.Collection([model]); collection.url = '/test'; model.on('error', function() { assert.ok(true); }); collection.create(model, {wait: true}); this.ajaxSettings.success(); model.trigger('error'); }); QUnit.test('initialize', function(assert) { assert.expect(1); var Collection = Backbone.Collection.extend({ initialize: function() { this.one = 1; } }); var coll = new Collection; assert.equal(coll.one, 1); }); QUnit.test('preinitialize', function(assert) { assert.expect(1); var Collection = Backbone.Collection.extend({ preinitialize: function() { this.one = 1; } }); var coll = new Collection; assert.equal(coll.one, 1); }); QUnit.test('preinitialize occurs before the collection is set up', function(assert) { assert.expect(2); var Collection = Backbone.Collection.extend({ preinitialize: function() { assert.notEqual(this.model, FooModel); } }); var FooModel = Backbone.Model.extend({id: 'foo'}); var coll = new Collection({}, { model: FooModel }); assert.equal(coll.model, FooModel); }); QUnit.test('toJSON', function(assert) { assert.expect(1); assert.equal(JSON.stringify(col), '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]'); }); QUnit.test('where and findWhere', function(assert) { assert.expect(8); var model = new Backbone.Model({a: 1}); var coll = new Backbone.Collection([ model, {a: 1}, {a: 1, b: 2}, {a: 2, b: 2}, {a: 3} ]); assert.equal(coll.where({a: 1}).length, 3); assert.equal(coll.where({a: 2}).length, 1); assert.equal(coll.where({a: 3}).length, 1); assert.equal(coll.where({b: 1}).length, 0); assert.equal(coll.where({b: 2}).length, 2); assert.equal(coll.where({a: 1, b: 2}).length, 1); assert.equal(coll.findWhere({a: 1}), model); assert.equal(coll.findWhere({a: 4}), void 0); }); QUnit.test('mixin', function(assert) { Backbone.Collection.mixin({ sum: function(models, iteratee) { return _.reduce(models, function(s, m) { return s + iteratee(m); }, 0); } }); var coll = new Backbone.Collection([ {a: 1}, {a: 1, b: 2}, {a: 2, b: 2}, {a: 3} ]); assert.equal(coll.sum(function(m) { return m.get('a'); }), 7); }); QUnit.test('Underscore methods', function(assert) { assert.expect(21); assert.equal(col.map(function(model){ return model.get('label'); }).join(' '), 'a b c d'); assert.equal(col.some(function(model){ return model.id === 100; }), false); assert.equal(col.some(function(model){ return model.id === 0; }), true); assert.equal(col.reduce(function(m1, m2) {return m1.id > m2.id ? m1 : m2;}).id, 3); assert.equal(col.reduceRight(function(m1, m2) {return m1.id > m2.id ? m1 : m2;}).id, 3); assert.equal(col.indexOf(b), 1); assert.equal(col.size(), 4); assert.equal(col.rest().length, 3); assert.ok(!_.includes(col.rest(), a)); assert.ok(_.includes(col.rest(), d)); assert.ok(!col.isEmpty()); assert.ok(!_.includes(col.without(d), d)); var wrapped = col.chain(); assert.equal(wrapped.map('id').max().value(), 3); assert.equal(wrapped.map('id').min().value(), 0); assert.deepEqual( wrapped .filter(function(o){ return o.id % 2 === 0; }) .map(function(o){ return o.id * 2; }) .value(), [4, 0] ); assert.deepEqual(col.difference([c, d]), [a, b]); assert.ok(col.includes(col.sample())); var first = col.first(); assert.deepEqual(col.groupBy(function(model){ return model.id; })[first.id], [first]); assert.deepEqual(col.countBy(function(model){ return model.id; }), {0: 1, 1: 1, 2: 1, 3: 1}); assert.deepEqual(col.sortBy(function(model){ return model.id; })[0], col.at(3)); assert.ok(col.indexBy('id')[first.id] === first); }); QUnit.test('Underscore methods with object-style and property-style iteratee', function(assert) { assert.expect(26); var model = new Backbone.Model({a: 4, b: 1, e: 3}); var coll = new Backbone.Collection([ {a: 1, b: 1}, {a: 2, b: 1, c: 1}, {a: 3, b: 1}, model ]); assert.equal(coll.find({a: 0}), undefined); assert.deepEqual(coll.find({a: 4}), model); assert.equal(coll.find('d'), undefined); assert.deepEqual(coll.find('e'), model); assert.equal(coll.filter({a: 0}), false); assert.deepEqual(coll.filter({a: 4}), [model]); assert.equal(coll.some({a: 0}), false); assert.equal(coll.some({a: 1}), true); assert.equal(coll.reject({a: 0}).length, 4); assert.deepEqual(coll.reject({a: 4}), _.without(coll.models, model)); assert.equal(coll.every({a: 0}), false); assert.equal(coll.every({b: 1}), true); assert.deepEqual(coll.partition({a: 0})[0], []); assert.deepEqual(coll.partition({a: 0})[1], coll.models); assert.deepEqual(coll.partition({a: 4})[0], [model]); assert.deepEqual(coll.partition({a: 4})[1], _.without(coll.models, model)); assert.deepEqual(coll.map({a: 2}), [false, true, false, false]); assert.deepEqual(coll.map('a'), [1, 2, 3, 4]); assert.deepEqual(coll.sortBy('a')[3], model); assert.deepEqual(coll.sortBy('e')[0], model); assert.deepEqual(coll.countBy({a: 4}), {'false': 3, 'true': 1}); assert.deepEqual(coll.countBy('d'), {undefined: 4}); assert.equal(coll.findIndex({b: 1}), 0); assert.equal(coll.findIndex({b: 9}), -1); assert.equal(coll.findLastIndex({b: 1}), 3); assert.equal(coll.findLastIndex({b: 9}), -1); }); QUnit.test('reset', function(assert) { assert.expect(16); var resetCount = 0; var models = col.models; col.on('reset', function() { resetCount += 1; }); col.reset([]); assert.equal(resetCount, 1); assert.equal(col.length, 0); assert.equal(col.last(), null); col.reset(models); assert.equal(resetCount, 2); assert.equal(col.length, 4); assert.equal(col.last(), d); col.reset(_.map(models, function(m){ return m.attributes; })); assert.equal(resetCount, 3); assert.equal(col.length, 4); assert.ok(col.last() !== d); assert.ok(_.isEqual(col.last().attributes, d.attributes)); col.reset(); assert.equal(col.length, 0); assert.equal(resetCount, 4); var f = new Backbone.Model({id: 20, label: 'f'}); col.reset([undefined, f]); assert.equal(col.length, 2); assert.equal(resetCount, 5); col.reset(new Array(4)); assert.equal(col.length, 4); assert.equal(resetCount, 6); }); QUnit.test('reset with different values', function(assert) { var collection = new Backbone.Collection({id: 1}); collection.reset({id: 1, a: 1}); assert.equal(collection.get(1).get('a'), 1); }); QUnit.test('same references in reset', function(assert) { var model = new Backbone.Model({id: 1}); var collection = new Backbone.Collection({id: 1}); collection.reset(model); assert.equal(collection.get(1), model); }); QUnit.test('reset passes caller options', function(assert) { assert.expect(3); var Model = Backbone.Model.extend({ initialize: function(attrs, options) { this.modelParameter = options.modelParameter; } }); var collection = new (Backbone.Collection.extend({model: Model}))(); collection.reset([{astring: 'green', anumber: 1}, {astring: 'blue', anumber: 2}], {modelParameter: 'model parameter'}); assert.equal(collection.length, 2); collection.each(function(model) { assert.equal(model.modelParameter, 'model parameter'); }); }); QUnit.test('reset does not alter options by reference', function(assert) { assert.expect(2); var collection = new Backbone.Collection([{id: 1}]); var origOpts = {}; collection.on('reset', function(coll, opts){ assert.equal(origOpts.previousModels, undefined); assert.equal(opts.previousModels[0].id, 1); }); collection.reset([], origOpts); }); QUnit.test('trigger custom events on models', function(assert) { assert.expect(1); var fired = null; a.on('custom', function() { fired = true; }); a.trigger('custom'); assert.equal(fired, true); }); QUnit.test('add does not alter arguments', function(assert) { assert.expect(2); var attrs = {}; var models = [attrs]; new Backbone.Collection().add(models); assert.equal(models.length, 1); assert.ok(attrs === models[0]); }); QUnit.test('#714: access `model.collection` in a brand new model.', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.url = '/test'; var Model = Backbone.Model.extend({ set: function(attrs) { assert.equal(attrs.prop, 'value'); assert.equal(this.collection, collection); return this; } }); collection.model = Model; collection.create({prop: 'value'}); }); QUnit.test('#574, remove its own reference to the .models array.', function(assert) { assert.expect(2); var collection = new Backbone.Collection([ {id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6} ]); assert.equal(collection.length, 6); collection.remove(collection.models); assert.equal(collection.length, 0); }); QUnit.test('#861, adding models to a collection which do not pass validation, with validate:true', function(assert) { assert.expect(2); var Model = Backbone.Model.extend({ validate: function(attrs) { if (attrs.id === 3) return "id can't be 3"; } }); var Collection = Backbone.Collection.extend({ model: Model }); var collection = new Collection; collection.on('invalid', function() { assert.ok(true); }); collection.add([{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}], {validate: true}); assert.deepEqual(collection.pluck('id'), [1, 2, 4, 5, 6]); }); QUnit.test('Invalid models are discarded with validate:true.', function(assert) { assert.expect(5); var collection = new Backbone.Collection; collection.on('test', function() { assert.ok(true); }); collection.model = Backbone.Model.extend({ validate: function(attrs){ if (!attrs.valid) return 'invalid'; } }); var model = new collection.model({id: 1, valid: true}); collection.add([model, {id: 2}], {validate: true}); model.trigger('test'); assert.ok(collection.get(model.cid)); assert.ok(collection.get(1)); assert.ok(!collection.get(2)); assert.equal(collection.length, 1); }); QUnit.test('multiple copies of the same model', function(assert) { assert.expect(3); var collection = new Backbone.Collection(); var model = new Backbone.Model(); collection.add([model, model]); assert.equal(collection.length, 1); collection.add([{id: 1}, {id: 1}]); assert.equal(collection.length, 2); assert.equal(collection.last().id, 1); }); QUnit.test('#964 - collection.get return inconsistent', function(assert) { assert.expect(2); var collection = new Backbone.Collection(); assert.ok(collection.get(null) === undefined); assert.ok(collection.get() === undefined); }); QUnit.test('#1112 - passing options.model sets collection.model', function(assert) { assert.expect(2); var Model = Backbone.Model.extend({}); var collection = new Backbone.Collection([{id: 1}], {model: Model}); assert.ok(collection.model === Model); assert.ok(collection.at(0) instanceof Model); }); QUnit.test('null and undefined are invalid ids.', function(assert) { assert.expect(2); var model = new Backbone.Model({id: 1}); var collection = new Backbone.Collection([model]); model.set({id: null}); assert.ok(!collection.get('null')); model.set({id: 1}); model.set({id: undefined}); assert.ok(!collection.get('undefined')); }); QUnit.test('falsy comparator', function(assert) { assert.expect(4); var Col = Backbone.Collection.extend({ comparator: function(model){ return model.id; } }); var collection = new Col(); var colFalse = new Col(null, {comparator: false}); var colNull = new Col(null, {comparator: null}); var colUndefined = new Col(null, {comparator: undefined}); assert.ok(collection.comparator); assert.ok(!colFalse.comparator); assert.ok(!colNull.comparator); assert.ok(colUndefined.comparator); }); QUnit.test('#1355 - `options` is passed to success callbacks', function(assert) { assert.expect(2); var m = new Backbone.Model({x: 1}); var collection = new Backbone.Collection(); var opts = { opts: true, success: function(coll, resp, options) { assert.ok(options.opts); } }; collection.sync = m.sync = function( method, coll, options ){ options.success({}); }; collection.fetch(opts); collection.create(m, opts); }); QUnit.test("#1412 - Trigger 'request' and 'sync' events.", function(assert) { assert.expect(4); var collection = new Backbone.Collection; collection.url = '/test'; Backbone.ajax = function(settings){ settings.success(); }; collection.on('request', function(obj, xhr, options) { assert.ok(obj === collection, "collection has correct 'request' event after fetching"); }); collection.on('sync', function(obj, response, options) { assert.ok(obj === collection, "collection has correct 'sync' event after fetching"); }); collection.fetch(); collection.off(); collection.on('request', function(obj, xhr, options) { assert.ok(obj === collection.get(1), "collection has correct 'request' event after one of its models save"); }); collection.on('sync', function(obj, response, options) { assert.ok(obj === collection.get(1), "collection has correct 'sync' event after one of its models save"); }); collection.create({id: 1}); collection.off(); }); QUnit.test('#3283 - fetch, create calls success with context', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.url = '/test'; Backbone.ajax = function(settings) { settings.success.call(settings.context); }; var obj = {}; var options = { context: obj, success: function() { assert.equal(this, obj); } }; collection.fetch(options); collection.create({id: 1}, options); }); QUnit.test('#1447 - create with wait adds model.', function(assert) { assert.expect(1); var collection = new Backbone.Collection; var model = new Backbone.Model; model.sync = function(method, m, options){ options.success(); }; collection.on('add', function(){ assert.ok(true); }); collection.create(model, {wait: true}); }); QUnit.test('#1448 - add sorts collection after merge.', function(assert) { assert.expect(1); var collection = new Backbone.Collection([ {id: 1, x: 1}, {id: 2, x: 2} ]); collection.comparator = function(model){ return model.get('x'); }; collection.add({id: 1, x: 3}, {merge: true}); assert.deepEqual(collection.pluck('id'), [2, 1]); }); QUnit.test('#1655 - groupBy can be used with a string argument.', function(assert) { assert.expect(3); var collection = new Backbone.Collection([{x: 1}, {x: 2}]); var grouped = collection.groupBy('x'); assert.strictEqual(_.keys(grouped).length, 2); assert.strictEqual(grouped[1][0].get('x'), 1); assert.strictEqual(grouped[2][0].get('x'), 2); }); QUnit.test('#1655 - sortBy can be used with a string argument.', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{x: 3}, {x: 1}, {x: 2}]); var values = _.map(collection.sortBy('x'), function(model) { return model.get('x'); }); assert.deepEqual(values, [1, 2, 3]); }); QUnit.test('#1604 - Removal during iteration.', function(assert) { assert.expect(0); var collection = new Backbone.Collection([{}, {}]); collection.on('add', function() { collection.at(0).destroy(); }); collection.add({}, {at: 0}); }); QUnit.test('#1638 - `sort` during `add` triggers correctly.', function(assert) { var collection = new Backbone.Collection; collection.comparator = function(model) { return model.get('x'); }; var added = []; collection.on('add', function(model) { model.set({x: 3}); collection.sort(); added.push(model.id); }); collection.add([{id: 1, x: 1}, {id: 2, x: 2}]); assert.deepEqual(added, [1, 2]); }); QUnit.test('fetch parses models by default', function(assert) { assert.expect(1); var model = {}; var Collection = Backbone.Collection.extend({ url: 'test', model: Backbone.Model.extend({ parse: function(resp) { assert.strictEqual(resp, model); } }) }); new Collection().fetch(); this.ajaxSettings.success([model]); }); QUnit.test("`sort` shouldn't always fire on `add`", function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}, {id: 2}, {id: 3}], { comparator: 'id' }); collection.sort = function(){ assert.ok(true); }; collection.add([]); collection.add({id: 1}); collection.add([{id: 2}, {id: 3}]); collection.add({id: 4}); }); QUnit.test('#1407 parse option on constructor parses collection and models', function(assert) { assert.expect(2); var model = { namespace: [{id: 1}, {id: 2}] }; var Collection = Backbone.Collection.extend({ model: Backbone.Model.extend({ parse: function(m) { m.name = 'test'; return m; } }), parse: function(m) { return m.namespace; } }); var collection = new Collection(model, {parse: true}); assert.equal(collection.length, 2); assert.equal(collection.at(0).get('name'), 'test'); }); QUnit.test('#1407 parse option on reset parses collection and models', function(assert) { assert.expect(2); var model = { namespace: [{id: 1}, {id: 2}] }; var Collection = Backbone.Collection.extend({ model: Backbone.Model.extend({ parse: function(m) { m.name = 'test'; return m; } }), parse: function(m) { return m.namespace; } }); var collection = new Collection(); collection.reset(model, {parse: true}); assert.equal(collection.length, 2); assert.equal(collection.at(0).get('name'), 'test'); }); QUnit.test('Reset includes previous models in triggered event.', function(assert) { assert.expect(1); var model = new Backbone.Model(); var collection = new Backbone.Collection([model]); collection.on('reset', function(coll, options) { assert.deepEqual(options.previousModels, [model]); }); collection.reset([]); }); QUnit.test('set', function(assert) { var m1 = new Backbone.Model(); var m2 = new Backbone.Model({id: 2}); var m3 = new Backbone.Model(); var collection = new Backbone.Collection([m1, m2]); // Test add/change/remove events collection.on('add', function(model) { assert.strictEqual(model, m3); }); collection.on('change', function(model) { assert.strictEqual(model, m2); }); collection.on('remove', function(model) { assert.strictEqual(model, m1); }); // remove: false doesn't remove any models collection.set([], {remove: false}); assert.strictEqual(collection.length, 2); // add: false doesn't add any models collection.set([m1, m2, m3], {add: false}); assert.strictEqual(collection.length, 2); // merge: false doesn't change any models collection.set([m1, {id: 2, a: 1}], {merge: false}); assert.strictEqual(m2.get('a'), void 0); // add: false, remove: false only merges existing models collection.set([m1, {id: 2, a: 0}, m3, {id: 4}], {add: false, remove: false}); assert.strictEqual(collection.length, 2); assert.strictEqual(m2.get('a'), 0); // default options add/remove/merge as appropriate collection.set([{id: 2, a: 1}, m3]); assert.strictEqual(collection.length, 2); assert.strictEqual(m2.get('a'), 1); // Test removing models not passing an argument collection.off('remove').on('remove', function(model) { assert.ok(model === m2 || model === m3); }); collection.set([]); assert.strictEqual(collection.length, 0); // Test null models on set doesn't clear collection collection.off(); collection.set([{id: 1}]); collection.set(); assert.strictEqual(collection.length, 1); }); QUnit.test('set with only cids', function(assert) { assert.expect(3); var m1 = new Backbone.Model; var m2 = new Backbone.Model; var collection = new Backbone.Collection; collection.set([m1, m2]); assert.equal(collection.length, 2); collection.set([m1]); assert.equal(collection.length, 1); collection.set([m1, m1, m1, m2, m2], {remove: false}); assert.equal(collection.length, 2); }); QUnit.test('set with only idAttribute', function(assert) { assert.expect(3); var m1 = {_id: 1}; var m2 = {_id: 2}; var Col = Backbone.Collection.extend({ model: Backbone.Model.extend({ idAttribute: '_id' }) }); var collection = new Col; collection.set([m1, m2]); assert.equal(collection.length, 2); collection.set([m1]); assert.equal(collection.length, 1); collection.set([m1, m1, m1, m2, m2], {remove: false}); assert.equal(collection.length, 2); }); QUnit.test('set + merge with default values defined', function(assert) { var Model = Backbone.Model.extend({ defaults: { key: 'value' } }); var m = new Model({id: 1}); var collection = new Backbone.Collection([m], {model: Model}); assert.equal(collection.first().get('key'), 'value'); collection.set({id: 1, key: 'other'}); assert.equal(collection.first().get('key'), 'other'); collection.set({id: 1, other: 'value'}); assert.equal(collection.first().get('key'), 'other'); assert.equal(collection.length, 1); }); QUnit.test('merge without mutation', function(assert) { var Model = Backbone.Model.extend({ initialize: function(attrs, options) { if (attrs.child) { this.set('child', new Model(attrs.child, options), options); } } }); var Collection = Backbone.Collection.extend({model: Model}); var data = [{id: 1, child: {id: 2}}]; var collection = new Collection(data); assert.equal(collection.first().id, 1); collection.set(data); assert.equal(collection.first().id, 1); collection.set([{id: 2, child: {id: 2}}].concat(data)); assert.deepEqual(collection.pluck('id'), [2, 1]); }); QUnit.test('`set` and model level `parse`', function(assert) { var Model = Backbone.Model.extend({}); var Collection = Backbone.Collection.extend({ model: Model, parse: function(res) { return _.map(res.models, 'model'); } }); var model = new Model({id: 1}); var collection = new Collection(model); collection.set({models: [ {model: {id: 1}}, {model: {id: 2}} ]}, {parse: true}); assert.equal(collection.first(), model); }); QUnit.test('`set` data is only parsed once', function(assert) { var collection = new Backbone.Collection(); collection.model = Backbone.Model.extend({ parse: function(data) { assert.equal(data.parsed, void 0); data.parsed = true; return data; } }); collection.set({}, {parse: true}); }); QUnit.test('`set` matches input order in the absence of a comparator', function(assert) { var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); collection.set([{id: 3}, {id: 2}, {id: 1}]); assert.deepEqual(collection.models, [three, two, one]); collection.set([{id: 1}, {id: 2}]); assert.deepEqual(collection.models, [one, two]); collection.set([two, three, one]); assert.deepEqual(collection.models, [two, three, one]); collection.set([{id: 1}, {id: 2}], {remove: false}); assert.deepEqual(collection.models, [two, three, one]); collection.set([{id: 1}, {id: 2}, {id: 3}], {merge: false}); assert.deepEqual(collection.models, [one, two, three]); collection.set([three, two, one, {id: 4}], {add: false}); assert.deepEqual(collection.models, [one, two, three]); }); QUnit.test('#1894 - Push should not trigger a sort', function(assert) { assert.expect(0); var Collection = Backbone.Collection.extend({ comparator: 'id', sort: function() { assert.ok(false); } }); new Collection().push({id: 1}); }); QUnit.test('#2428 - push duplicate models, return the correct one', function(assert) { assert.expect(1); var collection = new Backbone.Collection; var model1 = collection.push({id: 101}); var model2 = collection.push({id: 101}); assert.ok(model2.cid === model1.cid); }); QUnit.test('`set` with non-normal id', function(assert) { var Collection = Backbone.Collection.extend({ model: Backbone.Model.extend({idAttribute: '_id'}) }); var collection = new Collection({_id: 1}); collection.set([{_id: 1, a: 1}], {add: false}); assert.equal(collection.first().get('a'), 1); }); QUnit.test('#1894 - `sort` can optionally be turned off', function(assert) { assert.expect(0); var Collection = Backbone.Collection.extend({ comparator: 'id', sort: function() { assert.ok(false); } }); new Collection().add({id: 1}, {sort: false}); }); QUnit.test('#1915 - `parse` data in the right order in `set`', function(assert) { var collection = new (Backbone.Collection.extend({ parse: function(data) { assert.strictEqual(data.status, 'ok'); return data.data; } })); var res = {status: 'ok', data: [{id: 1}]}; collection.set(res, {parse: true}); }); QUnit.test('#1939 - `parse` is passed `options`', function(assert) { var done = assert.async(); assert.expect(1); var collection = new (Backbone.Collection.extend({ url: '/', parse: function(data, options) { assert.strictEqual(options.xhr.someHeader, 'headerValue'); return data; } })); var ajax = Backbone.ajax; Backbone.ajax = function(params) { _.defer(params.success, []); return {someHeader: 'headerValue'}; }; collection.fetch({ success: function() { done(); } }); Backbone.ajax = ajax; }); QUnit.test('fetch will pass extra options to success callback', function(assert) { assert.expect(1); var SpecialSyncCollection = Backbone.Collection.extend({ url: '/test', sync: function(method, collection, options) { _.extend(options, {specialSync: true}); return Backbone.Collection.prototype.sync.call(this, method, collection, options); } }); var collection = new SpecialSyncCollection(); var onSuccess = function(coll, resp, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; collection.fetch({success: onSuccess}); this.ajaxSettings.success(); }); QUnit.test('`add` only `sort`s when necessary', function(assert) { assert.expect(2); var collection = new (Backbone.Collection.extend({ comparator: 'a' }))([{id: 1}, {id: 2}, {id: 3}]); collection.on('sort', function() { assert.ok(true); }); collection.add({id: 4}); // do sort, new model collection.add({id: 1, a: 1}, {merge: true}); // do sort, comparator change collection.add({id: 1, b: 1}, {merge: true}); // don't sort, no comparator change collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no comparator change collection.add(collection.models); // don't sort, nothing new collection.add(collection.models, {merge: true}); // don't sort }); QUnit.test('`add` only `sort`s when necessary with comparator function', function(assert) { assert.expect(3); var collection = new (Backbone.Collection.extend({ comparator: function(m1, m2) { return m1.get('a') > m2.get('a') ? 1 : m1.get('a') < m2.get('a') ? -1 : 0; } }))([{id: 1}, {id: 2}, {id: 3}]); collection.on('sort', function() { assert.ok(true); }); collection.add({id: 4}); // do sort, new model collection.add({id: 1, a: 1}, {merge: true}); // do sort, model change collection.add({id: 1, b: 1}, {merge: true}); // do sort, model change collection.add({id: 1, a: 1}, {merge: true}); // don't sort, no model change collection.add(collection.models); // don't sort, nothing new collection.add(collection.models, {merge: true}); // don't sort }); QUnit.test('Attach options to collection.', function(assert) { assert.expect(2); var Model = Backbone.Model; var comparator = function(){}; var collection = new Backbone.Collection([], { model: Model, comparator: comparator }); assert.ok(collection.model === Model); assert.ok(collection.comparator === comparator); }); QUnit.test('Pass falsey for `models` for empty Col with `options`', function(assert) { assert.expect(9); var opts = {a: 1, b: 2}; _.forEach([undefined, null, false], function(falsey) { var Collection = Backbone.Collection.extend({ initialize: function(models, options) { assert.strictEqual(models, falsey); assert.strictEqual(options, opts); } }); var collection = new Collection(falsey, opts); assert.strictEqual(collection.length, 0); }); }); QUnit.test('`add` overrides `set` flags', function(assert) { var collection = new Backbone.Collection(); collection.once('add', function(model, coll, options) { coll.add({id: 2}, options); }); collection.set({id: 1}); assert.equal(collection.length, 2); }); QUnit.test('#2606 - Collection#create, success arguments', function(assert) { assert.expect(1); var collection = new Backbone.Collection; collection.url = 'test'; collection.create({}, { success: function(model, resp, options) { assert.strictEqual(resp, 'response'); } }); this.ajaxSettings.success('response'); }); QUnit.test('#2612 - nested `parse` works with `Collection#set`', function(assert) { var Job = Backbone.Model.extend({ constructor: function() { this.items = new Items(); Backbone.Model.apply(this, arguments); }, parse: function(attrs) { this.items.set(attrs.items, {parse: true}); return _.omit(attrs, 'items'); } }); var Item = Backbone.Model.extend({ constructor: function() { this.subItems = new Backbone.Collection(); Backbone.Model.apply(this, arguments); }, parse: function(attrs) { this.subItems.set(attrs.subItems, {parse: true}); return _.omit(attrs, 'subItems'); } }); var Items = Backbone.Collection.extend({ model: Item }); var data = { name: 'JobName', id: 1, items: [{ id: 1, name: 'Sub1', subItems: [ {id: 1, subName: 'One'}, {id: 2, subName: 'Two'} ] }, { id: 2, name: 'Sub2', subItems: [ {id: 3, subName: 'Three'}, {id: 4, subName: 'Four'} ] }] }; var newData = { name: 'NewJobName', id: 1, items: [{ id: 1, name: 'NewSub1', subItems: [ {id: 1, subName: 'NewOne'}, {id: 2, subName: 'NewTwo'} ] }, { id: 2, name: 'NewSub2', subItems: [ {id: 3, subName: 'NewThree'}, {id: 4, subName: 'NewFour'} ] }] }; var job = new Job(data, {parse: true}); assert.equal(job.get('name'), 'JobName'); assert.equal(job.items.at(0).get('name'), 'Sub1'); assert.equal(job.items.length, 2); assert.equal(job.items.get(1).subItems.get(1).get('subName'), 'One'); assert.equal(job.items.get(2).subItems.get(3).get('subName'), 'Three'); job.set(job.parse(newData, {parse: true})); assert.equal(job.get('name'), 'NewJobName'); assert.equal(job.items.at(0).get('name'), 'NewSub1'); assert.equal(job.items.length, 2); assert.equal(job.items.get(1).subItems.get(1).get('subName'), 'NewOne'); assert.equal(job.items.get(2).subItems.get(3).get('subName'), 'NewThree'); }); QUnit.test('_addReference binds all collection events & adds to the lookup hashes', function(assert) { assert.expect(8); var calls = {add: 0, remove: 0}; var Collection = Backbone.Collection.extend({ _addReference: function(model) { Backbone.Collection.prototype._addReference.apply(this, arguments); calls.add++; assert.equal(model, this._byId[model.id]); assert.equal(model, this._byId[model.cid]); assert.equal(model._events.all.length, 1); }, _removeReference: function(model) { Backbone.Collection.prototype._removeReference.apply(this, arguments); calls.remove++; assert.equal(this._byId[model.id], void 0); assert.equal(this._byId[model.cid], void 0); assert.equal(model.collection, void 0); } }); var collection = new Collection(); var model = collection.add({id: 1}); collection.remove(model); assert.equal(calls.add, 1); assert.equal(calls.remove, 1); }); QUnit.test('Do not allow duplicate models to be `add`ed or `set`', function(assert) { var collection = new Backbone.Collection(); collection.add([{id: 1}, {id: 1}]); assert.equal(collection.length, 1); assert.equal(collection.models.length, 1); collection.set([{id: 1}, {id: 1}]); assert.equal(collection.length, 1); assert.equal(collection.models.length, 1); }); QUnit.test('#3020: #set with {add: false} should not throw.', function(assert) { assert.expect(2); var collection = new Backbone.Collection; collection.set([{id: 1}], {add: false}); assert.strictEqual(collection.length, 0); assert.strictEqual(collection.models.length, 0); }); QUnit.test('create with wait, model instance, #3028', function(assert) { assert.expect(1); var collection = new Backbone.Collection(); var model = new Backbone.Model({id: 1}); model.sync = function(){ assert.equal(this.collection, collection); }; collection.create(model, {wait: true}); }); QUnit.test('modelId', function(assert) { var Stooge = Backbone.Model.extend(); var StoogeCollection = Backbone.Collection.extend(); // Default to using `id` if `model::idAttribute` and `Collection::model::idAttribute` not present. assert.equal(StoogeCollection.prototype.modelId({id: 1}), 1); // Default to using `model::idAttribute` if present. Stooge.prototype.idAttribute = '_id'; var model = new Stooge({_id: 1}); assert.equal(StoogeCollection.prototype.modelId(model.attributes, model.idAttribute), 1); // Default to using `Collection::model::idAttribute` if model::idAttribute not present. StoogeCollection.prototype.model = Stooge; assert.equal(StoogeCollection.prototype.modelId({_id: 1}), 1); }); QUnit.test('Polymorphic models work with "simple" constructors', function(assert) { var A = Backbone.Model.extend(); var B = Backbone.Model.extend(); var C = Backbone.Collection.extend({ model: function(attrs) { return attrs.type === 'a' ? new A(attrs) : new B(attrs); } }); var collection = new C([{id: 1, type: 'a'}, {id: 2, type: 'b'}]); assert.equal(collection.length, 2); assert.ok(collection.at(0) instanceof A); assert.equal(collection.at(0).id, 1); assert.ok(collection.at(1) instanceof B); assert.equal(collection.at(1).id, 2); }); QUnit.test('Polymorphic models work with "advanced" constructors', function(assert) { var A = Backbone.Model.extend({idAttribute: '_id'}); var B = Backbone.Model.extend({idAttribute: '_id'}); var C = Backbone.Collection.extend({ model: Backbone.Model.extend({ constructor: function(attrs) { return attrs.type === 'a' ? new A(attrs) : new B(attrs); }, idAttribute: '_id' }) }); var collection = new C([{_id: 1, type: 'a'}, {_id: 2, type: 'b'}]); assert.equal(collection.length, 2); assert.ok(collection.at(0) instanceof A); assert.equal(collection.at(0), collection.get(1)); assert.ok(collection.at(1) instanceof B); assert.equal(collection.at(1), collection.get(2)); C = Backbone.Collection.extend({ model: function(attrs) { return attrs.type === 'a' ? new A(attrs) : new B(attrs); }, modelId: function(attrs) { return attrs.type + '-' + attrs.id; } }); collection = new C([{id: 1, type: 'a'}, {id: 1, type: 'b'}]); assert.equal(collection.length, 2); assert.ok(collection.at(0) instanceof A); assert.equal(collection.at(0), collection.get('a-1')); assert.ok(collection.at(1) instanceof B); assert.equal(collection.at(1), collection.get('b-1')); }); QUnit.test('Collection with polymorphic models receives id from modelId using model instance idAttribute', function(assert) { assert.expect(6); // When the polymorphic models use 'id' for the idAttribute, all is fine. var C1 = Backbone.Collection.extend({ model: function(attrs) { return new Backbone.Model(attrs); } }); var c1 = new C1({id: 1}); assert.equal(c1.get(1).id, 1); assert.equal(c1.modelId({id: 1}), 1); // If the polymorphic models define their own idAttribute, // the modelId method will use the model's idAttribute property before the // collection's model constructor's. var M = Backbone.Model.extend({ idAttribute: '_id' }); var C2 = Backbone.Collection.extend({ model: function(attrs) { return new M(attrs); } }); var c2 = new C2({_id: 1}); assert.equal(c2.get(1), c2.at(0)); assert.equal(c2.modelId(c2.at(0).attributes, c2.at(0).idAttribute), 1); var m = new M({_id: 2}); c2.add(m); assert.equal(c2.get(2), m); assert.equal(c2.modelId(m.attributes, m.idAttribute), 2); }); QUnit.test('Collection implements Iterable, values is default iterator function', function(assert) { /* global Symbol */ var $$iterator = typeof Symbol === 'function' && Symbol.iterator; // This test only applies to environments which define Symbol.iterator. if (!$$iterator) { assert.expect(0); return; } assert.expect(2); var collection = new Backbone.Collection([]); assert.strictEqual(collection[$$iterator], collection.values); var iterator = collection[$$iterator](); assert.deepEqual(iterator.next(), {value: void 0, done: true}); }); QUnit.test('Collection.values iterates models in sorted order', function(assert) { assert.expect(4); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); var iterator = collection.values(); assert.strictEqual(iterator.next().value, one); assert.strictEqual(iterator.next().value, two); assert.strictEqual(iterator.next().value, three); assert.strictEqual(iterator.next().value, void 0); }); QUnit.test('Collection.keys iterates ids in sorted order', function(assert) { assert.expect(4); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); var iterator = collection.keys(); assert.strictEqual(iterator.next().value, 1); assert.strictEqual(iterator.next().value, 2); assert.strictEqual(iterator.next().value, 3); assert.strictEqual(iterator.next().value, void 0); }); QUnit.test('Collection.entries iterates ids and models in sorted order', function(assert) { assert.expect(4); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); var iterator = collection.entries(); assert.deepEqual(iterator.next().value, [1, one]); assert.deepEqual(iterator.next().value, [2, two]); assert.deepEqual(iterator.next().value, [3, three]); assert.strictEqual(iterator.next().value, void 0); }); QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) { assert.expect(4); var collection = new Backbone.Collection([{val: 0}, {val: 4}]); collection.on('add', function(model, coll, options) { assert.equal(model.get('val'), options.index); }); collection.add([{val: 1}, {val: 2}, {val: 3}], {at: 1}); collection.add({val: 5}, {at: 10}); }); QUnit.test('#3039: index is not sent when at is not specified', function(assert) { assert.expect(2); var collection = new Backbone.Collection([{at: 0}]); collection.on('add', function(model, coll, options) { assert.equal(undefined, options.index); }); collection.add([{at: 1}, {at: 2}]); }); QUnit.test('#3199 - Order changing should trigger a sort', function(assert) { assert.expect(1); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); collection.on('sort', function() { assert.ok(true); }); collection.set([{id: 3}, {id: 2}, {id: 1}]); }); QUnit.test('#3199 - Adding a model should trigger a sort', function(assert) { assert.expect(1); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); collection.on('sort', function() { assert.ok(true); }); collection.set([{id: 1}, {id: 2}, {id: 3}, {id: 0}]); }); QUnit.test('#3199 - Order not changing should not trigger a sort', function(assert) { assert.expect(0); var one = new Backbone.Model({id: 1}); var two = new Backbone.Model({id: 2}); var three = new Backbone.Model({id: 3}); var collection = new Backbone.Collection([one, two, three]); collection.on('sort', function() { assert.ok(false); }); collection.set([{id: 1}, {id: 2}, {id: 3}]); }); QUnit.test('add supports negative indexes', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}]); collection.add([{id: 2}, {id: 3}], {at: -1}); collection.add([{id: 2.5}], {at: -2}); collection.add([{id: 0.5}], {at: -6}); assert.equal(collection.pluck('id').join(','), '0.5,1,2,2.5,3'); }); QUnit.test('#set accepts options.at as a string', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}, {id: 2}]); collection.add([{id: 3}], {at: '1'}); assert.deepEqual(collection.pluck('id'), [1, 3, 2]); }); QUnit.test('adding multiple models triggers `update` event once', function(assert) { assert.expect(1); var collection = new Backbone.Collection; collection.on('update', function() { assert.ok(true); }); collection.add([{id: 1}, {id: 2}, {id: 3}]); }); QUnit.test('removing models triggers `update` event once', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}, {id: 2}, {id: 3}]); collection.on('update', function() { assert.ok(true); }); collection.remove([{id: 1}, {id: 2}]); }); QUnit.test('remove does not trigger `update` when nothing removed', function(assert) { assert.expect(0); var collection = new Backbone.Collection([{id: 1}, {id: 2}]); collection.on('update', function() { assert.ok(false); }); collection.remove([{id: 3}]); }); QUnit.test('set triggers `set` event once', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}, {id: 2}]); collection.on('update', function() { assert.ok(true); }); collection.set([{id: 1}, {id: 3}]); }); QUnit.test('set does not trigger `update` event when nothing added nor removed', function(assert) { var collection = new Backbone.Collection([{id: 1}, {id: 2}]); collection.on('update', function(coll, options) { assert.equal(options.changes.added.length, 0); assert.equal(options.changes.removed.length, 0); assert.equal(options.changes.merged.length, 2); }); collection.set([{id: 1}, {id: 2}]); }); QUnit.test('#3610 - invoke collects arguments', function(assert) { assert.expect(3); var Model = Backbone.Model.extend({ method: function(x, y, z) { assert.equal(x, 1); assert.equal(y, 2); assert.equal(z, 3); } }); var Collection = Backbone.Collection.extend({ model: Model }); var collection = new Collection([{id: 1}]); collection.invoke('method', 1, 2, 3); }); QUnit.test('#3662 - triggering change without model will not error', function(assert) { assert.expect(1); var collection = new Backbone.Collection([{id: 1}]); var model = collection.first(); collection.on('change', function(m) { assert.equal(m, undefined); }); model.trigger('change'); }); QUnit.test('#3871 - falsy parse result creates empty collection', function(assert) { var collection = new (Backbone.Collection.extend({ parse: function(data, options) {} })); collection.set('', {parse: true}); assert.equal(collection.length, 0); }); QUnit.test("#3711 - remove's `update` event returns one removed model", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var collection = new Backbone.Collection([model]); collection.on('update', function(context, options) { var changed = options.changes; assert.deepEqual(changed.added, []); assert.deepEqual(changed.merged, []); assert.strictEqual(changed.removed[0], model); }); collection.remove(model); }); QUnit.test("#3711 - remove's `update` event returns multiple removed models", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var collection = new Backbone.Collection([model, model2]); collection.on('update', function(context, options) { var changed = options.changes; assert.deepEqual(changed.added, []); assert.deepEqual(changed.merged, []); assert.ok(changed.removed.length === 2); assert.ok(_.indexOf(changed.removed, model) > -1 && _.indexOf(changed.removed, model2) > -1); }); collection.remove([model, model2]); }); QUnit.test("#3711 - set's `update` event returns one added model", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var collection = new Backbone.Collection(); collection.on('update', function(context, options) { var addedModels = options.changes.added; assert.ok(addedModels.length === 1); assert.strictEqual(addedModels[0], model); }); collection.set(model); }); QUnit.test("#3711 - set's `update` event returns multiple added models", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var collection = new Backbone.Collection(); collection.on('update', function(context, options) { var addedModels = options.changes.added; assert.ok(addedModels.length === 2); assert.strictEqual(addedModels[0], model); assert.strictEqual(addedModels[1], model2); }); collection.set([model, model2]); }); QUnit.test("#3711 - set's `update` event returns one removed model", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var model3 = new Backbone.Model({id: 3, title: 'My Last Post'}); var collection = new Backbone.Collection([model]); collection.on('update', function(context, options) { var changed = options.changes; assert.equal(changed.added.length, 2); assert.equal(changed.merged.length, 0); assert.ok(changed.removed.length === 1); assert.strictEqual(changed.removed[0], model); }); collection.set([model2, model3]); }); QUnit.test("#3711 - set's `update` event returns multiple removed models", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var model3 = new Backbone.Model({id: 3, title: 'My Last Post'}); var collection = new Backbone.Collection([model, model2]); collection.on('update', function(context, options) { var removedModels = options.changes.removed; assert.ok(removedModels.length === 2); assert.strictEqual(removedModels[0], model); assert.strictEqual(removedModels[1], model2); }); collection.set([model3]); }); QUnit.test("#3711 - set's `update` event returns one merged model", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var model2Update = new Backbone.Model({id: 2, title: 'Second Post V2'}); var collection = new Backbone.Collection([model, model2]); collection.on('update', function(context, options) { var mergedModels = options.changes.merged; assert.ok(mergedModels.length === 1); assert.strictEqual(mergedModels[0].get('title'), model2Update.get('title')); }); collection.set([model2Update]); }); QUnit.test("#3711 - set's `update` event returns multiple merged models", function(assert) { var model = new Backbone.Model({id: 1, title: 'First Post'}); var modelUpdate = new Backbone.Model({id: 1, title: 'First Post V2'}); var model2 = new Backbone.Model({id: 2, title: 'Second Post'}); var model2Update = new Backbone.Model({id: 2, title: 'Second Post V2'}); var collection = new Backbone.Collection([model, model2]); collection.on('update', function(context, options) { var mergedModels = options.changes.merged; assert.ok(mergedModels.length === 2); assert.strictEqual(mergedModels[0].get('title'), model2Update.get('title')); assert.strictEqual(mergedModels[1].get('title'), modelUpdate.get('title')); }); collection.set([model2Update, modelUpdate]); }); QUnit.test("#3711 - set's `update` event should not be triggered adding a model which already exists exactly alike", function(assert) { var fired = false; var model = new Backbone.Model({id: 1, title: 'First Post'}); var collection = new Backbone.Collection([model]); collection.on('update', function(context, options) { fired = true; }); collection.set([model]); assert.equal(fired, false); }); QUnit.test('get models with `attributes` key', function(assert) { var model = {id: 1, attributes: {}}; var collection = new Backbone.Collection([model]); assert.ok(collection.get(model)); }); QUnit.test('#3961 - add events sends options.index that correspond to wrong index', function(assert) { var numModels = 4; var models = _.each(['a', 'b', 'c', 'd'], function(val) { return new Backbone.Model({id: val}); }); var collection = new Backbone.Collection(models); models.shift(); // remove first element; models.push(new Backbone.Model({id: 'e'})); collection.on('add', function(model, coll, options){ assert.equal(options.index, undefined); }); collection.set(models); }); QUnit.test('#4233 - can instantiate new model in ES class Collection', function(assert) { var model; try { model = new Function('return ({\n' + ' model(attrs, options) {\n' + ' var MyModel = Backbone.Model.extend({});\n' + ' return new MyModel(attrs, options);\n' + ' }\n' + '}).model')(); } catch (error) { model = error; } if (model instanceof SyntaxError) { assert.expect(0); return; } assert.expect(1); var MyCollection = Backbone.Collection.extend({ modelId: function(attr) { return attr.x; }, model: model }); var instance = new MyCollection([{a: 2}]); assert.ok(instance, 'Should instantiate collection with model'); }); })(QUnit); ================================================ FILE: test/debuginfo.js ================================================ /* eslint-disable no-console */ (function(QUnit) { var logs, originalDebug = console.debug; function spyDebug() { logs.push(arguments); originalDebug.apply(console, arguments); } QUnit.module('Backbone.debugInfo', { beforeEach: function() { logs = []; console.debug = spyDebug; }, afterEach: function() { console.debug = originalDebug; logs = undefined; } }); QUnit.test('debugInfo', function(assert) { var info = Backbone.debugInfo(); assert.strictEqual(info.backbone, Backbone.VERSION, 'includes Backbone version'); assert.strictEqual(info.distribution, 'MARK_DEVELOPMENT', 'distribution mark sticks to development'); assert.strictEqual(info._, _.VERSION, 'includes Underscore version'); assert.strictEqual(info.$, $.fn.jquery, 'includes jQuery version'); if (typeof navigator !== 'undefined') { assert.ok(typeof info.navigator === 'object'); assert.strictEqual(info.navigator.userAgent, navigator.userAgent, 'includes user agent'); assert.strictEqual(info.navigator.platform, navigator.platform, 'includes navigator platform'); assert.strictEqual(info.navigator.webdriver, navigator.webdriver, 'includes webdriver state'); } assert.strictEqual(logs.length, 1, 'prints to console as side effect'); var debugArgs = logs[0]; var infoString = JSON.stringify(info, null, 4); assert.strictEqual(debugArgs[1], infoString, 'prints payload as second argument'); }); })(QUnit); ================================================ FILE: test/events.js ================================================ (function(QUnit) { QUnit.module('Backbone.Events'); QUnit.test('on and trigger', function(assert) { assert.expect(2); var obj = {counter: 0}; _.extend(obj, Backbone.Events); obj.on('event', function() { obj.counter += 1; }); obj.trigger('event'); assert.equal(obj.counter, 1, 'counter should be incremented.'); obj.trigger('event'); obj.trigger('event'); obj.trigger('event'); obj.trigger('event'); assert.equal(obj.counter, 5, 'counter should be incremented five times.'); }); QUnit.test('binding and triggering multiple events', function(assert) { assert.expect(4); var obj = {counter: 0}; _.extend(obj, Backbone.Events); obj.on('a b c', function() { obj.counter += 1; }); obj.trigger('a'); assert.equal(obj.counter, 1); obj.trigger('a b'); assert.equal(obj.counter, 3); obj.trigger('c'); assert.equal(obj.counter, 4); obj.off('a c'); obj.trigger('a b c'); assert.equal(obj.counter, 5); }); QUnit.test('binding and triggering with event maps', function(assert) { var obj = {counter: 0}; _.extend(obj, Backbone.Events); var increment = function() { this.counter += 1; }; obj.on({ a: increment, b: increment, c: increment }, obj); obj.trigger('a'); assert.equal(obj.counter, 1); obj.trigger('a b'); assert.equal(obj.counter, 3); obj.trigger('c'); assert.equal(obj.counter, 4); obj.off({ a: increment, c: increment }, obj); obj.trigger('a b c'); assert.equal(obj.counter, 5); }); QUnit.test('binding and triggering multiple event names with event maps', function(assert) { var obj = {counter: 0}; _.extend(obj, Backbone.Events); var increment = function() { this.counter += 1; }; obj.on({ 'a b c': increment }); obj.trigger('a'); assert.equal(obj.counter, 1); obj.trigger('a b'); assert.equal(obj.counter, 3); obj.trigger('c'); assert.equal(obj.counter, 4); obj.off({ 'a c': increment }); obj.trigger('a b c'); assert.equal(obj.counter, 5); }); QUnit.test('binding and trigger with event maps context', function(assert) { assert.expect(2); var obj = {counter: 0}; var context = {}; _.extend(obj, Backbone.Events); obj.on({ a: function() { assert.strictEqual(this, context, 'defaults `context` to `callback` param'); } }, context).trigger('a'); obj.off().on({ a: function() { assert.strictEqual(this, context, 'will not override explicit `context` param'); } }, this, context).trigger('a'); }); QUnit.test('listenTo and stopListening', function(assert) { assert.expect(1); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenTo(b, 'all', function(){ assert.ok(true); }); b.trigger('anything'); a.listenTo(b, 'all', function(){ assert.ok(false); }); a.stopListening(); b.trigger('anything'); }); QUnit.test('listenTo and stopListening with event maps', function(assert) { assert.expect(4); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); var cb = function(){ assert.ok(true); }; a.listenTo(b, {event: cb}); b.trigger('event'); a.listenTo(b, {event2: cb}); b.on('event2', cb); a.stopListening(b, {event2: cb}); b.trigger('event event2'); a.stopListening(); b.trigger('event event2'); }); QUnit.test('stopListening with omitted args', function(assert) { assert.expect(2); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); var cb = function() { assert.ok(true); }; a.listenTo(b, 'event', cb); b.on('event', cb); a.listenTo(b, 'event2', cb); a.stopListening(null, {event: cb}); b.trigger('event event2'); b.off(); a.listenTo(b, 'event event2', cb); a.stopListening(null, 'event'); a.stopListening(); b.trigger('event2'); }); QUnit.test('listenToOnce', function(assert) { assert.expect(2); // Same as the previous test, but we use once rather than having to explicitly unbind var obj = {counterA: 0, counterB: 0}; _.extend(obj, Backbone.Events); var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; var incrB = function(){ obj.counterB += 1; }; obj.listenToOnce(obj, 'event', incrA); obj.listenToOnce(obj, 'event', incrB); obj.trigger('event'); assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); }); QUnit.test('listenToOnce and stopListening', function(assert) { assert.expect(1); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenToOnce(b, 'all', function() { assert.ok(true); }); b.trigger('anything'); b.trigger('anything'); a.listenToOnce(b, 'all', function() { assert.ok(false); }); a.stopListening(); b.trigger('anything'); }); QUnit.test('listenTo, listenToOnce and stopListening', function(assert) { assert.expect(1); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenToOnce(b, 'all', function() { assert.ok(true); }); b.trigger('anything'); b.trigger('anything'); a.listenTo(b, 'all', function() { assert.ok(false); }); a.stopListening(); b.trigger('anything'); }); QUnit.test('listenTo and stopListening with event maps', function(assert) { assert.expect(1); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenTo(b, {change: function(){ assert.ok(true); }}); b.trigger('change'); a.listenTo(b, {change: function(){ assert.ok(false); }}); a.stopListening(); b.trigger('change'); }); QUnit.test('listenTo yourself', function(assert) { assert.expect(1); var e = _.extend({}, Backbone.Events); e.listenTo(e, 'foo', function(){ assert.ok(true); }); e.trigger('foo'); }); QUnit.test('listenTo yourself cleans yourself up with stopListening', function(assert) { assert.expect(1); var e = _.extend({}, Backbone.Events); e.listenTo(e, 'foo', function(){ assert.ok(true); }); e.trigger('foo'); e.stopListening(); e.trigger('foo'); }); QUnit.test('stopListening cleans up references', function(assert) { assert.expect(12); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); var fn = function() {}; b.on('event', fn); a.listenTo(b, 'event', fn).stopListening(); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn).stopListening(b); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn).stopListening(b, 'event'); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn).stopListening(b, 'event', fn); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); }); QUnit.test('stopListening cleans up references from listenToOnce', function(assert) { assert.expect(12); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); var fn = function() {}; b.on('event', fn); a.listenToOnce(b, 'event', fn).stopListening(); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenToOnce(b, 'event', fn).stopListening(b); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenToOnce(b, 'event', fn).stopListening(b, 'event'); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); a.listenToOnce(b, 'event', fn).stopListening(b, 'event', fn); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._events.event), 1); assert.equal(_.size(b._listeners), 0); }); QUnit.test('listenTo and off cleaning up references', function(assert) { assert.expect(8); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); var fn = function() {}; a.listenTo(b, 'event', fn); b.off(); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn); b.off('event'); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn); b.off(null, fn); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._listeners), 0); a.listenTo(b, 'event', fn); b.off(null, null, a); assert.equal(_.size(a._listeningTo), 0); assert.equal(_.size(b._listeners), 0); }); QUnit.test('listenTo and stopListening cleaning up references', function(assert) { assert.expect(2); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenTo(b, 'all', function(){ assert.ok(true); }); b.trigger('anything'); a.listenTo(b, 'other', function(){ assert.ok(false); }); a.stopListening(b, 'other'); a.stopListening(b, 'all'); assert.equal(_.size(a._listeningTo), 0); }); QUnit.test('listenToOnce without context cleans up references after the event has fired', function(assert) { assert.expect(2); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenToOnce(b, 'all', function(){ assert.ok(true); }); b.trigger('anything'); assert.equal(_.size(a._listeningTo), 0); }); QUnit.test('listenToOnce with event maps cleans up references', function(assert) { assert.expect(2); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenToOnce(b, { one: function() { assert.ok(true); }, two: function() { assert.ok(false); } }); b.trigger('one'); assert.equal(_.size(a._listeningTo), 1); }); QUnit.test('listenToOnce with event maps binds the correct `this`', function(assert) { assert.expect(1); var a = _.extend({}, Backbone.Events); var b = _.extend({}, Backbone.Events); a.listenToOnce(b, { one: function() { assert.ok(this === a); }, two: function() { assert.ok(false); } }); b.trigger('one'); }); QUnit.test("listenTo with empty callback doesn't throw an error", function(assert) { assert.expect(1); var e = _.extend({}, Backbone.Events); e.listenTo(e, 'foo', null); e.trigger('foo'); assert.ok(true); }); QUnit.test('trigger all for each event', function(assert) { assert.expect(3); var a, b, obj = {counter: 0}; _.extend(obj, Backbone.Events); obj.on('all', function(event) { obj.counter++; if (event === 'a') a = true; if (event === 'b') b = true; }) .trigger('a b'); assert.ok(a); assert.ok(b); assert.equal(obj.counter, 2); }); QUnit.test('on, then unbind all functions', function(assert) { assert.expect(1); var obj = {counter: 0}; _.extend(obj, Backbone.Events); var callback = function() { obj.counter += 1; }; obj.on('event', callback); obj.trigger('event'); obj.off('event'); obj.trigger('event'); assert.equal(obj.counter, 1, 'counter should have only been incremented once.'); }); QUnit.test('bind two callbacks, unbind only one', function(assert) { assert.expect(2); var obj = {counterA: 0, counterB: 0}; _.extend(obj, Backbone.Events); var callback = function() { obj.counterA += 1; }; obj.on('event', callback); obj.on('event', function() { obj.counterB += 1; }); obj.trigger('event'); obj.off('event', callback); obj.trigger('event'); assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); assert.equal(obj.counterB, 2, 'counterB should have been incremented twice.'); }); QUnit.test('unbind a callback in the midst of it firing', function(assert) { assert.expect(1); var obj = {counter: 0}; _.extend(obj, Backbone.Events); var callback = function() { obj.counter += 1; obj.off('event', callback); }; obj.on('event', callback); obj.trigger('event'); obj.trigger('event'); obj.trigger('event'); assert.equal(obj.counter, 1, 'the callback should have been unbound.'); }); QUnit.test('two binds that unbind themeselves', function(assert) { assert.expect(2); var obj = {counterA: 0, counterB: 0}; _.extend(obj, Backbone.Events); var incrA = function(){ obj.counterA += 1; obj.off('event', incrA); }; var incrB = function(){ obj.counterB += 1; obj.off('event', incrB); }; obj.on('event', incrA); obj.on('event', incrB); obj.trigger('event'); obj.trigger('event'); obj.trigger('event'); assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); }); QUnit.test('bind a callback with a default context when none supplied', function(assert) { assert.expect(1); var obj = _.extend({ assertTrue: function() { assert.equal(this, obj, '`this` was bound to the callback'); } }, Backbone.Events); obj.once('event', obj.assertTrue); obj.trigger('event'); }); QUnit.test('bind a callback with a supplied context', function(assert) { assert.expect(1); var TestClass = function() { return this; }; TestClass.prototype.assertTrue = function() { assert.ok(true, '`this` was bound to the callback'); }; var obj = _.extend({}, Backbone.Events); obj.on('event', function() { this.assertTrue(); }, new TestClass); obj.trigger('event'); }); QUnit.test('nested trigger with unbind', function(assert) { assert.expect(1); var obj = {counter: 0}; _.extend(obj, Backbone.Events); var incr1 = function(){ obj.counter += 1; obj.off('event', incr1); obj.trigger('event'); }; var incr2 = function(){ obj.counter += 1; }; obj.on('event', incr1); obj.on('event', incr2); obj.trigger('event'); assert.equal(obj.counter, 3, 'counter should have been incremented three times'); }); QUnit.test('callback list is not altered during trigger', function(assert) { assert.expect(2); var counter = 0, obj = _.extend({}, Backbone.Events); var incr = function(){ counter++; }; var incrOn = function(){ obj.on('event all', incr); }; var incrOff = function(){ obj.off('event all', incr); }; obj.on('event all', incrOn).trigger('event'); assert.equal(counter, 0, 'on does not alter callback list'); obj.off().on('event', incrOff).on('event all', incr).trigger('event'); assert.equal(counter, 2, 'off does not alter callback list'); }); QUnit.test("#1282 - 'all' callback list is retrieved after each event.", function(assert) { assert.expect(1); var counter = 0; var obj = _.extend({}, Backbone.Events); var incr = function(){ counter++; }; obj.on('x', function() { obj.on('y', incr).on('all', incr); }) .trigger('x y'); assert.strictEqual(counter, 2); }); QUnit.test('if no callback is provided, `on` is a noop', function(assert) { assert.expect(0); _.extend({}, Backbone.Events).on('test').trigger('test'); }); QUnit.test('if callback is truthy but not a function, `on` should throw an error just like jQuery', function(assert) { assert.expect(1); var view = _.extend({}, Backbone.Events).on('test', 'noop'); assert.raises(function() { view.trigger('test'); }); }); QUnit.test('remove all events for a specific context', function(assert) { assert.expect(4); var obj = _.extend({}, Backbone.Events); obj.on('x y all', function() { assert.ok(true); }); obj.on('x y all', function() { assert.ok(false); }, obj); obj.off(null, null, obj); obj.trigger('x y'); }); QUnit.test('remove all events for a specific callback', function(assert) { assert.expect(4); var obj = _.extend({}, Backbone.Events); var success = function() { assert.ok(true); }; var fail = function() { assert.ok(false); }; obj.on('x y all', success); obj.on('x y all', fail); obj.off(null, fail); obj.trigger('x y'); }); QUnit.test('#1310 - off does not skip consecutive events', function(assert) { assert.expect(0); var obj = _.extend({}, Backbone.Events); obj.on('event', function() { assert.ok(false); }, obj); obj.on('event', function() { assert.ok(false); }, obj); obj.off(null, null, obj); obj.trigger('event'); }); QUnit.test('once', function(assert) { assert.expect(2); // Same as the previous test, but we use once rather than having to explicitly unbind var obj = {counterA: 0, counterB: 0}; _.extend(obj, Backbone.Events); var incrA = function(){ obj.counterA += 1; obj.trigger('event'); }; var incrB = function(){ obj.counterB += 1; }; obj.once('event', incrA); obj.once('event', incrB); obj.trigger('event'); assert.equal(obj.counterA, 1, 'counterA should have only been incremented once.'); assert.equal(obj.counterB, 1, 'counterB should have only been incremented once.'); }); QUnit.test('once variant one', function(assert) { assert.expect(3); var f = function(){ assert.ok(true); }; var a = _.extend({}, Backbone.Events).once('event', f); var b = _.extend({}, Backbone.Events).on('event', f); a.trigger('event'); b.trigger('event'); b.trigger('event'); }); QUnit.test('once variant two', function(assert) { assert.expect(3); var f = function(){ assert.ok(true); }; var obj = _.extend({}, Backbone.Events); obj .once('event', f) .on('event', f) .trigger('event') .trigger('event'); }); QUnit.test('once with off', function(assert) { assert.expect(0); var f = function(){ assert.ok(true); }; var obj = _.extend({}, Backbone.Events); obj.once('event', f); obj.off('event', f); obj.trigger('event'); }); QUnit.test('once with event maps', function(assert) { var obj = {counter: 0}; _.extend(obj, Backbone.Events); var increment = function() { this.counter += 1; }; obj.once({ a: increment, b: increment, c: increment }, obj); obj.trigger('a'); assert.equal(obj.counter, 1); obj.trigger('a b'); assert.equal(obj.counter, 2); obj.trigger('c'); assert.equal(obj.counter, 3); obj.trigger('a b c'); assert.equal(obj.counter, 3); }); QUnit.test('bind a callback with a supplied context using once with object notation', function(assert) { assert.expect(1); var obj = {counter: 0}; var context = {}; _.extend(obj, Backbone.Events); obj.once({ a: function() { assert.strictEqual(this, context, 'defaults `context` to `callback` param'); } }, context).trigger('a'); }); QUnit.test('once with off only by context', function(assert) { assert.expect(0); var context = {}; var obj = _.extend({}, Backbone.Events); obj.once('event', function(){ assert.ok(false); }, context); obj.off(null, null, context); obj.trigger('event'); }); QUnit.test('Backbone object inherits Events', function(assert) { assert.ok(Backbone.on === Backbone.Events.on); }); QUnit.test('once with asynchronous events', function(assert) { var done = assert.async(); assert.expect(1); var func = _.debounce(function() { assert.ok(true); done(); }, 50); var obj = _.extend({}, Backbone.Events).once('async', func); obj.trigger('async'); obj.trigger('async'); }); QUnit.test('once with multiple events.', function(assert) { assert.expect(2); var obj = _.extend({}, Backbone.Events); obj.once('x y', function() { assert.ok(true); }); obj.trigger('x y'); }); QUnit.test('Off during iteration with once.', function(assert) { assert.expect(2); var obj = _.extend({}, Backbone.Events); var f = function(){ this.off('event', f); }; obj.on('event', f); obj.once('event', function(){}); obj.on('event', function(){ assert.ok(true); }); obj.trigger('event'); obj.trigger('event'); }); QUnit.test('`once` on `all` should work as expected', function(assert) { assert.expect(1); Backbone.once('all', function() { assert.ok(true); Backbone.trigger('all'); }); Backbone.trigger('all'); }); QUnit.test('once without a callback is a noop', function(assert) { assert.expect(0); _.extend({}, Backbone.Events).once('event').trigger('event'); }); QUnit.test('listenToOnce without a callback is a noop', function(assert) { assert.expect(0); var obj = _.extend({}, Backbone.Events); obj.listenToOnce(obj, 'event').trigger('event'); }); QUnit.test('event functions are chainable', function(assert) { var obj = _.extend({}, Backbone.Events); var obj2 = _.extend({}, Backbone.Events); var fn = function() {}; assert.equal(obj, obj.trigger('noeventssetyet')); assert.equal(obj, obj.off('noeventssetyet')); assert.equal(obj, obj.stopListening('noeventssetyet')); assert.equal(obj, obj.on('a', fn)); assert.equal(obj, obj.once('c', fn)); assert.equal(obj, obj.trigger('a')); assert.equal(obj, obj.listenTo(obj2, 'a', fn)); assert.equal(obj, obj.listenToOnce(obj2, 'b', fn)); assert.equal(obj, obj.off('a c')); assert.equal(obj, obj.stopListening(obj2, 'a')); assert.equal(obj, obj.stopListening()); }); QUnit.test('#3448 - listenToOnce with space-separated events', function(assert) { assert.expect(2); var one = _.extend({}, Backbone.Events); var two = _.extend({}, Backbone.Events); var count = 1; one.listenToOnce(two, 'x y', function(n) { assert.ok(n === count++); }); two.trigger('x', 1); two.trigger('x', 1); two.trigger('y', 2); two.trigger('y', 2); }); QUnit.test('#3611 - listenTo is compatible with non-Backbone event libraries', function(assert) { var obj = _.extend({}, Backbone.Events); var other = { events: {}, on: function(name, callback) { this.events[name] = callback; }, trigger: function(name) { this.events[name](); } }; obj.listenTo(other, 'test', function() { assert.ok(true); }); other.trigger('test'); }); QUnit.test('#3611 - stopListening is compatible with non-Backbone event libraries', function(assert) { var obj = _.extend({}, Backbone.Events); var other = { events: {}, on: function(name, callback) { this.events[name] = callback; }, off: function() { this.events = {}; }, trigger: function(name) { var fn = this.events[name]; if (fn) fn(); } }; obj.listenTo(other, 'test', function() { assert.ok(false); }); obj.stopListening(other); other.trigger('test'); assert.equal(_.size(obj._listeningTo), 0); }); })(QUnit); ================================================ FILE: test/index.html ================================================ Backbone Test Suite ================================================ FILE: test/model.coffee ================================================ # Quick Backbone/CoffeeScript tests to make sure that inheritance # works correctly. {ok, equal, deepEqual} = require 'assert' {Model, Collection, Events} = require '../backbone' # Patch `ok` to store a count of passed tests... count = 0 oldOk = ok ok = -> oldOk arguments... count++ class Document extends Model fullName: -> @get('name') + ' ' + @get('surname') tempest = new Document id : '1-the-tempest', title : "The Tempest", name : "William" surname : "Shakespeare" length : 123 ok tempest.fullName() is "William Shakespeare" ok tempest.get('length') is 123 class ProperDocument extends Document fullName: -> "Mr. " + super(arguments...) properTempest = new ProperDocument tempest.attributes ok properTempest.fullName() is "Mr. William Shakespeare" ok properTempest.get('length') is 123 console.log "passed #{count} tests" ================================================ FILE: test/model.js ================================================ (function(QUnit) { var ProxyModel = Backbone.Model.extend(); var Klass = Backbone.Collection.extend({ url: function() { return '/collection'; } }); var doc, collection; QUnit.module('Backbone.Model', { beforeEach: function(assert) { doc = new ProxyModel({ id: '1-the-tempest', title: 'The Tempest', author: 'Bill Shakespeare', length: 123 }); collection = new Klass(); collection.add(doc); } }); QUnit.test('initialize', function(assert) { assert.expect(3); var Model = Backbone.Model.extend({ initialize: function() { this.one = 1; assert.equal(this.collection, collection); } }); var model = new Model({}, {collection: collection}); assert.equal(model.one, 1); assert.equal(model.collection, collection); }); QUnit.test('Object.prototype properties are overridden by attributes', function(assert) { assert.expect(1); var model = new Backbone.Model({hasOwnProperty: true}); assert.equal(model.get('hasOwnProperty'), true); }); QUnit.test('initialize with attributes and options', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ initialize: function(attributes, options) { this.one = options.one; } }); var model = new Model({}, {one: 1}); assert.equal(model.one, 1); }); QUnit.test('initialize with parsed attributes', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ parse: function(attrs) { attrs.value += 1; return attrs; } }); var model = new Model({value: 1}, {parse: true}); assert.equal(model.get('value'), 2); }); QUnit.test('preinitialize', function(assert) { assert.expect(2); var Model = Backbone.Model.extend({ preinitialize: function() { this.one = 1; } }); var model = new Model({}, {collection: collection}); assert.equal(model.one, 1); assert.equal(model.collection, collection); }); QUnit.test('preinitialize occurs before the model is set up', function(assert) { assert.expect(6); var Model = Backbone.Model.extend({ preinitialize: function() { assert.equal(this.collection, undefined); assert.equal(this.cid, undefined); assert.equal(this.id, undefined); } }); var model = new Model({id: 'foo'}, {collection: collection}); assert.equal(model.collection, collection); assert.equal(model.id, 'foo'); assert.notEqual(model.cid, undefined); }); QUnit.test('parse can return null', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ parse: function(attrs) { attrs.value += 1; return null; } }); var model = new Model({value: 1}, {parse: true}); assert.equal(JSON.stringify(model.toJSON()), '{}'); }); QUnit.test('url', function(assert) { assert.expect(3); doc.urlRoot = null; assert.equal(doc.url(), '/collection/1-the-tempest'); doc.collection.url = '/collection/'; assert.equal(doc.url(), '/collection/1-the-tempest'); doc.collection = null; assert.raises(function() { doc.url(); }); doc.collection = collection; }); QUnit.test('url when using urlRoot, and uri encoding', function(assert) { assert.expect(2); var Model = Backbone.Model.extend({ urlRoot: '/collection' }); var model = new Model(); assert.equal(model.url(), '/collection'); model.set({id: '+1+'}); assert.equal(model.url(), '/collection/%2B1%2B'); }); QUnit.test('url when using urlRoot as a function to determine urlRoot at runtime', function(assert) { assert.expect(2); var Model = Backbone.Model.extend({ urlRoot: function() { return '/nested/' + this.get('parentId') + '/collection'; } }); var model = new Model({parentId: 1}); assert.equal(model.url(), '/nested/1/collection'); model.set({id: 2}); assert.equal(model.url(), '/nested/1/collection/2'); }); QUnit.test('underscore methods', function(assert) { assert.expect(5); var model = new Backbone.Model({foo: 'a', bar: 'b', baz: 'c'}); var model2 = model.clone(); assert.deepEqual(model.keys(), ['foo', 'bar', 'baz']); assert.deepEqual(model.values(), ['a', 'b', 'c']); assert.deepEqual(model.invert(), {a: 'foo', b: 'bar', c: 'baz'}); assert.deepEqual(model.pick('foo', 'baz'), {foo: 'a', baz: 'c'}); assert.deepEqual(model.omit('foo', 'bar'), {baz: 'c'}); }); QUnit.test('chain', function(assert) { var model = new Backbone.Model({a: 0, b: 1, c: 2}); assert.deepEqual(model.chain().pick('a', 'b', 'c').values().compact().value(), [1, 2]); }); QUnit.test('clone', function(assert) { assert.expect(10); var a = new Backbone.Model({foo: 1, bar: 2, baz: 3}); var b = a.clone(); assert.equal(a.get('foo'), 1); assert.equal(a.get('bar'), 2); assert.equal(a.get('baz'), 3); assert.equal(b.get('foo'), a.get('foo'), 'Foo should be the same on the clone.'); assert.equal(b.get('bar'), a.get('bar'), 'Bar should be the same on the clone.'); assert.equal(b.get('baz'), a.get('baz'), 'Baz should be the same on the clone.'); a.set({foo: 100}); assert.equal(a.get('foo'), 100); assert.equal(b.get('foo'), 1, 'Changing a parent attribute does not change the clone.'); var foo = new Backbone.Model({p: 1}); var bar = new Backbone.Model({p: 2}); bar.set(foo.clone().attributes, {unset: true}); assert.equal(foo.get('p'), 1); assert.equal(bar.get('p'), undefined); }); QUnit.test('isNew', function(assert) { assert.expect(6); var a = new Backbone.Model({foo: 1, bar: 2, baz: 3}); assert.ok(a.isNew(), 'it should be new'); a = new Backbone.Model({foo: 1, bar: 2, baz: 3, id: -5}); assert.ok(!a.isNew(), 'any defined ID is legal, negative or positive'); a = new Backbone.Model({foo: 1, bar: 2, baz: 3, id: 0}); assert.ok(!a.isNew(), 'any defined ID is legal, including zero'); assert.ok(new Backbone.Model().isNew(), 'is true when there is no id'); assert.ok(!new Backbone.Model({id: 2}).isNew(), 'is false for a positive integer'); assert.ok(!new Backbone.Model({id: -5}).isNew(), 'is false for a negative integer'); }); QUnit.test('get', function(assert) { assert.expect(2); assert.equal(doc.get('title'), 'The Tempest'); assert.equal(doc.get('author'), 'Bill Shakespeare'); }); QUnit.test('escape', function(assert) { assert.expect(5); assert.equal(doc.escape('title'), 'The Tempest'); doc.set({audience: 'Bill & Bob'}); assert.equal(doc.escape('audience'), 'Bill & Bob'); doc.set({audience: 'Tim > Joan'}); assert.equal(doc.escape('audience'), 'Tim > Joan'); doc.set({audience: 10101}); assert.equal(doc.escape('audience'), '10101'); doc.unset('audience'); assert.equal(doc.escape('audience'), ''); }); QUnit.test('has', function(assert) { assert.expect(10); var model = new Backbone.Model(); assert.strictEqual(model.has('name'), false); model.set({ '0': 0, '1': 1, 'true': true, 'false': false, 'empty': '', 'name': 'name', 'null': null, 'undefined': undefined }); assert.strictEqual(model.has('0'), true); assert.strictEqual(model.has('1'), true); assert.strictEqual(model.has('true'), true); assert.strictEqual(model.has('false'), true); assert.strictEqual(model.has('empty'), true); assert.strictEqual(model.has('name'), true); model.unset('name'); assert.strictEqual(model.has('name'), false); assert.strictEqual(model.has('null'), false); assert.strictEqual(model.has('undefined'), false); }); QUnit.test('matches', function(assert) { assert.expect(4); var model = new Backbone.Model(); assert.strictEqual(model.matches({name: 'Jonas', cool: true}), false); model.set({name: 'Jonas', cool: true}); assert.strictEqual(model.matches({name: 'Jonas'}), true); assert.strictEqual(model.matches({name: 'Jonas', cool: true}), true); assert.strictEqual(model.matches({name: 'Jonas', cool: false}), false); }); QUnit.test('matches with predicate', function(assert) { var model = new Backbone.Model({a: 0}); assert.strictEqual(model.matches(function(attr) { return attr.a > 1 && attr.b != null; }), false); model.set({a: 3, b: true}); assert.strictEqual(model.matches(function(attr) { return attr.a > 1 && attr.b != null; }), true); }); QUnit.test('set and unset', function(assert) { assert.expect(8); var a = new Backbone.Model({id: 'id', foo: 1, bar: 2, baz: 3}); var changeCount = 0; a.on('change:foo', function() { changeCount += 1; }); a.set({foo: 2}); assert.equal(a.get('foo'), 2, 'Foo should have changed.'); assert.equal(changeCount, 1, 'Change count should have incremented.'); // set with value that is not new shouldn't fire change event a.set({foo: 2}); assert.equal(a.get('foo'), 2, 'Foo should NOT have changed, still 2'); assert.equal(changeCount, 1, 'Change count should NOT have incremented.'); a.validate = function(attrs) { assert.equal(attrs.foo, void 0, 'validate:true passed while unsetting'); }; a.unset('foo', {validate: true}); assert.equal(a.get('foo'), void 0, 'Foo should have changed'); delete a.validate; assert.equal(changeCount, 2, 'Change count should have incremented for unset.'); a.unset('id'); assert.equal(a.id, undefined, 'Unsetting the id should remove the id property.'); }); QUnit.test('#2030 - set with failed validate, followed by another set triggers change', function(assert) { var attr = 0, main = 0, error = 0; var Model = Backbone.Model.extend({ validate: function(attrs) { if (attrs.x > 1) { error++; return 'this is an error'; } } }); var model = new Model({x: 0}); model.on('change:x', function() { attr++; }); model.on('change', function() { main++; }); model.set({x: 2}, {validate: true}); model.set({x: 1}, {validate: true}); assert.deepEqual([attr, main, error], [1, 1, 1]); }); QUnit.test('set triggers changes in the correct order', function(assert) { var value = null; var model = new Backbone.Model; model.on('last', function(){ value = 'last'; }); model.on('first', function(){ value = 'first'; }); model.trigger('first'); model.trigger('last'); assert.equal(value, 'last'); }); QUnit.test('set falsy values in the correct order', function(assert) { assert.expect(2); var model = new Backbone.Model({result: 'result'}); model.on('change', function() { assert.equal(model.changed.result, void 0); assert.equal(model.previous('result'), false); }); model.set({result: void 0}, {silent: true}); model.set({result: null}, {silent: true}); model.set({result: false}, {silent: true}); model.set({result: void 0}); }); QUnit.test('nested set triggers with the correct options', function(assert) { var model = new Backbone.Model(); var o1 = {}; var o2 = {}; var o3 = {}; model.on('change', function(__, options) { switch (model.get('a')) { case 1: assert.equal(options, o1); return model.set('a', 2, o2); case 2: assert.equal(options, o2); return model.set('a', 3, o3); case 3: assert.equal(options, o3); } }); model.set('a', 1, o1); }); QUnit.test('multiple unsets', function(assert) { assert.expect(1); var i = 0; var counter = function(){ i++; }; var model = new Backbone.Model({a: 1}); model.on('change:a', counter); model.set({a: 2}); model.unset('a'); model.unset('a'); assert.equal(i, 2, 'Unset does not fire an event for missing attributes.'); }); QUnit.test('unset and changedAttributes', function(assert) { assert.expect(1); var model = new Backbone.Model({a: 1}); model.on('change', function() { assert.ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties'); }); model.unset('a'); }); QUnit.test('using a non-default id attribute.', function(assert) { assert.expect(5); var MongoModel = Backbone.Model.extend({idAttribute: '_id'}); var model = new MongoModel({id: 'eye-dee', _id: 25, title: 'Model'}); assert.equal(model.get('id'), 'eye-dee'); assert.equal(model.id, 25); assert.equal(model.isNew(), false); model.unset('_id'); assert.equal(model.id, undefined); assert.equal(model.isNew(), true); }); QUnit.test('setting an alternative cid prefix', function(assert) { assert.expect(4); var Model = Backbone.Model.extend({ cidPrefix: 'm' }); var model = new Model(); assert.equal(model.cid.charAt(0), 'm'); model = new Backbone.Model(); assert.equal(model.cid.charAt(0), 'c'); var Collection = Backbone.Collection.extend({ model: Model }); var col = new Collection([{id: 'c5'}, {id: 'c6'}, {id: 'c7'}]); assert.equal(col.get('c6').cid.charAt(0), 'm'); col.set([{id: 'c6', value: 'test'}], { merge: true, add: true, remove: false }); assert.ok(col.get('c6').has('value')); }); QUnit.test('set an empty string', function(assert) { assert.expect(1); var model = new Backbone.Model({name: 'Model'}); model.set({name: ''}); assert.equal(model.get('name'), ''); }); QUnit.test('setting an object', function(assert) { assert.expect(1); var model = new Backbone.Model({ custom: {foo: 1} }); model.on('change', function() { assert.ok(1); }); model.set({ custom: {foo: 1} // no change should be fired }); model.set({ custom: {foo: 2} // change event should be fired }); }); QUnit.test('clear', function(assert) { assert.expect(3); var changed; var model = new Backbone.Model({id: 1, name: 'Model'}); model.on('change:name', function(){ changed = true; }); model.on('change', function() { var changedAttrs = model.changedAttributes(); assert.ok('name' in changedAttrs); }); model.clear(); assert.equal(changed, true); assert.equal(model.get('name'), undefined); }); QUnit.test('defaults', function(assert) { assert.expect(9); var Defaulted = Backbone.Model.extend({ defaults: { one: 1, two: 2 } }); var model = new Defaulted({two: undefined}); assert.equal(model.get('one'), 1); assert.equal(model.get('two'), 2); model = new Defaulted({two: 3}); assert.equal(model.get('one'), 1); assert.equal(model.get('two'), 3); Defaulted = Backbone.Model.extend({ defaults: function() { return { one: 3, two: 4 }; } }); model = new Defaulted({two: undefined}); assert.equal(model.get('one'), 3); assert.equal(model.get('two'), 4); Defaulted = Backbone.Model.extend({ defaults: {hasOwnProperty: true} }); model = new Defaulted(); assert.equal(model.get('hasOwnProperty'), true); model = new Defaulted({hasOwnProperty: undefined}); assert.equal(model.get('hasOwnProperty'), true); model = new Defaulted({hasOwnProperty: false}); assert.equal(model.get('hasOwnProperty'), false); }); QUnit.test('change, hasChanged, changedAttributes, previous, previousAttributes', function(assert) { assert.expect(9); var model = new Backbone.Model({name: 'Tim', age: 10}); assert.deepEqual(model.changedAttributes(), false); model.on('change', function() { assert.ok(model.hasChanged('name'), 'name changed'); assert.ok(!model.hasChanged('age'), 'age did not'); assert.ok(_.isEqual(model.changedAttributes(), {name: 'Rob'}), 'changedAttributes returns the changed attrs'); assert.equal(model.previous('name'), 'Tim'); assert.ok(_.isEqual(model.previousAttributes(), {name: 'Tim', age: 10}), 'previousAttributes is correct'); }); assert.equal(model.hasChanged(), false); assert.equal(model.hasChanged(undefined), false); model.set({name: 'Rob'}); assert.equal(model.get('name'), 'Rob'); }); QUnit.test('changedAttributes', function(assert) { assert.expect(3); var model = new Backbone.Model({a: 'a', b: 'b'}); assert.deepEqual(model.changedAttributes(), false); assert.equal(model.changedAttributes({a: 'a'}), false); assert.equal(model.changedAttributes({a: 'b'}).a, 'b'); }); QUnit.test('change with options', function(assert) { assert.expect(2); var value; var model = new Backbone.Model({name: 'Rob'}); model.on('change', function(m, options) { value = options.prefix + m.get('name'); }); model.set({name: 'Bob'}, {prefix: 'Mr. '}); assert.equal(value, 'Mr. Bob'); model.set({name: 'Sue'}, {prefix: 'Ms. '}); assert.equal(value, 'Ms. Sue'); }); QUnit.test('change after initialize', function(assert) { assert.expect(1); var changed = 0; var attrs = {id: 1, label: 'c'}; var obj = new Backbone.Model(attrs); obj.on('change', function() { changed += 1; }); obj.set(attrs); assert.equal(changed, 0); }); QUnit.test('save within change event', function(assert) { assert.expect(1); var env = this; var model = new Backbone.Model({firstName: 'Taylor', lastName: 'Swift'}); model.url = '/test'; model.on('change', function() { model.save(); assert.ok(_.isEqual(env.syncArgs.model, model)); }); model.set({lastName: 'Hicks'}); }); QUnit.test('validate after save', function(assert) { assert.expect(2); var lastError, model = new Backbone.Model(); model.validate = function(attrs) { if (attrs.admin) return "Can't change admin status."; }; model.sync = function(method, m, options) { options.success.call(this, {admin: true}); }; model.on('invalid', function(m, error) { lastError = error; }); model.save(null); assert.equal(lastError, "Can't change admin status."); assert.equal(model.validationError, "Can't change admin status."); }); QUnit.test('save', function(assert) { assert.expect(2); doc.save({title: 'Henry V'}); assert.equal(this.syncArgs.method, 'update'); assert.ok(_.isEqual(this.syncArgs.model, doc)); }); QUnit.test('save, fetch, destroy triggers error event when an error occurs', function(assert) { assert.expect(3); var model = new Backbone.Model(); model.on('error', function() { assert.ok(true); }); model.sync = function(method, m, options) { options.error(); }; model.save({data: 2, id: 1}); model.fetch(); model.destroy(); }); QUnit.test('#3283 - save, fetch, destroy calls success with context', function(assert) { assert.expect(3); var model = new Backbone.Model(); var obj = {}; var options = { context: obj, success: function() { assert.equal(this, obj); } }; model.sync = function(method, m, opts) { opts.success.call(opts.context); }; model.save({data: 2, id: 1}, options); model.fetch(options); model.destroy(options); }); QUnit.test('#3283 - save, fetch, destroy calls error with context', function(assert) { assert.expect(3); var model = new Backbone.Model(); var obj = {}; var options = { context: obj, error: function() { assert.equal(this, obj); } }; model.sync = function(method, m, opts) { opts.error.call(opts.context); }; model.save({data: 2, id: 1}, options); model.fetch(options); model.destroy(options); }); QUnit.test('#3470 - save and fetch with parse false', function(assert) { assert.expect(2); var i = 0; var model = new Backbone.Model(); model.parse = function() { assert.ok(false); }; model.sync = function(method, m, options) { options.success({i: ++i}); }; model.fetch({parse: false}); assert.equal(model.get('i'), i); model.save(null, {parse: false}); assert.equal(model.get('i'), i); }); QUnit.test('save with PATCH', function(assert) { doc.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4}); doc.save(); assert.equal(this.syncArgs.method, 'update'); assert.equal(this.syncArgs.options.attrs, undefined); doc.save({b: 2, d: 4}, {patch: true}); assert.equal(this.syncArgs.method, 'patch'); assert.equal(_.size(this.syncArgs.options.attrs), 2); assert.equal(this.syncArgs.options.attrs.d, 4); assert.equal(this.syncArgs.options.attrs.a, undefined); assert.equal(this.ajaxSettings.data, '{"b":2,"d":4}'); }); QUnit.test('save with PATCH and different attrs', function(assert) { doc.clear().save({b: 2, d: 4}, {patch: true, attrs: {B: 1, D: 3}}); assert.equal(this.syncArgs.options.attrs.D, 3); assert.equal(this.syncArgs.options.attrs.d, undefined); assert.equal(this.ajaxSettings.data, '{"B":1,"D":3}'); assert.deepEqual(doc.attributes, {b: 2, d: 4}); }); QUnit.test('save in positional style', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.sync = function(method, m, options) { options.success(); }; model.save('title', 'Twelfth Night'); assert.equal(model.get('title'), 'Twelfth Night'); }); QUnit.test('save with non-object success response', function(assert) { assert.expect(2); var model = new Backbone.Model(); model.sync = function(method, m, options) { options.success('', options); options.success(null, options); }; model.save({testing: 'empty'}, { success: function(m) { assert.deepEqual(m.attributes, {testing: 'empty'}); } }); }); QUnit.test('save with wait and supplied id', function(assert) { var Model = Backbone.Model.extend({ urlRoot: '/collection' }); var model = new Model(); model.save({id: 42}, {wait: true}); assert.equal(this.ajaxSettings.url, '/collection/42'); }); QUnit.test('save will pass extra options to success callback', function(assert) { assert.expect(1); var SpecialSyncModel = Backbone.Model.extend({ sync: function(method, m, options) { _.extend(options, {specialSync: true}); return Backbone.Model.prototype.sync.call(this, method, m, options); }, urlRoot: '/test' }); var model = new SpecialSyncModel(); var onSuccess = function(m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.save(null, {success: onSuccess}); this.ajaxSettings.success(); }); QUnit.test('failing save with wait:true triggers error event (#4262)', function(assert) { assert.expect(1); var model = new Backbone.Model; model.urlRoot = '/test'; model.on('error', function() { assert.ok(true); }); model.save({id: '1'}, {wait: true}); this.ajaxSettings.error(); }); QUnit.test('fetch', function(assert) { assert.expect(2); doc.fetch(); assert.equal(this.syncArgs.method, 'read'); assert.ok(_.isEqual(this.syncArgs.model, doc)); }); QUnit.test('fetch will pass extra options to success callback', function(assert) { assert.expect(1); var SpecialSyncModel = Backbone.Model.extend({ sync: function(method, m, options) { _.extend(options, {specialSync: true}); return Backbone.Model.prototype.sync.call(this, method, m, options); }, urlRoot: '/test' }); var model = new SpecialSyncModel(); var onSuccess = function(m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.fetch({success: onSuccess}); this.ajaxSettings.success(); }); QUnit.test('destroy', function(assert) { assert.expect(3); doc.destroy(); assert.equal(this.syncArgs.method, 'delete'); assert.ok(_.isEqual(this.syncArgs.model, doc)); var newModel = new Backbone.Model; assert.equal(newModel.destroy(), false); }); QUnit.test('destroy will pass extra options to success callback', function(assert) { assert.expect(1); var SpecialSyncModel = Backbone.Model.extend({ sync: function(method, m, options) { _.extend(options, {specialSync: true}); return Backbone.Model.prototype.sync.call(this, method, m, options); }, urlRoot: '/test' }); var model = new SpecialSyncModel({id: 'id'}); var onSuccess = function(m, response, options) { assert.ok(options.specialSync, 'Options were passed correctly to callback'); }; model.destroy({success: onSuccess}); this.ajaxSettings.success(); }); QUnit.test('non-persisted destroy', function(assert) { assert.expect(1); var a = new Backbone.Model({foo: 1, bar: 2, baz: 3}); a.sync = function() { throw 'should not be called'; }; a.destroy(); assert.ok(true, 'non-persisted model should not call sync'); }); QUnit.test('validate', function(assert) { var lastError; var model = new Backbone.Model(); model.validate = function(attrs) { if (attrs.admin !== this.get('admin')) return "Can't change admin status."; }; model.on('invalid', function(m, error) { lastError = error; }); var result = model.set({a: 100}); assert.equal(result, model); assert.equal(model.get('a'), 100); assert.equal(lastError, undefined); result = model.set({admin: true}); assert.equal(model.get('admin'), true); result = model.set({a: 200, admin: false}, {validate: true}); assert.equal(lastError, "Can't change admin status."); assert.equal(result, false); assert.equal(model.get('a'), 100); }); QUnit.test('validate on unset and clear', function(assert) { assert.expect(6); var error; var model = new Backbone.Model({name: 'One'}); model.validate = function(attrs) { if (!attrs.name) { error = true; return 'No thanks.'; } }; model.set({name: 'Two'}); assert.equal(model.get('name'), 'Two'); assert.equal(error, undefined); model.unset('name', {validate: true}); assert.equal(error, true); assert.equal(model.get('name'), 'Two'); model.clear({validate: true}); assert.equal(model.get('name'), 'Two'); delete model.validate; model.clear(); assert.equal(model.get('name'), undefined); }); QUnit.test('validate with error callback', function(assert) { assert.expect(8); var lastError, boundError; var model = new Backbone.Model(); model.validate = function(attrs) { if (attrs.admin) return "Can't change admin status."; }; model.on('invalid', function(m, error) { boundError = true; }); var result = model.set({a: 100}, {validate: true}); assert.equal(result, model); assert.equal(model.get('a'), 100); assert.equal(model.validationError, null); assert.equal(boundError, undefined); result = model.set({a: 200, admin: true}, {validate: true}); assert.equal(result, false); assert.equal(model.get('a'), 100); assert.equal(model.validationError, "Can't change admin status."); assert.equal(boundError, true); }); QUnit.test('defaults always extend attrs (#459)', function(assert) { assert.expect(2); var Defaulted = Backbone.Model.extend({ defaults: {one: 1}, initialize: function(attrs, opts) { assert.equal(this.attributes.one, 1); } }); var providedattrs = new Defaulted({}); var emptyattrs = new Defaulted(); }); QUnit.test('Inherit class properties', function(assert) { assert.expect(6); var Parent = Backbone.Model.extend({ instancePropSame: function() {}, instancePropDiff: function() {} }, { classProp: function() {} }); var Child = Parent.extend({ instancePropDiff: function() {} }); var adult = new Parent; var kid = new Child; assert.equal(Child.classProp, Parent.classProp); assert.notEqual(Child.classProp, undefined); assert.equal(kid.instancePropSame, adult.instancePropSame); assert.notEqual(kid.instancePropSame, undefined); assert.notEqual(Child.prototype.instancePropDiff, Parent.prototype.instancePropDiff); assert.notEqual(Child.prototype.instancePropDiff, undefined); }); QUnit.test("Nested change events don't clobber previous attributes", function(assert) { assert.expect(4); new Backbone.Model() .on('change:state', function(m, newState) { assert.equal(m.previous('state'), undefined); assert.equal(newState, 'hello'); // Fire a nested change event. m.set({other: 'whatever'}); }) .on('change:state', function(m, newState) { assert.equal(m.previous('state'), undefined); assert.equal(newState, 'hello'); }) .set({state: 'hello'}); }); QUnit.test('hasChanged/set should use same comparison', function(assert) { assert.expect(2); var changed = 0, model = new Backbone.Model({a: null}); model.on('change', function() { assert.ok(this.hasChanged('a')); }) .on('change:a', function() { changed++; }) .set({a: undefined}); assert.equal(changed, 1); }); QUnit.test('#582, #425, change:attribute callbacks should fire after all changes have occurred', function(assert) { assert.expect(9); var model = new Backbone.Model; var assertion = function() { assert.equal(model.get('a'), 'a'); assert.equal(model.get('b'), 'b'); assert.equal(model.get('c'), 'c'); }; model.on('change:a', assertion); model.on('change:b', assertion); model.on('change:c', assertion); model.set({a: 'a', b: 'b', c: 'c'}); }); QUnit.test('#871, set with attributes property', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.set({attributes: true}); assert.ok(model.has('attributes')); }); QUnit.test('set value regardless of equality/change', function(assert) { assert.expect(1); var model = new Backbone.Model({x: []}); var a = []; model.set({x: a}); assert.ok(model.get('x') === a); }); QUnit.test('set same value does not trigger change', function(assert) { assert.expect(0); var model = new Backbone.Model({x: 1}); model.on('change change:x', function() { assert.ok(false); }); model.set({x: 1}); model.set({x: 1}); }); QUnit.test('unset does not fire a change for undefined attributes', function(assert) { assert.expect(0); var model = new Backbone.Model({x: undefined}); model.on('change:x', function(){ assert.ok(false); }); model.unset('x'); }); QUnit.test('set: undefined values', function(assert) { assert.expect(1); var model = new Backbone.Model({x: undefined}); assert.ok('x' in model.attributes); }); QUnit.test('hasChanged works outside of change events, and true within', function(assert) { assert.expect(6); var model = new Backbone.Model({x: 1}); model.on('change:x', function() { assert.ok(model.hasChanged('x')); assert.equal(model.get('x'), 1); }); model.set({x: 2}, {silent: true}); assert.ok(model.hasChanged()); assert.equal(model.hasChanged('x'), true); model.set({x: 1}); assert.ok(model.hasChanged()); assert.equal(model.hasChanged('x'), true); }); QUnit.test('hasChanged gets cleared on the following set', function(assert) { assert.expect(4); var model = new Backbone.Model; model.set({x: 1}); assert.ok(model.hasChanged()); model.set({x: 1}); assert.ok(!model.hasChanged()); model.set({x: 2}); assert.ok(model.hasChanged()); model.set({}); assert.ok(!model.hasChanged()); }); QUnit.test('save with `wait` succeeds without `validate`', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.url = '/test'; model.save({x: 1}, {wait: true}); assert.ok(this.syncArgs.model === model); }); QUnit.test("save without `wait` doesn't set invalid attributes", function(assert) { var model = new Backbone.Model(); model.validate = function() { return 1; }; model.save({a: 1}); assert.equal(model.get('a'), void 0); }); QUnit.test("save doesn't validate twice", function(assert) { var model = new Backbone.Model(); var times = 0; model.sync = function() {}; model.validate = function() { ++times; }; model.save({}); assert.equal(times, 1); }); QUnit.test('`hasChanged` for falsey keys', function(assert) { assert.expect(2); var model = new Backbone.Model(); model.set({x: true}, {silent: true}); assert.ok(!model.hasChanged(0)); assert.ok(!model.hasChanged('')); }); QUnit.test('`previous` for falsey keys', function(assert) { assert.expect(2); var model = new Backbone.Model({'0': true, '': true}); model.set({'0': false, '': false}, {silent: true}); assert.equal(model.previous(0), true); assert.equal(model.previous(''), true); }); QUnit.test('`save` with `wait` sends correct attributes', function(assert) { assert.expect(5); var changed = 0; var model = new Backbone.Model({x: 1, y: 2}); model.url = '/test'; model.on('change:x', function() { changed++; }); model.save({x: 3}, {wait: true}); assert.deepEqual(JSON.parse(this.ajaxSettings.data), {x: 3, y: 2}); assert.equal(model.get('x'), 1); assert.equal(changed, 0); this.syncArgs.options.success({}); assert.equal(model.get('x'), 3); assert.equal(changed, 1); }); QUnit.test("a failed `save` with `wait` doesn't leave attributes behind", function(assert) { assert.expect(1); var model = new Backbone.Model; model.url = '/test'; model.save({x: 1}, {wait: true}); assert.equal(model.get('x'), void 0); }); QUnit.test('#1030 - `save` with `wait` results in correct attributes if success is called during sync', function(assert) { assert.expect(2); var model = new Backbone.Model({x: 1, y: 2}); model.sync = function(method, m, options) { options.success(); }; model.on('change:x', function() { assert.ok(true); }); model.save({x: 3}, {wait: true}); assert.equal(model.get('x'), 3); }); QUnit.test('save with wait validates attributes', function(assert) { var model = new Backbone.Model(); model.url = '/test'; model.validate = function() { assert.ok(true); }; model.save({x: 1}, {wait: true}); }); QUnit.test('save turns on parse flag', function(assert) { var Model = Backbone.Model.extend({ sync: function(method, m, options) { assert.ok(options.parse); } }); new Model().save(); }); QUnit.test("nested `set` during `'change:attr'`", function(assert) { assert.expect(2); var events = []; var model = new Backbone.Model(); model.on('all', function(event) { events.push(event); }); model.on('change', function() { model.set({z: true}, {silent: true}); }); model.on('change:x', function() { model.set({y: true}); }); model.set({x: true}); assert.deepEqual(events, ['change:y', 'change:x', 'change']); events = []; model.set({z: true}); assert.deepEqual(events, []); }); QUnit.test('nested `change` only fires once', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.on('change', function() { assert.ok(true); model.set({x: true}); }); model.set({x: true}); }); QUnit.test("nested `set` during `'change'`", function(assert) { assert.expect(6); var count = 0; var model = new Backbone.Model(); model.on('change', function() { switch (count++) { case 0: assert.deepEqual(this.changedAttributes(), {x: true}); assert.equal(model.previous('x'), undefined); model.set({y: true}); break; case 1: assert.deepEqual(this.changedAttributes(), {x: true, y: true}); assert.equal(model.previous('x'), undefined); model.set({z: true}); break; case 2: assert.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); assert.equal(model.previous('y'), undefined); break; default: assert.ok(false); } }); model.set({x: true}); }); QUnit.test('nested `change` with silent', function(assert) { assert.expect(3); var count = 0; var model = new Backbone.Model(); model.on('change:y', function() { assert.ok(false); }); model.on('change', function() { switch (count++) { case 0: assert.deepEqual(this.changedAttributes(), {x: true}); model.set({y: true}, {silent: true}); model.set({z: true}); break; case 1: assert.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); break; case 2: assert.deepEqual(this.changedAttributes(), {z: false}); break; default: assert.ok(false); } }); model.set({x: true}); model.set({z: false}); }); QUnit.test('nested `change:attr` with silent', function(assert) { assert.expect(0); var model = new Backbone.Model(); model.on('change:y', function(){ assert.ok(false); }); model.on('change', function() { model.set({y: true}, {silent: true}); model.set({z: true}); }); model.set({x: true}); }); QUnit.test('multiple nested changes with silent', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.on('change:x', function() { model.set({y: 1}, {silent: true}); model.set({y: 2}); }); model.on('change:y', function(m, val) { assert.equal(val, 2); }); model.set({x: true}); }); QUnit.test('multiple nested changes with silent', function(assert) { assert.expect(1); var changes = []; var model = new Backbone.Model(); model.on('change:b', function(m, val) { changes.push(val); }); model.on('change', function() { model.set({b: 1}); }); model.set({b: 0}); assert.deepEqual(changes, [0, 1]); }); QUnit.test('basic silent change semantics', function(assert) { assert.expect(1); var model = new Backbone.Model; model.set({x: 1}); model.on('change', function(){ assert.ok(true); }); model.set({x: 2}, {silent: true}); model.set({x: 1}); }); QUnit.test('nested set multiple times', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.on('change:b', function() { assert.ok(true); }); model.on('change:a', function() { model.set({b: true}); model.set({b: true}); }); model.set({a: true}); }); QUnit.test('#1122 - clear does not alter options.', function(assert) { assert.expect(1); var model = new Backbone.Model(); var options = {}; model.clear(options); assert.ok(!options.unset); }); QUnit.test('#1122 - unset does not alter options.', function(assert) { assert.expect(1); var model = new Backbone.Model(); var options = {}; model.unset('x', options); assert.ok(!options.unset); }); QUnit.test('#1355 - `options` is passed to success callbacks', function(assert) { assert.expect(3); var model = new Backbone.Model(); var opts = { success: function( m, resp, options ) { assert.ok(options); } }; model.sync = function(method, m, options) { options.success(); }; model.save({id: 1}, opts); model.fetch(opts); model.destroy(opts); }); QUnit.test("#1412 - Trigger 'sync' event.", function(assert) { assert.expect(3); var model = new Backbone.Model({id: 1}); model.sync = function(method, m, options) { options.success(); }; model.on('sync', function(){ assert.ok(true); }); model.fetch(); model.save(); model.destroy(); }); QUnit.test('#1365 - Destroy: New models execute success callback.', function(assert) { var done = assert.async(); assert.expect(2); new Backbone.Model() .on('sync', function() { assert.ok(false); }) .on('destroy', function(){ assert.ok(true); }) .destroy({success: function(){ assert.ok(true); done(); }}); }); QUnit.test('#1433 - Save: An invalid model cannot be persisted.', function(assert) { assert.expect(1); var model = new Backbone.Model; model.validate = function(){ return 'invalid'; }; model.sync = function(){ assert.ok(false); }; assert.strictEqual(model.save(), false); }); QUnit.test("#1377 - Save without attrs triggers 'error'.", function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ url: '/test/', sync: function(method, m, options){ options.success(); }, validate: function(){ return 'invalid'; } }); var model = new Model({id: 1}); model.on('invalid', function(){ assert.ok(true); }); model.save(); }); QUnit.test('#1545 - `undefined` can be passed to a model constructor without coersion', function(assert) { var Model = Backbone.Model.extend({ defaults: {one: 1}, initialize: function(attrs, opts) { assert.equal(attrs, undefined); } }); var emptyattrs = new Model(); var undefinedattrs = new Model(undefined); }); QUnit.test('#1478 - Model `save` does not trigger change on unchanged attributes', function(assert) { var done = assert.async(); assert.expect(0); var Model = Backbone.Model.extend({ sync: function(method, m, options) { setTimeout(function(){ options.success(); done(); }, 0); } }); new Model({x: true}) .on('change:x', function(){ assert.ok(false); }) .save(null, {wait: true}); }); QUnit.test('#1664 - Changing from one value, silently to another, back to original triggers a change.', function(assert) { assert.expect(1); var model = new Backbone.Model({x: 1}); model.on('change:x', function() { assert.ok(true); }); model.set({x: 2}, {silent: true}); model.set({x: 3}, {silent: true}); model.set({x: 1}); }); QUnit.test('#1664 - multiple silent changes nested inside a change event', function(assert) { assert.expect(2); var changes = []; var model = new Backbone.Model(); model.on('change', function() { model.set({a: 'c'}, {silent: true}); model.set({b: 2}, {silent: true}); model.unset('c', {silent: true}); }); model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); model.set({a: 'a', b: 1, c: 'item'}); assert.deepEqual(changes, ['a', 1, 'item']); assert.deepEqual(model.attributes, {a: 'c', b: 2}); }); QUnit.test('#1791 - `attributes` is available for `parse`', function(assert) { var Model = Backbone.Model.extend({ parse: function() { this.has('a'); } // shouldn't throw an error }); var model = new Model(null, {parse: true}); assert.expect(0); }); QUnit.test('silent changes in last `change` event back to original triggers change', function(assert) { assert.expect(2); var changes = []; var model = new Backbone.Model(); model.on('change:a change:b change:c', function(m, val) { changes.push(val); }); model.on('change', function() { model.set({a: 'c'}, {silent: true}); }); model.set({a: 'a'}); assert.deepEqual(changes, ['a']); model.set({a: 'a'}); assert.deepEqual(changes, ['a', 'a']); }); QUnit.test('#1943 change calculations should use _.isEqual', function(assert) { var model = new Backbone.Model({a: {key: 'value'}}); model.set('a', {key: 'value'}, {silent: true}); assert.equal(model.changedAttributes(), false); }); QUnit.test('#1964 - final `change` event is always fired, regardless of interim changes', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.on('change:property', function() { model.set('property', 'bar'); }); model.on('change', function() { assert.ok(true); }); model.set('property', 'foo'); }); QUnit.test('isValid', function(assert) { var model = new Backbone.Model({valid: true}); model.validate = function(attrs) { if (!attrs.valid) return 'invalid'; }; assert.equal(model.isValid(), true); assert.equal(model.set({valid: false}, {validate: true}), false); assert.equal(model.isValid(), true); model.set({valid: false}); assert.equal(model.isValid(), false); assert.ok(!model.set('valid', false, {validate: true})); }); QUnit.test('mixin', function(assert) { Backbone.Model.mixin({ isEqual: function(model1, model2) { return _.isEqual(model1, model2.attributes); } }); var model1 = new Backbone.Model({ a: {b: 2}, c: 3 }); var model2 = new Backbone.Model({ a: {b: 2}, c: 3 }); var model3 = new Backbone.Model({ a: {b: 4}, c: 3 }); assert.equal(model1.isEqual(model2), true); assert.equal(model1.isEqual(model3), false); }); QUnit.test('#1179 - isValid returns true in the absence of validate.', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.validate = null; assert.ok(model.isValid()); }); QUnit.test('#1961 - Creating a model with {validate:true} will call validate and use the error callback', function(assert) { var Model = Backbone.Model.extend({ validate: function(attrs) { if (attrs.id === 1) return "This shouldn't happen"; } }); var model = new Model({id: 1}, {validate: true}); assert.equal(model.validationError, "This shouldn't happen"); }); QUnit.test('toJSON receives attrs during save(..., {wait: true})', function(assert) { assert.expect(1); var Model = Backbone.Model.extend({ url: '/test', toJSON: function() { assert.strictEqual(this.attributes.x, 1); return _.clone(this.attributes); } }); var model = new Model; model.save({x: 1}, {wait: true}); }); QUnit.test('#2034 - nested set with silent only triggers one change', function(assert) { assert.expect(1); var model = new Backbone.Model(); model.on('change', function() { model.set({b: true}, {silent: true}); assert.ok(true); }); model.set({a: true}); }); QUnit.test('#3778 - id will only be updated if it is set', function(assert) { assert.expect(2); var model = new Backbone.Model({id: 1}); model.id = 2; model.set({foo: 'bar'}); assert.equal(model.id, 2); model.set({id: 3}); assert.equal(model.id, 3); }); QUnit.test('#4289 - Trigger "changeId" need to be generate only if the content id change', function(assert) { assert.expect(1); var model = new Backbone.Model({id: 1}); model.idAttribute = 'id'; model.on('changeId', function(m) { assert.equal(m.get('id'), 2); }); model.set({id: 1}); model.set({id: 2}); }); })(QUnit); ================================================ FILE: test/noconflict.js ================================================ (function(QUnit) { QUnit.module('Backbone.noConflict'); QUnit.test('noConflict', function(assert) { assert.expect(2); var noconflictBackbone = Backbone.noConflict(); assert.equal(window.Backbone, undefined, 'Returned window.Backbone'); window.Backbone = noconflictBackbone; assert.equal(window.Backbone, noconflictBackbone, 'Backbone is still pointing to the original Backbone'); }); })(QUnit); ================================================ FILE: test/router.js ================================================ (function(QUnit) { var router = null; var location = null; var lastRoute = null; var lastArgs = []; var onRoute = function(routerParam, route, args) { lastRoute = route; lastArgs = args; }; var Location = function(href) { this.replace(href); }; _.extend(Location.prototype, { parser: document.createElement('a'), replace: function(href) { this.parser.href = href; _.extend(this, _.pick(this.parser, 'href', 'hash', 'host', 'search', 'fragment', 'pathname', 'protocol' )); // In IE, anchor.pathname does not contain a leading slash though // window.location.pathname does. if (!/^\//.test(this.pathname)) this.pathname = '/' + this.pathname; }, toString: function() { return this.href; } }); QUnit.module('Backbone.Router', { beforeEach: function() { location = new Location('http://example.com'); Backbone.history = _.extend(new Backbone.History, {location: location}); router = new Router({testing: 101}); Backbone.history.interval = 9; Backbone.history.start({pushState: false}); lastRoute = null; lastArgs = []; Backbone.history.on('route', onRoute); }, afterEach: function() { Backbone.history.stop(); Backbone.history.off('route', onRoute); } }); var ExternalObject = { value: 'unset', routingFunction: function(value) { this.value = value; } }; ExternalObject.routingFunction = _.bind(ExternalObject.routingFunction, ExternalObject); var Router = Backbone.Router.extend({ count: 0, routes: { 'noCallback': 'noCallback', 'counter': 'counter', 'search/:query': 'search', 'search/:query/p:page': 'search', 'charñ': 'charUTF', 'char%C3%B1': 'charEscaped', 'contacts': 'contacts', 'contacts/new': 'newContact', 'contacts/:id': 'loadContact', 'route-event/:arg': 'routeEvent', 'optional(/:item)': 'optionalItem', 'named/optional/(y:z)': 'namedOptional', 'splat/*args/end': 'splat', ':repo/compare/*from...*to': 'github', 'decode/:named/*splat': 'decode', '*first/complex-*part/*rest': 'complex', 'query/:entity': 'query', 'function/:value': ExternalObject.routingFunction, '*anything': 'anything' }, preinitialize: function(options) { this.testpreinit = 'foo'; }, initialize: function(options) { this.testing = options.testing; this.route('implicit', 'implicit'); }, counter: function() { this.count++; }, implicit: function() { this.count++; }, search: function(query, page) { this.query = query; this.page = page; }, charUTF: function() { this.charType = 'UTF'; }, charEscaped: function() { this.charType = 'escaped'; }, contacts: function() { this.contact = 'index'; }, newContact: function() { this.contact = 'new'; }, loadContact: function() { this.contact = 'load'; }, optionalItem: function(arg) { this.arg = arg !== void 0 ? arg : null; }, splat: function(args) { this.args = args; }, github: function(repo, from, to) { this.repo = repo; this.from = from; this.to = to; }, complex: function(first, part, rest) { this.first = first; this.part = part; this.rest = rest; }, query: function(entity, args) { this.entity = entity; this.queryArgs = args; }, anything: function(whatever) { this.anything = whatever; }, namedOptional: function(z) { this.z = z; }, decode: function(named, path) { this.named = named; this.path = path; }, routeEvent: function(arg) { } }); QUnit.test('initialize', function(assert) { assert.expect(1); assert.equal(router.testing, 101); }); QUnit.test('preinitialize', function(assert) { assert.expect(1); assert.equal(router.testpreinit, 'foo'); }); QUnit.test('routes (simple)', function(assert) { assert.expect(4); location.replace('http://example.com#search/news'); Backbone.history.checkUrl(); assert.equal(router.query, 'news'); assert.equal(router.page, void 0); assert.equal(lastRoute, 'search'); assert.equal(lastArgs[0], 'news'); }); QUnit.test('routes (simple, but unicode)', function(assert) { assert.expect(4); location.replace('http://example.com#search/тест'); Backbone.history.checkUrl(); assert.equal(router.query, 'тест'); assert.equal(router.page, void 0); assert.equal(lastRoute, 'search'); assert.equal(lastArgs[0], 'тест'); }); QUnit.test('routes (two part)', function(assert) { assert.expect(2); location.replace('http://example.com#search/nyc/p10'); Backbone.history.checkUrl(); assert.equal(router.query, 'nyc'); assert.equal(router.page, '10'); }); QUnit.test('routes via navigate', function(assert) { assert.expect(2); Backbone.history.navigate('search/manhattan/p20', {trigger: true}); assert.equal(router.query, 'manhattan'); assert.equal(router.page, '20'); }); QUnit.test('routes via navigate with params', function(assert) { assert.expect(1); Backbone.history.navigate('query/test?a=b', {trigger: true}); assert.equal(router.queryArgs, 'a=b'); }); QUnit.test('routes via navigate for backwards-compatibility', function(assert) { assert.expect(2); Backbone.history.navigate('search/manhattan/p20', true); assert.equal(router.query, 'manhattan'); assert.equal(router.page, '20'); }); QUnit.test('reports matched route via nagivate', function(assert) { assert.expect(1); assert.ok(Backbone.history.navigate('search/manhattan/p20', true)); }); QUnit.test('route precedence via navigate', function(assert) { assert.expect(6); // Check both 0.9.x and backwards-compatibility options _.each([{trigger: true}, true], function(options) { Backbone.history.navigate('contacts', options); assert.equal(router.contact, 'index'); Backbone.history.navigate('contacts/new', options); assert.equal(router.contact, 'new'); Backbone.history.navigate('contacts/foo', options); assert.equal(router.contact, 'load'); }); }); QUnit.test('loadUrl is not called for identical routes.', function(assert) { assert.expect(0); Backbone.history.loadUrl = function() { assert.ok(false); }; location.replace('http://example.com#route'); Backbone.history.navigate('route'); Backbone.history.navigate('/route'); Backbone.history.navigate('/route'); }); QUnit.test('use implicit callback if none provided', function(assert) { assert.expect(1); router.count = 0; router.navigate('implicit', {trigger: true}); assert.equal(router.count, 1); }); QUnit.test('routes via navigate with {replace: true}', function(assert) { assert.expect(1); location.replace('http://example.com#start_here'); Backbone.history.checkUrl(); location.replace = function(href) { assert.strictEqual(href, new Location('http://example.com#end_here').href); }; Backbone.history.navigate('end_here', {replace: true}); }); QUnit.test('routes (splats)', function(assert) { assert.expect(1); location.replace('http://example.com#splat/long-list/of/splatted_99args/end'); Backbone.history.checkUrl(); assert.equal(router.args, 'long-list/of/splatted_99args'); }); QUnit.test('routes (github)', function(assert) { assert.expect(3); location.replace('http://example.com#backbone/compare/1.0...braddunbar:with/slash'); Backbone.history.checkUrl(); assert.equal(router.repo, 'backbone'); assert.equal(router.from, '1.0'); assert.equal(router.to, 'braddunbar:with/slash'); }); QUnit.test('routes (optional)', function(assert) { assert.expect(2); location.replace('http://example.com#optional'); Backbone.history.checkUrl(); assert.ok(!router.arg); location.replace('http://example.com#optional/thing'); Backbone.history.checkUrl(); assert.equal(router.arg, 'thing'); }); QUnit.test('routes (complex)', function(assert) { assert.expect(3); location.replace('http://example.com#one/two/three/complex-part/four/five/six/seven'); Backbone.history.checkUrl(); assert.equal(router.first, 'one/two/three'); assert.equal(router.part, 'part'); assert.equal(router.rest, 'four/five/six/seven'); }); QUnit.test('routes (query)', function(assert) { assert.expect(5); location.replace('http://example.com#query/mandel?a=b&c=d'); Backbone.history.checkUrl(); assert.equal(router.entity, 'mandel'); assert.equal(router.queryArgs, 'a=b&c=d'); assert.equal(lastRoute, 'query'); assert.equal(lastArgs[0], 'mandel'); assert.equal(lastArgs[1], 'a=b&c=d'); }); QUnit.test('routes (anything)', function(assert) { assert.expect(1); location.replace('http://example.com#doesnt-match-a-route'); Backbone.history.checkUrl(); assert.equal(router.anything, 'doesnt-match-a-route'); }); QUnit.test('routes (function)', function(assert) { assert.expect(3); router.on('route', function(name) { assert.ok(name === ''); }); assert.equal(ExternalObject.value, 'unset'); location.replace('http://example.com#function/set'); Backbone.history.checkUrl(); assert.equal(ExternalObject.value, 'set'); }); QUnit.test('Decode named parameters, not splats.', function(assert) { assert.expect(2); location.replace('http://example.com#decode/a%2Fb/c%2Fd/e'); Backbone.history.checkUrl(); assert.strictEqual(router.named, 'a/b'); assert.strictEqual(router.path, 'c/d/e'); }); QUnit.test('fires event when router doesn\'t have callback on it', function(assert) { assert.expect(1); router.on('route:noCallback', function() { assert.ok(true); }); location.replace('http://example.com#noCallback'); Backbone.history.checkUrl(); }); QUnit.test('No events are triggered if #execute returns false.', function(assert) { assert.expect(1); var MyRouter = Backbone.Router.extend({ routes: { foo: function() { assert.ok(true); } }, execute: function(callback, args) { callback.apply(this, args); return false; } }); var myRouter = new MyRouter; myRouter.on('route route:foo', function() { assert.ok(false); }); Backbone.history.on('route', function() { assert.ok(false); }); location.replace('http://example.com#foo'); Backbone.history.checkUrl(); }); QUnit.test('#933, #908 - leading slash', function(assert) { assert.expect(2); location.replace('http://example.com/root/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({root: '/root', hashChange: false, silent: true}); assert.strictEqual(Backbone.history.getFragment(), 'foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({root: '/root/', hashChange: false, silent: true}); assert.strictEqual(Backbone.history.getFragment(), 'foo'); }); QUnit.test('#967 - Route callback gets passed encoded values.', function(assert) { assert.expect(3); var route = 'has%2Fslash/complex-has%23hash/has%20space'; Backbone.history.navigate(route, {trigger: true}); assert.strictEqual(router.first, 'has/slash'); assert.strictEqual(router.part, 'has#hash'); assert.strictEqual(router.rest, 'has space'); }); QUnit.test('correctly handles URLs with % (#868)', function(assert) { assert.expect(3); location.replace('http://example.com#search/fat%3A1.5%25'); Backbone.history.checkUrl(); location.replace('http://example.com#search/fat'); Backbone.history.checkUrl(); assert.equal(router.query, 'fat'); assert.equal(router.page, void 0); assert.equal(lastRoute, 'search'); }); QUnit.test('#2666 - Hashes with UTF8 in them.', function(assert) { assert.expect(2); Backbone.history.navigate('charñ', {trigger: true}); assert.equal(router.charType, 'UTF'); Backbone.history.navigate('char%C3%B1', {trigger: true}); assert.equal(router.charType, 'UTF'); }); QUnit.test('#1185 - Use pathname when hashChange is not wanted.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/path/name#hash'); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({hashChange: false}); var fragment = Backbone.history.getFragment(); assert.strictEqual(fragment, location.pathname.replace(/^\//, '')); }); QUnit.test('#1206 - Strip leading slash before location.assign.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root/'); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({hashChange: false, root: '/root/'}); location.assign = function(pathname) { assert.strictEqual(pathname, '/root/fragment'); }; Backbone.history.navigate('/fragment'); }); QUnit.test('#1387 - Root fragment without trailing slash.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root'); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({hashChange: false, root: '/root/', silent: true}); assert.strictEqual(Backbone.history.getFragment(), ''); }); QUnit.test('#1366 - History does not prepend root to fragment.', function(assert) { assert.expect(2); Backbone.history.stop(); location.replace('http://example.com/root/'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/root/x'); } } }); Backbone.history.start({ root: '/root/', pushState: true, hashChange: false }); Backbone.history.navigate('x'); assert.strictEqual(Backbone.history.fragment, 'x'); }); QUnit.test('Normalize root.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/root/fragment'); } } }); Backbone.history.start({ pushState: true, root: '/root', hashChange: false }); Backbone.history.navigate('fragment'); }); QUnit.test('Normalize root.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root#fragment'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) {}, replaceState: function(state, title, url) { assert.strictEqual(url, '/root/fragment'); } } }); Backbone.history.start({ pushState: true, root: '/root' }); }); QUnit.test('Normalize root.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root'); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.loadUrl = function() { assert.ok(true); }; Backbone.history.start({ pushState: true, root: '/root' }); }); QUnit.test('Normalize root - leading slash.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function() {} } }); Backbone.history.start({root: 'root'}); assert.strictEqual(Backbone.history.root, '/root/'); }); QUnit.test('Transition from hashChange to pushState.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root#x/y'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function(state, title, url) { assert.strictEqual(url, '/root/x/y'); } } }); Backbone.history.start({ root: 'root', pushState: true }); }); QUnit.test('#1619: Router: Normalize empty root', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function() {} } }); Backbone.history.start({root: ''}); assert.strictEqual(Backbone.history.root, '/'); }); QUnit.test('#1619: Router: nagivate with empty root', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/fragment'); } } }); Backbone.history.start({ pushState: true, root: '', hashChange: false }); Backbone.history.navigate('fragment'); }); QUnit.test('Transition from pushState to hashChange.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root/x/y?a=b'); location.replace = function(url) { assert.strictEqual(url, '/root#x/y?a=b'); }; Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: null, replaceState: null } }); Backbone.history.start({ root: 'root', pushState: true }); }); QUnit.test('#1695 - hashChange to pushState with search.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root#x/y?a=b'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function(state, title, url) { assert.strictEqual(url, '/root/x/y?a=b'); } } }); Backbone.history.start({ root: 'root', pushState: true }); }); QUnit.test('#1746 - Router allows empty route.', function(assert) { assert.expect(1); var MyRouter = Backbone.Router.extend({ routes: {'': 'empty'}, empty: function() {}, route: function(route) { assert.strictEqual(route, ''); } }); new MyRouter; }); QUnit.test('#1794 - Trailing space in fragments.', function(assert) { assert.expect(1); var history = new Backbone.History; assert.strictEqual(history.getFragment('fragment '), 'fragment'); }); QUnit.test('#1820 - Leading slash and trailing space.', function(assert) { assert.expect(1); var history = new Backbone.History; assert.strictEqual(history.getFragment('/fragment '), 'fragment'); }); QUnit.test('#1980 - Optional parameters.', function(assert) { assert.expect(2); location.replace('http://example.com#named/optional/y'); Backbone.history.checkUrl(); assert.strictEqual(router.z, undefined); location.replace('http://example.com#named/optional/y123'); Backbone.history.checkUrl(); assert.strictEqual(router.z, '123'); }); QUnit.test('#2062 - Trigger "route" event on router instance.', function(assert) { assert.expect(2); router.on('route', function(name, args) { assert.strictEqual(name, 'routeEvent'); assert.deepEqual(args, ['x', null]); }); location.replace('http://example.com#route-event/x'); Backbone.history.checkUrl(); }); QUnit.test('#2255 - Extend routes by making routes a function.', function(assert) { assert.expect(1); var RouterBase = Backbone.Router.extend({ routes: function() { return { home: 'root', index: 'index.html' }; } }); var RouterExtended = RouterBase.extend({ routes: function() { var _super = RouterExtended.__super__.routes; return _.extend(_super(), {show: 'show', search: 'search'}); } }); var myRouter = new RouterExtended(); assert.deepEqual({home: 'root', index: 'index.html', show: 'show', search: 'search'}, myRouter.routes); }); QUnit.test('#2538 - hashChange to pushState only if both requested.', function(assert) { assert.expect(0); Backbone.history.stop(); location.replace('http://example.com/root?a=b#x/y'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function() { assert.ok(false); } } }); Backbone.history.start({ root: 'root', pushState: true, hashChange: false }); }); QUnit.test('No hash fallback.', function(assert) { assert.expect(0); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() {}, replaceState: function() {} } }); var MyRouter = Backbone.Router.extend({ routes: { hash: function() { assert.ok(false); } } }); var myRouter = new MyRouter; location.replace('http://example.com/'); Backbone.history.start({ pushState: true, hashChange: false }); location.replace('http://example.com/nomatch#hash'); Backbone.history.checkUrl(); }); QUnit.test('#2656 - No trailing slash on root.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/root'); } } }); location.replace('http://example.com/root/path'); Backbone.history.start({pushState: true, hashChange: false, root: 'root'}); Backbone.history.navigate(''); }); QUnit.test('#2656 - No trailing slash on root.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/'); } } }); location.replace('http://example.com/path'); Backbone.history.start({pushState: true, hashChange: false}); Backbone.history.navigate(''); }); QUnit.test('#2656 - No trailing slash on root.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/root?x=1'); } } }); location.replace('http://example.com/root/path'); Backbone.history.start({pushState: true, hashChange: false, root: 'root'}); Backbone.history.navigate('?x=1'); }); QUnit.test('#3391 - Empty root normalizes to single slash.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/'); } } }); location.replace('http://example.com/root/path'); Backbone.history.start({pushState: true, hashChange: false, root: ''}); Backbone.history.navigate(''); }); QUnit.test('#3391 - Use trailing slash on root when trailingSlash is true.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/root/'); } } }); location.replace('http://example.com/root/path'); Backbone.history.start({pushState: true, hashChange: false, root: 'root', trailingSlash: true}); Backbone.history.navigate(''); }); QUnit.test('#2765 - Fragment matching sans query/hash.', function(assert) { assert.expect(2); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function(state, title, url) { assert.strictEqual(url, '/path?query#hash'); } } }); var MyRouter = Backbone.Router.extend({ routes: { path: function() { assert.ok(true); } } }); var myRouter = new MyRouter; location.replace('http://example.com/'); Backbone.history.start({pushState: true, hashChange: false}); Backbone.history.navigate('path?query#hash', true); }); QUnit.test('Do not decode the search params.', function(assert) { assert.expect(1); var MyRouter = Backbone.Router.extend({ routes: { path: function(params) { assert.strictEqual(params, 'x=y%3Fz'); } } }); var myRouter = new MyRouter; Backbone.history.navigate('path?x=y%3Fz', true); }); QUnit.test('Navigate to a hash url.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({pushState: true}); var MyRouter = Backbone.Router.extend({ routes: { path: function(params) { assert.strictEqual(params, 'x=y'); } } }); var myRouter = new MyRouter; location.replace('http://example.com/path?x=y#hash'); Backbone.history.checkUrl(); }); QUnit.test('#navigate to a hash url.', function(assert) { assert.expect(1); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.start({pushState: true}); var MyRouter = Backbone.Router.extend({ routes: { path: function(params) { assert.strictEqual(params, 'x=y'); } } }); var myRouter = new MyRouter; Backbone.history.navigate('path?x=y#hash', true); }); QUnit.test('unicode pathname', function(assert) { assert.expect(1); location.replace('http://example.com/myyjä'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: { myyjä: function() { assert.ok(true); } } }); new MyRouter; Backbone.history.start({pushState: true}); }); QUnit.test('unicode pathname with % in a parameter', function(assert) { assert.expect(1); location.replace('http://example.com/myyjä/foo%20%25%3F%2f%40%25%20bar'); location.pathname = '/myyj%C3%A4/foo%20%25%3F%2f%40%25%20bar'; Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: { 'myyjä/:query': function(query) { assert.strictEqual(query, 'foo %?/@% bar'); } } }); new MyRouter; Backbone.history.start({pushState: true}); }); QUnit.test('newline in route', function(assert) { assert.expect(1); location.replace('http://example.com/stuff%0Anonsense?param=foo%0Abar'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: { 'stuff\nnonsense': function() { assert.ok(true); } } }); new MyRouter; Backbone.history.start({pushState: true}); }); QUnit.test('Router#execute receives callback, args, name.', function(assert) { assert.expect(3); location.replace('http://example.com#foo/123/bar?x=y'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: {'foo/:id/bar': 'foo'}, foo: function() {}, execute: function(callback, args, name) { assert.strictEqual(callback, this.foo); assert.deepEqual(args, ['123', 'x=y']); assert.strictEqual(name, 'foo'); } }); var myRouter = new MyRouter; Backbone.history.start(); }); QUnit.test('pushState to hashChange with only search params.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com?a=b'); location.replace = function(url) { assert.strictEqual(url, '/#?a=b'); }; Backbone.history = _.extend(new Backbone.History, { location: location, history: null }); Backbone.history.start({pushState: true}); }); QUnit.test('#3123 - History#navigate decodes before comparison.', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/shop/search?keyword=short%20dress'); Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: function() { assert.ok(false); }, replaceState: function() { assert.ok(false); } } }); Backbone.history.start({pushState: true}); Backbone.history.navigate('shop/search?keyword=short%20dress', true); assert.strictEqual(Backbone.history.fragment, 'shop/search?keyword=short dress'); }); QUnit.test('#3175 - Urls in the params', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com#login?a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); Backbone.history = _.extend(new Backbone.History, {location: location}); var myRouter = new Backbone.Router; myRouter.route('login', function(params) { assert.strictEqual(params, 'a=value&backUrl=https%3A%2F%2Fwww.msn.com%2Fidp%2Fidpdemo%3Fspid%3Dspdemo%26target%3Db'); }); Backbone.history.start(); }); QUnit.test('#3358 - pushState to hashChange transition with search params', function(assert) { assert.expect(1); Backbone.history.stop(); location.replace('http://example.com/root?foo=bar'); location.replace = function(url) { assert.strictEqual(url, '/root#?foo=bar'); }; Backbone.history = _.extend(new Backbone.History, { location: location, history: { pushState: undefined, replaceState: undefined } }); Backbone.history.start({root: '/root', pushState: true}); }); QUnit.test('Paths that don\'t match the root should not match no root', function(assert) { assert.expect(0); location.replace('http://example.com/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: { foo: function() { assert.ok(false, 'should not match unless root matches'); } } }); var myRouter = new MyRouter; Backbone.history.start({root: 'root', pushState: true}); }); QUnit.test('Paths that don\'t match the root should not match roots of the same length', function(assert) { assert.expect(0); location.replace('http://example.com/xxxx/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: { foo: function() { assert.ok(false, 'should not match unless root matches'); } } }); var myRouter = new MyRouter; Backbone.history.start({root: 'root', pushState: true}); }); QUnit.test('roots with regex characters', function(assert) { assert.expect(1); location.replace('http://example.com/x+y.z/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'x+y.z', pushState: true}); }); QUnit.test('roots with unicode characters', function(assert) { assert.expect(1); location.replace('http://example.com/®ooτ/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: '®ooτ', pushState: true}); }); QUnit.test('roots without slash', function(assert) { assert.expect(1); location.replace('http://example.com/®ooτ'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); var MyRouter = Backbone.Router.extend({ routes: {'': function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: '®ooτ', pushState: true}); }); QUnit.test('#4025 - navigate updates URL hash as is', function(assert) { assert.expect(1); var route = 'search/has%20space'; Backbone.history.navigate(route); assert.strictEqual(location.hash, '#' + route); }); QUnit.test('initial non-matching root triggers notfound event', function(assert) { assert.expect(1); location.replace('http://example.com/root#foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(false); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'other'}); }); QUnit.test('later non-matching root triggers notfound event', function(assert) { assert.expect(2); location.replace('http://example.com/root#foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'root'}); location.replace('http://example.com/other#foo'); Backbone.history.checkUrl(); }); QUnit.test('initial non-matching route triggers notfound event', function(assert) { assert.expect(1); location.replace('http://example.com/root#bar'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(false); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'root'}); }); QUnit.test('later non-matching route triggers notfound event', function(assert) { assert.expect(2); location.replace('http://example.com/root#foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'root'}); location.replace('http://example.com/other#bar'); Backbone.history.checkUrl(); }); QUnit.test('non-matching pushState route triggers notfound event', function(assert) { assert.expect(2); location.replace('http://example.com/root/foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'root', pushState: true}); location.replace('http://example.com/other/bar'); Backbone.history.checkUrl(); }); QUnit.test('non-matching navigate triggers notfound event', function(assert) { assert.expect(2); location.replace('http://example.com/root#foo'); Backbone.history.stop(); Backbone.history = _.extend(new Backbone.History, {location: location}); Backbone.history.on('notfound', function() { assert.ok(true); }); var MyRouter = Backbone.Router.extend({ routes: {foo: function() { assert.ok(true); }} }); var myRouter = new MyRouter; Backbone.history.start({root: 'root'}); Backbone.history.navigate('http://example.com/other#bar', {trigger: true}); }); })(QUnit); ================================================ FILE: test/setup/dom-setup.js ================================================ $('body').append( '
      ' + '
      ' ); ================================================ FILE: test/setup/environment.js ================================================ (function(QUnit) { var sync = Backbone.sync; var ajax = Backbone.ajax; var emulateHTTP = Backbone.emulateHTTP; var emulateJSON = Backbone.emulateJSON; var history = window.history; var pushState = history.pushState; var replaceState = history.replaceState; QUnit.config.noglobals = true; QUnit.testStart(function() { var env = QUnit.config.current.testEnvironment; // We never want to actually call these during tests. history.pushState = history.replaceState = function() {}; // Capture ajax settings for comparison. Backbone.ajax = function(settings) { env.ajaxSettings = settings; }; // Capture the arguments to Backbone.sync for comparison. Backbone.sync = function(method, model, options) { env.syncArgs = { method: method, model: model, options: options }; sync.apply(this, arguments); }; }); QUnit.testDone(function() { Backbone.sync = sync; Backbone.ajax = ajax; Backbone.emulateHTTP = emulateHTTP; Backbone.emulateJSON = emulateJSON; history.pushState = pushState; history.replaceState = replaceState; }); })(QUnit); ================================================ FILE: test/sync.js ================================================ (function(QUnit) { var Library = Backbone.Collection.extend({ url: function() { return '/library'; } }); var library; var attrs = { title: 'The Tempest', author: 'Bill Shakespeare', length: 123 }; QUnit.module('Backbone.sync', { beforeEach: function(assert) { library = new Library; library.create(attrs, {wait: false}); }, afterEach: function(assert) { Backbone.emulateHTTP = false; } }); QUnit.test('read', function(assert) { assert.expect(4); library.fetch(); assert.equal(this.ajaxSettings.url, '/library'); assert.equal(this.ajaxSettings.type, 'GET'); assert.equal(this.ajaxSettings.dataType, 'json'); assert.ok(_.isEmpty(this.ajaxSettings.data)); }); QUnit.test('passing data', function(assert) { assert.expect(3); library.fetch({data: {a: 'a', one: 1}}); assert.equal(this.ajaxSettings.url, '/library'); assert.equal(this.ajaxSettings.data.a, 'a'); assert.equal(this.ajaxSettings.data.one, 1); }); QUnit.test('create', function(assert) { assert.expect(6); assert.equal(this.ajaxSettings.url, '/library'); assert.equal(this.ajaxSettings.type, 'POST'); assert.equal(this.ajaxSettings.dataType, 'json'); var data = JSON.parse(this.ajaxSettings.data); assert.equal(data.title, 'The Tempest'); assert.equal(data.author, 'Bill Shakespeare'); assert.equal(data.length, 123); }); QUnit.test('update', function(assert) { assert.expect(7); library.first().save({id: '1-the-tempest', author: 'William Shakespeare'}); assert.equal(this.ajaxSettings.url, '/library/1-the-tempest'); assert.equal(this.ajaxSettings.type, 'PUT'); assert.equal(this.ajaxSettings.dataType, 'json'); var data = JSON.parse(this.ajaxSettings.data); assert.equal(data.id, '1-the-tempest'); assert.equal(data.title, 'The Tempest'); assert.equal(data.author, 'William Shakespeare'); assert.equal(data.length, 123); }); QUnit.test('update with emulateHTTP and emulateJSON', function(assert) { assert.expect(7); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}, { emulateHTTP: true, emulateJSON: true }); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'POST'); assert.equal(this.ajaxSettings.dataType, 'json'); assert.equal(this.ajaxSettings.data._method, 'PUT'); var data = JSON.parse(this.ajaxSettings.data.model); assert.equal(data.id, '2-the-tempest'); assert.equal(data.author, 'Tim Shakespeare'); assert.equal(data.length, 123); }); QUnit.test('update with just emulateHTTP', function(assert) { assert.expect(6); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}, { emulateHTTP: true }); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'POST'); assert.equal(this.ajaxSettings.contentType, 'application/json'); var data = JSON.parse(this.ajaxSettings.data); assert.equal(data.id, '2-the-tempest'); assert.equal(data.author, 'Tim Shakespeare'); assert.equal(data.length, 123); }); QUnit.test('update with just emulateJSON', function(assert) { assert.expect(6); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}, { emulateJSON: true }); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'PUT'); assert.equal(this.ajaxSettings.contentType, 'application/x-www-form-urlencoded'); var data = JSON.parse(this.ajaxSettings.data.model); assert.equal(data.id, '2-the-tempest'); assert.equal(data.author, 'Tim Shakespeare'); assert.equal(data.length, 123); }); QUnit.test('read model', function(assert) { assert.expect(3); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); library.first().fetch(); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'GET'); assert.ok(_.isEmpty(this.ajaxSettings.data)); }); QUnit.test('destroy', function(assert) { assert.expect(3); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); library.first().destroy({wait: true}); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'DELETE'); assert.equal(this.ajaxSettings.data, null); }); QUnit.test('destroy with emulateHTTP', function(assert) { assert.expect(3); library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); library.first().destroy({ emulateHTTP: true, emulateJSON: true }); assert.equal(this.ajaxSettings.url, '/library/2-the-tempest'); assert.equal(this.ajaxSettings.type, 'POST'); assert.equal(JSON.stringify(this.ajaxSettings.data), '{"_method":"DELETE"}'); }); QUnit.test('urlError', function(assert) { assert.expect(2); var model = new Backbone.Model(); assert.raises(function() { model.fetch(); }); model.fetch({url: '/one/two'}); assert.equal(this.ajaxSettings.url, '/one/two'); }); QUnit.test('#1052 - `options` is optional.', function(assert) { assert.expect(0); var model = new Backbone.Model(); model.url = '/test'; Backbone.sync('create', model); }); QUnit.test('Backbone.ajax', function(assert) { assert.expect(1); Backbone.ajax = function(settings) { assert.strictEqual(settings.url, '/test'); }; var model = new Backbone.Model(); model.url = '/test'; Backbone.sync('create', model); }); QUnit.test('Call provided error callback on error.', function(assert) { assert.expect(1); var model = new Backbone.Model; model.url = '/test'; Backbone.sync('read', model, { error: function() { assert.ok(true); } }); this.ajaxSettings.error(); }); QUnit.test('Use Backbone.emulateHTTP as default.', function(assert) { assert.expect(2); var model = new Backbone.Model; model.url = '/test'; Backbone.emulateHTTP = true; model.sync('create', model); assert.strictEqual(this.ajaxSettings.emulateHTTP, true); Backbone.emulateHTTP = false; model.sync('create', model); assert.strictEqual(this.ajaxSettings.emulateHTTP, false); }); QUnit.test('Use Backbone.emulateJSON as default.', function(assert) { assert.expect(2); var model = new Backbone.Model; model.url = '/test'; Backbone.emulateJSON = true; model.sync('create', model); assert.strictEqual(this.ajaxSettings.emulateJSON, true); Backbone.emulateJSON = false; model.sync('create', model); assert.strictEqual(this.ajaxSettings.emulateJSON, false); }); QUnit.test('#1756 - Call user provided beforeSend function.', function(assert) { assert.expect(4); Backbone.emulateHTTP = true; var model = new Backbone.Model; model.url = '/test'; var xhr = { setRequestHeader: function(header, value) { assert.strictEqual(header, 'X-HTTP-Method-Override'); assert.strictEqual(value, 'DELETE'); } }; model.sync('delete', model, { beforeSend: function(_xhr) { assert.ok(_xhr === xhr); return false; } }); assert.strictEqual(this.ajaxSettings.beforeSend(xhr), false); }); QUnit.test('#2928 - Pass along `textStatus` and `errorThrown`.', function(assert) { assert.expect(2); var model = new Backbone.Model; model.url = '/test'; model.on('error', function(m, xhr, options) { assert.strictEqual(options.textStatus, 'textStatus'); assert.strictEqual(options.errorThrown, 'errorThrown'); }); model.fetch(); this.ajaxSettings.error({}, 'textStatus', 'errorThrown'); }); })(QUnit); ================================================ FILE: test/vendor/jquery.js ================================================ /*! * jQuery JavaScript Library v1.8.3 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * * Copyright 2012 jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * * Date: Tue Nov 13 2012 08:20:33 GMT-0500 (Eastern Standard Time) */ (function( window, undefined ) { var // A central reference to the root jQuery(document) rootjQuery, // The deferred used on DOM ready readyList, // Use the correct document accordingly with window argument (sandbox) document = window.document, location = window.location, navigator = window.navigator, // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ in case of overwrite _$ = window.$, // Save a reference to some core methods core_push = Array.prototype.push, core_slice = Array.prototype.slice, core_indexOf = Array.prototype.indexOf, core_toString = Object.prototype.toString, core_hasOwn = Object.prototype.hasOwnProperty, core_trim = String.prototype.trim, // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); }, // Used for matching numbers core_pnum = /[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source, // Used for detecting and trimming whitespace core_rnotwhite = /\S/, core_rspace = /\s+/, // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, // A simple way to check for HTML strings // Prioritize #id over to avoid XSS via location.hash (#9521) rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, // JSON RegExp rvalidchars = /^[\],:{}\s]*$/, rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g, // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return ( letter + "" ).toUpperCase(); }, // The ready event handler and self cleanup method DOMContentLoaded = function() { if ( document.addEventListener ) { document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); jQuery.ready(); } else if ( document.readyState === "complete" ) { // we're here because readyState === "complete" in oldIE // which is good enough for us to call the dom ready! document.detachEvent( "onreadystatechange", DOMContentLoaded ); jQuery.ready(); } }, // [[Class]] -> type pairs class2type = {}; jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) { var match, elem, ret, doc; // Handle $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } // Handle $(DOMElement) if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; } // Handle HTML strings if ( typeof selector === "string" ) { if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); } // Match html or make sure no context is specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; doc = ( context && context.nodeType ? context.ownerDocument || context : document ); // scripts is true for back-compat selector = jQuery.parseHTML( match[1], doc, true ); if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { this.attr.call( selector, context, true ); } return jQuery.merge( this, selector ); // HANDLE: $(#id) } else { elem = document.getElementById( match[2] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id !== match[2] ) { return rootjQuery.find( selector ); } // Otherwise, we inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return rootjQuery.ready( selector ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }, // Start with an empty selector selector: "", // The current version of jQuery being used jquery: "1.8.3", // The default length of a jQuery object is 0 length: 0, // The number of elements contained in the matched element set size: function() { return this.length; }, toArray: function() { return core_slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num == null ? // Return a 'clean' array this.toArray() : // Return just the object ( num < 0 ? this[ this.length + num ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; if ( name === "find" ) { ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) each: function( callback, args ) { return jQuery.each( this, callback, args ); }, ready: function( fn ) { // Add the callback jQuery.ready.promise().done( fn ); return this; }, eq: function( i ) { i = +i; return i === -1 ? this.slice( i ) : this.slice( i, i + 1 ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, slice: function() { return this.pushStack( core_slice.apply( this, arguments ), "slice", core_slice.call(arguments).join(",") ); }, map: function( callback ) { return this.pushStack( jQuery.map(this, function( elem, i ) { return callback.call( elem, i, elem ); })); }, end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: core_push, sort: [].sort, splice: [].splice }; // Give the init function the jQuery prototype for later instantiation jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( length === i ) { target = this; --i; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend({ noConflict: function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }, // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, // Hold (or release) the ready event holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { // Abort if there are pending holds or we're already ready if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready, 1 ); } // Remember that the DOM is ready jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute readyList.resolveWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { jQuery( document ).trigger("ready").off("ready"); } }, // See test/unit/core.js for details concerning isFunction. // Since version 1.3, DOM methods and functions like alert // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { return jQuery.type(obj) === "function"; }, isArray: Array.isArray || function( obj ) { return jQuery.type(obj) === "array"; }, isWindow: function( obj ) { return obj != null && obj == obj.window; }, isNumeric: function( obj ) { return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { return obj == null ? String( obj ) : class2type[ core_toString.call(obj) ] || "object"; }, isPlainObject: function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } try { // Not own constructor property must be Object if ( obj.constructor && !core_hasOwn.call(obj, "constructor") && !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { return false; } } catch ( e ) { // IE8,9 Will throw exceptions on certain host objects #9897 return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for ( key in obj ) {} return key === undefined || core_hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { var name; for ( name in obj ) { return false; } return true; }, error: function( msg ) { throw new Error( msg ); }, // data: string of html // context (optional): If specified, the fragment will be created in this context, defaults to document // scripts (optional): If true, will include scripts passed in the html string parseHTML: function( data, context, scripts ) { var parsed; if ( !data || typeof data !== "string" ) { return null; } if ( typeof context === "boolean" ) { scripts = context; context = 0; } context = context || document; // Single tag if ( (parsed = rsingleTag.exec( data )) ) { return [ context.createElement( parsed[1] ) ]; } parsed = jQuery.buildFragment( [ data ], context, scripts ? null : [] ); return jQuery.merge( [], (parsed.cacheable ? jQuery.clone( parsed.fragment ) : parsed.fragment).childNodes ); }, parseJSON: function( data ) { if ( !data || typeof data !== "string") { return null; } // Make sure leading/trailing whitespace is removed (IE can't handle it) data = jQuery.trim( data ); // Attempt to parse using the native JSON parser first if ( window.JSON && window.JSON.parse ) { return window.JSON.parse( data ); } // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js if ( rvalidchars.test( data.replace( rvalidescape, "@" ) .replace( rvalidtokens, "]" ) .replace( rvalidbraces, "")) ) { return ( new Function( "return " + data ) )(); } jQuery.error( "Invalid JSON: " + data ); }, // Cross-browser xml parsing parseXML: function( data ) { var xml, tmp; if ( !data || typeof data !== "string" ) { return null; } try { if ( window.DOMParser ) { // Standard tmp = new DOMParser(); xml = tmp.parseFromString( data , "text/xml" ); } else { // IE xml = new ActiveXObject( "Microsoft.XMLDOM" ); xml.async = "false"; xml.loadXML( data ); } } catch( e ) { xml = undefined; } if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } return xml; }, noop: function() {}, // Evaluates a script in a global context // Workarounds based on findings by Jim Driscoll // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { if ( data && core_rnotwhite.test( data ) ) { // We use execScript on Internet Explorer // We use an anonymous function so that context is window // rather than jQuery in Firefox ( window.execScript || function( data ) { window[ "eval" ].call( window, data ); } )( data ); } }, // Convert dashed to camelCase; used by the css and data modules // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, // args is for internal usage only each: function( obj, callback, args ) { var name, i = 0, length = obj.length, isObj = length === undefined || jQuery.isFunction( obj ); if ( args ) { if ( isObj ) { for ( name in obj ) { if ( callback.apply( obj[ name ], args ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.apply( obj[ i++ ], args ) === false ) { break; } } } // A special, fast, case for the most common use of each } else { if ( isObj ) { for ( name in obj ) { if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) { break; } } } } return obj; }, // Use native String.trim function wherever possible trim: core_trim && !core_trim.call("\uFEFF\xA0") ? function( text ) { return text == null ? "" : core_trim.call( text ); } : // Otherwise use our own trimming functionality function( text ) { return text == null ? "" : ( text + "" ).replace( rtrim, "" ); }, // results is for internal usage only makeArray: function( arr, results ) { var type, ret = results || []; if ( arr != null ) { // The window, strings (and functions) also have 'length' // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 type = jQuery.type( arr ); if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) { core_push.call( ret, arr ); } else { jQuery.merge( ret, arr ); } } return ret; }, inArray: function( elem, arr, i ) { var len; if ( arr ) { if ( core_indexOf ) { return core_indexOf.call( arr, elem, i ); } len = arr.length; i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; for ( ; i < len; i++ ) { // Skip accessing in sparse arrays if ( i in arr && arr[ i ] === elem ) { return i; } } } return -1; }, merge: function( first, second ) { var l = second.length, i = first.length, j = 0; if ( typeof l === "number" ) { for ( ; j < l; j++ ) { first[ i++ ] = second[ j ]; } } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; } } first.length = i; return first; }, grep: function( elems, callback, inv ) { var retVal, ret = [], i = 0, length = elems.length; inv = !!inv; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { retVal = !!callback( elems[ i ], i ); if ( inv !== retVal ) { ret.push( elems[ i ] ); } } return ret; }, // arg is for internal usage only map: function( elems, callback, arg ) { var value, key, ret = [], i = 0, length = elems.length, // jquery objects are treated as arrays isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; // Go through the array, translating each of the items to their if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret[ ret.length ] = value; } } // Go through every key on the object, } else { for ( key in elems ) { value = callback( elems[ key ], key, arg ); if ( value != null ) { ret[ ret.length ] = value; } } } // Flatten any nested arrays return ret.concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { var tmp, args, proxy; if ( typeof context === "string" ) { tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind args = core_slice.call( arguments, 2 ); proxy = function() { return fn.apply( context, args.concat( core_slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || jQuery.guid++; return proxy; }, // Multifunctional method to get and set values of a collection // The value/s can optionally be executed if it's a function access: function( elems, fn, key, value, chainable, emptyGet, pass ) { var exec, bulk = key == null, i = 0, length = elems.length; // Sets many values if ( key && typeof key === "object" ) { for ( i in key ) { jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); } chainable = 1; // Sets one value } else if ( value !== undefined ) { // Optionally, function values get executed if exec is true exec = pass === undefined && jQuery.isFunction( value ); if ( bulk ) { // Bulk operations only iterate when executing function values if ( exec ) { exec = fn; fn = function( elem, key, value ) { return exec.call( jQuery( elem ), value ); }; // Otherwise they run against the entire set } else { fn.call( elems, value ); fn = null; } } if ( fn ) { for (; i < length; i++ ) { fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); } } chainable = 1; } return chainable ? elems : // Gets bulk ? fn.call( elems ) : length ? fn( elems[0], key ) : emptyGet; }, now: function() { return ( new Date() ).getTime(); } }); jQuery.ready.promise = function( obj ) { if ( !readyList ) { readyList = jQuery.Deferred(); // Catch cases where $(document).ready() is called after the browser event has already occurred. // we once tried to use readyState "interactive" here, but it caused issues like the one // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready setTimeout( jQuery.ready, 1 ); // Standards-based browsers support DOMContentLoaded } else if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else { // Ensure firing before onload, maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var top = false; try { top = window.frameElement == null && document.documentElement; } catch(e) {} if ( top && top.doScroll ) { (function doScrollCheck() { if ( !jQuery.isReady ) { try { // Use the trick by Diego Perini // http://javascript.nwbox.com/IEContentLoaded/ top.doScroll("left"); } catch(e) { return setTimeout( doScrollCheck, 50 ); } // and execute any waiting functions jQuery.ready(); } })(); } } } return readyList.promise( obj ); }; // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); // All jQuery objects should point back to these rootjQuery = jQuery(document); // String to Object options format cache var optionsCache = {}; // Convert String-formatted options into Object-formatted ones and store in cache function createOptions( options ) { var object = optionsCache[ options ] = {}; jQuery.each( options.split( core_rspace ), function( _, flag ) { object[ flag ] = true; }); return object; } /* * Create a callback list using the following parameters: * * options: an optional list of space-separated options that will change how * the callback list behaves or a more traditional option object * * By default a callback list will act like an event callback list and can be * "fired" multiple times. * * Possible options: * * once: will ensure the callback list can only be fired once (like a Deferred) * * memory: will keep track of previous values and will call any callback added * after the list has been fired right away with the latest "memorized" * values (like a Deferred) * * unique: will ensure a callback can only be added once (no duplicate in the list) * * stopOnFalse: interrupt callings when a callback returns false * */ jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? ( optionsCache[ options ] || createOptions( options ) ) : jQuery.extend( {}, options ); var // Last fire value (for non-forgettable lists) memory, // Flag to know if list was already fired fired, // Flag to know if list is currently firing firing, // First callback to fire (used internally by add and fireWith) firingStart, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, // Actual callback list list = [], // Stack of fire calls for repeatable lists stack = !options.once && [], // Fire callbacks fire = function( data ) { memory = options.memory && data; fired = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; firing = true; for ( ; list && firingIndex < firingLength; firingIndex++ ) { if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { memory = false; // To prevent further calls using add break; } } firing = false; if ( list ) { if ( stack ) { if ( stack.length ) { fire( stack.shift() ); } } else if ( memory ) { list = []; } else { self.disable(); } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { // First, we save the current length var start = list.length; (function add( args ) { jQuery.each( args, function( _, arg ) { var type = jQuery.type( arg ); if ( type === "function" ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } } else if ( arg && arg.length && type !== "string" ) { // Inspect recursively add( arg ); } }); })( arguments ); // Do we need to add the callbacks to the // current firing batch? if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away } else if ( memory ) { firingStart = start; fire( memory ); } } return this; }, // Remove a callback from the list remove: function() { if ( list ) { jQuery.each( arguments, function( _, arg ) { var index; while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( firing ) { if ( index <= firingLength ) { firingLength--; } if ( index <= firingIndex ) { firingIndex--; } } } }); } return this; }, // Control if a given callback is in the list has: function( fn ) { return jQuery.inArray( fn, list ) > -1; }, // Remove all callbacks from the list empty: function() { list = []; return this; }, // Have the list do nothing anymore disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; }, // Lock the list in its current state lock: function() { stack = undefined; if ( !memory ) { self.disable(); } return this; }, // Is it locked? locked: function() { return !stack; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; if ( list && ( !fired || stack ) ) { if ( firing ) { stack.push( args ); } else { fire( args ); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!fired; } }; return self; }; jQuery.extend({ Deferred: function( func ) { var tuples = [ // action, add listener, listener list, final state [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], [ "notify", "progress", jQuery.Callbacks("memory") ] ], state = "pending", promise = { state: function() { return state; }, always: function() { deferred.done( arguments ).fail( arguments ); return this; }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var action = tuple[ 0 ], fn = fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[1] ]( jQuery.isFunction( fn ) ? function() { var returned = fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() .done( newDefer.resolve ) .fail( newDefer.reject ) .progress( newDefer.notify ); } else { newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); } } : newDefer[ action ] ); }); fns = null; }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { return obj != null ? jQuery.extend( obj, promise ) : promise; } }, deferred = {}; // Keep pipe for back-compat promise.pipe = promise.then; // Add list-specific methods jQuery.each( tuples, function( i, tuple ) { var list = tuple[ 2 ], stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add promise[ tuple[1] ] = list.add; // Handle state if ( stateString ) { list.add(function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); } // deferred[ resolve | reject | notify ] = list.fire deferred[ tuple[0] ] = list.fire; deferred[ tuple[0] + "With" ] = list.fireWith; }); // Make the deferred a promise promise.promise( deferred ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, resolveValues = core_slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, // the master Deferred. If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; if( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); } }; }, progressValues, progressContexts, resolveContexts; // add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); resolveContexts = new Array( length ); for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() .done( updateFunc( i, resolveContexts, resolveValues ) ) .fail( deferred.reject ) .progress( updateFunc( i, progressContexts, progressValues ) ); } else { --remaining; } } } // if we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } }); jQuery.support = (function() { var support, all, a, select, opt, input, fragment, eventName, i, isSupported, clickFn, div = document.createElement("div"); // Setup div.setAttribute( "className", "t" ); div.innerHTML = "
      a"; // Support tests won't run in some limited or non-browser environments all = div.getElementsByTagName("*"); a = div.getElementsByTagName("a")[ 0 ]; if ( !all || !a || !all.length ) { return {}; } // First batch of tests select = document.createElement("select"); opt = select.appendChild( document.createElement("option") ); input = div.getElementsByTagName("input")[ 0 ]; a.style.cssText = "top:1px;float:left;opacity:.5"; support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables tbody: !div.getElementsByTagName("tbody").length, // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute // (IE uses .cssText instead) style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 opacity: /^0.5/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) cssFloat: !!a.style.cssFloat, // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) checkOn: ( input.value === "on" ), // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) getSetAttribute: div.className !== "t", // Tests for enctype support on a form (#6743) enctype: !!document.createElement("form").enctype, // Makes sure cloning an html5 element does not cause problems // Where outerHTML is undefined, this still works html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode boxModel: ( document.compatMode === "CSS1Compat" ), // Will be defined later submitBubbles: true, changeBubbles: true, focusinBubbles: false, deleteExpando: true, noCloneEvent: true, inlineBlockNeedsLayout: false, shrinkWrapBlocks: false, reliableMarginRight: true, boxSizingReliable: true, pixelPosition: false }; // Make sure checked status is properly cloned input.checked = true; support.noCloneChecked = input.cloneNode( true ).checked; // Make sure that the options inside disabled selects aren't marked as disabled // (WebKit marks them as disabled) select.disabled = true; support.optDisabled = !opt.disabled; // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { delete div.test; } catch( e ) { support.deleteExpando = false; } if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { div.attachEvent( "onclick", clickFn = function() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) support.noCloneEvent = false; }); div.cloneNode( true ).fireEvent("onclick"); div.detachEvent( "onclick", clickFn ); } // Check if a radio maintains its value // after being appended to the DOM input = document.createElement("input"); input.value = "t"; input.setAttribute( "type", "radio" ); support.radioValue = input.value === "t"; input.setAttribute( "checked", "checked" ); // #11217 - WebKit loses check when the name is after the checked attribute input.setAttribute( "name", "t" ); div.appendChild( input ); fragment = document.createDocumentFragment(); fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; // Check if a disconnected checkbox will retain its checked // value of true after appended to the DOM (IE6/7) support.appendChecked = input.checked; fragment.removeChild( input ); fragment.appendChild( div ); // Technique from Juriy Zaytsev // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ // We only care about the case where non-standard event systems // are used, namely in IE. Short-circuiting here helps us to // avoid an eval call (in setAttribute) which can cause CSP // to go haywire. See: https://developer.mozilla.org/en/Security/CSP if ( div.attachEvent ) { for ( i in { submit: true, change: true, focusin: true }) { eventName = "on" + i; isSupported = ( eventName in div ); if ( !isSupported ) { div.setAttribute( eventName, "return;" ); isSupported = ( typeof div[ eventName ] === "function" ); } support[ i + "Bubbles" ] = isSupported; } } // Run tests that need a body at doc ready jQuery(function() { var container, div, tds, marginDiv, divReset = "padding:0;margin:0;border:0;display:block;overflow:hidden;", body = document.getElementsByTagName("body")[0]; if ( !body ) { // Return for frameset docs that don't have a body return; } container = document.createElement("div"); container.style.cssText = "visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px"; body.insertBefore( container, body.firstChild ); // Construct the test element div = document.createElement("div"); container.appendChild( div ); // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a // table row; if so, offsetWidth/Height are not reliable for use when // determining if an element has been hidden directly using // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). // (only IE 8 fails this test) div.innerHTML = "
      t
      "; tds = div.getElementsByTagName("td"); tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; isSupported = ( tds[ 0 ].offsetHeight === 0 ); tds[ 0 ].style.display = ""; tds[ 1 ].style.display = "none"; // Check if empty table cells still have offsetWidth/Height // (IE <= 8 fail this test) support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); // Check box-sizing and margin behavior div.innerHTML = ""; div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; support.boxSizing = ( div.offsetWidth === 4 ); support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); // NOTE: To any future maintainer, we've window.getComputedStyle // because jsdom on node.js will break without it. if ( window.getComputedStyle ) { support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; // Check if div with explicit width and no margin-right incorrectly // gets computed margin-right based on width of container. For more // info see bug #3333 // Fails in WebKit before Feb 2011 nightlies // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right marginDiv = document.createElement("div"); marginDiv.style.cssText = div.style.cssText = divReset; marginDiv.style.marginRight = marginDiv.style.width = "0"; div.style.width = "1px"; div.appendChild( marginDiv ); support.reliableMarginRight = !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); } if ( typeof div.style.zoom !== "undefined" ) { // Check if natively block-level elements act like inline-block // elements when setting their display to 'inline' and giving // them layout // (IE < 8 does this) div.innerHTML = ""; div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); // Check if elements with layout shrink-wrap their children // (IE 6 does this) div.style.display = "block"; div.style.overflow = "visible"; div.innerHTML = "
      "; div.firstChild.style.width = "5px"; support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); container.style.zoom = 1; } // Null elements to avoid leaks in IE body.removeChild( container ); container = div = tds = marginDiv = null; }); // Null elements to avoid leaks in IE fragment.removeChild( div ); all = a = select = opt = input = fragment = div = null; return support; })(); var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, rmultiDash = /([A-Z])/g; jQuery.extend({ cache: {}, deletedIds: [], // Remove at next major release (1.9/2.0) uuid: 0, // Unique for each copy of jQuery on the page // Non-digits removed to match rinlinejQuery expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { "embed": true, // Ban all objects except for Flash (which handle expandos) "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", "applet": true }, hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !isEmptyDataObject( elem ); }, data: function( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var thisCache, ret, internalKey = jQuery.expando, getByName = typeof name === "string", // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType, // Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically cache = isNode ? jQuery.cache : elem, // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { return; } if ( !id ) { // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { elem[ internalKey ] = id = jQuery.deletedIds.pop() || jQuery.guid++; } else { id = internalKey; } } if ( !cache[ id ] ) { cache[ id ] = {}; // Avoids exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify if ( !isNode ) { cache[ id ].toJSON = jQuery.noop; } } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name ); } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } thisCache = cache[ id ]; // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; } thisCache = thisCache.data; } if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; } // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( getByName ) { // First Try to find as-is property data ret = thisCache[ name ]; // Test for null|undefined property data if ( ret == null ) { // Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; } return ret; }, removeData: function( elem, name, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var thisCache, i, l, isNode = elem.nodeType, // See jQuery.data for more information cache = isNode ? jQuery.cache : elem, id = isNode ? elem[ jQuery.expando ] : jQuery.expando; // If there is already no cache entry for this object, there is no // purpose in continuing if ( !cache[ id ] ) { return; } if ( name ) { thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { // Support array or space separated string names for data keys if ( !jQuery.isArray( name ) ) { // try the string as a key before any manipulation if ( name in thisCache ) { name = [ name ]; } else { // split the camel cased version by spaces unless a key with the spaces exists name = jQuery.camelCase( name ); if ( name in thisCache ) { name = [ name ]; } else { name = name.split(" "); } } } for ( i = 0, l = name.length; i < l; i++ ) { delete thisCache[ name[i] ]; } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { return; } } } // See jQuery.data for more information if ( !pvt ) { delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it if ( !isEmptyDataObject( cache[ id ] ) ) { return; } } // Destroy the cache if ( isNode ) { jQuery.cleanData( [ elem ], true ); // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) } else if ( jQuery.support.deleteExpando || cache != cache.window ) { delete cache[ id ]; // When all else fails, null } else { cache[ id ] = null; } }, // For internal use only. _data: function( elem, name, data ) { return jQuery.data( elem, name, data, true ); }, // A method for determining if a DOM node can handle the data expando acceptData: function( elem ) { var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; // nodes accept data unless otherwise specified; rejection can be conditional return !noData || noData !== true && elem.getAttribute("classid") === noData; } }); jQuery.fn.extend({ data: function( key, value ) { var parts, part, attr, name, l, elem = this[0], i = 0, data = null; // Gets all values if ( key === undefined ) { if ( this.length ) { data = jQuery.data( elem ); if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { attr = elem.attributes; for ( l = attr.length; i < l; i++ ) { name = attr[i].name; if ( !name.indexOf( "data-" ) ) { name = jQuery.camelCase( name.substring(5) ); dataAttr( elem, name, data[ name ] ); } } jQuery._data( elem, "parsedAttrs", true ); } } return data; } // Sets multiple values if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } parts = key.split( ".", 2 ); parts[1] = parts[1] ? "." + parts[1] : ""; part = parts[1] + "!"; return jQuery.access( this, function( value ) { if ( value === undefined ) { data = this.triggerHandler( "getData" + part, [ parts[0] ] ); // Try to fetch any internally stored data first if ( data === undefined && elem ) { data = jQuery.data( elem, key ); data = dataAttr( elem, key, data ); } return data === undefined && parts[1] ? this.data( parts[0] ) : data; } parts[1] = value; this.each(function() { var self = jQuery( this ); self.triggerHandler( "setData" + part, parts ); jQuery.data( this, key, value ); self.triggerHandler( "changeData" + part, parts ); }); }, null, value, arguments.length > 1, null, false ); }, removeData: function( key ) { return this.each(function() { jQuery.removeData( this, key ); }); } }); function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : // Only convert to a number if it doesn't change the string +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} // Make sure we set the data so it isn't changed later jQuery.data( elem, key, data ); } else { data = undefined; } } return data; } // checks a cache object for emptiness function isEmptyDataObject( obj ) { var name; for ( name in obj ) { // if the public data object is empty, the private is still empty if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { continue; } if ( name !== "toJSON" ) { return false; } } return true; } jQuery.extend({ queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; queue = jQuery._data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !queue || jQuery.isArray(data) ) { queue = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { queue.push( data ); } } return queue || []; } }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), startLength = queue.length, fn = queue.shift(), hooks = jQuery._queueHooks( elem, type ), next = function() { jQuery.dequeue( elem, type ); }; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); startLength--; } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift( "inprogress" ); } // clear up the last queue stop function delete hooks.stop; fn.call( elem, next, hooks ); } if ( !startLength && hooks ) { hooks.empty.fire(); } }, // not intended for public consumption - generates a queueHooks object, or returns the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; return jQuery._data( elem, key ) || jQuery._data( elem, key, { empty: jQuery.Callbacks("once memory").add(function() { jQuery.removeData( elem, type + "queue", true ); jQuery.removeData( elem, key, true ); }) }); } }); jQuery.fn.extend({ queue: function( type, data ) { var setter = 2; if ( typeof type !== "string" ) { data = type; type = "fx"; setter--; } if ( arguments.length < setter ) { return jQuery.queue( this[0], type ); } return data === undefined ? this : this.each(function() { var queue = jQuery.queue( this, type, data ); // ensure a hooks for this queue jQuery._queueHooks( this, type ); if ( type === "fx" && queue[0] !== "inprogress" ) { jQuery.dequeue( this, type ); } }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; return this.queue( type, function( next, hooks ) { var timeout = setTimeout( next, time ); hooks.stop = function() { clearTimeout( timeout ); }; }); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, obj ) { var tmp, count = 1, defer = jQuery.Deferred(), elements = this, i = this.length, resolve = function() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } }; if ( typeof type !== "string" ) { obj = type; type = undefined; } type = type || "fx"; while( i-- ) { tmp = jQuery._data( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); } } resolve(); return defer.promise( obj ); } }); var nodeHook, boolHook, fixSpecified, rclass = /[\t\r\n]/g, rreturn = /\r/g, rtype = /^(?:button|input)$/i, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea|)$/i, rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, getSetAttribute = jQuery.support.getSetAttribute; jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); }, removeAttr: function( name ) { return this.each(function() { jQuery.removeAttr( this, name ); }); }, prop: function( name, value ) { return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); }, removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { // try/catch handles cases where IE balks (such as removing a property on window) try { this[ name ] = undefined; delete this[ name ]; } catch( e ) {} }); }, addClass: function( value ) { var classNames, i, l, elem, setClass, c, cl; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).addClass( value.call(this, j, this.className) ); }); } if ( value && typeof value === "string" ) { classNames = value.split( core_rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 ) { if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { setClass = " " + elem.className + " "; for ( c = 0, cl = classNames.length; c < cl; c++ ) { if ( setClass.indexOf( " " + classNames[ c ] + " " ) < 0 ) { setClass += classNames[ c ] + " "; } } elem.className = jQuery.trim( setClass ); } } } } return this; }, removeClass: function( value ) { var removes, className, elem, c, cl, i, l; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).removeClass( value.call(this, j, this.className) ); }); } if ( (value && typeof value === "string") || value === undefined ) { removes = ( value || "" ).split( core_rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 && elem.className ) { className = (" " + elem.className + " ").replace( rclass, " " ); // loop over each item in the removal list for ( c = 0, cl = removes.length; c < cl; c++ ) { // Remove until there is nothing to remove, while ( className.indexOf(" " + removes[ c ] + " ") >= 0 ) { className = className.replace( " " + removes[ c ] + " " , " " ); } } elem.className = value ? jQuery.trim( className ) : ""; } } } return this; }, toggleClass: function( value, stateVal ) { var type = typeof value, isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function( i ) { jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery( this ), state = stateVal, classNames = value.split( core_rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space separated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set jQuery._data( this, "__className__", this.className ); } // toggle whole className this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { var className = " " + selector + " ", i = 0, l = this.length; for ( ; i < l; i++ ) { if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { return true; } } return false; }, val: function( value ) { var hooks, ret, isFunction, elem = this[0]; if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } ret = elem.value; return typeof ret === "string" ? // handle most common string cases ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; } return; } isFunction = jQuery.isFunction( value ); return this.each(function( i ) { var val, self = jQuery(this); if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call( this, i, self.val() ); } else { val = value; } // Treat null/undefined as ""; convert numbers to string if ( val == null ) { val = ""; } else if ( typeof val === "number" ) { val += ""; } else if ( jQuery.isArray( val ) ) { val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; // If set returns undefined, fall back to normal setting if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); } }); jQuery.extend({ valHooks: { option: { get: function( elem ) { // attributes.value is undefined in Blackberry 4.7 but // uses .value. See #6932 var val = elem.attributes.value; return !val || val.specified ? elem.value : elem.text; } }, select: { get: function( elem ) { var value, option, options = elem.options, index = elem.selectedIndex, one = elem.type === "select-one" || index < 0, values = one ? null : [], max = one ? index + 1 : options.length, i = index < 0 ? max : one ? index : 0; // Loop through all the selected options for ( ; i < max; i++ ) { option = options[ i ]; // oldIE doesn't update selected after form reset (#2551) if ( ( option.selected || i === index ) && // Don't return options that are disabled or in a disabled optgroup ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { // Get the specific value for the option value = jQuery( option ).val(); // We don't need an array for one selects if ( one ) { return value; } // Multi-Selects return an array values.push( value ); } } return values; }, set: function( elem, value ) { var values = jQuery.makeArray( value ); jQuery(elem).find("option").each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { elem.selectedIndex = -1; } return values; } } }, // Unused in 1.8, left in so attrFn-stabbers won't die; remove in 1.9 attrFn: {}, attr: function( elem, name, value, pass ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set attributes on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } if ( pass && jQuery.isFunction( jQuery.fn[ name ] ) ) { return jQuery( elem )[ name ]( value ); } // Fallback to prop when attributes are not supported if ( typeof elem.getAttribute === "undefined" ) { return jQuery.prop( elem, name, value ); } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); // All attributes are lowercase // Grab necessary hook if one is defined if ( notxml ) { name = name.toLowerCase(); hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); return; } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { elem.setAttribute( name, value + "" ); return value; } } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { ret = elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return ret === null ? undefined : ret; } }, removeAttr: function( elem, value ) { var propName, attrNames, name, isBool, i = 0; if ( value && elem.nodeType === 1 ) { attrNames = value.split( core_rspace ); for ( ; i < attrNames.length; i++ ) { name = attrNames[ i ]; if ( name ) { propName = jQuery.propFix[ name ] || name; isBool = rboolean.test( name ); // See #9699 for explanation of this approach (setting first, then removal) // Do not do this for boolean attributes (see #10870) if ( !isBool ) { jQuery.attr( elem, name, "" ); } elem.removeAttribute( getSetAttribute ? name : propName ); // Set corresponding property to false for boolean attributes if ( isBool && propName in elem ) { elem[ propName ] = false; } } } } }, attrHooks: { type: { set: function( elem, value ) { // We can't allow the type property to be changed (since it causes problems in IE) if ( rtype.test( elem.nodeName ) && elem.parentNode ) { jQuery.error( "type property can't be changed" ); } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { // Setting the type on a radio button after the value resets the value in IE6-9 // Reset value to it's default in case type is set after value // This is for element creation var val = elem.value; elem.setAttribute( "type", value ); if ( val ) { elem.value = val; } return value; } } }, // Use the value property for back compat // Use the nodeHook for button elements in IE6/7 (#1954) value: { get: function( elem, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.get( elem, name ); } return name in elem ? elem.value : null; }, set: function( elem, value, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.set( elem, value, name ); } // Does not return so that setAttribute is also used elem.value = value; } } }, propFix: { tabindex: "tabIndex", readonly: "readOnly", "for": "htmlFor", "class": "className", maxlength: "maxLength", cellspacing: "cellSpacing", cellpadding: "cellPadding", rowspan: "rowSpan", colspan: "colSpan", usemap: "useMap", frameborder: "frameBorder", contenteditable: "contentEditable" }, prop: function( elem, name, value ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set properties on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); if ( notxml ) { // Fix name and attach hooks name = jQuery.propFix[ name ] || name; hooks = jQuery.propHooks[ name ]; } if ( value !== undefined ) { if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { return ( elem[ name ] = value ); } } else { if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { return elem[ name ]; } } }, propHooks: { tabIndex: { get: function( elem ) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ var attributeNode = elem.getAttributeNode("tabindex"); return attributeNode && attributeNode.specified ? parseInt( attributeNode.value, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } } } }); // Hook for boolean attributes boolHook = { get: function( elem, name ) { // Align boolean attributes with corresponding properties // Fall back to attribute presence where some booleans are not supported var attrNode, property = jQuery.prop( elem, name ); return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? name.toLowerCase() : undefined; }, set: function( elem, value, name ) { var propName; if ( value === false ) { // Remove boolean attributes when set to false jQuery.removeAttr( elem, name ); } else { // value is true since we know at this point it's type boolean and not false // Set boolean attributes to the same name and set the DOM property propName = jQuery.propFix[ name ] || name; if ( propName in elem ) { // Only set the IDL specifically if it already exists on the element elem[ propName ] = true; } elem.setAttribute( name, name.toLowerCase() ); } return name; } }; // IE6/7 do not support getting/setting some attributes with get/setAttribute if ( !getSetAttribute ) { fixSpecified = { name: true, id: true, coords: true }; // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue nodeHook = jQuery.valHooks.button = { get: function( elem, name ) { var ret; ret = elem.getAttributeNode( name ); return ret && ( fixSpecified[ name ] ? ret.value !== "" : ret.specified ) ? ret.value : undefined; }, set: function( elem, value, name ) { // Set the existing or create a new attribute node var ret = elem.getAttributeNode( name ); if ( !ret ) { ret = document.createAttribute( name ); elem.setAttributeNode( ret ); } return ( ret.value = value + "" ); } }; // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { set: function( elem, value ) { if ( value === "" ) { elem.setAttribute( name, "auto" ); return value; } } }); }); // Set contenteditable to false on removals(#10429) // Setting to empty string throws an error as an invalid value jQuery.attrHooks.contenteditable = { get: nodeHook.get, set: function( elem, value, name ) { if ( value === "" ) { value = "false"; } nodeHook.set( elem, value, name ); } }; } // Some attributes require a special call on IE if ( !jQuery.support.hrefNormalized ) { jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { get: function( elem ) { var ret = elem.getAttribute( name, 2 ); return ret === null ? undefined : ret; } }); }); } if ( !jQuery.support.style ) { jQuery.attrHooks.style = { get: function( elem ) { // Return undefined in the case of empty string // Normalize to lowercase since IE uppercases css property names return elem.style.cssText.toLowerCase() || undefined; }, set: function( elem, value ) { return ( elem.style.cssText = value + "" ); } }; } // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( !jQuery.support.optSelected ) { jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { get: function( elem ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // Make sure that it also works with optgroups, see #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } return null; } }); } // IE6/7 call enctype encoding if ( !jQuery.support.enctype ) { jQuery.propFix.enctype = "encoding"; } // Radios and checkboxes getter/setter if ( !jQuery.support.checkOn ) { jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = { get: function( elem ) { // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified return elem.getAttribute("value") === null ? "on" : elem.value; } }; }); } jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }); }); var rformElems = /^(?:textarea|input|select)$/i, rtypenamespace = /^([^\.]*|)(?:\.(.+)|)$/, rhoverHack = /(?:^|\s)hover(\.\S+|)\b/, rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|contextmenu)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, hoverHack = function( events ) { return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); }; /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { add: function( elem, types, handler, data, selector ) { var elemData, eventHandle, events, t, tns, type, namespaces, handleObj, handleObjIn, handlers, special; // Don't attach events to noData or text/comment nodes (allow plain objects tho) if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first events = elemData.events; if ( !events ) { elemData.events = events = {}; } eventHandle = elemData.handle; if ( !eventHandle ) { elemData.handle = eventHandle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events eventHandle.elem = elem; } // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); types = jQuery.trim( hoverHack(types) ).split( " " ); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = tns[1]; namespaces = ( tns[2] || "" ).split( "." ).sort(); // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend({ type: type, origType: tns[1], data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, global: {}, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var t, tns, type, origType, namespaces, origCount, j, events, special, eventType, handleObj, elemData = jQuery.hasData( elem ) && jQuery._data( elem ); if ( !elemData || !(events = elemData.events) ) { return; } // Once for each type.namespace in types; type may be omitted types = jQuery.trim( hoverHack( types || "" ) ).split(" "); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = origType = tns[1]; namespaces = tns[2]; // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector? special.delegateType : special.bindType ) || type; eventType = events[ type ] || []; origCount = eventType.length; namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null; // Remove matching events for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !namespaces || namespaces.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { eventType.splice( j--, 1 ); if ( handleObj.selector ) { eventType.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { delete elemData.handle; // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery.removeData( elem, "events", true ); } }, // Events that are safe to short-circuit if no handlers are attached. // Native DOM events should not be added, they may have inline handlers. customEvent: { "getData": true, "setData": true, "changeData": true }, trigger: function( event, data, elem, onlyHandlers ) { // Don't do events on text and comment nodes if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { return; } // Event object or event type var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType, type = event.type || event, namespaces = []; // focus/blur morphs to focusin/out; ensure we're not firing them right now if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } if ( type.indexOf( "!" ) >= 0 ) { // Exclusive events trigger only for the exact event (no namespaces) type = type.slice(0, -1); exclusive = true; } if ( type.indexOf( "." ) >= 0 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { // No jQuery handlers for this event type, and it can't have inline handlers return; } // Caller can pass in an Event, Object, or just an event type string event = typeof event === "object" ? // jQuery.Event object event[ jQuery.expando ] ? event : // Object literal new jQuery.Event( type, event ) : // Just the event type (string) new jQuery.Event( type ); event.type = type; event.isTrigger = true; event.exclusive = exclusive; event.namespace = namespaces.join( "." ); event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null; ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; // Handle a global trigger if ( !elem ) { // TODO: Stop taunting the data cache; remove global events and always attach to document cache = jQuery.cache; for ( i in cache ) { if ( cache[ i ].events && cache[ i ].events[ type ] ) { jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); } } return; } // Clean up the event in case it is being reused event.result = undefined; if ( !event.target ) { event.target = elem; } // Clone any incoming data and prepend the event, creating the handler arg list data = data != null ? jQuery.makeArray( data ) : []; data.unshift( event ); // Allow special events to draw outside the lines special = jQuery.event.special[ type ] || {}; if ( special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) eventPath = [[ elem, special.bindType || type ]]; if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; for ( old = elem; cur; cur = cur.parentNode ) { eventPath.push([ cur, bubbleType ]); old = cur; } // Only add window if we got to document (e.g., not plain obj or detached DOM) if ( old === (elem.ownerDocument || document) ) { eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); } } // Fire handlers on the event path for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { cur = eventPath[i][0]; event.type = eventPath[i][1]; handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Note that this is a bare JS function and not a jQuery handler handle = ontype && cur[ ontype ]; if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { event.preventDefault(); } } event.type = type; // If nobody prevented the default action, do it now if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) // IE<9 dies on focus/blur to hidden element (#1486) if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method old = elem[ ontype ]; if ( old ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; elem[ type ](); jQuery.event.triggered = undefined; if ( old ) { elem[ ontype ] = old; } } } } return event.result; }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related, handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), delegateCount = handlers.delegateCount, args = core_slice.call( arguments ), run_all = !event.exclusive && !event.namespace, special = jQuery.event.special[ event.type ] || {}, handlerQueue = []; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; event.delegateTarget = this; // Call the preDispatch hook for the mapped type, and let it bail if desired if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } // Determine handlers that should run if there are delegated events // Avoid non-left-click bubbling in Firefox (#3861) if ( delegateCount && !(event.button && event.type === "click") ) { for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { // Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764) if ( cur.disabled !== true || event.type !== "click" ) { selMatch = {}; matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; sel = handleObj.selector; if ( selMatch[ sel ] === undefined ) { selMatch[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) >= 0 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( selMatch[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, matches: matches }); } } } } // Add the remaining (directly-bound) handlers if ( handlers.length > delegateCount ) { handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); } // Run delegates first; they may want to stop propagation beneath us for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { matched = handlerQueue[ i ]; event.currentTarget = matched.elem; for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { handleObj = matched.matches[ j ]; // Triggered event must either 1) be non-exclusive and have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; event.handleObj = handleObj; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { event.result = ret; if ( ret === false ) { event.preventDefault(); event.stopPropagation(); } } } } } // Call the postDispatch hook for the mapped type if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; }, // Includes some event props shared by KeyEvent and MouseEvent // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { var eventDoc, doc, body, button = original.button, fromElement = original.fromElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add relatedTarget, if necessary if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, originalEvent = event, fixHook = jQuery.event.fixHooks[ event.type ] || {}, copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = jQuery.Event( originalEvent ); for ( i = copy.length; i; ) { prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { event.target = originalEvent.srcElement || document; } // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // For mouse/key events, metaKey==false if it's undefined (#3368, #11328; IE6/7/8) event.metaKey = !!event.metaKey; return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, special: { load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { delegateType: "focusin" }, blur: { delegateType: "focusout" }, beforeunload: { setup: function( data, namespaces, eventHandle ) { // We only want to do this special case on windows if ( jQuery.isWindow( this ) ) { this.onbeforeunload = eventHandle; } }, teardown: function( namespaces, eventHandle ) { if ( this.onbeforeunload === eventHandle ) { this.onbeforeunload = null; } } } }, simulate: function( type, elem, event, bubble ) { // Piggyback on a donor event to simulate a different one. // Fake originalEvent to avoid donor's stopPropagation, but if the // simulated event prevents default then we do the same on the donor. var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; // Some plugins are using, but it's undocumented/deprecated and will be removed. // The 1.7 special event interface should provide all the hooks needed now. jQuery.event.handle = jQuery.event.dispatch; jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } } : function( elem, type, handle ) { var name = "on" + type; if ( elem.detachEvent ) { // #8545, #7054, preventing memory leaks for custom events in IE6-8 // detachEvent needed property on element, by name of that event, to properly expose it to GC if ( typeof elem[ name ] === "undefined" ) { elem[ name ] = null; } elem.detachEvent( name, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !(this instanceof jQuery.Event) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; function returnFalse() { return false; } function returnTrue() { return true; } // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if preventDefault exists run it on the original event if ( e.preventDefault ) { e.preventDefault(); // otherwise set the returnValue property of the original event to false (IE) } else { e.returnValue = false; } }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if stopPropagation exists run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // otherwise set the cancelBubble property of the original event to true (IE) e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; // Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var ret, target = this, related = event.relatedTarget, handleObj = event.handleObj, selector = handleObj.selector; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !jQuery.contains( target, related )) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; }); // IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { setup: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Lazy-add a submit handler when a descendant form may potentially be submitted jQuery.event.add( this, "click._submit keypress._submit", function( e ) { // Node name check avoids a VML-related crash in IE (#9807) var elem = e.target, form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; if ( form && !jQuery._data( form, "_submit_attached" ) ) { jQuery.event.add( form, "submit._submit", function( event ) { event._submit_bubble = true; }); jQuery._data( form, "_submit_attached", true ); } }); // return undefined since we don't need an event listener }, postDispatch: function( event ) { // If form was submitted by the user, bubble the event up the tree if ( event._submit_bubble ) { delete event._submit_bubble; if ( this.parentNode && !event.isTrigger ) { jQuery.event.simulate( "submit", this.parentNode, event, true ); } } }, teardown: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Remove delegated handlers; cleanData eventually reaps submit handlers attached above jQuery.event.remove( this, "._submit" ); } }; } // IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { jQuery.event.special.change = { setup: function() { if ( rformElems.test( this.nodeName ) ) { // IE doesn't fire change on a check/radio until blur; trigger it on click // after a propertychange. Eat the blur-change in special.change.handle. // This still fires onchange a second time for check/radio after blur. if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._just_changed = true; } }); jQuery.event.add( this, "click._change", function( event ) { if ( this._just_changed && !event.isTrigger ) { this._just_changed = false; } // Allow triggered, simulated change events (#11500) jQuery.event.simulate( "change", this, event, true ); }); } return false; } // Delegated event; lazy-add a change handler on descendant inputs jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "_change_attached" ) ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event, true ); } }); jQuery._data( elem, "_change_attached", true ); } }); }, handle: function( event ) { var elem = event.target; // Swallow native change events from checkbox/radio, we already triggered them above if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { return event.handleObj.handler.apply( this, arguments ); } }, teardown: function() { jQuery.event.remove( this, "._change" ); return !rformElems.test( this.nodeName ); } }; } // Create "bubbling" focus and blur events if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler while someone wants focusin/focusout var attaches = 0, handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); }; jQuery.event.special[ fix ] = { setup: function() { if ( attaches++ === 0 ) { document.addEventListener( orig, handler, true ); } }, teardown: function() { if ( --attaches === 0 ) { document.removeEventListener( orig, handler, true ); } } }; }); } jQuery.fn.extend({ on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // && selector != null // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { this.on( type, selector, data, types[ type ], one ); } return this; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return this; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); }); }, one: function( types, selector, data, fn ) { return this.on( types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { var handleObj, type; if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each(function() { jQuery.event.remove( this, types, fn, selector ); }); }, bind: function( types, data, fn ) { return this.on( types, null, data, fn ); }, unbind: function( types, fn ) { return this.off( types, null, fn ); }, live: function( types, data, fn ) { jQuery( this.context ).on( types, this.selector, data, fn ); return this; }, die: function( types, fn ) { jQuery( this.context ).off( types, this.selector || "**", fn ); return this; }, delegate: function( selector, types, data, fn ) { return this.on( types, selector, data, fn ); }, undelegate: function( selector, types, fn ) { // ( namespace ) or ( selector, types [, fn] ) return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); }, trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, triggerHandler: function( type, data ) { if ( this[0] ) { return jQuery.event.trigger( type, data, this[0], true ); } }, toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, guid = fn.guid || jQuery.guid++, i = 0, toggler = function( event ) { // Figure out which function to execute var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); // and execute the function return args[ lastToggle ].apply( this, arguments ) || false; }; // link all the functions, so any of them can unbind this click handler toggler.guid = guid; while ( i < args.length ) { args[ i++ ].guid = guid; } return this.click( toggler ); }, hover: function( fnOver, fnOut ) { return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); } }); jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { if ( fn == null ) { fn = data; data = null; } return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; if ( rkeyEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; } if ( rmouseEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; } }); /*! * Sizzle CSS Selector Engine * Copyright 2012 jQuery Foundation and other contributors * Released under the MIT license * http://sizzlejs.com/ */ (function( window, undefined ) { var cachedruns, assertGetIdNotName, Expr, getText, isXML, contains, compile, sortOrder, hasDuplicate, outermostContext, baseHasDuplicate = true, strundefined = "undefined", expando = ( "sizcache" + Math.random() ).replace( ".", "" ), Token = String, document = window.document, docElem = document.documentElement, dirruns = 0, done = 0, pop = [].pop, push = [].push, slice = [].slice, // Use a stripped-down indexOf if a native one is unavailable indexOf = [].indexOf || function( elem ) { var i = 0, len = this.length; for ( ; i < len; i++ ) { if ( this[i] === elem ) { return i; } } return -1; }, // Augment a function for special use by Sizzle markFunction = function( fn, value ) { fn[ expando ] = value == null || value; return fn; }, createCache = function() { var cache = {}, keys = []; return markFunction(function( key, value ) { // Only keep the most recent entries if ( keys.push( key ) > Expr.cacheLength ) { delete cache[ keys.shift() ]; } // Retrieve with (key + " ") to avoid collision with native Object.prototype properties (see Issue #157) return (cache[ key + " " ] = value); }, cache ); }, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), // Regex // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", // http://www.w3.org/TR/css3-syntax/#characters characterEncoding = "(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+", // Loosely modeled on CSS identifier characters // An unquoted value should be a CSS identifier (http://www.w3.org/TR/css3-selectors/#attribute-selectors) // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = characterEncoding.replace( "w", "w#" ), // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors operators = "([*^$|!~]?=)", attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", // Prefer arguments not in parens/brackets, // then attribute selectors and non-pseudos (denoted by :), // then anything else // These preferences are here to reduce the number of selectors // needing tokenize in the PSEUDO preFilter pseudos = ":(" + characterEncoding + ")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:" + attributes + ")|[^:]|\\\\.)*|.*))\\)|)", // For matchExpr.POS and matchExpr.needsContext pos = ":(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), rpseudo = new RegExp( pseudos ), // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/, rnot = /^:not/, rsibling = /[\x20\t\r\n\f]*[+~]/, rendsWithNot = /:not\($/, rheader = /h\d/i, rinputs = /input|select|textarea|button/i, rbackslash = /\\(?!\\)/g, matchExpr = { "ID": new RegExp( "^#(" + characterEncoding + ")" ), "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "POS": new RegExp( pos, "i" ), "CHILD": new RegExp( "^:(only|nth|first|last)-child(?:\\(" + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), // For use in libraries implementing .is() "needsContext": new RegExp( "^" + whitespace + "*[>+~]|" + pos, "i" ) }, // Support // Used for testing something on an element assert = function( fn ) { var div = document.createElement("div"); try { return fn( div ); } catch (e) { return false; } finally { // release memory in IE div = null; } }, // Check if getElementsByTagName("*") returns only elements assertTagNameNoComments = assert(function( div ) { div.appendChild( document.createComment("") ); return !div.getElementsByTagName("*").length; }), // Check if getAttribute returns normalized href attributes assertHrefNotNormalized = assert(function( div ) { div.innerHTML = ""; return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && div.firstChild.getAttribute("href") === "#"; }), // Check if attributes should be retrieved by attribute nodes assertAttributes = assert(function( div ) { div.innerHTML = ""; var type = typeof div.lastChild.getAttribute("multiple"); // IE8 returns a string for some attributes even when not present return type !== "boolean" && type !== "string"; }), // Check if getElementsByClassName can be trusted assertUsableClassName = assert(function( div ) { // Opera can't find a second classname (in 9.6) div.innerHTML = ""; if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { return false; } // Safari 3.2 caches class attributes and doesn't catch changes div.lastChild.className = "e"; return div.getElementsByClassName("e").length === 2; }), // Check if getElementById returns elements by name // Check if getElementsByName privileges form controls or returns elements by ID assertUsableName = assert(function( div ) { // Inject content div.id = expando + 0; div.innerHTML = "
      "; docElem.insertBefore( div, docElem.firstChild ); // Test var pass = document.getElementsByName && // buggy browsers will return fewer than the correct 2 document.getElementsByName( expando ).length === 2 + // buggy browsers will return more than the correct 0 document.getElementsByName( expando + 0 ).length; assertGetIdNotName = !document.getElementById( expando ); // Cleanup docElem.removeChild( div ); return pass; }); // If slice is not available, provide a backup try { slice.call( docElem.childNodes, 0 )[0].nodeType; } catch ( e ) { slice = function( i ) { var elem, results = []; for ( ; (elem = this[i]); i++ ) { results.push( elem ); } return results; }; } function Sizzle( selector, context, results, seed ) { results = results || []; context = context || document; var match, elem, xml, m, nodeType = context.nodeType; if ( !selector || typeof selector !== "string" ) { return results; } if ( nodeType !== 1 && nodeType !== 9 ) { return []; } xml = isXML( context ); if ( !xml && !seed ) { if ( (match = rquickExpr.exec( selector )) ) { // Speed-up: Sizzle("#ID") if ( (m = match[1]) ) { if ( nodeType === 9 ) { elem = context.getElementById( m ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE, Opera, and Webkit return items // by name instead of ID if ( elem.id === m ) { results.push( elem ); return results; } } else { return results; } } else { // Context is not a document if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && contains( context, elem ) && elem.id === m ) { results.push( elem ); return results; } } // Speed-up: Sizzle("TAG") } else if ( match[2] ) { push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); return results; // Speed-up: Sizzle(".CLASS") } else if ( (m = match[3]) && assertUsableClassName && context.getElementsByClassName ) { push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); return results; } } } // All others return select( selector.replace( rtrim, "$1" ), context, results, seed, xml ); } Sizzle.matches = function( expr, elements ) { return Sizzle( expr, null, null, elements ); }; Sizzle.matchesSelector = function( elem, expr ) { return Sizzle( expr, null, null, [ elem ] ).length > 0; }; // Returns a function to use in pseudos for input types function createInputPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === type; }; } // Returns a function to use in pseudos for buttons function createButtonPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && elem.type === type; }; } // Returns a function to use in pseudos for positionals function createPositionalPseudo( fn ) { return markFunction(function( argument ) { argument = +argument; return markFunction(function( seed, matches ) { var j, matchIndexes = fn( [], seed.length, argument ), i = matchIndexes.length; // Match elements found at the specified indexes while ( i-- ) { if ( seed[ (j = matchIndexes[i]) ] ) { seed[j] = !(matches[j] = seed[j]); } } }); }); } /** * Utility function for retrieving the text value of an array of DOM nodes * @param {Array|Element} elem */ getText = Sizzle.getText = function( elem ) { var node, ret = "", i = 0, nodeType = elem.nodeType; if ( nodeType ) { if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements // innerText usage removed for consistency of new lines (see #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { // Traverse its children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { ret += getText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } // Do not include comment or processing instruction nodes } else { // If no nodeType, this is expected to be an array for ( ; (node = elem[i]); i++ ) { // Do not traverse comment nodes ret += getText( node ); } } return ret; }; isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = elem && (elem.ownerDocument || elem).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; // Element contains another contains = Sizzle.contains = docElem.contains ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && adown.contains && adown.contains(bup) ); } : docElem.compareDocumentPosition ? function( a, b ) { return b && !!( a.compareDocumentPosition( b ) & 16 ); } : function( a, b ) { while ( (b = b.parentNode) ) { if ( b === a ) { return true; } } return false; }; Sizzle.attr = function( elem, name ) { var val, xml = isXML( elem ); if ( !xml ) { name = name.toLowerCase(); } if ( (val = Expr.attrHandle[ name ]) ) { return val( elem ); } if ( xml || assertAttributes ) { return elem.getAttribute( name ); } val = elem.getAttributeNode( name ); return val ? typeof elem[ name ] === "boolean" ? elem[ name ] ? name : null : val.specified ? val.value : null : null; }; Expr = Sizzle.selectors = { // Can be adjusted by the user cacheLength: 50, createPseudo: markFunction, match: matchExpr, // IE6/7 return a modified href attrHandle: assertHrefNotNormalized ? {} : { "href": function( elem ) { return elem.getAttribute( "href", 2 ); }, "type": function( elem ) { return elem.getAttribute("type"); } }, find: { "ID": assertGetIdNotName ? function( id, context, xml ) { if ( typeof context.getElementById !== strundefined && !xml ) { var m = context.getElementById( id ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 return m && m.parentNode ? [m] : []; } } : function( id, context, xml ) { if ( typeof context.getElementById !== strundefined && !xml ) { var m = context.getElementById( id ); return m ? m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? [m] : undefined : []; } }, "TAG": assertTagNameNoComments ? function( tag, context ) { if ( typeof context.getElementsByTagName !== strundefined ) { return context.getElementsByTagName( tag ); } } : function( tag, context ) { var results = context.getElementsByTagName( tag ); // Filter out possible comments if ( tag === "*" ) { var elem, tmp = [], i = 0; for ( ; (elem = results[i]); i++ ) { if ( elem.nodeType === 1 ) { tmp.push( elem ); } } return tmp; } return results; }, "NAME": assertUsableName && function( tag, context ) { if ( typeof context.getElementsByName !== strundefined ) { return context.getElementsByName( name ); } }, "CLASS": assertUsableClassName && function( className, context, xml ) { if ( typeof context.getElementsByClassName !== strundefined && !xml ) { return context.getElementsByClassName( className ); } } }, relative: { ">": { dir: "parentNode", first: true }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: true }, "~": { dir: "previousSibling" } }, preFilter: { "ATTR": function( match ) { match[1] = match[1].replace( rbackslash, "" ); // Move the given value to match[3] whether quoted or unquoted match[3] = ( match[4] || match[5] || "" ).replace( rbackslash, "" ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; } return match.slice( 0, 4 ); }, "CHILD": function( match ) { /* matches from matchExpr["CHILD"] 1 type (only|nth|...) 2 argument (even|odd|\d*|\d*n([+-]\d+)?|...) 3 xn-component of xn+y argument ([+-]?\d*n|) 4 sign of xn-component 5 x of xn-component 6 sign of y-component 7 y of y-component */ match[1] = match[1].toLowerCase(); if ( match[1] === "nth" ) { // nth-child requires argument if ( !match[2] ) { Sizzle.error( match[0] ); } // numeric x and y parameters for Expr.filter.CHILD // remember that false/true cast respectively to 0/1 match[3] = +( match[3] ? match[4] + (match[5] || 1) : 2 * ( match[2] === "even" || match[2] === "odd" ) ); match[4] = +( ( match[6] + match[7] ) || match[2] === "odd" ); // other types prohibit arguments } else if ( match[2] ) { Sizzle.error( match[0] ); } return match; }, "PSEUDO": function( match ) { var unquoted, excess; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } if ( match[3] ) { match[2] = match[3]; } else if ( (unquoted = match[4]) ) { // Only check arguments that contain a pseudo if ( rpseudo.test(unquoted) && // Get excess from tokenize (recursively) (excess = tokenize( unquoted, true )) && // advance to the next closing parenthesis (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { // excess is a negative index unquoted = unquoted.slice( 0, excess ); match[0] = match[0].slice( 0, excess ); } match[2] = unquoted; } // Return only captures needed by the pseudo filter method (type and argument) return match.slice( 0, 3 ); } }, filter: { "ID": assertGetIdNotName ? function( id ) { id = id.replace( rbackslash, "" ); return function( elem ) { return elem.getAttribute("id") === id; }; } : function( id ) { id = id.replace( rbackslash, "" ); return function( elem ) { var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); return node && node.value === id; }; }, "TAG": function( nodeName ) { if ( nodeName === "*" ) { return function() { return true; }; } nodeName = nodeName.replace( rbackslash, "" ).toLowerCase(); return function( elem ) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; }, "CLASS": function( className ) { var pattern = classCache[ expando ][ className + " " ]; return pattern || (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && classCache( className, function( elem ) { return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); }); }, "ATTR": function( name, operator, check ) { return function( elem, context ) { var result = Sizzle.attr( elem, name ); if ( result == null ) { return operator === "!="; } if ( !operator ) { return true; } result += ""; return operator === "=" ? result === check : operator === "!=" ? result !== check : operator === "^=" ? check && result.indexOf( check ) === 0 : operator === "*=" ? check && result.indexOf( check ) > -1 : operator === "$=" ? check && result.substr( result.length - check.length ) === check : operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" : false; }; }, "CHILD": function( type, argument, first, last ) { if ( type === "nth" ) { return function( elem ) { var node, diff, parent = elem.parentNode; if ( first === 1 && last === 0 ) { return true; } if ( parent ) { diff = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { diff++; if ( elem === node ) { break; } } } } // Incorporate the offset (or cast to NaN), then check against cycle size diff -= last; return diff === first || ( diff % first === 0 && diff / first >= 0 ); }; } return function( elem ) { var node = elem; switch ( type ) { case "only": case "first": while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } if ( type === "first" ) { return true; } node = elem; /* falls through */ case "last": while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } return true; } }; }, "PSEUDO": function( pseudo, argument ) { // pseudo-class names are case-insensitive // http://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters // Remember that setFilters inherits from pseudos var args, fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || Sizzle.error( "unsupported pseudo: " + pseudo ); // The user may use createPseudo to indicate that // arguments are needed to create the filter function // just as Sizzle does if ( fn[ expando ] ) { return fn( argument ); } // But maintain support for old signatures if ( fn.length > 1 ) { args = [ pseudo, pseudo, "", argument ]; return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? markFunction(function( seed, matches ) { var idx, matched = fn( seed, argument ), i = matched.length; while ( i-- ) { idx = indexOf.call( seed, matched[i] ); seed[ idx ] = !( matches[ idx ] = matched[i] ); } }) : function( elem ) { return fn( elem, 0, args ); }; } return fn; } }, pseudos: { "not": markFunction(function( selector ) { // Trim the selector passed to compile // to avoid treating leading and trailing // spaces as combinators var input = [], results = [], matcher = compile( selector.replace( rtrim, "$1" ) ); return matcher[ expando ] ? markFunction(function( seed, matches, context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), i = seed.length; // Match elements unmatched by `matcher` while ( i-- ) { if ( (elem = unmatched[i]) ) { seed[i] = !(matches[i] = elem); } } }) : function( elem, context, xml ) { input[0] = elem; matcher( input, null, xml, results ); return !results.pop(); }; }), "has": markFunction(function( selector ) { return function( elem ) { return Sizzle( selector, elem ).length > 0; }; }), "contains": markFunction(function( text ) { return function( elem ) { return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; }; }), "enabled": function( elem ) { return elem.disabled === false; }, "disabled": function( elem ) { return elem.disabled === true; }, "checked": function( elem ) { // In CSS3, :checked should return both checked and selected elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked var nodeName = elem.nodeName.toLowerCase(); return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); }, "selected": function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { elem.parentNode.selectedIndex; } return elem.selected === true; }, "parent": function( elem ) { return !Expr.pseudos["empty"]( elem ); }, "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), // not comment, processing instructions, or others // Thanks to Diego Perini for the nodeName shortcut // Greater than "@" means alpha characters (specifically not starting with "#" or "?") var nodeType; elem = elem.firstChild; while ( elem ) { if ( elem.nodeName > "@" || (nodeType = elem.nodeType) === 3 || nodeType === 4 ) { return false; } elem = elem.nextSibling; } return true; }, "header": function( elem ) { return rheader.test( elem.nodeName ); }, "text": function( elem ) { var type, attr; // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && (type = elem.type) === "text" && ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === type ); }, // Input types "radio": createInputPseudo("radio"), "checkbox": createInputPseudo("checkbox"), "file": createInputPseudo("file"), "password": createInputPseudo("password"), "image": createInputPseudo("image"), "submit": createButtonPseudo("submit"), "reset": createButtonPseudo("reset"), "button": function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && elem.type === "button" || name === "button"; }, "input": function( elem ) { return rinputs.test( elem.nodeName ); }, "focus": function( elem ) { var doc = elem.ownerDocument; return elem === doc.activeElement && (!doc.hasFocus || doc.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); }, "active": function( elem ) { return elem === elem.ownerDocument.activeElement; }, // Positional types "first": createPositionalPseudo(function() { return [ 0 ]; }), "last": createPositionalPseudo(function( matchIndexes, length ) { return [ length - 1 ]; }), "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { return [ argument < 0 ? argument + length : argument ]; }), "even": createPositionalPseudo(function( matchIndexes, length ) { for ( var i = 0; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "odd": createPositionalPseudo(function( matchIndexes, length ) { for ( var i = 1; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; }), "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { for ( var i = argument < 0 ? argument + length : argument; --i >= 0; ) { matchIndexes.push( i ); } return matchIndexes; }), "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { for ( var i = argument < 0 ? argument + length : argument; ++i < length; ) { matchIndexes.push( i ); } return matchIndexes; }) } }; function siblingCheck( a, b, ret ) { if ( a === b ) { return ret; } var cur = a.nextSibling; while ( cur ) { if ( cur === b ) { return -1; } cur = cur.nextSibling; } return 1; } sortOrder = docElem.compareDocumentPosition ? function( a, b ) { if ( a === b ) { hasDuplicate = true; return 0; } return ( !a.compareDocumentPosition || !b.compareDocumentPosition ? a.compareDocumentPosition : a.compareDocumentPosition(b) & 4 ) ? -1 : 1; } : function( a, b ) { // The nodes are identical, we can exit early if ( a === b ) { hasDuplicate = true; return 0; // Fallback to using sourceIndex (in IE) if it's available on both nodes } else if ( a.sourceIndex && b.sourceIndex ) { return a.sourceIndex - b.sourceIndex; } var al, bl, ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, cur = aup; // If the nodes are siblings (or identical) we can do a quick check if ( aup === bup ) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected } else if ( !aup ) { return -1; } else if ( !bup ) { return 1; } // Otherwise they're somewhere else in the tree so we need // to build up a full list of the parentNodes for comparison while ( cur ) { ap.unshift( cur ); cur = cur.parentNode; } cur = bup; while ( cur ) { bp.unshift( cur ); cur = cur.parentNode; } al = ap.length; bl = bp.length; // Start walking down the tree looking for a discrepancy for ( var i = 0; i < al && i < bl; i++ ) { if ( ap[i] !== bp[i] ) { return siblingCheck( ap[i], bp[i] ); } } // We ended someplace up the tree so do a sibling check return i === al ? siblingCheck( a, bp[i], -1 ) : siblingCheck( ap[i], b, 1 ); }; // Always assume the presence of duplicates if sort doesn't // pass them to our comparison function (as in Google Chrome). [0, 0].sort( sortOrder ); baseHasDuplicate = !hasDuplicate; // Document sorting and removing duplicates Sizzle.uniqueSort = function( results ) { var elem, duplicates = [], i = 1, j = 0; hasDuplicate = baseHasDuplicate; results.sort( sortOrder ); if ( hasDuplicate ) { for ( ; (elem = results[i]); i++ ) { if ( elem === results[ i - 1 ] ) { j = duplicates.push( i ); } } while ( j-- ) { results.splice( duplicates[ j ], 1 ); } } return results; }; Sizzle.error = function( msg ) { throw new Error( "Syntax error, unrecognized expression: " + msg ); }; function tokenize( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ expando ][ selector + " " ]; if ( cached ) { return parseOnly ? 0 : cached.slice( 0 ); } soFar = selector; groups = []; preFilters = Expr.preFilter; while ( soFar ) { // Comma and first run if ( !matched || (match = rcomma.exec( soFar )) ) { if ( match ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } groups.push( tokens = [] ); } matched = false; // Combinators if ( (match = rcombinators.exec( soFar )) ) { tokens.push( matched = new Token( match.shift() ) ); soFar = soFar.slice( matched.length ); // Cast descendant combinators to space matched.type = match[0].replace( rtrim, " " ); } // Filters for ( type in Expr.filter ) { if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || (match = preFilters[ type ]( match ))) ) { tokens.push( matched = new Token( match.shift() ) ); soFar = soFar.slice( matched.length ); matched.type = type; matched.matches = match; } } if ( !matched ) { break; } } // Return the length of the invalid excess // if we're just parsing // Otherwise, throw an error or return tokens return parseOnly ? soFar.length : soFar ? Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); } function addCombinator( matcher, combinator, base ) { var dir = combinator.dir, checkNonElements = base && combinator.dir === "parentNode", doneName = done++; return combinator.first ? // Check against closest ancestor/preceding element function( elem, context, xml ) { while ( (elem = elem[ dir ]) ) { if ( checkNonElements || elem.nodeType === 1 ) { return matcher( elem, context, xml ); } } } : // Check against all ancestor/preceding elements function( elem, context, xml ) { // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching if ( !xml ) { var cache, dirkey = dirruns + " " + doneName + " ", cachedkey = dirkey + cachedruns; while ( (elem = elem[ dir ]) ) { if ( checkNonElements || elem.nodeType === 1 ) { if ( (cache = elem[ expando ]) === cachedkey ) { return elem.sizset; } else if ( typeof cache === "string" && cache.indexOf(dirkey) === 0 ) { if ( elem.sizset ) { return elem; } } else { elem[ expando ] = cachedkey; if ( matcher( elem, context, xml ) ) { elem.sizset = true; return elem; } elem.sizset = false; } } } } else { while ( (elem = elem[ dir ]) ) { if ( checkNonElements || elem.nodeType === 1 ) { if ( matcher( elem, context, xml ) ) { return elem; } } } } }; } function elementMatcher( matchers ) { return matchers.length > 1 ? function( elem, context, xml ) { var i = matchers.length; while ( i-- ) { if ( !matchers[i]( elem, context, xml ) ) { return false; } } return true; } : matchers[0]; } function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], i = 0, len = unmatched.length, mapped = map != null; for ( ; i < len; i++ ) { if ( (elem = unmatched[i]) ) { if ( !filter || filter( elem, context, xml ) ) { newUnmatched.push( elem ); if ( mapped ) { map.push( i ); } } } } return newUnmatched; } function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { if ( postFilter && !postFilter[ expando ] ) { postFilter = setMatcher( postFilter ); } if ( postFinder && !postFinder[ expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } return markFunction(function( seed, results, context, xml ) { var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? condense( elems, preMap, preFilter, context, xml ) : elems, matcherOut = matcher ? // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, postFinder || ( seed ? preFilter : preexisting || postFilter ) ? // ...intermediate processing is necessary [] : // ...otherwise use results directly results : matcherIn; // Find primary matches if ( matcher ) { matcher( matcherIn, matcherOut, context, xml ); } // Apply postFilter if ( postFilter ) { temp = condense( matcherOut, postMap ); postFilter( temp, [], context, xml ); // Un-match failing elements by moving them back to matcherIn i = temp.length; while ( i-- ) { if ( (elem = temp[i]) ) { matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); } } } if ( seed ) { if ( postFinder || preFilter ) { if ( postFinder ) { // Get the final matcherOut by condensing this intermediate into postFinder contexts temp = []; i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) ) { // Restore matcherIn since elem is not yet a final match temp.push( (matcherIn[i] = elem) ); } } postFinder( null, (matcherOut = []), temp, xml ); } // Move matched elements from seed to results to keep them synchronized i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } } } // Add elements to results, through postFinder if defined } else { matcherOut = condense( matcherOut === results ? matcherOut.splice( preexisting, matcherOut.length ) : matcherOut ); if ( postFinder ) { postFinder( null, results, matcherOut, xml ); } else { push.apply( results, matcherOut ); } } }); } function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, leadingRelative = Expr.relative[ tokens[0].type ], implicitRelative = leadingRelative || Expr.relative[" "], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) matchContext = addCombinator( function( elem ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { return indexOf.call( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); } ]; for ( ; i < len; i++ ) { if ( (matcher = Expr.relative[ tokens[i].type ]) ) { matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; } else { matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); // Return special upon seeing a positional matcher if ( matcher[ expando ] ) { // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { if ( Expr.relative[ tokens[j].type ] ) { break; } } return setMatcher( i > 1 && elementMatcher( matchers ), i > 1 && tokens.slice( 0, i - 1 ).join("").replace( rtrim, "$1" ), matcher, i < j && matcherFromTokens( tokens.slice( i, j ) ), j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), j < len && tokens.join("") ); } matchers.push( matcher ); } } return elementMatcher( matchers ); } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, superMatcher = function( seed, context, xml, results, expandContext ) { var elem, j, matcher, setMatched = [], matchedCount = 0, i = "0", unmatched = seed && [], outermost = expandContext != null, contextBackup = outermostContext, // We must always have either seed elements or context elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), // Nested matchers should use non-integer dirruns dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.E); if ( outermost ) { outermostContext = context !== document && context; cachedruns = superMatcher.el; } // Add elements passing elementMatchers directly to results for ( ; (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { for ( j = 0; (matcher = elementMatchers[j]); j++ ) { if ( matcher( elem, context, xml ) ) { results.push( elem ); break; } } if ( outermost ) { dirruns = dirrunsUnique; cachedruns = ++superMatcher.el; } } // Track unmatched elements for set filters if ( bySet ) { // They will have gone through all possible matchers if ( (elem = !matcher && elem) ) { matchedCount--; } // Lengthen the array for every element, matched or not if ( seed ) { unmatched.push( elem ); } } } // Apply set filters to unmatched elements matchedCount += i; if ( bySet && i !== matchedCount ) { for ( j = 0; (matcher = setMatchers[j]); j++ ) { matcher( unmatched, setMatched, context, xml ); } if ( seed ) { // Reintegrate element matches to eliminate the need for sorting if ( matchedCount > 0 ) { while ( i-- ) { if ( !(unmatched[i] || setMatched[i]) ) { setMatched[i] = pop.call( results ); } } } // Discard index placeholder values to get only actual matches setMatched = condense( setMatched ); } // Add matches to results push.apply( results, setMatched ); // Seedless set matches succeeding multiple successful matchers stipulate sorting if ( outermost && !seed && setMatched.length > 0 && ( matchedCount + setMatchers.length ) > 1 ) { Sizzle.uniqueSort( results ); } } // Override manipulation of globals by nested matchers if ( outermost ) { dirruns = dirrunsUnique; outermostContext = contextBackup; } return unmatched; }; superMatcher.el = 0; return bySet ? markFunction( superMatcher ) : superMatcher; } compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], cached = compilerCache[ expando ][ selector + " " ]; if ( !cached ) { // Generate a function of recursive functions that can be used to check each element if ( !group ) { group = tokenize( selector ); } i = group.length; while ( i-- ) { cached = matcherFromTokens( group[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { elementMatchers.push( cached ); } } // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); } return cached; }; function multipleContexts( selector, contexts, results ) { var i = 0, len = contexts.length; for ( ; i < len; i++ ) { Sizzle( selector, contexts[i], results ); } return results; } function select( selector, context, results, seed, xml ) { var i, tokens, token, type, find, match = tokenize( selector ), j = match.length; if ( !seed ) { // Try to minimize operations if there is only one group if ( match.length === 1 ) { // Take a shortcut and set the context if the root selector is an ID tokens = match[0] = match[0].slice( 0 ); if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && context.nodeType === 9 && !xml && Expr.relative[ tokens[1].type ] ) { context = Expr.find["ID"]( token.matches[0].replace( rbackslash, "" ), context, xml )[0]; if ( !context ) { return results; } selector = selector.slice( tokens.shift().length ); } // Fetch a seed set for right-to-left matching for ( i = matchExpr["POS"].test( selector ) ? -1 : tokens.length - 1; i >= 0; i-- ) { token = tokens[i]; // Abort if we hit a combinator if ( Expr.relative[ (type = token.type) ] ) { break; } if ( (find = Expr.find[ type ]) ) { // Search, expanding context for leading sibling combinators if ( (seed = find( token.matches[0].replace( rbackslash, "" ), rsibling.test( tokens[0].type ) && context.parentNode || context, xml )) ) { // If seed is empty or no tokens remain, we can return early tokens.splice( i, 1 ); selector = seed.length && tokens.join(""); if ( !selector ) { push.apply( results, slice.call( seed, 0 ) ); return results; } break; } } } } } // Compile and execute a filtering function // Provide `match` to avoid retokenization if we modified the selector above compile( selector, match )( seed, context, xml, results, rsibling.test( selector ) ); return results; } if ( document.querySelectorAll ) { (function() { var disconnectedMatch, oldSelect = select, rescape = /'|\\/g, rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, // qSa(:focus) reports false when true (Chrome 21), no need to also add to buggyMatches since matches checks buggyQSA // A support test would require too much code (would include document ready) rbuggyQSA = [ ":focus" ], // matchesSelector(:active) reports false when true (IE9/Opera 11.5) // A support test would require too much code (would include document ready) // just skip matchesSelector for :active rbuggyMatches = [ ":active" ], matches = docElem.matchesSelector || docElem.mozMatchesSelector || docElem.webkitMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector; // Build QSA regex // Regex strategy adopted from Diego Perini assert(function( div ) { // Select is set to empty string on purpose // This is to test IE's treatment of not explictly // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 div.innerHTML = ""; // IE8 - Some boolean attributes are not treated correctly if ( !div.querySelectorAll("[selected]").length ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); } // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here (do not put tests after this one) if ( !div.querySelectorAll(":checked").length ) { rbuggyQSA.push(":checked"); } }); assert(function( div ) { // Opera 10-12/IE9 - ^= $= *= and empty values // Should not select anything div.innerHTML = "

      "; if ( div.querySelectorAll("[test^='']").length ) { rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) // IE8 throws error here (do not put tests after this one) div.innerHTML = ""; if ( !div.querySelectorAll(":enabled").length ) { rbuggyQSA.push(":enabled", ":disabled"); } }); // rbuggyQSA always contains :focus, so no need for a length check rbuggyQSA = /* rbuggyQSA.length && */ new RegExp( rbuggyQSA.join("|") ); select = function( selector, context, results, seed, xml ) { // Only use querySelectorAll when not filtering, // when this is not xml, // and when no QSA bugs apply if ( !seed && !xml && !rbuggyQSA.test( selector ) ) { var groups, i, old = true, nid = expando, newContext = context, newSelector = context.nodeType === 9 && selector; // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { groups = tokenize( selector ); if ( (old = context.getAttribute("id")) ) { nid = old.replace( rescape, "\\$&" ); } else { context.setAttribute( "id", nid ); } nid = "[id='" + nid + "'] "; i = groups.length; while ( i-- ) { groups[i] = nid + groups[i].join(""); } newContext = rsibling.test( selector ) && context.parentNode || context; newSelector = groups.join(","); } if ( newSelector ) { try { push.apply( results, slice.call( newContext.querySelectorAll( newSelector ), 0 ) ); return results; } catch(qsaError) { } finally { if ( !old ) { context.removeAttribute("id"); } } } } return oldSelect( selector, context, results, seed, xml ); }; if ( matches ) { assert(function( div ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9) disconnectedMatch = matches.call( div, "div" ); // This should fail with an exception // Gecko does not error, returns false instead try { matches.call( div, "[test!='']:sizzle" ); rbuggyMatches.push( "!=", pseudos ); } catch ( e ) {} }); // rbuggyMatches always contains :active and :focus, so no need for a length check rbuggyMatches = /* rbuggyMatches.length && */ new RegExp( rbuggyMatches.join("|") ); Sizzle.matchesSelector = function( elem, expr ) { // Make sure that attribute selectors are quoted expr = expr.replace( rattributeQuotes, "='$1']" ); // rbuggyMatches always contains :active, so no need for an existence check if ( !isXML( elem ) && !rbuggyMatches.test( expr ) && !rbuggyQSA.test( expr ) ) { try { var ret = matches.call( elem, expr ); // IE 9's matchesSelector returns false on disconnected nodes if ( ret || disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9 elem.document && elem.document.nodeType !== 11 ) { return ret; } } catch(e) {} } return Sizzle( expr, null, null, [ elem ] ).length > 0; }; } })(); } // Deprecated Expr.pseudos["nth"] = Expr.pseudos["eq"]; // Back-compat function setFilters() {} Expr.filters = setFilters.prototype = Expr.pseudos; Expr.setFilters = new setFilters(); // Override sizzle attribute retrieval Sizzle.attr = jQuery.attr; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.pseudos; jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; })( window ); var runtil = /Until$/, rparentsprev = /^(?:parents|prev(?:Until|All))/, isSimple = /^.[^:#\[\.,]*$/, rneedsContext = jQuery.expr.match.needsContext, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, contents: true, next: true, prev: true }; jQuery.fn.extend({ find: function( selector ) { var i, l, length, n, r, ret, self = this; if ( typeof selector !== "string" ) { return jQuery( selector ).filter(function() { for ( i = 0, l = self.length; i < l; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } }); } ret = this.pushStack( "", "find", selector ); for ( i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique for ( n = length; n < ret.length; n++ ) { for ( r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; } } } } } return ret; }, has: function( target ) { var i, targets = jQuery( target, this ), len = targets.length; return this.filter(function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } } }); }, not: function( selector ) { return this.pushStack( winnow(this, selector, false), "not", selector); }, filter: function( selector ) { return this.pushStack( winnow(this, selector, true), "filter", selector ); }, is: function( selector ) { return !!selector && ( typeof selector === "string" ? // If this is a positional/relative selector, check membership in the returned set // so $("p:first").is("p:last") won't return true for a doc with two "p". rneedsContext.test( selector ) ? jQuery( selector, this.context ).index( this[0] ) >= 0 : jQuery.filter( selector, this ).length > 0 : this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var cur, i = 0, l = this.length, ret = [], pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? jQuery( selectors, context || this.context ) : 0; for ( ; i < l; i++ ) { cur = this[i]; while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { ret.push( cur ); break; } cur = cur.parentNode; } } ret = ret.length > 1 ? jQuery.unique( ret ) : ret; return this.pushStack( ret, "closest", selectors ); }, // Determine the position of an element within // the matched set of elements index: function( elem ) { // No argument, return index in parent if ( !elem ) { return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; } // index in selector if ( typeof elem === "string" ) { return jQuery.inArray( this[0], jQuery( elem ) ); } // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used elem.jquery ? elem[0] : elem, this ); }, add: function( selector, context ) { var set = typeof selector === "string" ? jQuery( selector, context ) : jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? all : jQuery.unique( all ) ); }, addBack: function( selector ) { return this.add( selector == null ? this.prevObject : this.prevObject.filter(selector) ); } }); jQuery.fn.andSelf = jQuery.fn.addBack; // A painfully simple check to see if an element is disconnected // from a document (should be improved, where feasible). function isDisconnected( node ) { return !node || !node.parentNode || node.parentNode.nodeType === 11; } function sibling( cur, dir ) { do { cur = cur[ dir ]; } while ( cur && cur.nodeType !== 1 ); return cur; } jQuery.each({ parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return jQuery.dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { return sibling( elem, "nextSibling" ); }, prev: function( elem ) { return sibling( elem, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return jQuery.dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return jQuery.dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); }, contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; } if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; if ( this.length > 1 && rparentsprev.test( name ) ) { ret = ret.reverse(); } return this.pushStack( ret, name, core_slice.call( arguments ).join(",") ); }; }); jQuery.extend({ filter: function( expr, elems, not ) { if ( not ) { expr = ":not(" + expr + ")"; } return elems.length === 1 ? jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : jQuery.find.matches(expr, elems); }, dir: function( elem, dir, until ) { var matched = [], cur = elem[ dir ]; while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); } cur = cur[dir]; } return matched; }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; } }); // Implement the identical functionality for filter and not function winnow( elements, qualifier, keep ) { // Can't pass null or undefined to indexOf in Firefox 4 // Set to 0 to skip string check qualifier = qualifier || 0; if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { var retVal = !!qualifier.call( elem, i, elem ); return retVal === keep; }); } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { var filtered = jQuery.grep(elements, function( elem ) { return elem.nodeType === 1; }); if ( isSimple.test( qualifier ) ) { return jQuery.filter(qualifier, filtered, !keep); } else { qualifier = jQuery.filter( qualifier, filtered ); } } return jQuery.grep(elements, function( elem, i ) { return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } function createSafeFragment( document ) { var list = nodeNames.split( "|" ), safeFrag = document.createDocumentFragment(); if ( safeFrag.createElement ) { while ( list.length ) { safeFrag.createElement( list.pop() ); } } return safeFrag; } var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, rtagName = /<([\w:]+)/, rtbody = /]", "i"), rcheckableType = /^(?:checkbox|radio)$/, // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /\/(java|ecma)script/i, rcleanScript = /^\s*\s*$/g, wrapMap = { option: [ 1, "" ], legend: [ 1, "
      ", "
      " ], thead: [ 1, "", "
      " ], tr: [ 2, "", "
      " ], td: [ 3, "", "
      " ], col: [ 2, "", "
      " ], area: [ 1, "", "" ], _default: [ 0, "", "" ] }, safeFragment = createSafeFragment( document ), fragmentDiv = safeFragment.appendChild( document.createElement("div") ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, // unless wrapped in a div with non-breaking characters in front of it. if ( !jQuery.support.htmlSerialize ) { wrapMap._default = [ 1, "X
      ", "
      " ]; } jQuery.fn.extend({ text: function( value ) { return jQuery.access( this, function( value ) { return value === undefined ? jQuery.text( this ) : this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); }, null, value, arguments.length ); }, wrapAll: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapAll( html.call(this, i) ); }); } if ( this[0] ) { // The elements to wrap the target around var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); if ( this[0].parentNode ) { wrap.insertBefore( this[0] ); } wrap.map(function() { var elem = this; while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { elem = elem.firstChild; } return elem; }).append( this ); } return this; }, wrapInner: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapInner( html.call(this, i) ); }); } return this.each(function() { var self = jQuery( this ), contents = self.contents(); if ( contents.length ) { contents.wrapAll( html ); } else { self.append( html ); } }); }, wrap: function( html ) { var isFunction = jQuery.isFunction( html ); return this.each(function(i) { jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, unwrap: function() { return this.parent().each(function() { if ( !jQuery.nodeName( this, "body" ) ) { jQuery( this ).replaceWith( this.childNodes ); } }).end(); }, append: function() { return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 ) { this.appendChild( elem ); } }); }, prepend: function() { return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 ) { this.insertBefore( elem, this.firstChild ); } }); }, before: function() { if ( !isDisconnected( this[0] ) ) { return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this ); }); } if ( arguments.length ) { var set = jQuery.clean( arguments ); return this.pushStack( jQuery.merge( set, this ), "before", this.selector ); } }, after: function() { if ( !isDisconnected( this[0] ) ) { return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this.nextSibling ); }); } if ( arguments.length ) { var set = jQuery.clean( arguments ); return this.pushStack( jQuery.merge( this, set ), "after", this.selector ); } }, // keepData is for internal use only--do not document remove: function( selector, keepData ) { var elem, i = 0; for ( ; (elem = this[i]) != null; i++ ) { if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { if ( !keepData && elem.nodeType === 1 ) { jQuery.cleanData( elem.getElementsByTagName("*") ); jQuery.cleanData( [ elem ] ); } if ( elem.parentNode ) { elem.parentNode.removeChild( elem ); } } } return this; }, empty: function() { var elem, i = 0; for ( ; (elem = this[i]) != null; i++ ) { // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { jQuery.cleanData( elem.getElementsByTagName("*") ); } // Remove any remaining nodes while ( elem.firstChild ) { elem.removeChild( elem.firstChild ); } } return this; }, clone: function( dataAndEvents, deepDataAndEvents ) { dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map( function () { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); }); }, html: function( value ) { return jQuery.access( this, function( value ) { var elem = this[0] || {}, i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1>" ); try { for (; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( elem.getElementsByTagName( "*" ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch(e) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }, replaceWith: function( value ) { if ( !isDisconnected( this[0] ) ) { // Make sure that the elements are removed from the DOM before they are inserted // this can help fix replacing a parent with child elements if ( jQuery.isFunction( value ) ) { return this.each(function(i) { var self = jQuery(this), old = self.html(); self.replaceWith( value.call( this, i, old ) ); }); } if ( typeof value !== "string" ) { value = jQuery( value ).detach(); } return this.each(function() { var next = this.nextSibling, parent = this.parentNode; jQuery( this ).remove(); if ( next ) { jQuery(next).before( value ); } else { jQuery(parent).append( value ); } }); } return this.length ? this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : this; }, detach: function( selector ) { return this.remove( selector, true ); }, domManip: function( args, table, callback ) { // Flatten any nested arrays args = [].concat.apply( [], args ); var results, first, fragment, iNoClone, i = 0, value = args[0], scripts = [], l = this.length; // We can't cloneNode fragments that contain checked, in WebKit if ( !jQuery.support.checkClone && l > 1 && typeof value === "string" && rchecked.test( value ) ) { return this.each(function() { jQuery(this).domManip( args, table, callback ); }); } if ( jQuery.isFunction(value) ) { return this.each(function(i) { var self = jQuery(this); args[0] = value.call( this, i, table ? self.html() : undefined ); self.domManip( args, table, callback ); }); } if ( this[0] ) { results = jQuery.buildFragment( args, this, scripts ); fragment = results.fragment; first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { fragment = first; } if ( first ) { table = table && jQuery.nodeName( first, "tr" ); // Use the original fragment for the last item instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). // Fragments from the fragment cache must always be cloned and never used in place. for ( iNoClone = results.cacheable || l - 1; i < l; i++ ) { callback.call( table && jQuery.nodeName( this[i], "table" ) ? findOrAppend( this[i], "tbody" ) : this[i], i === iNoClone ? fragment : jQuery.clone( fragment, true, true ) ); } } // Fix #11809: Avoid leaking memory fragment = first = null; if ( scripts.length ) { jQuery.each( scripts, function( i, elem ) { if ( elem.src ) { if ( jQuery.ajax ) { jQuery.ajax({ url: elem.src, type: "GET", dataType: "script", async: false, global: false, "throws": true }); } else { jQuery.error("no ajax"); } } else { jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "" ) ); } if ( elem.parentNode ) { elem.parentNode.removeChild( elem ); } }); } } return this; } }); function findOrAppend( elem, tag ) { return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); } function cloneCopyEvent( src, dest ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { return; } var type, i, l, oldData = jQuery._data( src ), curData = jQuery._data( dest, oldData ), events = oldData.events; if ( events ) { delete curData.handle; curData.events = {}; for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { jQuery.event.add( dest, type, events[ type ][ i ] ); } } } // make the cloned public data object a copy from the original if ( curData.data ) { curData.data = jQuery.extend( {}, curData.data ); } } function cloneFixAttributes( src, dest ) { var nodeName; // We do not need to do anything for non-Elements if ( dest.nodeType !== 1 ) { return; } // clearAttributes removes the attributes, which we don't want, // but also removes the attachEvent events, which we *do* want if ( dest.clearAttributes ) { dest.clearAttributes(); } // mergeAttributes, in contrast, only merges back on the // original attributes, not the events if ( dest.mergeAttributes ) { dest.mergeAttributes( src ); } nodeName = dest.nodeName.toLowerCase(); if ( nodeName === "object" ) { // IE6-10 improperly clones children of object elements using classid. // IE10 throws NoModificationAllowedError if parent is null, #12132. if ( dest.parentNode ) { dest.outerHTML = src.outerHTML; } // This path appears unavoidable for IE9. When cloning an object // element in IE9, the outerHTML strategy above is not sufficient. // If the src has innerHTML and the destination does not, // copy the src.innerHTML into the dest.innerHTML. #10324 if ( jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML)) ) { dest.innerHTML = src.innerHTML; } } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { // IE6-8 fails to persist the checked state of a cloned checkbox // or radio button. Worse, IE6-7 fail to give the cloned element // a checked appearance if the defaultChecked value isn't also set dest.defaultChecked = dest.checked = src.checked; // IE6-7 get confused and end up setting the value of a cloned // checkbox/radio button to an empty string instead of "on" if ( dest.value !== src.value ) { dest.value = src.value; } // IE6-8 fails to return the selected option to the default selected // state when cloning options } else if ( nodeName === "option" ) { dest.selected = src.defaultSelected; // IE6-8 fails to set the defaultValue to the correct value when // cloning other types of input fields } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; // IE blanks contents when cloning scripts } else if ( nodeName === "script" && dest.text !== src.text ) { dest.text = src.text; } // Event data gets referenced instead of copied if the expando // gets copied too dest.removeAttribute( jQuery.expando ); } jQuery.buildFragment = function( args, context, scripts ) { var fragment, cacheable, cachehit, first = args[ 0 ]; // Set context from what may come in as undefined or a jQuery collection or a node // Updated to fix #12266 where accessing context[0] could throw an exception in IE9/10 & // also doubles as fix for #8950 where plain objects caused createDocumentFragment exception context = context || document; context = !context.nodeType && context[0] || context; context = context.ownerDocument || context; // Only cache "small" (1/2 KB) HTML strings that are associated with the main document // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put or elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 if ( args.length === 1 && typeof first === "string" && first.length < 512 && context === document && first.charAt(0) === "<" && !rnocache.test( first ) && (jQuery.support.checkClone || !rchecked.test( first )) && (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { // Mark cacheable and look for a hit cacheable = true; fragment = jQuery.fragments[ first ]; cachehit = fragment !== undefined; } if ( !fragment ) { fragment = context.createDocumentFragment(); jQuery.clean( args, context, fragment, scripts ); // Update the cache, but only store false // unless this is a second parsing of the same content if ( cacheable ) { jQuery.fragments[ first ] = cachehit && fragment; } } return { fragment: fragment, cacheable: cacheable }; }; jQuery.fragments = {}; jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, i = 0, ret = [], insert = jQuery( selector ), l = insert.length, parent = this.length === 1 && this[0].parentNode; if ( (parent == null || parent && parent.nodeType === 11 && parent.childNodes.length === 1) && l === 1 ) { insert[ original ]( this[0] ); return this; } else { for ( ; i < l; i++ ) { elems = ( i > 0 ? this.clone(true) : this ).get(); jQuery( insert[i] )[ original ]( elems ); ret = ret.concat( elems ); } return this.pushStack( ret, name, insert.selector ); } }; }); function getAll( elem ) { if ( typeof elem.getElementsByTagName !== "undefined" ) { return elem.getElementsByTagName( "*" ); } else if ( typeof elem.querySelectorAll !== "undefined" ) { return elem.querySelectorAll( "*" ); } else { return []; } } // Used in clean, fixes the defaultChecked property function fixDefaultChecked( elem ) { if ( rcheckableType.test( elem.type ) ) { elem.defaultChecked = elem.checked; } } jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { var srcElements, destElements, i, clone; if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { clone = elem.cloneNode( true ); // IE<=8 does not properly clone detached, unknown element nodes } else { fragmentDiv.innerHTML = elem.outerHTML; fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); } if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { // IE copies events bound via attachEvent when using cloneNode. // Calling detachEvent on the clone will also remove the events // from the original. In order to get around this, we use some // proprietary methods to clear the events. Thanks to MooTools // guys for this hotness. cloneFixAttributes( elem, clone ); // Using Sizzle here is crazy slow, so we use getElementsByTagName instead srcElements = getAll( elem ); destElements = getAll( clone ); // Weird iteration because IE will replace the length property // with an element if you are cloning the body and one of the // elements on the page has a name or id of "length" for ( i = 0; srcElements[i]; ++i ) { // Ensure that the destination node is not null; Fixes #9587 if ( destElements[i] ) { cloneFixAttributes( srcElements[i], destElements[i] ); } } } // Copy the events from the original to the clone if ( dataAndEvents ) { cloneCopyEvent( elem, clone ); if ( deepDataAndEvents ) { srcElements = getAll( elem ); destElements = getAll( clone ); for ( i = 0; srcElements[i]; ++i ) { cloneCopyEvent( srcElements[i], destElements[i] ); } } } srcElements = destElements = null; // Return the cloned set return clone; }, clean: function( elems, context, fragment, scripts ) { var i, j, elem, tag, wrap, depth, div, hasBody, tbody, len, handleScript, jsTags, safe = context === document && safeFragment, ret = []; // Ensure that context is a document if ( !context || typeof context.createDocumentFragment === "undefined" ) { context = document; } // Use the already-created safe fragment if context permits for ( i = 0; (elem = elems[i]) != null; i++ ) { if ( typeof elem === "number" ) { elem += ""; } if ( !elem ) { continue; } // Convert html string into DOM nodes if ( typeof elem === "string" ) { if ( !rhtml.test( elem ) ) { elem = context.createTextNode( elem ); } else { // Ensure a safe container in which to render the html safe = safe || createSafeFragment( context ); div = context.createElement("div"); safe.appendChild( div ); // Fix "XHTML"-style tags in all browsers elem = elem.replace(rxhtmlTag, "<$1>"); // Go to html and back, then peel off extra wrappers tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); wrap = wrapMap[ tag ] || wrapMap._default; depth = wrap[0]; div.innerHTML = wrap[1] + elem + wrap[2]; // Move to the right depth while ( depth-- ) { div = div.lastChild; } // Remove IE's autoinserted from table fragments if ( !jQuery.support.tbody ) { // String was a , *may* have spurious hasBody = rtbody.test(elem); tbody = tag === "table" && !hasBody ? div.firstChild && div.firstChild.childNodes : // String was a bare or wrap[1] === "
      " && !hasBody ? div.childNodes : []; for ( j = tbody.length - 1; j >= 0 ; --j ) { if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { tbody[ j ].parentNode.removeChild( tbody[ j ] ); } } } // IE completely kills leading whitespace when innerHTML is used if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); } elem = div.childNodes; // Take out of fragment container (we need a fresh div each time) div.parentNode.removeChild( div ); } } if ( elem.nodeType ) { ret.push( elem ); } else { jQuery.merge( ret, elem ); } } // Fix #11356: Clear elements from safeFragment if ( div ) { elem = div = safe = null; } // Reset defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) if ( !jQuery.support.appendChecked ) { for ( i = 0; (elem = ret[i]) != null; i++ ) { if ( jQuery.nodeName( elem, "input" ) ) { fixDefaultChecked( elem ); } else if ( typeof elem.getElementsByTagName !== "undefined" ) { jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); } } } // Append elements to a provided document fragment if ( fragment ) { // Special handling of each script element handleScript = function( elem ) { // Check if we consider it executable if ( !elem.type || rscriptType.test( elem.type ) ) { // Detach the script and store it in the scripts array (if provided) or the fragment // Return truthy to indicate that it has been handled return scripts ? scripts.push( elem.parentNode ? elem.parentNode.removeChild( elem ) : elem ) : fragment.appendChild( elem ); } }; for ( i = 0; (elem = ret[i]) != null; i++ ) { // Check if we're done after handling an executable script if ( !( jQuery.nodeName( elem, "script" ) && handleScript( elem ) ) ) { // Append to fragment and handle embedded scripts fragment.appendChild( elem ); if ( typeof elem.getElementsByTagName !== "undefined" ) { // handleScript alters the DOM, so use jQuery.merge to ensure snapshot iteration jsTags = jQuery.grep( jQuery.merge( [], elem.getElementsByTagName("script") ), handleScript ); // Splice the scripts into ret after their former ancestor and advance our index beyond them ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); i += jsTags.length; } } } } return ret; }, cleanData: function( elems, /* internal */ acceptData ) { var data, id, elem, type, i = 0, internalKey = jQuery.expando, cache = jQuery.cache, deleteExpando = jQuery.support.deleteExpando, special = jQuery.event.special; for ( ; (elem = elems[i]) != null; i++ ) { if ( acceptData || jQuery.acceptData( elem ) ) { id = elem[ internalKey ]; data = id && cache[ id ]; if ( data ) { if ( data.events ) { for ( type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } } // Remove cache only if it was not already removed by jQuery.event.remove if ( cache[ id ] ) { delete cache[ id ]; // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( deleteExpando ) { delete elem[ internalKey ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( internalKey ); } else { elem[ internalKey ] = null; } jQuery.deletedIds.push( id ); } } } } } }); // Limit scope pollution from any deprecated API (function() { var matched, browser; // Use of jQuery.browser is frowned upon. // More details: http://api.jquery.com/jQuery.browser // jQuery.uaMatch maintained for back-compat jQuery.uaMatch = function( ua ) { ua = ua.toLowerCase(); var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || /(webkit)[ \/]([\w.]+)/.exec( ua ) || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || /(msie) ([\w.]+)/.exec( ua ) || ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || []; return { browser: match[ 1 ] || "", version: match[ 2 ] || "0" }; }; matched = jQuery.uaMatch( navigator.userAgent ); browser = {}; if ( matched.browser ) { browser[ matched.browser ] = true; browser.version = matched.version; } // Chrome is Webkit, but Webkit is also Safari. if ( browser.chrome ) { browser.webkit = true; } else if ( browser.webkit ) { browser.safari = true; } jQuery.browser = browser; jQuery.sub = function() { function jQuerySub( selector, context ) { return new jQuerySub.fn.init( selector, context ); } jQuery.extend( true, jQuerySub, this ); jQuerySub.superclass = this; jQuerySub.fn = jQuerySub.prototype = this(); jQuerySub.fn.constructor = jQuerySub; jQuerySub.sub = this.sub; jQuerySub.fn.init = function init( selector, context ) { if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { context = jQuerySub( context ); } return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); }; jQuerySub.fn.init.prototype = jQuerySub.fn; var rootjQuerySub = jQuerySub(document); return jQuerySub; }; })(); var curCSS, iframe, iframeDoc, ralpha = /alpha\([^)]*\)/i, ropacity = /opacity=([^)]*)/, rposition = /^(top|right|bottom|left)$/, // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display rdisplayswap = /^(none|table(?!-c[ea]).+)/, rmargin = /^margin/, rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), rrelNum = new RegExp( "^([-+])=(" + core_pnum + ")", "i" ), elemdisplay = { BODY: "block" }, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssNormalTransform = { letterSpacing: 0, fontWeight: 400 }, cssExpand = [ "Top", "Right", "Bottom", "Left" ], cssPrefixes = [ "Webkit", "O", "Moz", "ms" ], eventsToggle = jQuery.fn.toggle; // return a css property mapped to a potentially vendor prefixed property function vendorPropName( style, name ) { // shortcut for names that are not vendor prefixed if ( name in style ) { return name; } // check for vendor prefixed names var capName = name.charAt(0).toUpperCase() + name.slice(1), origName = name, i = cssPrefixes.length; while ( i-- ) { name = cssPrefixes[ i ] + capName; if ( name in style ) { return name; } } return origName; } function isHidden( elem, el ) { elem = el || elem; return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); } function showHide( elements, show ) { var elem, display, values = [], index = 0, length = elements.length; for ( ; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } values[ index ] = jQuery._data( elem, "olddisplay" ); if ( show ) { // Reset the inline display of this element to learn if it is // being hidden by cascaded rules or not if ( !values[ index ] && elem.style.display === "none" ) { elem.style.display = ""; } // Set elements which have been overridden with display: none // in a stylesheet to whatever the default browser style is // for such an element if ( elem.style.display === "" && isHidden( elem ) ) { values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); } } else { display = curCSS( elem, "display" ); if ( !values[ index ] && display !== "none" ) { jQuery._data( elem, "olddisplay", display ); } } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( index = 0; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } if ( !show || elem.style.display === "none" || elem.style.display === "" ) { elem.style.display = show ? values[ index ] || "" : "none"; } } return elements; } jQuery.fn.extend({ css: function( name, value ) { return jQuery.access( this, function( elem, name, value ) { return value !== undefined ? jQuery.style( elem, name, value ) : jQuery.css( elem, name ); }, name, value, arguments.length > 1 ); }, show: function() { return showHide( this, true ); }, hide: function() { return showHide( this ); }, toggle: function( state, fn2 ) { var bool = typeof state === "boolean"; if ( jQuery.isFunction( state ) && jQuery.isFunction( fn2 ) ) { return eventsToggle.apply( this, arguments ); } return this.each(function() { if ( bool ? state : isHidden( this ) ) { jQuery( this ).show(); } else { jQuery( this ).hide(); } }); } }); jQuery.extend({ // Add in style property hooks for overriding the default // behavior of getting and setting a style property cssHooks: { opacity: { get: function( elem, computed ) { if ( computed ) { // We should always get a number back from opacity var ret = curCSS( elem, "opacity" ); return ret === "" ? "1" : ret; } } } }, // Exclude the following css properties to add px cssNumber: { "fillOpacity": true, "fontWeight": true, "lineHeight": true, "opacity": true, "orphans": true, "widows": true, "zIndex": true, "zoom": true }, // Add in properties whose names you wish to fix before // setting or getting the value cssProps: { // normalize float css property "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" }, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { // Don't set styles on text and comment nodes if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { return; } // Make sure that we're working with the right name var ret, type, hooks, origName = jQuery.camelCase( name ), style = elem.style; name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); // gets hook for the prefixed version // followed by the unprefixed version hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; // Check if we're setting a value if ( value !== undefined ) { type = typeof value; // convert relative number strings (+= or -=) to relative numbers. #7345 if ( type === "string" && (ret = rrelNum.exec( value )) ) { value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); // Fixes bug #9237 type = "number"; } // Make sure that NaN and null values aren't set. See: #7116 if ( value == null || type === "number" && isNaN( value ) ) { return; } // If a number was passed in, add 'px' to the (except for certain CSS properties) if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } // If a hook was provided, use that value, otherwise just set the specified value if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { // Wrapped to prevent IE from throwing errors when 'invalid' values are provided // Fixes bug #5509 try { style[ name ] = value; } catch(e) {} } } else { // If a hook was provided get the non-computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { return ret; } // Otherwise just get the value from the style object return style[ name ]; } }, css: function( elem, name, numeric, extra ) { var val, num, hooks, origName = jQuery.camelCase( name ); // Make sure that we're working with the right name name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); // gets hook for the prefixed version // followed by the unprefixed version hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; // If a hook was provided get the computed value from there if ( hooks && "get" in hooks ) { val = hooks.get( elem, true, extra ); } // Otherwise, if a way to get the computed value exists, use that if ( val === undefined ) { val = curCSS( elem, name ); } //convert "normal" to computed value if ( val === "normal" && name in cssNormalTransform ) { val = cssNormalTransform[ name ]; } // Return, converting to number if forced or a qualifier was provided and val looks numeric if ( numeric || extra !== undefined ) { num = parseFloat( val ); return numeric || jQuery.isNumeric( num ) ? num || 0 : val; } return val; }, // A method for quickly swapping in/out CSS properties to get correct calculations swap: function( elem, options, callback ) { var ret, name, old = {}; // Remember the old values, and insert the new ones for ( name in options ) { old[ name ] = elem.style[ name ]; elem.style[ name ] = options[ name ]; } ret = callback.call( elem ); // Revert the old values for ( name in options ) { elem.style[ name ] = old[ name ]; } return ret; } }); // NOTE: To any future maintainer, we've window.getComputedStyle // because jsdom on node.js will break without it. if ( window.getComputedStyle ) { curCSS = function( elem, name ) { var ret, width, minWidth, maxWidth, computed = window.getComputedStyle( elem, null ), style = elem.style; if ( computed ) { // getPropertyValue is only needed for .css('filter') in IE9, see #12537 ret = computed.getPropertyValue( name ) || computed[ name ]; if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { ret = jQuery.style( elem, name ); } // A tribute to the "awesome hack by Dean Edwards" // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { width = style.width; minWidth = style.minWidth; maxWidth = style.maxWidth; style.minWidth = style.maxWidth = style.width = ret; ret = computed.width; style.width = width; style.minWidth = minWidth; style.maxWidth = maxWidth; } } return ret; }; } else if ( document.documentElement.currentStyle ) { curCSS = function( elem, name ) { var left, rsLeft, ret = elem.currentStyle && elem.currentStyle[ name ], style = elem.style; // Avoid setting ret to empty string here // so we don't default to auto if ( ret == null && style && style[ name ] ) { ret = style[ name ]; } // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels // but not position css attributes, as those are proportional to the parent element instead // and we can't measure the parent instead because it might trigger a "stacking dolls" problem if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { // Remember the original values left = style.left; rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; // Put in the new values to get a computed value out if ( rsLeft ) { elem.runtimeStyle.left = elem.currentStyle.left; } style.left = name === "fontSize" ? "1em" : ret; ret = style.pixelLeft + "px"; // Revert the changed values style.left = left; if ( rsLeft ) { elem.runtimeStyle.left = rsLeft; } } return ret === "" ? "auto" : ret; }; } function setPositiveNumber( elem, value, subtract ) { var matches = rnumsplit.exec( value ); return matches ? Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : value; } function augmentWidthOrHeight( elem, name, extra, isBorderBox ) { var i = extra === ( isBorderBox ? "border" : "content" ) ? // If we already have the right measurement, avoid augmentation 4 : // Otherwise initialize for horizontal or vertical properties name === "width" ? 1 : 0, val = 0; for ( ; i < 4; i += 2 ) { // both box models exclude margin, so add it if we want it if ( extra === "margin" ) { // we use jQuery.css instead of curCSS here // because of the reliableMarginRight CSS hook! val += jQuery.css( elem, extra + cssExpand[ i ], true ); } // From this point on we use curCSS for maximum performance (relevant in animations) if ( isBorderBox ) { // border-box includes padding, so remove it if we want content if ( extra === "content" ) { val -= parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; } // at this point, extra isn't border nor margin, so remove border if ( extra !== "margin" ) { val -= parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; } } else { // at this point, extra isn't content, so add padding val += parseFloat( curCSS( elem, "padding" + cssExpand[ i ] ) ) || 0; // at this point, extra isn't content nor padding, so add border if ( extra !== "padding" ) { val += parseFloat( curCSS( elem, "border" + cssExpand[ i ] + "Width" ) ) || 0; } } } return val; } function getWidthOrHeight( elem, name, extra ) { // Start with offset property, which is equivalent to the border-box value var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, valueIsBorderBox = true, isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box"; // some non-html elements return undefined for offsetWidth, so check for null/undefined // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 if ( val <= 0 || val == null ) { // Fall back to computed then uncomputed css if necessary val = curCSS( elem, name ); if ( val < 0 || val == null ) { val = elem.style[ name ]; } // Computed unit is not pixels. Stop here and return. if ( rnumnonpx.test(val) ) { return val; } // we need the check for style in case a browser which returns unreliable values // for getComputedStyle silently falls back to the reliable elem.style valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); // Normalize "", auto, and prepare for extra val = parseFloat( val ) || 0; } // use the active box-sizing model to add/subtract irrelevant styles return ( val + augmentWidthOrHeight( elem, name, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox ) ) + "px"; } // Try to determine the default display value of an element function css_defaultDisplay( nodeName ) { if ( elemdisplay[ nodeName ] ) { return elemdisplay[ nodeName ]; } var elem = jQuery( "<" + nodeName + ">" ).appendTo( document.body ), display = elem.css("display"); elem.remove(); // If the simple way fails, // get element's real default display by attaching it to a temp iframe if ( display === "none" || display === "" ) { // Use the already-created iframe if possible iframe = document.body.appendChild( iframe || jQuery.extend( document.createElement("iframe"), { frameBorder: 0, width: 0, height: 0 }) ); // Create a cacheable copy of the iframe document on first call. // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML // document to it; WebKit & Firefox won't allow reusing the iframe document. if ( !iframeDoc || !iframe.createElement ) { iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; iframeDoc.write(""); iframeDoc.close(); } elem = iframeDoc.body.appendChild( iframeDoc.createElement(nodeName) ); display = curCSS( elem, "display" ); document.body.removeChild( iframe ); } // Store the correct default display elemdisplay[ nodeName ] = display; return display; } jQuery.each([ "height", "width" ], function( i, name ) { jQuery.cssHooks[ name ] = { get: function( elem, computed, extra ) { if ( computed ) { // certain elements can have dimension info if we invisibly show them // however, it must have a current display style that would benefit from this if ( elem.offsetWidth === 0 && rdisplayswap.test( curCSS( elem, "display" ) ) ) { return jQuery.swap( elem, cssShow, function() { return getWidthOrHeight( elem, name, extra ); }); } else { return getWidthOrHeight( elem, name, extra ); } } }, set: function( elem, value, extra ) { return setPositiveNumber( elem, value, extra ? augmentWidthOrHeight( elem, name, extra, jQuery.support.boxSizing && jQuery.css( elem, "boxSizing" ) === "border-box" ) : 0 ); } }; }); if ( !jQuery.support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE uses filters for opacity return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { var style = elem.style, currentStyle = elem.currentStyle, opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && style.removeAttribute ) { // Setting style.filter to null, "" & " " still leave "filter:" in the cssText // if "filter:" is present at all, clearType is disabled, we want to avoid this // style.removeAttribute is IE Only, but so apparently is this code path... style.removeAttribute( "filter" ); // if there there is no filter style applied in a css rule, we are done if ( currentStyle && !currentStyle.filter ) { return; } } // otherwise, set new filter values style.filter = ralpha.test( filter ) ? filter.replace( ralpha, opacity ) : filter + " " + opacity; } }; } // These hooks cannot be added until DOM ready because the support test // for it is not run until after DOM ready jQuery(function() { if ( !jQuery.support.reliableMarginRight ) { jQuery.cssHooks.marginRight = { get: function( elem, computed ) { // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right // Work around by temporarily setting element display to inline-block return jQuery.swap( elem, { "display": "inline-block" }, function() { if ( computed ) { return curCSS( elem, "marginRight" ); } }); } }; } // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 // getComputedStyle returns percent when specified for top/left/bottom/right // rather than make the css module depend on the offset module, we just check for it here if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { jQuery.each( [ "top", "left" ], function( i, prop ) { jQuery.cssHooks[ prop ] = { get: function( elem, computed ) { if ( computed ) { var ret = curCSS( elem, prop ); // if curCSS returns percentage, fallback to offset return rnumnonpx.test( ret ) ? jQuery( elem ).position()[ prop ] + "px" : ret; } } }; }); } }); if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.hidden = function( elem ) { return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || curCSS( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { return !jQuery.expr.filters.hidden( elem ); }; } // These hooks are used by animate to expand properties jQuery.each({ margin: "", padding: "", border: "Width" }, function( prefix, suffix ) { jQuery.cssHooks[ prefix + suffix ] = { expand: function( value ) { var i, // assumes a single number if not a string parts = typeof value === "string" ? value.split(" ") : [ value ], expanded = {}; for ( i = 0; i < 4; i++ ) { expanded[ prefix + cssExpand[ i ] + suffix ] = parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; } return expanded; } }; if ( !rmargin.test( prefix ) ) { jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; } }); var r20 = /%20/g, rbracket = /\[\]$/, rCRLF = /\r?\n/g, rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, rselectTextarea = /^(?:select|textarea)/i; jQuery.fn.extend({ serialize: function() { return jQuery.param( this.serializeArray() ); }, serializeArray: function() { return this.map(function(){ return this.elements ? jQuery.makeArray( this.elements ) : this; }) .filter(function(){ return this.name && !this.disabled && ( this.checked || rselectTextarea.test( this.nodeName ) || rinput.test( this.type ) ); }) .map(function( i, elem ){ var val = jQuery( this ).val(); return val == null ? null : jQuery.isArray( val ) ? jQuery.map( val, function( val, i ){ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }) : { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }).get(); } }); //Serialize an array of form elements or a set of //key/values into a query string jQuery.param = function( a, traditional ) { var prefix, s = [], add = function( key, value ) { // If value is a function, invoke it and return its value value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); }; // Set traditional to true for jQuery <= 1.3.2 behavior. if ( traditional === undefined ) { traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; } // If an array was passed in, assume that it is an array of form elements. if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { // Serialize the form elements jQuery.each( a, function() { add( this.name, this.value ); }); } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for ( prefix in a ) { buildParams( prefix, a[ prefix ], traditional, add ); } } // Return the resulting serialization return s.join( "&" ).replace( r20, "+" ); }; function buildParams( prefix, obj, traditional, add ) { var name; if ( jQuery.isArray( obj ) ) { // Serialize array item. jQuery.each( obj, function( i, v ) { if ( traditional || rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); } else { // If array item is non-scalar (array or object), encode its // numeric index to resolve deserialization ambiguity issues. // Note that rack (as of 1.0.0) can't currently deserialize // nested arrays properly, and attempting to do so may cause // a server error. Possible fixes are to modify rack's // deserialization algorithm or to provide an option or flag // to force array serialization to be shallow. buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); } }); } else if ( !traditional && jQuery.type( obj ) === "object" ) { // Serialize object item. for ( name in obj ) { buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); } } else { // Serialize scalar item. add( prefix, obj ); } } var // Document location ajaxLocParts, ajaxLocation, rhash = /#.*$/, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL // #7653, #8125, #8152: local protocol detection rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, rnoContent = /^(?:GET|HEAD)$/, rprotocol = /^\/\//, rquery = /\?/, rscript = /)<[^<]*)*<\/script>/gi, rts = /([?&])_=[^&]*/, rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, // Keep a copy of the old load method _load = jQuery.fn.load, /* Prefilters * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) * 2) These are called: * - BEFORE asking for a transport * - AFTER param serialization (s.data is a string if s.processData is true) * 3) key is the dataType * 4) the catchall symbol "*" can be used * 5) execution will start with transport dataType and THEN continue down to "*" if needed */ prefilters = {}, /* Transports bindings * 1) key is the dataType * 2) the catchall symbol "*" can be used * 3) selection will start with transport dataType and THEN go to "*" if needed */ transports = {}, // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression allTypes = ["*/"] + ["*"]; // #8138, IE may throw an exception when accessing // a field from window.location if document.domain has been set try { ajaxLocation = location.href; } catch( e ) { // Use the href attribute of an A element // since IE will modify it given document.location ajaxLocation = document.createElement( "a" ); ajaxLocation.href = ""; ajaxLocation = ajaxLocation.href; } // Segment location into parts ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { // dataTypeExpression is optional and defaults to "*" return function( dataTypeExpression, func ) { if ( typeof dataTypeExpression !== "string" ) { func = dataTypeExpression; dataTypeExpression = "*"; } var dataType, list, placeBefore, dataTypes = dataTypeExpression.toLowerCase().split( core_rspace ), i = 0, length = dataTypes.length; if ( jQuery.isFunction( func ) ) { // For each dataType in the dataTypeExpression for ( ; i < length; i++ ) { dataType = dataTypes[ i ]; // We control if we're asked to add before // any existing element placeBefore = /^\+/.test( dataType ); if ( placeBefore ) { dataType = dataType.substr( 1 ) || "*"; } list = structure[ dataType ] = structure[ dataType ] || []; // then we add to the structure accordingly list[ placeBefore ? "unshift" : "push" ]( func ); } } }; } // Base inspection function for prefilters and transports function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, dataType /* internal */, inspected /* internal */ ) { dataType = dataType || options.dataTypes[ 0 ]; inspected = inspected || {}; inspected[ dataType ] = true; var selection, list = structure[ dataType ], i = 0, length = list ? list.length : 0, executeOnly = ( structure === prefilters ); for ( ; i < length && ( executeOnly || !selection ); i++ ) { selection = list[ i ]( options, originalOptions, jqXHR ); // If we got redirected to another dataType // we try there if executing only and not done already if ( typeof selection === "string" ) { if ( !executeOnly || inspected[ selection ] ) { selection = undefined; } else { options.dataTypes.unshift( selection ); selection = inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, selection, inspected ); } } } // If we're only executing or nothing was selected // we try the catchall dataType if not done already if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { selection = inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, "*", inspected ); } // unnecessary when only executing (prefilters) // but it'll be ignored by the caller in that case return selection; } // A special extend for ajax options // that takes "flat" options (not to be deep extended) // Fixes #9887 function ajaxExtend( target, src ) { var key, deep, flatOptions = jQuery.ajaxSettings.flatOptions || {}; for ( key in src ) { if ( src[ key ] !== undefined ) { ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; } } if ( deep ) { jQuery.extend( true, target, deep ); } } jQuery.fn.load = function( url, params, callback ) { if ( typeof url !== "string" && _load ) { return _load.apply( this, arguments ); } // Don't do a request if no elements are being requested if ( !this.length ) { return this; } var selector, type, response, self = this, off = url.indexOf(" "); if ( off >= 0 ) { selector = url.slice( off, url.length ); url = url.slice( 0, off ); } // If it's a function if ( jQuery.isFunction( params ) ) { // We assume that it's the callback callback = params; params = undefined; // Otherwise, build a param string } else if ( params && typeof params === "object" ) { type = "POST"; } // Request the remote document jQuery.ajax({ url: url, // if "type" variable is undefined, then "GET" method will be used type: type, dataType: "html", data: params, complete: function( jqXHR, status ) { if ( callback ) { self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); } } }).done(function( responseText ) { // Save response for use in complete callback response = arguments; // See if a selector was specified self.html( selector ? // Create a dummy div to hold the results jQuery("
      ") // inject the contents of the document in, removing the scripts // to avoid any 'Permission Denied' errors in IE .append( responseText.replace( rscript, "" ) ) // Locate the specified elements .find( selector ) : // If not, just inject the full result responseText ); }); return this; }; // Attach a bunch of functions for handling common AJAX events jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ jQuery.fn[ o ] = function( f ){ return this.on( o, f ); }; }); jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // shift arguments if data argument was omitted if ( jQuery.isFunction( data ) ) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ type: method, url: url, data: data, success: callback, dataType: type }); }; }); jQuery.extend({ getScript: function( url, callback ) { return jQuery.get( url, undefined, callback, "script" ); }, getJSON: function( url, data, callback ) { return jQuery.get( url, data, callback, "json" ); }, // Creates a full fledged settings object into target // with both ajaxSettings and settings fields. // If target is omitted, writes into ajaxSettings. ajaxSetup: function( target, settings ) { if ( settings ) { // Building a settings object ajaxExtend( target, jQuery.ajaxSettings ); } else { // Extending ajaxSettings settings = target; target = jQuery.ajaxSettings; } ajaxExtend( target, settings ); return target; }, ajaxSettings: { url: ajaxLocation, isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), global: true, type: "GET", contentType: "application/x-www-form-urlencoded; charset=UTF-8", processData: true, async: true, /* timeout: 0, data: null, dataType: null, username: null, password: null, cache: null, throws: false, traditional: false, headers: {}, */ accepts: { xml: "application/xml, text/xml", html: "text/html", text: "text/plain", json: "application/json, text/javascript", "*": allTypes }, contents: { xml: /xml/, html: /html/, json: /json/ }, responseFields: { xml: "responseXML", text: "responseText" }, // List of data converters // 1) key format is "source_type destination_type" (a single space in-between) // 2) the catchall symbol "*" can be used for source_type converters: { // Convert anything to text "* text": window.String, // Text to html (true = no transformation) "text html": true, // Evaluate text as a json expression "text json": jQuery.parseJSON, // Parse text as xml "text xml": jQuery.parseXML }, // For options that shouldn't be deep extended: // you can add your own custom options here if // and when you create one that shouldn't be // deep extended (see ajaxExtend) flatOptions: { context: true, url: true } }, ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), ajaxTransport: addToPrefiltersOrTransports( transports ), // Main method ajax: function( url, options ) { // If url is an object, simulate pre-1.5 signature if ( typeof url === "object" ) { options = url; url = undefined; } // Force options to be an object options = options || {}; var // ifModified key ifModifiedKey, // Response headers responseHeadersString, responseHeaders, // transport transport, // timeout handle timeoutTimer, // Cross-domain detection vars parts, // To know if global events are to be dispatched fireGlobals, // Loop variable i, // Create the final options object s = jQuery.ajaxSetup( {}, options ), // Callbacks context callbackContext = s.context || s, // Context for global events // It's the callbackContext if one was provided in the options // and if it's a DOM node or a jQuery collection globalEventContext = callbackContext !== s && ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? jQuery( callbackContext ) : jQuery.event, // Deferreds deferred = jQuery.Deferred(), completeDeferred = jQuery.Callbacks( "once memory" ), // Status-dependent callbacks statusCode = s.statusCode || {}, // Headers (they are sent all at once) requestHeaders = {}, requestHeadersNames = {}, // The jqXHR state state = 0, // Default abort message strAbort = "canceled", // Fake xhr jqXHR = { readyState: 0, // Caches the header setRequestHeader: function( name, value ) { if ( !state ) { var lname = name.toLowerCase(); name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; requestHeaders[ name ] = value; } return this; }, // Raw string getAllResponseHeaders: function() { return state === 2 ? responseHeadersString : null; }, // Builds headers hashtable if needed getResponseHeader: function( key ) { var match; if ( state === 2 ) { if ( !responseHeaders ) { responseHeaders = {}; while( ( match = rheaders.exec( responseHeadersString ) ) ) { responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; } } match = responseHeaders[ key.toLowerCase() ]; } return match === undefined ? null : match; }, // Overrides response content-type header overrideMimeType: function( type ) { if ( !state ) { s.mimeType = type; } return this; }, // Cancel the request abort: function( statusText ) { statusText = statusText || strAbort; if ( transport ) { transport.abort( statusText ); } done( 0, statusText ); return this; } }; // Callback for when everything is done // It is defined here because jslint complains if it is declared // at the end of the function (which would be more logical and readable) function done( status, nativeStatusText, responses, headers ) { var isSuccess, success, error, response, modified, statusText = nativeStatusText; // Called once if ( state === 2 ) { return; } // State is "done" now state = 2; // Clear timeout if it exists if ( timeoutTimer ) { clearTimeout( timeoutTimer ); } // Dereference transport for early garbage collection // (no matter how long the jqXHR object will be used) transport = undefined; // Cache response headers responseHeadersString = headers || ""; // Set readyState jqXHR.readyState = status > 0 ? 4 : 0; // Get response data if ( responses ) { response = ajaxHandleResponses( s, jqXHR, responses ); } // If successful, handle type chaining if ( status >= 200 && status < 300 || status === 304 ) { // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { modified = jqXHR.getResponseHeader("Last-Modified"); if ( modified ) { jQuery.lastModified[ ifModifiedKey ] = modified; } modified = jqXHR.getResponseHeader("Etag"); if ( modified ) { jQuery.etag[ ifModifiedKey ] = modified; } } // If not modified if ( status === 304 ) { statusText = "notmodified"; isSuccess = true; // If we have data } else { isSuccess = ajaxConvert( s, response ); statusText = isSuccess.state; success = isSuccess.data; error = isSuccess.error; isSuccess = !error; } } else { // We extract error from statusText // then normalize statusText and status for non-aborts error = statusText; if ( !statusText || status ) { statusText = "error"; if ( status < 0 ) { status = 0; } } } // Set data for the fake xhr object jqXHR.status = status; jqXHR.statusText = ( nativeStatusText || statusText ) + ""; // Success/Error if ( isSuccess ) { deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); } else { deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); } // Status-dependent callbacks jqXHR.statusCode( statusCode ); statusCode = undefined; if ( fireGlobals ) { globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), [ jqXHR, s, isSuccess ? success : error ] ); } // Complete completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); if ( fireGlobals ) { globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); // Handle the global AJAX counter if ( !( --jQuery.active ) ) { jQuery.event.trigger( "ajaxStop" ); } } } // Attach deferreds deferred.promise( jqXHR ); jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail; jqXHR.complete = completeDeferred.add; // Status-dependent callbacks jqXHR.statusCode = function( map ) { if ( map ) { var tmp; if ( state < 2 ) { for ( tmp in map ) { statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; } } else { tmp = map[ jqXHR.status ]; jqXHR.always( tmp ); } } return this; }; // Remove hash character (#7531: and string promotion) // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) // We also use the url parameter if available s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); // Extract dataTypes list s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( core_rspace ); // A cross-domain request is in order when we have a protocol:host:port mismatch if ( s.crossDomain == null ) { parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) ); } // Convert data if not already a string if ( s.data && s.processData && typeof s.data !== "string" ) { s.data = jQuery.param( s.data, s.traditional ); } // Apply prefilters inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); // If request was aborted inside a prefilter, stop there if ( state === 2 ) { return jqXHR; } // We can fire global events as of now if asked to fireGlobals = s.global; // Uppercase the type s.type = s.type.toUpperCase(); // Determine if request has content s.hasContent = !rnoContent.test( s.type ); // Watch for a new set of requests if ( fireGlobals && jQuery.active++ === 0 ) { jQuery.event.trigger( "ajaxStart" ); } // More options handling for requests with no content if ( !s.hasContent ) { // If data is available, append data to url if ( s.data ) { s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; // #9682: remove data so that it's not used in an eventual retry delete s.data; } // Get ifModifiedKey before adding the anti-cache parameter ifModifiedKey = s.url; // Add anti-cache in url if needed if ( s.cache === false ) { var ts = jQuery.now(), // try replacing _= if it is there ret = s.url.replace( rts, "$1_=" + ts ); // if nothing was replaced, add timestamp to the end s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); } } // Set the correct header, if data is being sent if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { jqXHR.setRequestHeader( "Content-Type", s.contentType ); } // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { ifModifiedKey = ifModifiedKey || s.url; if ( jQuery.lastModified[ ifModifiedKey ] ) { jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); } if ( jQuery.etag[ ifModifiedKey ] ) { jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); } } // Set the Accepts header for the server, depending on the dataType jqXHR.setRequestHeader( "Accept", s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : s.accepts[ "*" ] ); // Check for headers option for ( i in s.headers ) { jqXHR.setRequestHeader( i, s.headers[ i ] ); } // Allow custom headers/mimetypes and early abort if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { // Abort if not done already and return return jqXHR.abort(); } // aborting is no longer a cancellation strAbort = "abort"; // Install callbacks on deferreds for ( i in { success: 1, error: 1, complete: 1 } ) { jqXHR[ i ]( s[ i ] ); } // Get transport transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); // If no transport, we auto-abort if ( !transport ) { done( -1, "No Transport" ); } else { jqXHR.readyState = 1; // Send global event if ( fireGlobals ) { globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); } // Timeout if ( s.async && s.timeout > 0 ) { timeoutTimer = setTimeout( function(){ jqXHR.abort( "timeout" ); }, s.timeout ); } try { state = 1; transport.send( requestHeaders, done ); } catch (e) { // Propagate exception as error if not done if ( state < 2 ) { done( -1, e ); // Simply rethrow otherwise } else { throw e; } } } return jqXHR; }, // Counter for holding the number of active queries active: 0, // Last-Modified header cache for next request lastModified: {}, etag: {} }); /* Handles responses to an ajax request: * - sets all responseXXX fields accordingly * - finds the right dataType (mediates between content-type and expected dataType) * - returns the corresponding response */ function ajaxHandleResponses( s, jqXHR, responses ) { var ct, type, finalDataType, firstDataType, contents = s.contents, dataTypes = s.dataTypes, responseFields = s.responseFields; // Fill responseXXX fields for ( type in responseFields ) { if ( type in responses ) { jqXHR[ responseFields[type] ] = responses[ type ]; } } // Remove auto dataType and get content-type in the process while( dataTypes[ 0 ] === "*" ) { dataTypes.shift(); if ( ct === undefined ) { ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); } } // Check if we're dealing with a known content-type if ( ct ) { for ( type in contents ) { if ( contents[ type ] && contents[ type ].test( ct ) ) { dataTypes.unshift( type ); break; } } } // Check to see if we have a response for the expected dataType if ( dataTypes[ 0 ] in responses ) { finalDataType = dataTypes[ 0 ]; } else { // Try convertible dataTypes for ( type in responses ) { if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { finalDataType = type; break; } if ( !firstDataType ) { firstDataType = type; } } // Or just use first one finalDataType = finalDataType || firstDataType; } // If we found a dataType // We add the dataType to the list if needed // and return the corresponding response if ( finalDataType ) { if ( finalDataType !== dataTypes[ 0 ] ) { dataTypes.unshift( finalDataType ); } return responses[ finalDataType ]; } } // Chain conversions given the request and the original response function ajaxConvert( s, response ) { var conv, conv2, current, tmp, // Work with a copy of dataTypes in case we need to modify it for conversion dataTypes = s.dataTypes.slice(), prev = dataTypes[ 0 ], converters = {}, i = 0; // Apply the dataFilter if provided if ( s.dataFilter ) { response = s.dataFilter( response, s.dataType ); } // Create converters map with lowercased keys if ( dataTypes[ 1 ] ) { for ( conv in s.converters ) { converters[ conv.toLowerCase() ] = s.converters[ conv ]; } } // Convert to each sequential dataType, tolerating list modification for ( ; (current = dataTypes[++i]); ) { // There's only work to do if current dataType is non-auto if ( current !== "*" ) { // Convert response if prev dataType is non-auto and differs from current if ( prev !== "*" && prev !== current ) { // Seek a direct converter conv = converters[ prev + " " + current ] || converters[ "* " + current ]; // If none found, seek a pair if ( !conv ) { for ( conv2 in converters ) { // If conv2 outputs current tmp = conv2.split(" "); if ( tmp[ 1 ] === current ) { // If prev can be converted to accepted input conv = converters[ prev + " " + tmp[ 0 ] ] || converters[ "* " + tmp[ 0 ] ]; if ( conv ) { // Condense equivalence converters if ( conv === true ) { conv = converters[ conv2 ]; // Otherwise, insert the intermediate dataType } else if ( converters[ conv2 ] !== true ) { current = tmp[ 0 ]; dataTypes.splice( i--, 0, current ); } break; } } } } // Apply converter (if not an equivalence) if ( conv !== true ) { // Unless errors are allowed to bubble, catch and return them if ( conv && s["throws"] ) { response = conv( response ); } else { try { response = conv( response ); } catch ( e ) { return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; } } } } // Update prev for next iteration prev = current; } } return { state: "success", data: response }; } var oldCallbacks = [], rquestion = /\?/, rjsonp = /(=)\?(?=&|$)|\?\?/, nonce = jQuery.now(); // Default jsonp settings jQuery.ajaxSetup({ jsonp: "callback", jsonpCallback: function() { var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); this[ callback ] = true; return callback; } }); // Detect, normalize options and install callbacks for jsonp requests jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { var callbackName, overwritten, responseContainer, data = s.data, url = s.url, hasCallback = s.jsonp !== false, replaceInUrl = hasCallback && rjsonp.test( url ), replaceInData = hasCallback && !replaceInUrl && typeof data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( data ); // Handle iff the expected data type is "jsonp" or we have a parameter to set if ( s.dataTypes[ 0 ] === "jsonp" || replaceInUrl || replaceInData ) { // Get callback name, remembering preexisting value associated with it callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback; overwritten = window[ callbackName ]; // Insert callback into url or form data if ( replaceInUrl ) { s.url = url.replace( rjsonp, "$1" + callbackName ); } else if ( replaceInData ) { s.data = data.replace( rjsonp, "$1" + callbackName ); } else if ( hasCallback ) { s.url += ( rquestion.test( url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; } // Use data converter to retrieve json after script execution s.converters["script json"] = function() { if ( !responseContainer ) { jQuery.error( callbackName + " was not called" ); } return responseContainer[ 0 ]; }; // force json dataType s.dataTypes[ 0 ] = "json"; // Install callback window[ callbackName ] = function() { responseContainer = arguments; }; // Clean-up function (fires after converters) jqXHR.always(function() { // Restore preexisting value window[ callbackName ] = overwritten; // Save back as free if ( s[ callbackName ] ) { // make sure that re-using the options doesn't screw things around s.jsonpCallback = originalSettings.jsonpCallback; // save the callback name for future use oldCallbacks.push( callbackName ); } // Call if it was a function and we have a response if ( responseContainer && jQuery.isFunction( overwritten ) ) { overwritten( responseContainer[ 0 ] ); } responseContainer = overwritten = undefined; }); // Delegate to script return "script"; } }); // Install script dataType jQuery.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /javascript|ecmascript/ }, converters: { "text script": function( text ) { jQuery.globalEval( text ); return text; } } }); // Handle cache's special case and global jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } if ( s.crossDomain ) { s.type = "GET"; s.global = false; } }); // Bind script tag hack transport jQuery.ajaxTransport( "script", function(s) { // This transport only deals with cross domain requests if ( s.crossDomain ) { var script, head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; return { send: function( _, callback ) { script = document.createElement( "script" ); script.async = "async"; if ( s.scriptCharset ) { script.charset = s.scriptCharset; } script.src = s.url; // Attach handlers for all browsers script.onload = script.onreadystatechange = function( _, isAbort ) { if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; // Remove the script if ( head && script.parentNode ) { head.removeChild( script ); } // Dereference the script script = undefined; // Callback if not abort if ( !isAbort ) { callback( 200, "success" ); } } }; // Use insertBefore instead of appendChild to circumvent an IE6 bug. // This arises when a base node is used (#2709 and #4378). head.insertBefore( script, head.firstChild ); }, abort: function() { if ( script ) { script.onload( 0, 1 ); } } }; } }); var xhrCallbacks, // #5280: Internet Explorer will keep connections alive if we don't abort on unload xhrOnUnloadAbort = window.ActiveXObject ? function() { // Abort all pending requests for ( var key in xhrCallbacks ) { xhrCallbacks[ key ]( 0, 1 ); } } : false, xhrId = 0; // Functions to create xhrs function createStandardXHR() { try { return new window.XMLHttpRequest(); } catch( e ) {} } function createActiveXHR() { try { return new window.ActiveXObject( "Microsoft.XMLHTTP" ); } catch( e ) {} } // Create the request object // (This is still attached to ajaxSettings for backward compatibility) jQuery.ajaxSettings.xhr = window.ActiveXObject ? /* Microsoft failed to properly * implement the XMLHttpRequest in IE7 (can't request local files), * so we use the ActiveXObject when it is available * Additionally XMLHttpRequest can be disabled in IE7/IE8 so * we need a fallback. */ function() { return !this.isLocal && createStandardXHR() || createActiveXHR(); } : // For all other browsers, use the standard XMLHttpRequest object createStandardXHR; // Determine support properties (function( xhr ) { jQuery.extend( jQuery.support, { ajax: !!xhr, cors: !!xhr && ( "withCredentials" in xhr ) }); })( jQuery.ajaxSettings.xhr() ); // Create transport if the browser can provide an xhr if ( jQuery.support.ajax ) { jQuery.ajaxTransport(function( s ) { // Cross domain only allowed if supported through XMLHttpRequest if ( !s.crossDomain || jQuery.support.cors ) { var callback; return { send: function( headers, complete ) { // Get a new xhr var handle, i, xhr = s.xhr(); // Open the socket // Passing null username, generates a login popup on Opera (#2865) if ( s.username ) { xhr.open( s.type, s.url, s.async, s.username, s.password ); } else { xhr.open( s.type, s.url, s.async ); } // Apply custom fields if provided if ( s.xhrFields ) { for ( i in s.xhrFields ) { xhr[ i ] = s.xhrFields[ i ]; } } // Override mime type if needed if ( s.mimeType && xhr.overrideMimeType ) { xhr.overrideMimeType( s.mimeType ); } // X-Requested-With header // For cross-domain requests, seeing as conditions for a preflight are // akin to a jigsaw puzzle, we simply never set it to be sure. // (it can always be set on a per-request basis or even using ajaxSetup) // For same-domain requests, won't change header if already provided. if ( !s.crossDomain && !headers["X-Requested-With"] ) { headers[ "X-Requested-With" ] = "XMLHttpRequest"; } // Need an extra try/catch for cross domain requests in Firefox 3 try { for ( i in headers ) { xhr.setRequestHeader( i, headers[ i ] ); } } catch( _ ) {} // Do send the request // This may raise an exception which is actually // handled in jQuery.ajax (so no try/catch here) xhr.send( ( s.hasContent && s.data ) || null ); // Listener callback = function( _, isAbort ) { var status, statusText, responseHeaders, responses, xml; // Firefox throws exceptions when accessing properties // of an xhr when a network error occurred // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) try { // Was never called and is aborted or complete if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // Only called once callback = undefined; // Do not keep as active anymore if ( handle ) { xhr.onreadystatechange = jQuery.noop; if ( xhrOnUnloadAbort ) { delete xhrCallbacks[ handle ]; } } // If it's an abort if ( isAbort ) { // Abort it manually if needed if ( xhr.readyState !== 4 ) { xhr.abort(); } } else { status = xhr.status; responseHeaders = xhr.getAllResponseHeaders(); responses = {}; xml = xhr.responseXML; // Construct response list if ( xml && xml.documentElement /* #4958 */ ) { responses.xml = xml; } // When requesting binary data, IE6-9 will throw an exception // on any attempt to access responseText (#11426) try { responses.text = xhr.responseText; } catch( e ) { } // Firefox throws an exception when accessing // statusText for faulty cross-domain requests try { statusText = xhr.statusText; } catch( e ) { // We normalize with Webkit giving an empty statusText statusText = ""; } // Filter status for non standard behaviors // If the request is local and we have data: assume a success // (success with no data won't get notified, that's the best we // can do given current implementations) if ( !status && s.isLocal && !s.crossDomain ) { status = responses.text ? 200 : 404; // IE - #1450: sometimes returns 1223 when it should be 204 } else if ( status === 1223 ) { status = 204; } } } } catch( firefoxAccessException ) { if ( !isAbort ) { complete( -1, firefoxAccessException ); } } // Call complete if needed if ( responses ) { complete( status, statusText, responses, responseHeaders ); } }; if ( !s.async ) { // if we're in sync mode we fire the callback callback(); } else if ( xhr.readyState === 4 ) { // (IE6 & IE7) if it's in cache and has been // retrieved directly we need to fire the callback setTimeout( callback, 0 ); } else { handle = ++xhrId; if ( xhrOnUnloadAbort ) { // Create the active xhrs callbacks list if needed // and attach the unload handler if ( !xhrCallbacks ) { xhrCallbacks = {}; jQuery( window ).unload( xhrOnUnloadAbort ); } // Add to list of active xhrs callbacks xhrCallbacks[ handle ] = callback; } xhr.onreadystatechange = callback; } }, abort: function() { if ( callback ) { callback(0,1); } } }; } }); } var fxNow, timerId, rfxtypes = /^(?:toggle|show|hide)$/, rfxnum = new RegExp( "^(?:([-+])=|)(" + core_pnum + ")([a-z%]*)$", "i" ), rrun = /queueHooks$/, animationPrefilters = [ defaultPrefilter ], tweeners = { "*": [function( prop, value ) { var end, unit, tween = this.createTween( prop, value ), parts = rfxnum.exec( value ), target = tween.cur(), start = +target || 0, scale = 1, maxIterations = 20; if ( parts ) { end = +parts[2]; unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" ); // We need to compute starting value if ( unit !== "px" && start ) { // Iteratively approximate from a nonzero starting point // Prefer the current property, because this process will be trivial if it uses the same units // Fallback to end or a simple constant start = jQuery.css( tween.elem, prop, true ) || end || 1; do { // If previous iteration zeroed out, double until we get *something* // Use a string for doubling factor so we don't accidentally see scale as unchanged below scale = scale || ".5"; // Adjust and apply start = start / scale; jQuery.style( tween.elem, prop, start + unit ); // Update scale, tolerating zero or NaN from tween.cur() // And breaking the loop if scale is unchanged or perfect, or if we've just had enough } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); } tween.unit = unit; tween.start = start; // If a +=/-= token was provided, we're doing a relative animation tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end; } return tween; }] }; // Animations created synchronously will run synchronously function createFxNow() { setTimeout(function() { fxNow = undefined; }, 0 ); return ( fxNow = jQuery.now() ); } function createTweens( animation, props ) { jQuery.each( props, function( prop, value ) { var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), index = 0, length = collection.length; for ( ; index < length; index++ ) { if ( collection[ index ].call( animation, prop, value ) ) { // we're done with this property return; } } }); } function Animation( elem, properties, options ) { var result, index = 0, tweenerIndex = 0, length = animationPrefilters.length, deferred = jQuery.Deferred().always( function() { // don't match elem in the :animated selector delete tick.elem; }), tick = function() { var currentTime = fxNow || createFxNow(), remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) temp = remaining / animation.duration || 0, percent = 1 - temp, index = 0, length = animation.tweens.length; for ( ; index < length ; index++ ) { animation.tweens[ index ].run( percent ); } deferred.notifyWith( elem, [ animation, percent, remaining ]); if ( percent < 1 && length ) { return remaining; } else { deferred.resolveWith( elem, [ animation ] ); return false; } }, animation = deferred.promise({ elem: elem, props: jQuery.extend( {}, properties ), opts: jQuery.extend( true, { specialEasing: {} }, options ), originalProperties: properties, originalOptions: options, startTime: fxNow || createFxNow(), duration: options.duration, tweens: [], createTween: function( prop, end, easing ) { var tween = jQuery.Tween( elem, animation.opts, prop, end, animation.opts.specialEasing[ prop ] || animation.opts.easing ); animation.tweens.push( tween ); return tween; }, stop: function( gotoEnd ) { var index = 0, // if we are going to the end, we want to run all the tweens // otherwise we skip this part length = gotoEnd ? animation.tweens.length : 0; for ( ; index < length ; index++ ) { animation.tweens[ index ].run( 1 ); } // resolve when we played the last frame // otherwise, reject if ( gotoEnd ) { deferred.resolveWith( elem, [ animation, gotoEnd ] ); } else { deferred.rejectWith( elem, [ animation, gotoEnd ] ); } return this; } }), props = animation.props; propFilter( props, animation.opts.specialEasing ); for ( ; index < length ; index++ ) { result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); if ( result ) { return result; } } createTweens( animation, props ); if ( jQuery.isFunction( animation.opts.start ) ) { animation.opts.start.call( elem, animation ); } jQuery.fx.timer( jQuery.extend( tick, { anim: animation, queue: animation.opts.queue, elem: elem }) ); // attach callbacks from options return animation.progress( animation.opts.progress ) .done( animation.opts.done, animation.opts.complete ) .fail( animation.opts.fail ) .always( animation.opts.always ); } function propFilter( props, specialEasing ) { var index, name, easing, value, hooks; // camelCase, specialEasing and expand cssHook pass for ( index in props ) { name = jQuery.camelCase( index ); easing = specialEasing[ name ]; value = props[ index ]; if ( jQuery.isArray( value ) ) { easing = value[ 1 ]; value = props[ index ] = value[ 0 ]; } if ( index !== name ) { props[ name ] = value; delete props[ index ]; } hooks = jQuery.cssHooks[ name ]; if ( hooks && "expand" in hooks ) { value = hooks.expand( value ); delete props[ name ]; // not quite $.extend, this wont overwrite keys already present. // also - reusing 'index' from above because we have the correct "name" for ( index in value ) { if ( !( index in props ) ) { props[ index ] = value[ index ]; specialEasing[ index ] = easing; } } } else { specialEasing[ name ] = easing; } } } jQuery.Animation = jQuery.extend( Animation, { tweener: function( props, callback ) { if ( jQuery.isFunction( props ) ) { callback = props; props = [ "*" ]; } else { props = props.split(" "); } var prop, index = 0, length = props.length; for ( ; index < length ; index++ ) { prop = props[ index ]; tweeners[ prop ] = tweeners[ prop ] || []; tweeners[ prop ].unshift( callback ); } }, prefilter: function( callback, prepend ) { if ( prepend ) { animationPrefilters.unshift( callback ); } else { animationPrefilters.push( callback ); } } }); function defaultPrefilter( elem, props, opts ) { var index, prop, value, length, dataShow, toggle, tween, hooks, oldfire, anim = this, style = elem.style, orig = {}, handled = [], hidden = elem.nodeType && isHidden( elem ); // handle queue: false promises if ( !opts.queue ) { hooks = jQuery._queueHooks( elem, "fx" ); if ( hooks.unqueued == null ) { hooks.unqueued = 0; oldfire = hooks.empty.fire; hooks.empty.fire = function() { if ( !hooks.unqueued ) { oldfire(); } }; } hooks.unqueued++; anim.always(function() { // doing this makes sure that the complete handler will be called // before this completes anim.always(function() { hooks.unqueued--; if ( !jQuery.queue( elem, "fx" ).length ) { hooks.empty.fire(); } }); }); } // height/width overflow pass if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and // overflowY are set to the same value opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; // Set display property to inline-block for height/width // animations on inline elements that are having width/height animated if ( jQuery.css( elem, "display" ) === "inline" && jQuery.css( elem, "float" ) === "none" ) { // inline-level elements accept inline-block; // block-level elements need to be inline with layout if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { style.display = "inline-block"; } else { style.zoom = 1; } } } if ( opts.overflow ) { style.overflow = "hidden"; if ( !jQuery.support.shrinkWrapBlocks ) { anim.done(function() { style.overflow = opts.overflow[ 0 ]; style.overflowX = opts.overflow[ 1 ]; style.overflowY = opts.overflow[ 2 ]; }); } } // show/hide pass for ( index in props ) { value = props[ index ]; if ( rfxtypes.exec( value ) ) { delete props[ index ]; toggle = toggle || value === "toggle"; if ( value === ( hidden ? "hide" : "show" ) ) { continue; } handled.push( index ); } } length = handled.length; if ( length ) { dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} ); if ( "hidden" in dataShow ) { hidden = dataShow.hidden; } // store state if its toggle - enables .stop().toggle() to "reverse" if ( toggle ) { dataShow.hidden = !hidden; } if ( hidden ) { jQuery( elem ).show(); } else { anim.done(function() { jQuery( elem ).hide(); }); } anim.done(function() { var prop; jQuery.removeData( elem, "fxshow", true ); for ( prop in orig ) { jQuery.style( elem, prop, orig[ prop ] ); } }); for ( index = 0 ; index < length ; index++ ) { prop = handled[ index ]; tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 ); orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop ); if ( !( prop in dataShow ) ) { dataShow[ prop ] = tween.start; if ( hidden ) { tween.end = tween.start; tween.start = prop === "width" || prop === "height" ? 1 : 0; } } } } } function Tween( elem, options, prop, end, easing ) { return new Tween.prototype.init( elem, options, prop, end, easing ); } jQuery.Tween = Tween; Tween.prototype = { constructor: Tween, init: function( elem, options, prop, end, easing, unit ) { this.elem = elem; this.prop = prop; this.easing = easing || "swing"; this.options = options; this.start = this.now = this.cur(); this.end = end; this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); }, cur: function() { var hooks = Tween.propHooks[ this.prop ]; return hooks && hooks.get ? hooks.get( this ) : Tween.propHooks._default.get( this ); }, run: function( percent ) { var eased, hooks = Tween.propHooks[ this.prop ]; if ( this.options.duration ) { this.pos = eased = jQuery.easing[ this.easing ]( percent, this.options.duration * percent, 0, 1, this.options.duration ); } else { this.pos = eased = percent; } this.now = ( this.end - this.start ) * eased + this.start; if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); } if ( hooks && hooks.set ) { hooks.set( this ); } else { Tween.propHooks._default.set( this ); } return this; } }; Tween.prototype.init.prototype = Tween.prototype; Tween.propHooks = { _default: { get: function( tween ) { var result; if ( tween.elem[ tween.prop ] != null && (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { return tween.elem[ tween.prop ]; } // passing any value as a 4th parameter to .css will automatically // attempt a parseFloat and fallback to a string if the parse fails // so, simple values such as "10px" are parsed to Float. // complex values such as "rotate(1rad)" are returned as is. result = jQuery.css( tween.elem, tween.prop, false, "" ); // Empty strings, null, undefined and "auto" are converted to 0. return !result || result === "auto" ? 0 : result; }, set: function( tween ) { // use step hook for back compat - use cssHook if its there - use .style if its // available and use plain properties where available if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { tween.elem[ tween.prop ] = tween.now; } } } }; // Remove in 2.0 - this supports IE8's panic based approach // to setting things on disconnected nodes Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { set: function( tween ) { if ( tween.elem.nodeType && tween.elem.parentNode ) { tween.elem[ tween.prop ] = tween.now; } } }; jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { var cssFn = jQuery.fn[ name ]; jQuery.fn[ name ] = function( speed, easing, callback ) { return speed == null || typeof speed === "boolean" || // special check for .toggle( handler, handler, ... ) ( !i && jQuery.isFunction( speed ) && jQuery.isFunction( easing ) ) ? cssFn.apply( this, arguments ) : this.animate( genFx( name, true ), speed, easing, callback ); }; }); jQuery.fn.extend({ fadeTo: function( speed, to, easing, callback ) { // show any hidden elements after setting opacity to 0 return this.filter( isHidden ).css( "opacity", 0 ).show() // animate to the value specified .end().animate({ opacity: to }, speed, easing, callback ); }, animate: function( prop, speed, easing, callback ) { var empty = jQuery.isEmptyObject( prop ), optall = jQuery.speed( speed, easing, callback ), doAnimation = function() { // Operate on a copy of prop so per-property easing won't be lost var anim = Animation( this, jQuery.extend( {}, prop ), optall ); // Empty animations resolve immediately if ( empty ) { anim.stop( true ); } }; return empty || optall.queue === false ? this.each( doAnimation ) : this.queue( optall.queue, doAnimation ); }, stop: function( type, clearQueue, gotoEnd ) { var stopQueue = function( hooks ) { var stop = hooks.stop; delete hooks.stop; stop( gotoEnd ); }; if ( typeof type !== "string" ) { gotoEnd = clearQueue; clearQueue = type; type = undefined; } if ( clearQueue && type !== false ) { this.queue( type || "fx", [] ); } return this.each(function() { var dequeue = true, index = type != null && type + "queueHooks", timers = jQuery.timers, data = jQuery._data( this ); if ( index ) { if ( data[ index ] && data[ index ].stop ) { stopQueue( data[ index ] ); } } else { for ( index in data ) { if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { stopQueue( data[ index ] ); } } } for ( index = timers.length; index--; ) { if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { timers[ index ].anim.stop( gotoEnd ); dequeue = false; timers.splice( index, 1 ); } } // start the next in the queue if the last step wasn't forced // timers currently will call their complete callbacks, which will dequeue // but only if they were gotoEnd if ( dequeue || !gotoEnd ) { jQuery.dequeue( this, type ); } }); } }); // Generate parameters to create a standard animation function genFx( type, includeWidth ) { var which, attrs = { height: type }, i = 0; // if we include width, step value is 1 to do all cssExpand values, // if we don't include width, step value is 2 to skip over Left and Right includeWidth = includeWidth? 1 : 0; for( ; i < 4 ; i += 2 - includeWidth ) { which = cssExpand[ i ]; attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; } if ( includeWidth ) { attrs.opacity = attrs.width = type; } return attrs; } // Generate shortcuts for custom animations jQuery.each({ slideDown: genFx("show"), slideUp: genFx("hide"), slideToggle: genFx("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function( name, props ) { jQuery.fn[ name ] = function( speed, easing, callback ) { return this.animate( props, speed, easing, callback ); }; }); jQuery.speed = function( speed, easing, fn ) { var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; // normalize opt.queue - true/undefined/null -> "fx" if ( opt.queue == null || opt.queue === true ) { opt.queue = "fx"; } // Queueing opt.old = opt.complete; opt.complete = function() { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } if ( opt.queue ) { jQuery.dequeue( this, opt.queue ); } }; return opt; }; jQuery.easing = { linear: function( p ) { return p; }, swing: function( p ) { return 0.5 - Math.cos( p*Math.PI ) / 2; } }; jQuery.timers = []; jQuery.fx = Tween.prototype.init; jQuery.fx.tick = function() { var timer, timers = jQuery.timers, i = 0; fxNow = jQuery.now(); for ( ; i < timers.length; i++ ) { timer = timers[ i ]; // Checks the timer has not already been removed if ( !timer() && timers[ i ] === timer ) { timers.splice( i--, 1 ); } } if ( !timers.length ) { jQuery.fx.stop(); } fxNow = undefined; }; jQuery.fx.timer = function( timer ) { if ( timer() && jQuery.timers.push( timer ) && !timerId ) { timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); } }; jQuery.fx.interval = 13; jQuery.fx.stop = function() { clearInterval( timerId ); timerId = null; }; jQuery.fx.speeds = { slow: 600, fast: 200, // Default speed _default: 400 }; // Back Compat <1.8 extension point jQuery.fx.step = {}; if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { return elem === fn.elem; }).length; }; } var rroot = /^(?:body|html)$/i; jQuery.fn.offset = function( options ) { if ( arguments.length ) { return options === undefined ? this : this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } var docElem, body, win, clientTop, clientLeft, scrollTop, scrollLeft, box = { top: 0, left: 0 }, elem = this[ 0 ], doc = elem && elem.ownerDocument; if ( !doc ) { return; } if ( (body = doc.body) === elem ) { return jQuery.offset.bodyOffset( elem ); } docElem = doc.documentElement; // Make sure it's not a disconnected DOM node if ( !jQuery.contains( docElem, elem ) ) { return box; } // If we don't have gBCR, just use 0,0 rather than error // BlackBerry 5, iOS 3 (original iPhone) if ( typeof elem.getBoundingClientRect !== "undefined" ) { box = elem.getBoundingClientRect(); } win = getWindow( doc ); clientTop = docElem.clientTop || body.clientTop || 0; clientLeft = docElem.clientLeft || body.clientLeft || 0; scrollTop = win.pageYOffset || docElem.scrollTop; scrollLeft = win.pageXOffset || docElem.scrollLeft; return { top: box.top + scrollTop - clientTop, left: box.left + scrollLeft - clientLeft }; }; jQuery.offset = { bodyOffset: function( body ) { var top = body.offsetTop, left = body.offsetLeft; if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { top += parseFloat( jQuery.css(body, "marginTop") ) || 0; left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; } return { top: top, left: left }; }, setOffset: function( elem, options, i ) { var position = jQuery.css( elem, "position" ); // set position first, in-case top/left are set even on static elem if ( position === "static" ) { elem.style.position = "relative"; } var curElem = jQuery( elem ), curOffset = curElem.offset(), curCSSTop = jQuery.css( elem, "top" ), curCSSLeft = jQuery.css( elem, "left" ), calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, props = {}, curPosition = {}, curTop, curLeft; // need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); curTop = curPosition.top; curLeft = curPosition.left; } else { curTop = parseFloat( curCSSTop ) || 0; curLeft = parseFloat( curCSSLeft ) || 0; } if ( jQuery.isFunction( options ) ) { options = options.call( elem, i, curOffset ); } if ( options.top != null ) { props.top = ( options.top - curOffset.top ) + curTop; } if ( options.left != null ) { props.left = ( options.left - curOffset.left ) + curLeft; } if ( "using" in options ) { options.using.call( elem, props ); } else { curElem.css( props ); } } }; jQuery.fn.extend({ position: function() { if ( !this[0] ) { return; } var elem = this[0], // Get *real* offsetParent offsetParent = this.offsetParent(), // Get correct offsets offset = this.offset(), parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); // Subtract element margins // note: when an element has margin: auto the offsetLeft and marginLeft // are the same in Safari causing offset.left to incorrectly be 0 offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; // Add offsetParent borders parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; // Subtract the two offsets return { top: offset.top - parentOffset.top, left: offset.left - parentOffset.left }; }, offsetParent: function() { return this.map(function() { var offsetParent = this.offsetParent || document.body; while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { offsetParent = offsetParent.offsetParent; } return offsetParent || document.body; }); } }); // Create scrollLeft and scrollTop methods jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) { var top = /Y/.test( prop ); jQuery.fn[ method ] = function( val ) { return jQuery.access( this, function( elem, method, val ) { var win = getWindow( elem ); if ( val === undefined ) { return win ? (prop in win) ? win[ prop ] : win.document.documentElement[ method ] : elem[ method ]; } if ( win ) { win.scrollTo( !top ? val : jQuery( win ).scrollLeft(), top ? val : jQuery( win ).scrollTop() ); } else { elem[ method ] = val; } }, method, val, arguments.length, null ); }; }); function getWindow( elem ) { return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; } // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { // margin is only for outerHeight, outerWidth jQuery.fn[ funcName ] = function( margin, value ) { var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); return jQuery.access( this, function( elem, type, value ) { var doc; if ( jQuery.isWindow( elem ) ) { // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there // isn't a whole lot we can do. See pull request at this URL for discussion: // https://github.com/jquery/jquery/pull/764 return elem.document.documentElement[ "client" + name ]; } // Get document width or height if ( elem.nodeType === 9 ) { doc = elem.documentElement; // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. return Math.max( elem.body[ "scroll" + name ], doc[ "scroll" + name ], elem.body[ "offset" + name ], doc[ "offset" + name ], doc[ "client" + name ] ); } return value === undefined ? // Get width or height on the element, requesting but not forcing parseFloat jQuery.css( elem, type, value, extra ) : // Set width or height on the element jQuery.style( elem, type, value, extra ); }, type, chainable ? margin : undefined, chainable, null ); }; }); }); // Expose jQuery to the global object window.jQuery = window.$ = jQuery; // Expose jQuery as an AMD module, but only for AMD loaders that // understand the issues with loading multiple versions of jQuery // in a page that all might call define(). The loader will indicate // they have special allowances for multiple jQuery versions by // specifying define.amd.jQuery = true. Register as a named module, // since jQuery can be concatenated with other files that may use define, // but not use a proper concatenation script that understands anonymous // AMD modules. A named AMD is safest and most robust way to register. // Lowercase jquery is used because AMD module names are derived from // file names, and jQuery is normally delivered in a lowercase file name. // Do this after creating the global so that if an AMD module wants to call // noConflict to hide this version of jQuery, it will work. if ( typeof define === "function" && define.amd && define.amd.jQuery ) { define( "jquery", [], function () { return jQuery; } ); } })( window ); ================================================ FILE: test/vendor/json2.js ================================================ /* http://www.JSON.org/json2.js 2009-09-29 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, strict: false */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (!this.JSON) { this.JSON = {}; } (function () { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { k = rep[i]; if (typeof k === 'string') { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/. test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }()); ================================================ FILE: test/vendor/qunit.css ================================================ /*! * QUnit 1.21.0 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * * Date: 2016-02-01T13:07Z */ /** Font Family and Sizes */ #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; } #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } #qunit-tests { font-size: smaller; } /** Resets */ #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { margin: 0; padding: 0; } /** Header */ #qunit-header { padding: 0.5em 0 0.5em 1em; color: #8699A4; background-color: #0D3349; font-size: 1.5em; line-height: 1em; font-weight: 400; border-radius: 5px 5px 0 0; } #qunit-header a { text-decoration: none; color: #C2CCD1; } #qunit-header a:hover, #qunit-header a:focus { color: #FFF; } #qunit-testrunner-toolbar label { display: inline-block; padding: 0 0.5em 0 0.1em; } #qunit-banner { height: 5px; } #qunit-testrunner-toolbar { padding: 0.5em 1em 0.5em 1em; color: #5E740B; background-color: #EEE; overflow: hidden; } #qunit-filteredTest { padding: 0.5em 1em 0.5em 1em; background-color: #F4FF77; color: #366097; } #qunit-userAgent { padding: 0.5em 1em 0.5em 1em; background-color: #2B81AF; color: #FFF; text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } #qunit-modulefilter-container { float: right; padding: 0.2em; } .qunit-url-config { display: inline-block; padding: 0.1em; } .qunit-filter { display: block; float: right; margin-left: 1em; } /** Tests: Pass/Fail */ #qunit-tests { list-style-position: inside; } #qunit-tests li { padding: 0.4em 1em 0.4em 1em; border-bottom: 1px solid #FFF; list-style-position: inside; } #qunit-tests > li { display: none; } #qunit-tests li.running, #qunit-tests li.pass, #qunit-tests li.fail, #qunit-tests li.skipped { display: list-item; } #qunit-tests.hidepass { position: relative; } #qunit-tests.hidepass li.running, #qunit-tests.hidepass li.pass { visibility: hidden; position: absolute; width: 0; height: 0; padding: 0; border: 0; margin: 0; } #qunit-tests li strong { cursor: pointer; } #qunit-tests li.skipped strong { cursor: default; } #qunit-tests li a { padding: 0.5em; color: #C2CCD1; text-decoration: none; } #qunit-tests li p a { padding: 0.25em; color: #6B6464; } #qunit-tests li a:hover, #qunit-tests li a:focus { color: #000; } #qunit-tests li .runtime { float: right; font-size: smaller; } .qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #FFF; border-radius: 5px; } .qunit-source { margin: 0.6em 0 0.3em; } .qunit-collapsed { display: none; } #qunit-tests table { border-collapse: collapse; margin-top: 0.2em; } #qunit-tests th { text-align: right; vertical-align: top; padding: 0 0.5em 0 0; } #qunit-tests td { vertical-align: top; } #qunit-tests pre { margin: 0; white-space: pre-wrap; word-wrap: break-word; } #qunit-tests del { background-color: #E0F2BE; color: #374E0C; text-decoration: none; } #qunit-tests ins { background-color: #FFCACA; color: #500; text-decoration: none; } /*** Test Counts */ #qunit-tests b.counts { color: #000; } #qunit-tests b.passed { color: #5E740B; } #qunit-tests b.failed { color: #710909; } #qunit-tests li li { padding: 5px; background-color: #FFF; border-bottom: none; list-style-position: inside; } /*** Passing Styles */ #qunit-tests li li.pass { color: #3C510C; background-color: #FFF; border-left: 10px solid #C6E746; } #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } #qunit-tests .pass .test-name { color: #366097; } #qunit-tests .pass .test-actual, #qunit-tests .pass .test-expected { color: #999; } #qunit-banner.qunit-pass { background-color: #C6E746; } /*** Failing Styles */ #qunit-tests li li.fail { color: #710909; background-color: #FFF; border-left: 10px solid #EE5757; white-space: pre; } #qunit-tests > li:last-child { border-radius: 0 0 5px 5px; } #qunit-tests .fail { color: #000; background-color: #EE5757; } #qunit-tests .fail .test-name, #qunit-tests .fail .module-name { color: #000; } #qunit-tests .fail .test-actual { color: #EE5757; } #qunit-tests .fail .test-expected { color: #008000; } #qunit-banner.qunit-fail { background-color: #EE5757; } /*** Skipped tests */ #qunit-tests .skipped { background-color: #EBECE9; } #qunit-tests .qunit-skipped-label { background-color: #F4FF77; display: inline-block; font-style: normal; color: #366097; line-height: 1.8em; padding: 0 0.5em; margin: -0.4em 0.4em -0.4em 0; } /** Result */ #qunit-testresult { padding: 0.5em 1em 0.5em 1em; color: #2B81AF; background-color: #D2E0E6; border-bottom: 1px solid #FFF; } #qunit-testresult .module-name { font-weight: 700; } /** Fixture */ #qunit-fixture { position: absolute; top: -10000px; left: -10000px; width: 1000px; height: 1000px; } ================================================ FILE: test/vendor/qunit.js ================================================ /*! * QUnit 1.21.0 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * * Date: 2016-02-01T13:07Z */ (function( global ) { var QUnit = {}; var Date = global.Date; var now = Date.now || function() { return new Date().getTime(); }; var setTimeout = global.setTimeout; var clearTimeout = global.clearTimeout; // Store a local window from the global to allow direct references. var window = global.window; var defined = { document: window && window.document !== undefined, setTimeout: setTimeout !== undefined, sessionStorage: (function() { var x = "qunit-test-string"; try { sessionStorage.setItem( x, x ); sessionStorage.removeItem( x ); return true; } catch ( e ) { return false; } }() ) }; var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); var globalStartCalled = false; var runStarted = false; var toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty; // returns a new Array with the elements that are in a but not in b function diff( a, b ) { var i, j, result = a.slice(); for ( i = 0; i < result.length; i++ ) { for ( j = 0; j < b.length; j++ ) { if ( result[ i ] === b[ j ] ) { result.splice( i, 1 ); i--; break; } } } return result; } // from jquery.js function inArray( elem, array ) { if ( array.indexOf ) { return array.indexOf( elem ); } for ( var i = 0, length = array.length; i < length; i++ ) { if ( array[ i ] === elem ) { return i; } } return -1; } /** * Makes a clone of an object using only Array or Object as base, * and copies over the own enumerable properties. * * @param {Object} obj * @return {Object} New object with only the own properties (recursively). */ function objectValues ( obj ) { var key, val, vals = QUnit.is( "array", obj ) ? [] : {}; for ( key in obj ) { if ( hasOwn.call( obj, key ) ) { val = obj[ key ]; vals[ key ] = val === Object( val ) ? objectValues( val ) : val; } } return vals; } function extend( a, b, undefOnly ) { for ( var prop in b ) { if ( hasOwn.call( b, prop ) ) { // Avoid "Member not found" error in IE8 caused by messing with window.constructor // This block runs on every environment, so `global` is being used instead of `window` // to avoid errors on node. if ( prop !== "constructor" || a !== global ) { if ( b[ prop ] === undefined ) { delete a[ prop ]; } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { a[ prop ] = b[ prop ]; } } } } return a; } function objectType( obj ) { if ( typeof obj === "undefined" ) { return "undefined"; } // Consider: typeof null === object if ( obj === null ) { return "null"; } var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), type = match && match[ 1 ]; switch ( type ) { case "Number": if ( isNaN( obj ) ) { return "nan"; } return "number"; case "String": case "Boolean": case "Array": case "Set": case "Map": case "Date": case "RegExp": case "Function": case "Symbol": return type.toLowerCase(); } if ( typeof obj === "object" ) { return "object"; } } // Safe object type checking function is( type, obj ) { return QUnit.objectType( obj ) === type; } var getUrlParams = function() { var i, current; var urlParams = {}; var location = window.location; var params = location.search.slice( 1 ).split( "&" ); var length = params.length; if ( params[ 0 ] ) { for ( i = 0; i < length; i++ ) { current = params[ i ].split( "=" ); current[ 0 ] = decodeURIComponent( current[ 0 ] ); // allow just a key to turn on a flag, e.g., test.html?noglobals current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; if ( urlParams[ current[ 0 ] ] ) { urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); } else { urlParams[ current[ 0 ] ] = current[ 1 ]; } } } return urlParams; }; // Doesn't support IE6 to IE9, it will return undefined on these browsers // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack function extractStacktrace( e, offset ) { offset = offset === undefined ? 4 : offset; var stack, include, i; if ( e.stack ) { stack = e.stack.split( "\n" ); if ( /^error$/i.test( stack[ 0 ] ) ) { stack.shift(); } if ( fileName ) { include = []; for ( i = offset; i < stack.length; i++ ) { if ( stack[ i ].indexOf( fileName ) !== -1 ) { break; } include.push( stack[ i ] ); } if ( include.length ) { return include.join( "\n" ); } } return stack[ offset ]; // Support: Safari <=6 only } else if ( e.sourceURL ) { // exclude useless self-reference for generated Error objects if ( /qunit.js$/.test( e.sourceURL ) ) { return; } // for actual exceptions, this is useful return e.sourceURL + ":" + e.line; } } function sourceFromStacktrace( offset ) { var error = new Error(); // Support: Safari <=7 only, IE <=10 - 11 only // Not all browsers generate the `stack` property for `new Error()`, see also #636 if ( !error.stack ) { try { throw error; } catch ( err ) { error = err; } } return extractStacktrace( error, offset ); } /** * Config object: Maintain internal state * Later exposed as QUnit.config * `config` initialized at top of scope */ var config = { // The queue of tests to run queue: [], // block until document ready blocking: true, // by default, run previously failed tests first // very useful in combination with "Hide passed tests" checked reorder: true, // by default, modify document.title when suite is done altertitle: true, // HTML Reporter: collapse every test except the first failing test // If false, all failing tests will be expanded collapse: true, // by default, scroll to top of the page when suite is done scrolltop: true, // depth up-to which object will be dumped maxDepth: 5, // when enabled, all tests must call expect() requireExpects: false, // add checkboxes that are persisted in the query-string // when enabled, the id is set to `true` as a `QUnit.config` property urlConfig: [ { id: "hidepassed", label: "Hide passed tests", tooltip: "Only show tests and assertions that fail. Stored as query-strings." }, { id: "noglobals", label: "Check for Globals", tooltip: "Enabling this will test if any test introduces new properties on the " + "global object (`window` in Browsers). Stored as query-strings." }, { id: "notrycatch", label: "No try-catch", tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + "exceptions in IE reasonable. Stored as query-strings." } ], // Set of all modules. modules: [], // Stack of nested modules moduleStack: [], // The first unnamed module currentModule: { name: "", tests: [] }, callbacks: {} }; var urlParams = defined.document ? getUrlParams() : {}; // Push a loose unnamed module to the modules collection config.modules.push( config.currentModule ); if ( urlParams.filter === true ) { delete urlParams.filter; } // String search anywhere in moduleName+testName config.filter = urlParams.filter; config.testId = []; if ( urlParams.testId ) { // Ensure that urlParams.testId is an array urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); for (var i = 0; i < urlParams.testId.length; i++ ) { config.testId.push( urlParams.testId[ i ] ); } } var loggingCallbacks = {}; // Register logging callbacks function registerLoggingCallbacks( obj ) { var i, l, key, callbackNames = [ "begin", "done", "log", "testStart", "testDone", "moduleStart", "moduleDone" ]; function registerLoggingCallback( key ) { var loggingCallback = function( callback ) { if ( objectType( callback ) !== "function" ) { throw new Error( "QUnit logging methods require a callback function as their first parameters." ); } config.callbacks[ key ].push( callback ); }; // DEPRECATED: This will be removed on QUnit 2.0.0+ // Stores the registered functions allowing restoring // at verifyLoggingCallbacks() if modified loggingCallbacks[ key ] = loggingCallback; return loggingCallback; } for ( i = 0, l = callbackNames.length; i < l; i++ ) { key = callbackNames[ i ]; // Initialize key collection of logging callback if ( objectType( config.callbacks[ key ] ) === "undefined" ) { config.callbacks[ key ] = []; } obj[ key ] = registerLoggingCallback( key ); } } function runLoggingCallbacks( key, args ) { var i, l, callbacks; callbacks = config.callbacks[ key ]; for ( i = 0, l = callbacks.length; i < l; i++ ) { callbacks[ i ]( args ); } } // DEPRECATED: This will be removed on 2.0.0+ // This function verifies if the loggingCallbacks were modified by the user // If so, it will restore it, assign the given callback and print a console warning function verifyLoggingCallbacks() { var loggingCallback, userCallback; for ( loggingCallback in loggingCallbacks ) { if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { userCallback = QUnit[ loggingCallback ]; // Restore the callback function QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; // Assign the deprecated given callback QUnit[ loggingCallback ]( userCallback ); if ( global.console && global.console.warn ) { global.console.warn( "QUnit." + loggingCallback + " was replaced with a new value.\n" + "Please, check out the documentation on how to apply logging callbacks.\n" + "Reference: https://api.qunitjs.com/category/callbacks/" ); } } } } ( function() { if ( !defined.document ) { return; } // `onErrorFnPrev` initialized at top of scope // Preserve other handlers var onErrorFnPrev = window.onerror; // Cover uncaught exceptions // Returning true will suppress the default browser handler, // returning false will let it run. window.onerror = function( error, filePath, linerNr ) { var ret = false; if ( onErrorFnPrev ) { ret = onErrorFnPrev( error, filePath, linerNr ); } // Treat return value as window.onerror itself does, // Only do our handling if not suppressed. if ( ret !== true ) { if ( QUnit.config.current ) { if ( QUnit.config.current.ignoreGlobalErrors ) { return true; } QUnit.pushFailure( error, filePath + ":" + linerNr ); } else { QUnit.test( "global failure", extend(function() { QUnit.pushFailure( error, filePath + ":" + linerNr ); }, { validTest: true } ) ); } return false; } return ret; }; } )(); QUnit.urlParams = urlParams; // Figure out if we're running the tests from a server or not QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); // Expose the current QUnit version QUnit.version = "1.21.0"; extend( QUnit, { // call on start of module test to prepend name to all tests module: function( name, testEnvironment, executeNow ) { var module, moduleFns; var currentModule = config.currentModule; if ( arguments.length === 2 ) { if ( testEnvironment instanceof Function ) { executeNow = testEnvironment; testEnvironment = undefined; } } // DEPRECATED: handles setup/teardown functions, // beforeEach and afterEach should be used instead if ( testEnvironment && testEnvironment.setup ) { testEnvironment.beforeEach = testEnvironment.setup; delete testEnvironment.setup; } if ( testEnvironment && testEnvironment.teardown ) { testEnvironment.afterEach = testEnvironment.teardown; delete testEnvironment.teardown; } module = createModule(); moduleFns = { beforeEach: setHook( module, "beforeEach" ), afterEach: setHook( module, "afterEach" ) }; if ( executeNow instanceof Function ) { config.moduleStack.push( module ); setCurrentModule( module ); executeNow.call( module.testEnvironment, moduleFns ); config.moduleStack.pop(); module = module.parentModule || currentModule; } setCurrentModule( module ); function createModule() { var parentModule = config.moduleStack.length ? config.moduleStack.slice( -1 )[ 0 ] : null; var moduleName = parentModule !== null ? [ parentModule.name, name ].join( " > " ) : name; var module = { name: moduleName, parentModule: parentModule, tests: [] }; var env = {}; if ( parentModule ) { extend( env, parentModule.testEnvironment ); delete env.beforeEach; delete env.afterEach; } extend( env, testEnvironment ); module.testEnvironment = env; config.modules.push( module ); return module; } function setCurrentModule( module ) { config.currentModule = module; } }, // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. asyncTest: asyncTest, test: test, skip: skip, only: only, // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. start: function( count ) { var globalStartAlreadyCalled = globalStartCalled; if ( !config.current ) { globalStartCalled = true; if ( runStarted ) { throw new Error( "Called start() outside of a test context while already started" ); } else if ( globalStartAlreadyCalled || count > 1 ) { throw new Error( "Called start() outside of a test context too many times" ); } else if ( config.autostart ) { throw new Error( "Called start() outside of a test context when " + "QUnit.config.autostart was true" ); } else if ( !config.pageLoaded ) { // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it config.autostart = true; return; } } else { // If a test is running, adjust its semaphore config.current.semaphore -= count || 1; // If semaphore is non-numeric, throw error if ( isNaN( config.current.semaphore ) ) { config.current.semaphore = 0; QUnit.pushFailure( "Called start() with a non-numeric decrement.", sourceFromStacktrace( 2 ) ); return; } // Don't start until equal number of stop-calls if ( config.current.semaphore > 0 ) { return; } // throw an Error if start is called more often than stop if ( config.current.semaphore < 0 ) { config.current.semaphore = 0; QUnit.pushFailure( "Called start() while already started (test's semaphore was 0 already)", sourceFromStacktrace( 2 ) ); return; } } resumeProcessing(); }, // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. stop: function( count ) { // If there isn't a test running, don't allow QUnit.stop() to be called if ( !config.current ) { throw new Error( "Called stop() outside of a test context" ); } // If a test is running, adjust its semaphore config.current.semaphore += count || 1; pauseProcessing(); }, config: config, is: is, objectType: objectType, extend: extend, load: function() { config.pageLoaded = true; // Initialize the configuration options extend( config, { stats: { all: 0, bad: 0 }, moduleStats: { all: 0, bad: 0 }, started: 0, updateRate: 1000, autostart: true, filter: "" }, true ); config.blocking = false; if ( config.autostart ) { resumeProcessing(); } }, stack: function( offset ) { offset = ( offset || 0 ) + 2; return sourceFromStacktrace( offset ); } }); registerLoggingCallbacks( QUnit ); function begin() { var i, l, modulesLog = []; // If the test run hasn't officially begun yet if ( !config.started ) { // Record the time of the test run's beginning config.started = now(); verifyLoggingCallbacks(); // Delete the loose unnamed module if unused. if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { config.modules.shift(); } // Avoid unnecessary information by not logging modules' test environments for ( i = 0, l = config.modules.length; i < l; i++ ) { modulesLog.push({ name: config.modules[ i ].name, tests: config.modules[ i ].tests }); } // The test run is officially beginning now runLoggingCallbacks( "begin", { totalTests: Test.count, modules: modulesLog }); } config.blocking = false; process( true ); } function process( last ) { function next() { process( last ); } var start = now(); config.depth = ( config.depth || 0 ) + 1; while ( config.queue.length && !config.blocking ) { if ( !defined.setTimeout || config.updateRate <= 0 || ( ( now() - start ) < config.updateRate ) ) { if ( config.current ) { // Reset async tracking for each phase of the Test lifecycle config.current.usedAsync = false; } config.queue.shift()(); } else { setTimeout( next, 13 ); break; } } config.depth--; if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { done(); } } function pauseProcessing() { config.blocking = true; if ( config.testTimeout && defined.setTimeout ) { clearTimeout( config.timeout ); config.timeout = setTimeout(function() { if ( config.current ) { config.current.semaphore = 0; QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); } else { throw new Error( "Test timed out" ); } resumeProcessing(); }, config.testTimeout ); } } function resumeProcessing() { runStarted = true; // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) if ( defined.setTimeout ) { setTimeout(function() { if ( config.current && config.current.semaphore > 0 ) { return; } if ( config.timeout ) { clearTimeout( config.timeout ); } begin(); }, 13 ); } else { begin(); } } function done() { var runtime, passed; config.autorun = true; // Log the last module results if ( config.previousModule ) { runLoggingCallbacks( "moduleDone", { name: config.previousModule.name, tests: config.previousModule.tests, failed: config.moduleStats.bad, passed: config.moduleStats.all - config.moduleStats.bad, total: config.moduleStats.all, runtime: now() - config.moduleStats.started }); } delete config.previousModule; runtime = now() - config.started; passed = config.stats.all - config.stats.bad; runLoggingCallbacks( "done", { failed: config.stats.bad, passed: passed, total: config.stats.all, runtime: runtime }); } function setHook( module, hookName ) { if ( module.testEnvironment === undefined ) { module.testEnvironment = {}; } return function( callback ) { module.testEnvironment[ hookName ] = callback; }; } var focused = false; var priorityCount = 0; function Test( settings ) { var i, l; ++Test.count; extend( this, settings ); this.assertions = []; this.semaphore = 0; this.usedAsync = false; this.module = config.currentModule; this.stack = sourceFromStacktrace( 3 ); // Register unique strings for ( i = 0, l = this.module.tests; i < l.length; i++ ) { if ( this.module.tests[ i ].name === this.testName ) { this.testName += " "; } } this.testId = generateHash( this.module.name, this.testName ); this.module.tests.push({ name: this.testName, testId: this.testId }); if ( settings.skip ) { // Skipped tests will fully ignore any sent callback this.callback = function() {}; this.async = false; this.expected = 0; } else { this.assert = new Assert( this ); } } Test.count = 0; Test.prototype = { before: function() { if ( // Emit moduleStart when we're switching from one module to another this.module !== config.previousModule || // They could be equal (both undefined) but if the previousModule property doesn't // yet exist it means this is the first test in a suite that isn't wrapped in a // module, in which case we'll just emit a moduleStart event for 'undefined'. // Without this, reporters can get testStart before moduleStart which is a problem. !hasOwn.call( config, "previousModule" ) ) { if ( hasOwn.call( config, "previousModule" ) ) { runLoggingCallbacks( "moduleDone", { name: config.previousModule.name, tests: config.previousModule.tests, failed: config.moduleStats.bad, passed: config.moduleStats.all - config.moduleStats.bad, total: config.moduleStats.all, runtime: now() - config.moduleStats.started }); } config.previousModule = this.module; config.moduleStats = { all: 0, bad: 0, started: now() }; runLoggingCallbacks( "moduleStart", { name: this.module.name, tests: this.module.tests }); } config.current = this; if ( this.module.testEnvironment ) { delete this.module.testEnvironment.beforeEach; delete this.module.testEnvironment.afterEach; } this.testEnvironment = extend( {}, this.module.testEnvironment ); this.started = now(); runLoggingCallbacks( "testStart", { name: this.testName, module: this.module.name, testId: this.testId }); if ( !config.pollution ) { saveGlobal(); } }, run: function() { var promise; config.current = this; if ( this.async ) { QUnit.stop(); } this.callbackStarted = now(); if ( config.notrycatch ) { runTest( this ); return; } try { runTest( this ); } catch ( e ) { this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); // else next test will carry the responsibility saveGlobal(); // Restart the tests if they're blocking if ( config.blocking ) { QUnit.start(); } } function runTest( test ) { promise = test.callback.call( test.testEnvironment, test.assert ); test.resolvePromise( promise ); } }, after: function() { checkPollution(); }, queueHook: function( hook, hookName ) { var promise, test = this; return function runHook() { config.current = test; if ( config.notrycatch ) { callHook(); return; } try { callHook(); } catch ( error ) { test.pushFailure( hookName + " failed on " + test.testName + ": " + ( error.message || error ), extractStacktrace( error, 0 ) ); } function callHook() { promise = hook.call( test.testEnvironment, test.assert ); test.resolvePromise( promise, hookName ); } }; }, // Currently only used for module level hooks, can be used to add global level ones hooks: function( handler ) { var hooks = []; function processHooks( test, module ) { if ( module.parentModule ) { processHooks( test, module.parentModule ); } if ( module.testEnvironment && QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); } } // Hooks are ignored on skipped tests if ( !this.skip ) { processHooks( this, this.module ); } return hooks; }, finish: function() { config.current = this; if ( config.requireExpects && this.expected === null ) { this.pushFailure( "Expected number of assertions to be defined, but expect() was " + "not called.", this.stack ); } else if ( this.expected !== null && this.expected !== this.assertions.length ) { this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); } else if ( this.expected === null && !this.assertions.length ) { this.pushFailure( "Expected at least one assertion, but none were run - call " + "expect(0) to accept zero assertions.", this.stack ); } var i, bad = 0; this.runtime = now() - this.started; config.stats.all += this.assertions.length; config.moduleStats.all += this.assertions.length; for ( i = 0; i < this.assertions.length; i++ ) { if ( !this.assertions[ i ].result ) { bad++; config.stats.bad++; config.moduleStats.bad++; } } runLoggingCallbacks( "testDone", { name: this.testName, module: this.module.name, skipped: !!this.skip, failed: bad, passed: this.assertions.length - bad, total: this.assertions.length, runtime: this.runtime, // HTML Reporter use assertions: this.assertions, testId: this.testId, // Source of Test source: this.stack, // DEPRECATED: this property will be removed in 2.0.0, use runtime instead duration: this.runtime }); // QUnit.reset() is deprecated and will be replaced for a new // fixture reset function on QUnit 2.0/2.1. // It's still called here for backwards compatibility handling QUnit.reset(); config.current = undefined; }, queue: function() { var priority, test = this; if ( !this.valid() ) { return; } function run() { // each of these can by async synchronize([ function() { test.before(); }, test.hooks( "beforeEach" ), function() { test.run(); }, test.hooks( "afterEach" ).reverse(), function() { test.after(); }, function() { test.finish(); } ]); } // Prioritize previously failed tests, detected from sessionStorage priority = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); return synchronize( run, priority ); }, push: function( result, actual, expected, message, negative ) { var source, details = { module: this.module.name, name: this.testName, result: result, message: message, actual: actual, expected: expected, testId: this.testId, negative: negative || false, runtime: now() - this.started }; if ( !result ) { source = sourceFromStacktrace(); if ( source ) { details.source = source; } } runLoggingCallbacks( "log", details ); this.assertions.push({ result: !!result, message: message }); }, pushFailure: function( message, source, actual ) { if ( !( this instanceof Test ) ) { throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) ); } var details = { module: this.module.name, name: this.testName, result: false, message: message || "error", actual: actual || null, testId: this.testId, runtime: now() - this.started }; if ( source ) { details.source = source; } runLoggingCallbacks( "log", details ); this.assertions.push({ result: false, message: message }); }, resolvePromise: function( promise, phase ) { var then, message, test = this; if ( promise != null ) { then = promise.then; if ( QUnit.objectType( then ) === "function" ) { QUnit.stop(); then.call( promise, function() { QUnit.start(); }, function( error ) { message = "Promise rejected " + ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + " " + test.testName + ": " + ( error.message || error ); test.pushFailure( message, extractStacktrace( error, 0 ) ); // else next test will carry the responsibility saveGlobal(); // Unblock QUnit.start(); } ); } } }, valid: function() { var filter = config.filter, regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ), module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), fullName = ( this.module.name + ": " + this.testName ); function testInModuleChain( testModule ) { var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; if ( testModuleName === module ) { return true; } else if ( testModule.parentModule ) { return testInModuleChain( testModule.parentModule ); } else { return false; } } // Internally-generated tests are always valid if ( this.callback && this.callback.validTest ) { return true; } if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { return false; } if ( module && !testInModuleChain( this.module ) ) { return false; } if ( !filter ) { return true; } return regexFilter ? this.regexFilter( !!regexFilter[1], regexFilter[2], regexFilter[3], fullName ) : this.stringFilter( filter, fullName ); }, regexFilter: function( exclude, pattern, flags, fullName ) { var regex = new RegExp( pattern, flags ); var match = regex.test( fullName ); return match !== exclude; }, stringFilter: function( filter, fullName ) { filter = filter.toLowerCase(); fullName = fullName.toLowerCase(); var include = filter.charAt( 0 ) !== "!"; if ( !include ) { filter = filter.slice( 1 ); } // If the filter matches, we need to honour include if ( fullName.indexOf( filter ) !== -1 ) { return include; } // Otherwise, do the opposite return !include; } }; // Resets the test setup. Useful for tests that modify the DOM. /* DEPRECATED: Use multiple tests instead of resetting inside a test. Use testStart or testDone for custom cleanup. This method will throw an error in 2.0, and will be removed in 2.1 */ QUnit.reset = function() { // Return on non-browser environments // This is necessary to not break on node tests if ( !defined.document ) { return; } var fixture = defined.document && document.getElementById && document.getElementById( "qunit-fixture" ); if ( fixture ) { fixture.innerHTML = config.fixture; } }; QUnit.pushFailure = function() { if ( !QUnit.config.current ) { throw new Error( "pushFailure() assertion outside test context, in " + sourceFromStacktrace( 2 ) ); } // Gets current test obj var currentTest = QUnit.config.current; return currentTest.pushFailure.apply( currentTest, arguments ); }; // Based on Java's String.hashCode, a simple but not // rigorously collision resistant hashing function function generateHash( module, testName ) { var hex, i = 0, hash = 0, str = module + "\x1C" + testName, len = str.length; for ( ; i < len; i++ ) { hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); hash |= 0; } // Convert the possibly negative integer hash code into an 8 character hex string, which isn't // strictly necessary but increases user understanding that the id is a SHA-like hash hex = ( 0x100000000 + hash ).toString( 16 ); if ( hex.length < 8 ) { hex = "0000000" + hex; } return hex.slice( -8 ); } function synchronize( callback, priority ) { var last = !priority; if ( QUnit.objectType( callback ) === "array" ) { while ( callback.length ) { synchronize( callback.shift() ); } return; } if ( priority ) { config.queue.splice( priorityCount++, 0, callback ); } else { config.queue.push( callback ); } if ( config.autorun && !config.blocking ) { process( last ); } } function saveGlobal() { config.pollution = []; if ( config.noglobals ) { for ( var key in global ) { if ( hasOwn.call( global, key ) ) { // in Opera sometimes DOM element ids show up here, ignore them if ( /^qunit-test-output/.test( key ) ) { continue; } config.pollution.push( key ); } } } } function checkPollution() { var newGlobals, deletedGlobals, old = config.pollution; saveGlobal(); newGlobals = diff( config.pollution, old ); if ( newGlobals.length > 0 ) { QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); } deletedGlobals = diff( old, config.pollution ); if ( deletedGlobals.length > 0 ) { QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); } } // Will be exposed as QUnit.asyncTest function asyncTest( testName, expected, callback ) { if ( arguments.length === 2 ) { callback = expected; expected = null; } QUnit.test( testName, expected, callback, true ); } // Will be exposed as QUnit.test function test( testName, expected, callback, async ) { if ( focused ) { return; } var newTest; if ( arguments.length === 2 ) { callback = expected; expected = null; } newTest = new Test({ testName: testName, expected: expected, async: async, callback: callback }); newTest.queue(); } // Will be exposed as QUnit.skip function skip( testName ) { if ( focused ) { return; } var test = new Test({ testName: testName, skip: true }); test.queue(); } // Will be exposed as QUnit.only function only( testName, expected, callback, async ) { var newTest; if ( focused ) { return; } QUnit.config.queue.length = 0; focused = true; if ( arguments.length === 2 ) { callback = expected; expected = null; } newTest = new Test({ testName: testName, expected: expected, async: async, callback: callback }); newTest.queue(); } function Assert( testContext ) { this.test = testContext; } // Assert helpers QUnit.assert = Assert.prototype = { // Specify the number of expected assertions to guarantee that failed test // (no assertions are run at all) don't slip through. expect: function( asserts ) { if ( arguments.length === 1 ) { this.test.expected = asserts; } else { return this.test.expected; } }, // Increment this Test's semaphore counter, then return a function that // decrements that counter a maximum of once. async: function( count ) { var test = this.test, popped = false, acceptCallCount = count; if ( typeof acceptCallCount === "undefined" ) { acceptCallCount = 1; } test.semaphore += 1; test.usedAsync = true; pauseProcessing(); return function done() { if ( popped ) { test.pushFailure( "Too many calls to the `assert.async` callback", sourceFromStacktrace( 2 ) ); return; } acceptCallCount -= 1; if ( acceptCallCount > 0 ) { return; } test.semaphore -= 1; popped = true; resumeProcessing(); }; }, // Exports test.push() to the user API push: function( /* result, actual, expected, message, negative */ ) { var assert = this, currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; // Backwards compatibility fix. // Allows the direct use of global exported assertions and QUnit.assert.* // Although, it's use is not recommended as it can leak assertions // to other tests from async tests, because we only get a reference to the current test, // not exactly the test where assertion were intended to be called. if ( !currentTest ) { throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); } if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", sourceFromStacktrace( 2 ) ); // Allow this assertion to continue running anyway... } if ( !( assert instanceof Assert ) ) { assert = currentTest.assert; } return assert.test.push.apply( assert.test, arguments ); }, ok: function( result, message ) { message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + QUnit.dump.parse( result ) ); this.push( !!result, result, true, message ); }, notOk: function( result, message ) { message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + QUnit.dump.parse( result ) ); this.push( !result, result, false, message ); }, equal: function( actual, expected, message ) { /*jshint eqeqeq:false */ this.push( expected == actual, actual, expected, message ); }, notEqual: function( actual, expected, message ) { /*jshint eqeqeq:false */ this.push( expected != actual, actual, expected, message, true ); }, propEqual: function( actual, expected, message ) { actual = objectValues( actual ); expected = objectValues( expected ); this.push( QUnit.equiv( actual, expected ), actual, expected, message ); }, notPropEqual: function( actual, expected, message ) { actual = objectValues( actual ); expected = objectValues( expected ); this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); }, deepEqual: function( actual, expected, message ) { this.push( QUnit.equiv( actual, expected ), actual, expected, message ); }, notDeepEqual: function( actual, expected, message ) { this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true ); }, strictEqual: function( actual, expected, message ) { this.push( expected === actual, actual, expected, message ); }, notStrictEqual: function( actual, expected, message ) { this.push( expected !== actual, actual, expected, message, true ); }, "throws": function( block, expected, message ) { var actual, expectedType, expectedOutput = expected, ok = false, currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; // 'expected' is optional unless doing string comparison if ( message == null && typeof expected === "string" ) { message = expected; expected = null; } currentTest.ignoreGlobalErrors = true; try { block.call( currentTest.testEnvironment ); } catch (e) { actual = e; } currentTest.ignoreGlobalErrors = false; if ( actual ) { expectedType = QUnit.objectType( expected ); // we don't want to validate thrown error if ( !expected ) { ok = true; expectedOutput = null; // expected is a regexp } else if ( expectedType === "regexp" ) { ok = expected.test( errorString( actual ) ); // expected is a string } else if ( expectedType === "string" ) { ok = expected === errorString( actual ); // expected is a constructor, maybe an Error constructor } else if ( expectedType === "function" && actual instanceof expected ) { ok = true; // expected is an Error object } else if ( expectedType === "object" ) { ok = actual instanceof expected.constructor && actual.name === expected.name && actual.message === expected.message; // expected is a validation function which returns true if validation passed } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { expectedOutput = null; ok = true; } } currentTest.assert.push( ok, actual, expectedOutput, message ); } }; // Provide an alternative to assert.throws(), for environments that consider throws a reserved word // Known to us are: Closure Compiler, Narwhal (function() { /*jshint sub:true */ Assert.prototype.raises = Assert.prototype[ "throws" ]; }()); function errorString( error ) { var name, message, resultErrorString = error.toString(); if ( resultErrorString.substring( 0, 7 ) === "[object" ) { name = error.name ? error.name.toString() : "Error"; message = error.message ? error.message.toString() : ""; if ( name && message ) { return name + ": " + message; } else if ( name ) { return name; } else if ( message ) { return message; } else { return "Error"; } } else { return resultErrorString; } } // Test for equality any JavaScript type. // Author: Philippe Rathé QUnit.equiv = (function() { // Stack to decide between skip/abort functions var callers = []; // Stack to avoiding loops from circular referencing var parents = []; var parentsB = []; var getProto = Object.getPrototypeOf || function( obj ) { /*jshint proto: true */ return obj.__proto__; }; function useStrictEquality( b, a ) { // To catch short annotation VS 'new' annotation of a declaration. e.g.: // `var i = 1;` // `var j = new Number(1);` if ( typeof a === "object" ) { a = a.valueOf(); } if ( typeof b === "object" ) { b = b.valueOf(); } return a === b; } function compareConstructors( a, b ) { var protoA = getProto( a ); var protoB = getProto( b ); // Comparing constructors is more strict than using `instanceof` if ( a.constructor === b.constructor ) { return true; } // Ref #851 // If the obj prototype descends from a null constructor, treat it // as a null prototype. if ( protoA && protoA.constructor === null ) { protoA = null; } if ( protoB && protoB.constructor === null ) { protoB = null; } // Allow objects with no prototype to be equivalent to // objects with Object as their constructor. if ( ( protoA === null && protoB === Object.prototype ) || ( protoB === null && protoA === Object.prototype ) ) { return true; } return false; } function getRegExpFlags( regexp ) { return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ]; } var callbacks = { "string": useStrictEquality, "boolean": useStrictEquality, "number": useStrictEquality, "null": useStrictEquality, "undefined": useStrictEquality, "symbol": useStrictEquality, "date": useStrictEquality, "nan": function() { return true; }, "regexp": function( b, a ) { return a.source === b.source && // Include flags in the comparison getRegExpFlags( a ) === getRegExpFlags( b ); }, // - skip when the property is a method of an instance (OOP) // - abort otherwise, // initial === would have catch identical references anyway "function": function() { var caller = callers[ callers.length - 1 ]; return caller !== Object && typeof caller !== "undefined"; }, "array": function( b, a ) { var i, j, len, loop, aCircular, bCircular; len = a.length; if ( len !== b.length ) { // safe and faster return false; } // Track reference to avoid circular references parents.push( a ); parentsB.push( b ); for ( i = 0; i < len; i++ ) { loop = false; for ( j = 0; j < parents.length; j++ ) { aCircular = parents[ j ] === a[ i ]; bCircular = parentsB[ j ] === b[ i ]; if ( aCircular || bCircular ) { if ( a[ i ] === b[ i ] || aCircular && bCircular ) { loop = true; } else { parents.pop(); parentsB.pop(); return false; } } } if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { parents.pop(); parentsB.pop(); return false; } } parents.pop(); parentsB.pop(); return true; }, "set": function( b, a ) { var aArray, bArray; aArray = []; a.forEach( function( v ) { aArray.push( v ); }); bArray = []; b.forEach( function( v ) { bArray.push( v ); }); return innerEquiv( bArray, aArray ); }, "map": function( b, a ) { var aArray, bArray; aArray = []; a.forEach( function( v, k ) { aArray.push( [ k, v ] ); }); bArray = []; b.forEach( function( v, k ) { bArray.push( [ k, v ] ); }); return innerEquiv( bArray, aArray ); }, "object": function( b, a ) { var i, j, loop, aCircular, bCircular; // Default to true var eq = true; var aProperties = []; var bProperties = []; if ( compareConstructors( a, b ) === false ) { return false; } // Stack constructor before traversing properties callers.push( a.constructor ); // Track reference to avoid circular references parents.push( a ); parentsB.push( b ); // Be strict: don't ensure hasOwnProperty and go deep for ( i in a ) { loop = false; for ( j = 0; j < parents.length; j++ ) { aCircular = parents[ j ] === a[ i ]; bCircular = parentsB[ j ] === b[ i ]; if ( aCircular || bCircular ) { if ( a[ i ] === b[ i ] || aCircular && bCircular ) { loop = true; } else { eq = false; break; } } } aProperties.push( i ); if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { eq = false; break; } } parents.pop(); parentsB.pop(); // Unstack, we are done callers.pop(); for ( i in b ) { // Collect b's properties bProperties.push( i ); } // Ensures identical properties name return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); } }; function typeEquiv( a, b ) { var type = QUnit.objectType( a ); return QUnit.objectType( b ) === type && callbacks[ type ]( b, a ); } // The real equiv function function innerEquiv( a, b ) { // We're done when there's nothing more to compare if ( arguments.length < 2 ) { return true; } // Require type-specific equality return ( a === b || typeEquiv( a, b ) ) && // ...across all consecutive argument pairs ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) ); } return innerEquiv; }()); // Based on jsDump by Ariel Flesler // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html QUnit.dump = (function() { function quote( str ) { return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; } function literal( o ) { return o + ""; } function join( pre, arr, post ) { var s = dump.separator(), base = dump.indent(), inner = dump.indent( 1 ); if ( arr.join ) { arr = arr.join( "," + s + inner ); } if ( !arr ) { return pre + post; } return [ pre, inner + arr, base + post ].join( s ); } function array( arr, stack ) { var i = arr.length, ret = new Array( i ); if ( dump.maxDepth && dump.depth > dump.maxDepth ) { return "[object Array]"; } this.up(); while ( i-- ) { ret[ i ] = this.parse( arr[ i ], undefined, stack ); } this.down(); return join( "[", ret, "]" ); } var reName = /^function (\w+)/, dump = { // objType is used mostly internally, you can fix a (custom) type in advance parse: function( obj, objType, stack ) { stack = stack || []; var res, parser, parserType, inStack = inArray( obj, stack ); if ( inStack !== -1 ) { return "recursion(" + ( inStack - stack.length ) + ")"; } objType = objType || this.typeOf( obj ); parser = this.parsers[ objType ]; parserType = typeof parser; if ( parserType === "function" ) { stack.push( obj ); res = parser.call( this, obj, stack ); stack.pop(); return res; } return ( parserType === "string" ) ? parser : this.parsers.error; }, typeOf: function( obj ) { var type; if ( obj === null ) { type = "null"; } else if ( typeof obj === "undefined" ) { type = "undefined"; } else if ( QUnit.is( "regexp", obj ) ) { type = "regexp"; } else if ( QUnit.is( "date", obj ) ) { type = "date"; } else if ( QUnit.is( "function", obj ) ) { type = "function"; } else if ( obj.setInterval !== undefined && obj.document !== undefined && obj.nodeType === undefined ) { type = "window"; } else if ( obj.nodeType === 9 ) { type = "document"; } else if ( obj.nodeType ) { type = "node"; } else if ( // native arrays toString.call( obj ) === "[object Array]" || // NodeList objects ( typeof obj.length === "number" && obj.item !== undefined && ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && obj[ 0 ] === undefined ) ) ) ) { type = "array"; } else if ( obj.constructor === Error.prototype.constructor ) { type = "error"; } else { type = typeof obj; } return type; }, separator: function() { return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; }, // extra can be a number, shortcut for increasing-calling-decreasing indent: function( extra ) { if ( !this.multiline ) { return ""; } var chr = this.indentChar; if ( this.HTML ) { chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); } return new Array( this.depth + ( extra || 0 ) ).join( chr ); }, up: function( a ) { this.depth += a || 1; }, down: function( a ) { this.depth -= a || 1; }, setParser: function( name, parser ) { this.parsers[ name ] = parser; }, // The next 3 are exposed so you can use them quote: quote, literal: literal, join: join, // depth: 1, maxDepth: QUnit.config.maxDepth, // This is the list of parsers, to modify them, use dump.setParser parsers: { window: "[Window]", document: "[Document]", error: function( error ) { return "Error(\"" + error.message + "\")"; }, unknown: "[Unknown]", "null": "null", "undefined": "undefined", "function": function( fn ) { var ret = "function", // functions never have name in IE name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; if ( name ) { ret += " " + name; } ret += "( "; ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); return join( ret, dump.parse( fn, "functionCode" ), "}" ); }, array: array, nodelist: array, "arguments": array, object: function( map, stack ) { var keys, key, val, i, nonEnumerableProperties, ret = []; if ( dump.maxDepth && dump.depth > dump.maxDepth ) { return "[object Object]"; } dump.up(); keys = []; for ( key in map ) { keys.push( key ); } // Some properties are not always enumerable on Error objects. nonEnumerableProperties = [ "message", "name" ]; for ( i in nonEnumerableProperties ) { key = nonEnumerableProperties[ i ]; if ( key in map && inArray( key, keys ) < 0 ) { keys.push( key ); } } keys.sort(); for ( i = 0; i < keys.length; i++ ) { key = keys[ i ]; val = map[ key ]; ret.push( dump.parse( key, "key" ) + ": " + dump.parse( val, undefined, stack ) ); } dump.down(); return join( "{", ret, "}" ); }, node: function( node ) { var len, i, val, open = dump.HTML ? "<" : "<", close = dump.HTML ? ">" : ">", tag = node.nodeName.toLowerCase(), ret = open + tag, attrs = node.attributes; if ( attrs ) { for ( i = 0, len = attrs.length; i < len; i++ ) { val = attrs[ i ].nodeValue; // IE6 includes all attributes in .attributes, even ones not explicitly // set. Those have values like undefined, null, 0, false, "" or // "inherit". if ( val && val !== "inherit" ) { ret += " " + attrs[ i ].nodeName + "=" + dump.parse( val, "attribute" ); } } } ret += close; // Show content of TextNode or CDATASection if ( node.nodeType === 3 || node.nodeType === 4 ) { ret += node.nodeValue; } return ret + open + "/" + tag + close; }, // function calls it internally, it's the arguments part of the function functionArgs: function( fn ) { var args, l = fn.length; if ( !l ) { return ""; } args = new Array( l ); while ( l-- ) { // 97 is 'a' args[ l ] = String.fromCharCode( 97 + l ); } return " " + args.join( ", " ) + " "; }, // object calls it internally, the key part of an item in a map key: quote, // function calls it internally, it's the content of the function functionCode: "[code]", // node calls it internally, it's a html attribute value attribute: quote, string: quote, date: quote, regexp: literal, number: literal, "boolean": literal }, // if true, entities are escaped ( <, >, \t, space and \n ) HTML: false, // indentation unit indentChar: " ", // if true, items in a collection, are separated by a \n, else just a space. multiline: true }; return dump; }()); // back compat QUnit.jsDump = QUnit.dump; // For browser, export only select globals if ( defined.document ) { // Deprecated // Extend assert methods to QUnit and Global scope through Backwards compatibility (function() { var i, assertions = Assert.prototype; function applyCurrent( current ) { return function() { var assert = new Assert( QUnit.config.current ); current.apply( assert, arguments ); }; } for ( i in assertions ) { QUnit[ i ] = applyCurrent( assertions[ i ] ); } })(); (function() { var i, l, keys = [ "test", "module", "expect", "asyncTest", "start", "stop", "ok", "notOk", "equal", "notEqual", "propEqual", "notPropEqual", "deepEqual", "notDeepEqual", "strictEqual", "notStrictEqual", "throws", "raises" ]; for ( i = 0, l = keys.length; i < l; i++ ) { window[ keys[ i ] ] = QUnit[ keys[ i ] ]; } })(); window.QUnit = QUnit; } // For nodejs if ( typeof module !== "undefined" && module && module.exports ) { module.exports = QUnit; // For consistency with CommonJS environments' exports module.exports.QUnit = QUnit; } // For CommonJS with exports, but without module.exports, like Rhino if ( typeof exports !== "undefined" && exports ) { exports.QUnit = QUnit; } if ( typeof define === "function" && define.amd ) { define( function() { return QUnit; } ); QUnit.config.autostart = false; } /* * This file is a modified version of google-diff-match-patch's JavaScript implementation * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), * modifications are licensed as more fully set forth in LICENSE.txt. * * The original source of google-diff-match-patch is attributable and licensed as follows: * * Copyright 2006 Google Inc. * https://code.google.com/p/google-diff-match-patch/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * More Info: * https://code.google.com/p/google-diff-match-patch/ * * Usage: QUnit.diff(expected, actual) * */ QUnit.diff = ( function() { function DiffMatchPatch() { } // DIFF FUNCTIONS /** * The data structure representing a diff is an array of tuples: * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] * which means: delete 'Hello', add 'Goodbye' and keep ' world.' */ var DIFF_DELETE = -1, DIFF_INSERT = 1, DIFF_EQUAL = 0; /** * Find the differences between two texts. Simplifies the problem by stripping * any common prefix or suffix off the texts before diffing. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean=} optChecklines Optional speedup flag. If present and false, * then don't run a line-level diff first to identify the changed areas. * Defaults to true, which does a faster, slightly less optimal diff. * @return {!Array.} Array of diff tuples. */ DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; // The diff must be complete in up to 1 second. deadline = ( new Date() ).getTime() + 1000; // Check for null inputs. if ( text1 === null || text2 === null ) { throw new Error( "Null input. (DiffMain)" ); } // Check for equality (speedup). if ( text1 === text2 ) { if ( text1 ) { return [ [ DIFF_EQUAL, text1 ] ]; } return []; } if ( typeof optChecklines === "undefined" ) { optChecklines = true; } checklines = optChecklines; // Trim off common prefix (speedup). commonlength = this.diffCommonPrefix( text1, text2 ); commonprefix = text1.substring( 0, commonlength ); text1 = text1.substring( commonlength ); text2 = text2.substring( commonlength ); // Trim off common suffix (speedup). commonlength = this.diffCommonSuffix( text1, text2 ); commonsuffix = text1.substring( text1.length - commonlength ); text1 = text1.substring( 0, text1.length - commonlength ); text2 = text2.substring( 0, text2.length - commonlength ); // Compute the diff on the middle block. diffs = this.diffCompute( text1, text2, checklines, deadline ); // Restore the prefix and suffix. if ( commonprefix ) { diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); } if ( commonsuffix ) { diffs.push( [ DIFF_EQUAL, commonsuffix ] ); } this.diffCleanupMerge( diffs ); return diffs; }; /** * Reduce the number of edits by eliminating operationally trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Is there an insertion operation before the last equality. preIns = false; // Is there a deletion operation before the last equality. preDel = false; // Is there an insertion operation after the last equality. postIns = false; // Is there a deletion operation after the last equality. postDel = false; while ( pointer < diffs.length ) { // Equality found. if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { // Candidate found. equalities[ equalitiesLength++ ] = pointer; preIns = postIns; preDel = postDel; lastequality = diffs[ pointer ][ 1 ]; } else { // Not a candidate, and can never become one. equalitiesLength = 0; lastequality = null; } postIns = postDel = false; // An insertion or deletion. } else { if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { postDel = true; } else { postIns = true; } /* * Five types to be split: * ABXYCD * AXCD * ABXC * AXCD * ABXC */ if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || ( ( lastequality.length < 2 ) && ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { // Duplicate record. diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); // Change second copy to insert. diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; equalitiesLength--; // Throw away the equality we just deleted; lastequality = null; if ( preIns && preDel ) { // No changes made which could affect previous entry, keep going. postIns = postDel = true; equalitiesLength = 0; } else { equalitiesLength--; // Throw away the previous equality. pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; postIns = postDel = false; } changes = true; } } pointer++; } if ( changes ) { this.diffCleanupMerge( diffs ); } }; /** * Convert a diff array into a pretty HTML report. * @param {!Array.} diffs Array of diff tuples. * @param {integer} string to be beautified. * @return {string} HTML representation. */ DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { var op, data, x, html = []; for ( x = 0; x < diffs.length; x++ ) { op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) data = diffs[ x ][ 1 ]; // Text of change. switch ( op ) { case DIFF_INSERT: html[ x ] = "" + data + ""; break; case DIFF_DELETE: html[ x ] = "" + data + ""; break; case DIFF_EQUAL: html[ x ] = "" + data + ""; break; } } return html.join( "" ); }; /** * Determine the common prefix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the start of each * string. */ DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { var pointermid, pointermax, pointermin, pointerstart; // Quick check for common null cases. if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min( text1.length, text2.length ); pointermid = pointermax; pointerstart = 0; while ( pointermin < pointermid ) { if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) { pointermin = pointermid; pointerstart = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); } return pointermid; }; /** * Determine the common suffix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of each string. */ DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { var pointermid, pointermax, pointermin, pointerend; // Quick check for common null cases. if ( !text1 || !text2 || text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min( text1.length, text2.length ); pointermid = pointermax; pointerend = 0; while ( pointermin < pointermid ) { if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { pointermin = pointermid; pointerend = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); } return pointermid; }; /** * Find the differences between two texts. Assumes that the texts do not * have any common prefix or suffix. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean} checklines Speedup flag. If false, then don't run a * line-level diff first to identify the changed areas. * If true, then run a faster, slightly less optimal diff. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; if ( !text1 ) { // Just add some text (speedup). return [ [ DIFF_INSERT, text2 ] ]; } if ( !text2 ) { // Just delete some text (speedup). return [ [ DIFF_DELETE, text1 ] ]; } longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; i = longtext.indexOf( shorttext ); if ( i !== -1 ) { // Shorter text is inside the longer text (speedup). diffs = [ [ DIFF_INSERT, longtext.substring( 0, i ) ], [ DIFF_EQUAL, shorttext ], [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] ]; // Swap insertions for deletions if diff is reversed. if ( text1.length > text2.length ) { diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; } return diffs; } if ( shorttext.length === 1 ) { // Single character string. // After the previous speedup, the character can't be an equality. return [ [ DIFF_DELETE, text1 ], [ DIFF_INSERT, text2 ] ]; } // Check to see if the problem can be split in two. hm = this.diffHalfMatch( text1, text2 ); if ( hm ) { // A half-match was found, sort out the return data. text1A = hm[ 0 ]; text1B = hm[ 1 ]; text2A = hm[ 2 ]; text2B = hm[ 3 ]; midCommon = hm[ 4 ]; // Send both pairs off for separate processing. diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); // Merge the results. return diffsA.concat( [ [ DIFF_EQUAL, midCommon ] ], diffsB ); } if ( checklines && text1.length > 100 && text2.length > 100 ) { return this.diffLineMode( text1, text2, deadline ); } return this.diffBisect( text1, text2, deadline ); }; /** * Do the two texts share a substring which is at least half the length of the * longer text? * This speedup can produce non-minimal diffs. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {Array.} Five element Array, containing the prefix of * text1, the suffix of text1, the prefix of text2, the suffix of * text2 and the common middle. Or null if there was no match. * @private */ DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { return null; // Pointless. } dmp = this; // 'this' becomes 'window' in a closure. /** * Does a substring of shorttext exist within longtext such that the substring * is at least half the length of longtext? * Closure, but does not reference any external variables. * @param {string} longtext Longer string. * @param {string} shorttext Shorter string. * @param {number} i Start index of quarter length substring within longtext. * @return {Array.} Five element Array, containing the prefix of * longtext, the suffix of longtext, the prefix of shorttext, the suffix * of shorttext and the common middle. Or null if there was no match. * @private */ function diffHalfMatchI( longtext, shorttext, i ) { var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; // Start with a 1/4 length substring at position i as a seed. seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); j = -1; bestCommon = ""; while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), shorttext.substring( j ) ); suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), shorttext.substring( 0, j ) ); if ( bestCommon.length < suffixLength + prefixLength ) { bestCommon = shorttext.substring( j - suffixLength, j ) + shorttext.substring( j, j + prefixLength ); bestLongtextA = longtext.substring( 0, i - suffixLength ); bestLongtextB = longtext.substring( i + prefixLength ); bestShorttextA = shorttext.substring( 0, j - suffixLength ); bestShorttextB = shorttext.substring( j + prefixLength ); } } if ( bestCommon.length * 2 >= longtext.length ) { return [ bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon ]; } else { return null; } } // First check if the second quarter is the seed for a half-match. hm1 = diffHalfMatchI( longtext, shorttext, Math.ceil( longtext.length / 4 ) ); // Check again based on the third quarter. hm2 = diffHalfMatchI( longtext, shorttext, Math.ceil( longtext.length / 2 ) ); if ( !hm1 && !hm2 ) { return null; } else if ( !hm2 ) { hm = hm1; } else if ( !hm1 ) { hm = hm2; } else { // Both matched. Select the longest. hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; } // A half-match was found, sort out the return data. text1A, text1B, text2A, text2B; if ( text1.length > text2.length ) { text1A = hm[ 0 ]; text1B = hm[ 1 ]; text2A = hm[ 2 ]; text2B = hm[ 3 ]; } else { text2A = hm[ 0 ]; text2B = hm[ 1 ]; text1A = hm[ 2 ]; text1B = hm[ 3 ]; } midCommon = hm[ 4 ]; return [ text1A, text1B, text2A, text2B, midCommon ]; }; /** * Do a quick line-level diff on both strings, then rediff the parts for * greater accuracy. * This speedup can produce non-minimal diffs. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; // Scan the text on a line-by-line basis first. a = this.diffLinesToChars( text1, text2 ); text1 = a.chars1; text2 = a.chars2; linearray = a.lineArray; diffs = this.DiffMain( text1, text2, false, deadline ); // Convert the diff back to original text. this.diffCharsToLines( diffs, linearray ); // Eliminate freak matches (e.g. blank lines) this.diffCleanupSemantic( diffs ); // Rediff any replacement blocks, this time character-by-character. // Add a dummy entry at the end. diffs.push( [ DIFF_EQUAL, "" ] ); pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; while ( pointer < diffs.length ) { switch ( diffs[ pointer ][ 0 ] ) { case DIFF_INSERT: countInsert++; textInsert += diffs[ pointer ][ 1 ]; break; case DIFF_DELETE: countDelete++; textDelete += diffs[ pointer ][ 1 ]; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if ( countDelete >= 1 && countInsert >= 1 ) { // Delete the offending records and add the merged ones. diffs.splice( pointer - countDelete - countInsert, countDelete + countInsert ); pointer = pointer - countDelete - countInsert; a = this.DiffMain( textDelete, textInsert, false, deadline ); for ( j = a.length - 1; j >= 0; j-- ) { diffs.splice( pointer, 0, a[ j ] ); } pointer = pointer + a.length; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } pointer++; } diffs.pop(); // Remove the dummy entry at the end. return diffs; }; /** * Find the 'middle snake' of a diff, split the problem in two * and return the recursively constructed diff. * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); vOffset = maxD; vLength = 2 * maxD; v1 = new Array( vLength ); v2 = new Array( vLength ); // Setting all elements to -1 is faster in Chrome & Firefox than mixing // integers and undefined. for ( x = 0; x < vLength; x++ ) { v1[ x ] = -1; v2[ x ] = -1; } v1[ vOffset + 1 ] = 0; v2[ vOffset + 1 ] = 0; delta = text1Length - text2Length; // If the total number of characters is odd, then the front path will collide // with the reverse path. front = ( delta % 2 !== 0 ); // Offsets for start and end of k loop. // Prevents mapping of space beyond the grid. k1start = 0; k1end = 0; k2start = 0; k2end = 0; for ( d = 0; d < maxD; d++ ) { // Bail out if deadline is reached. if ( ( new Date() ).getTime() > deadline ) { break; } // Walk the front path one step. for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { k1Offset = vOffset + k1; if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { x1 = v1[ k1Offset + 1 ]; } else { x1 = v1[ k1Offset - 1 ] + 1; } y1 = x1 - k1; while ( x1 < text1Length && y1 < text2Length && text1.charAt( x1 ) === text2.charAt( y1 ) ) { x1++; y1++; } v1[ k1Offset ] = x1; if ( x1 > text1Length ) { // Ran off the right of the graph. k1end += 2; } else if ( y1 > text2Length ) { // Ran off the bottom of the graph. k1start += 2; } else if ( front ) { k2Offset = vOffset + delta - k1; if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { // Mirror x2 onto top-left coordinate system. x2 = text1Length - v2[ k2Offset ]; if ( x1 >= x2 ) { // Overlap detected. return this.diffBisectSplit( text1, text2, x1, y1, deadline ); } } } } // Walk the reverse path one step. for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { k2Offset = vOffset + k2; if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { x2 = v2[ k2Offset + 1 ]; } else { x2 = v2[ k2Offset - 1 ] + 1; } y2 = x2 - k2; while ( x2 < text1Length && y2 < text2Length && text1.charAt( text1Length - x2 - 1 ) === text2.charAt( text2Length - y2 - 1 ) ) { x2++; y2++; } v2[ k2Offset ] = x2; if ( x2 > text1Length ) { // Ran off the left of the graph. k2end += 2; } else if ( y2 > text2Length ) { // Ran off the top of the graph. k2start += 2; } else if ( !front ) { k1Offset = vOffset + delta - k2; if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { x1 = v1[ k1Offset ]; y1 = vOffset + x1 - k1Offset; // Mirror x2 onto top-left coordinate system. x2 = text1Length - x2; if ( x1 >= x2 ) { // Overlap detected. return this.diffBisectSplit( text1, text2, x1, y1, deadline ); } } } } } // Diff took too long and hit the deadline or // number of diffs equals number of characters, no commonality at all. return [ [ DIFF_DELETE, text1 ], [ DIFF_INSERT, text2 ] ]; }; /** * Given the location of the 'middle snake', split the diff in two parts * and recurse. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} x Index of split point in text1. * @param {number} y Index of split point in text2. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { var text1a, text1b, text2a, text2b, diffs, diffsb; text1a = text1.substring( 0, x ); text2a = text2.substring( 0, y ); text1b = text1.substring( x ); text2b = text2.substring( y ); // Compute both diffs serially. diffs = this.DiffMain( text1a, text2a, false, deadline ); diffsb = this.DiffMain( text1b, text2b, false, deadline ); return diffs.concat( diffsb ); }; /** * Reduce the number of edits by eliminating semantically trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Number of characters that changed prior to the equality. lengthInsertions1 = 0; lengthDeletions1 = 0; // Number of characters that changed after the equality. lengthInsertions2 = 0; lengthDeletions2 = 0; while ( pointer < diffs.length ) { if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. equalities[ equalitiesLength++ ] = pointer; lengthInsertions1 = lengthInsertions2; lengthDeletions1 = lengthDeletions2; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = diffs[ pointer ][ 1 ]; } else { // An insertion or deletion. if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { lengthInsertions2 += diffs[ pointer ][ 1 ].length; } else { lengthDeletions2 += diffs[ pointer ][ 1 ].length; } // Eliminate an equality that is smaller or equal to the edits on both // sides of it. if ( lastequality && ( lastequality.length <= Math.max( lengthInsertions1, lengthDeletions1 ) ) && ( lastequality.length <= Math.max( lengthInsertions2, lengthDeletions2 ) ) ) { // Duplicate record. diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); // Change second copy to insert. diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; // Throw away the equality we just deleted. equalitiesLength--; // Throw away the previous equality (it needs to be reevaluated). equalitiesLength--; pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; // Reset the counters. lengthInsertions1 = 0; lengthDeletions1 = 0; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = null; changes = true; } } pointer++; } // Normalize the diff. if ( changes ) { this.diffCleanupMerge( diffs ); } // Find any overlaps between deletions and insertions. // e.g: abcxxxxxxdef // -> abcxxxdef // e.g: xxxabcdefxxx // -> defxxxabc // Only extract an overlap if it is as big as the edit ahead or behind it. pointer = 1; while ( pointer < diffs.length ) { if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && diffs[ pointer ][ 0 ] === DIFF_INSERT ) { deletion = diffs[ pointer - 1 ][ 1 ]; insertion = diffs[ pointer ][ 1 ]; overlapLength1 = this.diffCommonOverlap( deletion, insertion ); overlapLength2 = this.diffCommonOverlap( insertion, deletion ); if ( overlapLength1 >= overlapLength2 ) { if ( overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2 ) { // Overlap found. Insert an equality and trim the surrounding edits. diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] ); diffs[ pointer - 1 ][ 1 ] = deletion.substring( 0, deletion.length - overlapLength1 ); diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); pointer++; } } else { if ( overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2 ) { // Reverse overlap found. // Insert an equality and swap and trim the surrounding edits. diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] ); diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; diffs[ pointer - 1 ][ 1 ] = insertion.substring( 0, insertion.length - overlapLength2 ); diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; diffs[ pointer + 1 ][ 1 ] = deletion.substring( overlapLength2 ); pointer++; } } pointer++; } pointer++; } }; /** * Determine if the suffix of one string is the prefix of another. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of the first * string and the start of the second string. * @private */ DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { var text1Length, text2Length, textLength, best, length, pattern, found; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; // Eliminate the null case. if ( text1Length === 0 || text2Length === 0 ) { return 0; } // Truncate the longer string. if ( text1Length > text2Length ) { text1 = text1.substring( text1Length - text2Length ); } else if ( text1Length < text2Length ) { text2 = text2.substring( 0, text1Length ); } textLength = Math.min( text1Length, text2Length ); // Quick check for the worst case. if ( text1 === text2 ) { return textLength; } // Start by looking for a single character match // and increase length until no match is found. // Performance analysis: https://neil.fraser.name/news/2010/11/04/ best = 0; length = 1; while ( true ) { pattern = text1.substring( textLength - length ); found = text2.indexOf( pattern ); if ( found === -1 ) { return best; } length += found; if ( found === 0 || text1.substring( textLength - length ) === text2.substring( 0, length ) ) { best = length; length++; } } }; /** * Split two texts into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {{chars1: string, chars2: string, lineArray: !Array.}} * An object containing the encoded text1, the encoded text2 and * the array of unique strings. * The zeroth element of the array of unique strings is intentionally blank. * @private */ DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { var lineArray, lineHash, chars1, chars2; lineArray = []; // e.g. lineArray[4] === 'Hello\n' lineHash = {}; // e.g. lineHash['Hello\n'] === 4 // '\x00' is a valid character, but various debuggers don't like it. // So we'll insert a junk entry to avoid generating a null character. lineArray[ 0 ] = ""; /** * Split a text into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * Modifies linearray and linehash through being a closure. * @param {string} text String to encode. * @return {string} Encoded string. * @private */ function diffLinesToCharsMunge( text ) { var chars, lineStart, lineEnd, lineArrayLength, line; chars = ""; // Walk the text, pulling out a substring for each line. // text.split('\n') would would temporarily double our memory footprint. // Modifying text would create many large strings to garbage collect. lineStart = 0; lineEnd = -1; // Keeping our own length variable is faster than looking it up. lineArrayLength = lineArray.length; while ( lineEnd < text.length - 1 ) { lineEnd = text.indexOf( "\n", lineStart ); if ( lineEnd === -1 ) { lineEnd = text.length - 1; } line = text.substring( lineStart, lineEnd + 1 ); lineStart = lineEnd + 1; if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : ( lineHash[ line ] !== undefined ) ) { chars += String.fromCharCode( lineHash[ line ] ); } else { chars += String.fromCharCode( lineArrayLength ); lineHash[ line ] = lineArrayLength; lineArray[ lineArrayLength++ ] = line; } } return chars; } chars1 = diffLinesToCharsMunge( text1 ); chars2 = diffLinesToCharsMunge( text2 ); return { chars1: chars1, chars2: chars2, lineArray: lineArray }; }; /** * Rehydrate the text in a diff from a string of line hashes to real lines of * text. * @param {!Array.} diffs Array of diff tuples. * @param {!Array.} lineArray Array of unique strings. * @private */ DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { var x, chars, text, y; for ( x = 0; x < diffs.length; x++ ) { chars = diffs[ x ][ 1 ]; text = []; for ( y = 0; y < chars.length; y++ ) { text[ y ] = lineArray[ chars.charCodeAt( y ) ]; } diffs[ x ][ 1 ] = text.join( "" ); } }; /** * Reorder and merge like edit sections. Merge equalities. * Any edit section can move as long as it doesn't cross an equality. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { var pointer, countDelete, countInsert, textInsert, textDelete, commonlength, changes, diffPointer, position; diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; commonlength; while ( pointer < diffs.length ) { switch ( diffs[ pointer ][ 0 ] ) { case DIFF_INSERT: countInsert++; textInsert += diffs[ pointer ][ 1 ]; pointer++; break; case DIFF_DELETE: countDelete++; textDelete += diffs[ pointer ][ 1 ]; pointer++; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if ( countDelete + countInsert > 1 ) { if ( countDelete !== 0 && countInsert !== 0 ) { // Factor out any common prefixes. commonlength = this.diffCommonPrefix( textInsert, textDelete ); if ( commonlength !== 0 ) { if ( ( pointer - countDelete - countInsert ) > 0 && diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === DIFF_EQUAL ) { diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += textInsert.substring( 0, commonlength ); } else { diffs.splice( 0, 0, [ DIFF_EQUAL, textInsert.substring( 0, commonlength ) ] ); pointer++; } textInsert = textInsert.substring( commonlength ); textDelete = textDelete.substring( commonlength ); } // Factor out any common suffixies. commonlength = this.diffCommonSuffix( textInsert, textDelete ); if ( commonlength !== 0 ) { diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - commonlength ) + diffs[ pointer ][ 1 ]; textInsert = textInsert.substring( 0, textInsert.length - commonlength ); textDelete = textDelete.substring( 0, textDelete.length - commonlength ); } } // Delete the offending records and add the merged ones. if ( countDelete === 0 ) { diffs.splice( pointer - countInsert, countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); } else if ( countInsert === 0 ) { diffs.splice( pointer - countDelete, countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); } else { diffs.splice( pointer - countDelete - countInsert, countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] ); } pointer = pointer - countDelete - countInsert + ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { // Merge this equality with the previous one. diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; diffs.splice( pointer, 1 ); } else { pointer++; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } } if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { diffs.pop(); // Remove the dummy entry at the end. } // Second pass: look for single edits surrounded on both sides by equalities // which can be shifted sideways to eliminate an equality. // e.g: ABAC -> ABAC changes = false; pointer = 1; // Intentionally ignore the first and last element (don't need checking). while ( pointer < diffs.length - 1 ) { if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { diffPointer = diffs[ pointer ][ 1 ]; position = diffPointer.substring( diffPointer.length - diffs[ pointer - 1 ][ 1 ].length ); // This is a single edit surrounded by equalities. if ( position === diffs[ pointer - 1 ][ 1 ] ) { // Shift the edit over the previous equality. diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - diffs[ pointer - 1 ][ 1 ].length ); diffs[ pointer + 1 ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; diffs.splice( pointer - 1, 1 ); changes = true; } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === diffs[ pointer + 1 ][ 1 ] ) { // Shift the edit over the next equality. diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; diffs[ pointer ][ 1 ] = diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + diffs[ pointer + 1 ][ 1 ]; diffs.splice( pointer + 1, 1 ); changes = true; } } pointer++; } // If shifts were made, the diff needs reordering and another shift sweep. if ( changes ) { this.diffCleanupMerge( diffs ); } }; return function( o, n ) { var diff, output, text; diff = new DiffMatchPatch(); output = diff.DiffMain( o, n ); diff.diffCleanupEfficiency( output ); text = diff.diffPrettyHtml( output ); return text; }; }() ); // Get a reference to the global object, like window in browsers }( (function() { return this; })() )); (function() { // Don't load the HTML Reporter on non-Browser environments if ( typeof window === "undefined" || !window.document ) { return; } // Deprecated QUnit.init - Ref #530 // Re-initialize the configuration options QUnit.init = function() { var tests, banner, result, qunit, config = QUnit.config; config.stats = { all: 0, bad: 0 }; config.moduleStats = { all: 0, bad: 0 }; config.started = 0; config.updateRate = 1000; config.blocking = false; config.autostart = true; config.autorun = false; config.filter = ""; config.queue = []; // Return on non-browser environments // This is necessary to not break on node tests if ( typeof window === "undefined" ) { return; } qunit = id( "qunit" ); if ( qunit ) { qunit.innerHTML = "

      " + escapeText( document.title ) + "

      " + "

      " + "
      " + "

      " + "
        "; } tests = id( "qunit-tests" ); banner = id( "qunit-banner" ); result = id( "qunit-testresult" ); if ( tests ) { tests.innerHTML = ""; } if ( banner ) { banner.className = ""; } if ( result ) { result.parentNode.removeChild( result ); } if ( tests ) { result = document.createElement( "p" ); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore( result, tests ); result.innerHTML = "Running...
         "; } }; var config = QUnit.config, collapseNext = false, hasOwn = Object.prototype.hasOwnProperty, defined = { document: window.document !== undefined, sessionStorage: (function() { var x = "qunit-test-string"; try { sessionStorage.setItem( x, x ); sessionStorage.removeItem( x ); return true; } catch ( e ) { return false; } }()) }, modulesList = []; /** * Escape text for attribute or text content. */ function escapeText( s ) { if ( !s ) { return ""; } s = s + ""; // Both single quotes and double quotes (for attributes) return s.replace( /['"<>&]/g, function( s ) { switch ( s ) { case "'": return "'"; case "\"": return """; case "<": return "<"; case ">": return ">"; case "&": return "&"; } }); } /** * @param {HTMLElement} elem * @param {string} type * @param {Function} fn */ function addEvent( elem, type, fn ) { if ( elem.addEventListener ) { // Standards-based browsers elem.addEventListener( type, fn, false ); } else if ( elem.attachEvent ) { // support: IE <9 elem.attachEvent( "on" + type, function() { var event = window.event; if ( !event.target ) { event.target = event.srcElement || document; } fn.call( elem, event ); }); } } /** * @param {Array|NodeList} elems * @param {string} type * @param {Function} fn */ function addEvents( elems, type, fn ) { var i = elems.length; while ( i-- ) { addEvent( elems[ i ], type, fn ); } } function hasClass( elem, name ) { return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; } function addClass( elem, name ) { if ( !hasClass( elem, name ) ) { elem.className += ( elem.className ? " " : "" ) + name; } } function toggleClass( elem, name ) { if ( hasClass( elem, name ) ) { removeClass( elem, name ); } else { addClass( elem, name ); } } function removeClass( elem, name ) { var set = " " + elem.className + " "; // Class name may appear multiple times while ( set.indexOf( " " + name + " " ) >= 0 ) { set = set.replace( " " + name + " ", " " ); } // trim for prettiness elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); } function id( name ) { return defined.document && document.getElementById && document.getElementById( name ); } function getUrlConfigHtml() { var i, j, val, escaped, escapedTooltip, selection = false, len = config.urlConfig.length, urlConfigHtml = ""; for ( i = 0; i < len; i++ ) { val = config.urlConfig[ i ]; if ( typeof val === "string" ) { val = { id: val, label: val }; } escaped = escapeText( val.id ); escapedTooltip = escapeText( val.tooltip ); if ( config[ val.id ] === undefined ) { config[ val.id ] = QUnit.urlParams[ val.id ]; } if ( !val.value || typeof val.value === "string" ) { urlConfigHtml += ""; } else { urlConfigHtml += ""; } } return urlConfigHtml; } // Handle "click" events on toolbar checkboxes and "change" for select menus. // Updates the URL with the new state of `config.urlConfig` values. function toolbarChanged() { var updatedUrl, value, field = this, params = {}; // Detect if field is a select menu or a checkbox if ( "selectedIndex" in field ) { value = field.options[ field.selectedIndex ].value || undefined; } else { value = field.checked ? ( field.defaultValue || true ) : undefined; } params[ field.name ] = value; updatedUrl = setUrl( params ); if ( "hidepassed" === field.name && "replaceState" in window.history ) { config[ field.name ] = value || false; if ( value ) { addClass( id( "qunit-tests" ), "hidepass" ); } else { removeClass( id( "qunit-tests" ), "hidepass" ); } // It is not necessary to refresh the whole page window.history.replaceState( null, "", updatedUrl ); } else { window.location = updatedUrl; } } function setUrl( params ) { var key, querystring = "?"; params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); for ( key in params ) { if ( hasOwn.call( params, key ) ) { if ( params[ key ] === undefined ) { continue; } querystring += encodeURIComponent( key ); if ( params[ key ] !== true ) { querystring += "=" + encodeURIComponent( params[ key ] ); } querystring += "&"; } } return location.protocol + "//" + location.host + location.pathname + querystring.slice( 0, -1 ); } function applyUrlParams() { var selectedModule, modulesList = id( "qunit-modulefilter" ), filter = id( "qunit-filter-input" ).value; selectedModule = modulesList ? decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : undefined; window.location = setUrl({ module: ( selectedModule === "" ) ? undefined : selectedModule, filter: ( filter === "" ) ? undefined : filter, // Remove testId filter testId: undefined }); } function toolbarUrlConfigContainer() { var urlConfigContainer = document.createElement( "span" ); urlConfigContainer.innerHTML = getUrlConfigHtml(); addClass( urlConfigContainer, "qunit-url-config" ); // For oldIE support: // * Add handlers to the individual elements instead of the container // * Use "click" instead of "change" for checkboxes addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); return urlConfigContainer; } function toolbarLooseFilter() { var filter = document.createElement( "form" ), label = document.createElement( "label" ), input = document.createElement( "input" ), button = document.createElement( "button" ); addClass( filter, "qunit-filter" ); label.innerHTML = "Filter: "; input.type = "text"; input.value = config.filter || ""; input.name = "filter"; input.id = "qunit-filter-input"; button.innerHTML = "Go"; label.appendChild( input ); filter.appendChild( label ); filter.appendChild( button ); addEvent( filter, "submit", function( ev ) { applyUrlParams(); if ( ev && ev.preventDefault ) { ev.preventDefault(); } return false; }); return filter; } function toolbarModuleFilterHtml() { var i, moduleFilterHtml = ""; if ( !modulesList.length ) { return false; } modulesList.sort(function( a, b ) { return a.localeCompare( b ); }); moduleFilterHtml += "" + ""; return moduleFilterHtml; } function toolbarModuleFilter() { var toolbar = id( "qunit-testrunner-toolbar" ), moduleFilter = document.createElement( "span" ), moduleFilterHtml = toolbarModuleFilterHtml(); if ( !toolbar || !moduleFilterHtml ) { return false; } moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); moduleFilter.innerHTML = moduleFilterHtml; addEvent( moduleFilter.lastChild, "change", applyUrlParams ); toolbar.appendChild( moduleFilter ); } function appendToolbar() { var toolbar = id( "qunit-testrunner-toolbar" ); if ( toolbar ) { toolbar.appendChild( toolbarUrlConfigContainer() ); toolbar.appendChild( toolbarLooseFilter() ); } } function appendHeader() { var header = id( "qunit-header" ); if ( header ) { header.innerHTML = "" + header.innerHTML + " "; } } function appendBanner() { var banner = id( "qunit-banner" ); if ( banner ) { banner.className = ""; } } function appendTestResults() { var tests = id( "qunit-tests" ), result = id( "qunit-testresult" ); if ( result ) { result.parentNode.removeChild( result ); } if ( tests ) { tests.innerHTML = ""; result = document.createElement( "p" ); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore( result, tests ); result.innerHTML = "Running...
         "; } } function storeFixture() { var fixture = id( "qunit-fixture" ); if ( fixture ) { config.fixture = fixture.innerHTML; } } function appendFilteredTest() { var testId = QUnit.config.testId; if ( !testId || testId.length <= 0 ) { return ""; } return "
        Rerunning selected tests: " + testId.join(", ") + " " + "Run all tests" + "
        "; } function appendUserAgent() { var userAgent = id( "qunit-userAgent" ); if ( userAgent ) { userAgent.innerHTML = ""; userAgent.appendChild( document.createTextNode( "QUnit " + QUnit.version + "; " + navigator.userAgent ) ); } } function appendTestsList( modules ) { var i, l, x, z, test, moduleObj; for ( i = 0, l = modules.length; i < l; i++ ) { moduleObj = modules[ i ]; if ( moduleObj.name ) { modulesList.push( moduleObj.name ); } for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { test = moduleObj.tests[ x ]; appendTest( test.name, test.testId, moduleObj.name ); } } } function appendTest( name, testId, moduleName ) { var title, rerunTrigger, testBlock, assertList, tests = id( "qunit-tests" ); if ( !tests ) { return; } title = document.createElement( "strong" ); title.innerHTML = getNameHtml( name, moduleName ); rerunTrigger = document.createElement( "a" ); rerunTrigger.innerHTML = "Rerun"; rerunTrigger.href = setUrl({ testId: testId }); testBlock = document.createElement( "li" ); testBlock.appendChild( title ); testBlock.appendChild( rerunTrigger ); testBlock.id = "qunit-test-output-" + testId; assertList = document.createElement( "ol" ); assertList.className = "qunit-assert-list"; testBlock.appendChild( assertList ); tests.appendChild( testBlock ); } // HTML Reporter initialization and load QUnit.begin(function( details ) { var qunit = id( "qunit" ); // Fixture is the only one necessary to run without the #qunit element storeFixture(); if ( qunit ) { qunit.innerHTML = "

        " + escapeText( document.title ) + "

        " + "

        " + "
        " + appendFilteredTest() + "

        " + "
          "; } appendHeader(); appendBanner(); appendTestResults(); appendUserAgent(); appendToolbar(); appendTestsList( details.modules ); toolbarModuleFilter(); if ( qunit && config.hidepassed ) { addClass( qunit.lastChild, "hidepass" ); } }); QUnit.done(function( details ) { var i, key, banner = id( "qunit-banner" ), tests = id( "qunit-tests" ), html = [ "Tests completed in ", details.runtime, " milliseconds.
          ", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed." ].join( "" ); if ( banner ) { banner.className = details.failed ? "qunit-fail" : "qunit-pass"; } if ( tests ) { id( "qunit-testresult" ).innerHTML = html; } if ( config.altertitle && defined.document && document.title ) { // show ✖ for good, ✔ for bad suite result in title // use escape sequences in case file gets loaded with non-utf-8-charset document.title = [ ( details.failed ? "\u2716" : "\u2714" ), document.title.replace( /^[\u2714\u2716] /i, "" ) ].join( " " ); } // clear own sessionStorage items if all tests passed if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { for ( i = 0; i < sessionStorage.length; i++ ) { key = sessionStorage.key( i++ ); if ( key.indexOf( "qunit-test-" ) === 0 ) { sessionStorage.removeItem( key ); } } } // scroll back to top to show results if ( config.scrolltop && window.scrollTo ) { window.scrollTo( 0, 0 ); } }); function getNameHtml( name, module ) { var nameHtml = ""; if ( module ) { nameHtml = "" + escapeText( module ) + ": "; } nameHtml += "" + escapeText( name ) + ""; return nameHtml; } QUnit.testStart(function( details ) { var running, testBlock, bad; testBlock = id( "qunit-test-output-" + details.testId ); if ( testBlock ) { testBlock.className = "running"; } else { // Report later registered tests appendTest( details.name, details.testId, details.module ); } running = id( "qunit-testresult" ); if ( running ) { bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); running.innerHTML = ( bad ? "Rerunning previously failed test:
          " : "Running:
          " ) + getNameHtml( details.name, details.module ); } }); function stripHtml( string ) { // strip tags, html entity and whitespaces return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); } QUnit.log(function( details ) { var assertList, assertLi, message, expected, actual, diff, showDiff = false, testItem = id( "qunit-test-output-" + details.testId ); if ( !testItem ) { return; } message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); message = "" + message + ""; message += "@ " + details.runtime + " ms"; // pushFailure doesn't provide details.expected // when it calls, it's implicit to also not show expected and diff stuff // Also, we need to check details.expected existence, as it can exist and be undefined if ( !details.result && hasOwn.call( details, "expected" ) ) { if ( details.negative ) { expected = escapeText( "NOT " + QUnit.dump.parse( details.expected ) ); } else { expected = escapeText( QUnit.dump.parse( details.expected ) ); } actual = escapeText( QUnit.dump.parse( details.actual ) ); message += "
          "; if ( actual !== expected ) { message += ""; // Don't show diff if actual or expected are booleans if ( !( /^(true|false)$/.test( actual ) ) && !( /^(true|false)$/.test( expected ) ) ) { diff = QUnit.diff( expected, actual ); showDiff = stripHtml( diff ).length !== stripHtml( expected ).length + stripHtml( actual ).length; } // Don't show diff if expected and actual are totally different if ( showDiff ) { message += ""; } } else if ( expected.indexOf( "[object Array]" ) !== -1 || expected.indexOf( "[object Object]" ) !== -1 ) { message += ""; } if ( details.source ) { message += ""; } message += "
          Expected:
          " +
          			expected +
          			"
          Result:
          " +
          				actual + "
          Diff:
          " +
          					diff + "
          Message: " + "Diff suppressed as the depth of object is more than current max depth (" + QUnit.config.maxDepth + ").

          Hint: Use QUnit.dump.maxDepth to " + " run with a higher max depth or " + "Rerun without max depth.

          Source:
          " +
          				escapeText( details.source ) + "
          "; // this occurs when pushFailure is set and we have an extracted stack trace } else if ( !details.result && details.source ) { message += "" + "" + "
          Source:
          " +
          			escapeText( details.source ) + "
          "; } assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; assertLi = document.createElement( "li" ); assertLi.className = details.result ? "pass" : "fail"; assertLi.innerHTML = message; assertList.appendChild( assertLi ); }); QUnit.testDone(function( details ) { var testTitle, time, testItem, assertList, good, bad, testCounts, skipped, sourceName, tests = id( "qunit-tests" ); if ( !tests ) { return; } testItem = id( "qunit-test-output-" + details.testId ); assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; good = details.passed; bad = details.failed; // store result when possible if ( config.reorder && defined.sessionStorage ) { if ( bad ) { sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); } else { sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); } } if ( bad === 0 ) { // Collapse the passing tests addClass( assertList, "qunit-collapsed" ); } else if ( bad && config.collapse && !collapseNext ) { // Skip collapsing the first failing test collapseNext = true; } else { // Collapse remaining tests addClass( assertList, "qunit-collapsed" ); } // testItem.firstChild is the test name testTitle = testItem.firstChild; testCounts = bad ? "" + bad + ", " + "" + good + ", " : ""; testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; if ( details.skipped ) { testItem.className = "skipped"; skipped = document.createElement( "em" ); skipped.className = "qunit-skipped-label"; skipped.innerHTML = "skipped"; testItem.insertBefore( skipped, testTitle ); } else { addEvent( testTitle, "click", function() { toggleClass( assertList, "qunit-collapsed" ); }); testItem.className = bad ? "fail" : "pass"; time = document.createElement( "span" ); time.className = "runtime"; time.innerHTML = details.runtime + " ms"; testItem.insertBefore( time, assertList ); } // Show the source of the test when showing assertions if ( details.source ) { sourceName = document.createElement( "p" ); sourceName.innerHTML = "Source: " + details.source; addClass( sourceName, "qunit-source" ); if ( bad === 0 ) { addClass( sourceName, "qunit-collapsed" ); } addEvent( testTitle, "click", function() { toggleClass( sourceName, "qunit-collapsed" ); }); testItem.appendChild( sourceName ); } }); if ( defined.document ) { // Avoid readyState issue with phantomjs // Ref: #818 var notPhantom = ( function( p ) { return !( p && p.version && p.version.major > 0 ); } )( window.phantom ); if ( notPhantom && document.readyState === "complete" ) { QUnit.load(); } else { addEvent( window, "load", QUnit.load ); } } else { config.pageLoaded = true; config.autorun = true; } })(); ================================================ FILE: test/vendor/require.js ================================================ /** vim: et:ts=4:sw=4:sts=4 * @license RequireJS 2.1.9 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/requirejs for details */ //Not using strict: uneven strict support in browsers, #392, and causes //problems with requirejs.exec()/transpiler plugins that may not be strict. /*jslint regexp: true, nomen: true, sloppy: true */ /*global window, navigator, document, importScripts, setTimeout, opera */ var requirejs, require, define; (function (global) { var req, s, head, baseElement, dataMain, src, interactiveScript, currentlyAddingScript, mainScript, subPath, version = '2.1.9', commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, jsSuffixRegExp = /\.js$/, currDirRegExp = /^\.\//, op = Object.prototype, ostring = op.toString, hasOwn = op.hasOwnProperty, ap = Array.prototype, apsp = ap.splice, isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), isWebWorker = !isBrowser && typeof importScripts !== 'undefined', //PS3 indicates loaded and complete, but need to wait for complete //specifically. Sequence is 'loading', 'loaded', execution, // then 'complete'. The UA check is unfortunate, but not sure how //to feature test w/o causing perf issues. readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? /^complete$/ : /^(complete|loaded)$/, defContextName = '_', //Oh the tragedy, detecting opera. See the usage of isOpera for reason. isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', contexts = {}, cfg = {}, globalDefQueue = [], useInteractive = false; function isFunction(it) { return ostring.call(it) === '[object Function]'; } function isArray(it) { return ostring.call(it) === '[object Array]'; } /** * Helper function for iterating over an array. If the func returns * a true value, it will break out of the loop. */ function each(ary, func) { if (ary) { var i; for (i = 0; i < ary.length; i += 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } } /** * Helper function for iterating over an array backwards. If the func * returns a true value, it will break out of the loop. */ function eachReverse(ary, func) { if (ary) { var i; for (i = ary.length - 1; i > -1; i -= 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } } function hasProp(obj, prop) { return hasOwn.call(obj, prop); } function getOwn(obj, prop) { return hasProp(obj, prop) && obj[prop]; } /** * Cycles over properties in an object and calls a function for each * property value. If the function returns a truthy value, then the * iteration is stopped. */ function eachProp(obj, func) { var prop; for (prop in obj) { if (hasProp(obj, prop)) { if (func(obj[prop], prop)) { break; } } } } /** * Simple function to mix in properties from source into target, * but only if target does not already have a property of the same name. */ function mixin(target, source, force, deepStringMixin) { if (source) { eachProp(source, function (value, prop) { if (force || !hasProp(target, prop)) { if (deepStringMixin && typeof value !== 'string') { if (!target[prop]) { target[prop] = {}; } mixin(target[prop], value, force, deepStringMixin); } else { target[prop] = value; } } }); } return target; } //Similar to Function.prototype.bind, but the 'this' object is specified //first, since it is easier to read/figure out what 'this' will be. function bind(obj, fn) { return function () { return fn.apply(obj, arguments); }; } function scripts() { return document.getElementsByTagName('script'); } function defaultOnError(err) { throw err; } //Allow getting a global that expressed in //dot notation, like 'a.b.c'. function getGlobal(value) { if (!value) { return value; } var g = global; each(value.split('.'), function (part) { g = g[part]; }); return g; } /** * Constructs an error with a pointer to an URL with more information. * @param {String} id the error ID that maps to an ID on a web page. * @param {String} message human readable error. * @param {Error} [err] the original error, if there is one. * * @returns {Error} */ function makeError(id, msg, err, requireModules) { var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); e.requireType = id; e.requireModules = requireModules; if (err) { e.originalError = err; } return e; } if (typeof define !== 'undefined') { //If a define is already in play via another AMD loader, //do not overwrite. return; } if (typeof requirejs !== 'undefined') { if (isFunction(requirejs)) { //Do not overwrite and existing requirejs instance. return; } cfg = requirejs; requirejs = undefined; } //Allow for a require config object if (typeof require !== 'undefined' && !isFunction(require)) { //assume it is a config object. cfg = require; require = undefined; } function newContext(contextName) { var inCheckLoaded, Module, context, handlers, checkLoadedTimeoutId, config = { //Defaults. Do not set a default for map //config to speed up normalize(), which //will run faster if there is no default. waitSeconds: 7, baseUrl: './', paths: {}, pkgs: {}, shim: {}, config: {} }, registry = {}, //registry of just enabled modules, to speed //cycle breaking code when lots of modules //are registered, but not activated. enabledRegistry = {}, undefEvents = {}, defQueue = [], defined = {}, urlFetched = {}, requireCounter = 1, unnormalizedCounter = 1; /** * Trims the . and .. from an array of path segments. * It will keep a leading path segment if a .. will become * the first path segment, to help with module name lookups, * which act like paths, but can be remapped. But the end result, * all paths that use this function should look normalized. * NOTE: this method MODIFIES the input array. * @param {Array} ary the array of path segments. */ function trimDots(ary) { var i, part; for (i = 0; ary[i]; i += 1) { part = ary[i]; if (part === '.') { ary.splice(i, 1); i -= 1; } else if (part === '..') { if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { //End of the line. Keep at least one non-dot //path segment at the front so it can be mapped //correctly to disk. Otherwise, there is likely //no path mapping for a path starting with '..'. //This can still fail, but catches the most reasonable //uses of .. break; } else if (i > 0) { ary.splice(i - 1, 2); i -= 2; } } } } /** * Given a relative module name, like ./something, normalize it to * a real name that can be mapped to a path. * @param {String} name the relative name * @param {String} baseName a real name that the name arg is relative * to. * @param {Boolean} applyMap apply the map config to the value. Should * only be done if this normalization is for a dependency ID. * @returns {String} normalized name */ function normalize(name, baseName, applyMap) { var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment, foundMap, foundI, foundStarMap, starI, baseParts = baseName && baseName.split('/'), normalizedBaseParts = baseParts, map = config.map, starMap = map && map['*']; //Adjust any relative paths. if (name && name.charAt(0) === '.') { //If have a base name, try to normalize against it, //otherwise, assume it is a top-level require that will //be relative to baseUrl in the end. if (baseName) { if (getOwn(config.pkgs, baseName)) { //If the baseName is a package name, then just treat it as one //name to concat the name with. normalizedBaseParts = baseParts = [baseName]; } else { //Convert baseName to array, and lop off the last part, //so that . matches that 'directory' and not name of the baseName's //module. For instance, baseName of 'one/two/three', maps to //'one/two/three.js', but we want the directory, 'one/two' for //this normalization. normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); } name = normalizedBaseParts.concat(name.split('/')); trimDots(name); //Some use of packages may use a . path to reference the //'main' module name, so normalize for that. pkgConfig = getOwn(config.pkgs, (pkgName = name[0])); name = name.join('/'); if (pkgConfig && name === pkgName + '/' + pkgConfig.main) { name = pkgName; } } else if (name.indexOf('./') === 0) { // No baseName, so this is ID is resolved relative // to baseUrl, pull off the leading dot. name = name.substring(2); } } //Apply map config if available. if (applyMap && map && (baseParts || starMap)) { nameParts = name.split('/'); for (i = nameParts.length; i > 0; i -= 1) { nameSegment = nameParts.slice(0, i).join('/'); if (baseParts) { //Find the longest baseName segment match in the config. //So, do joins on the biggest to smallest lengths of baseParts. for (j = baseParts.length; j > 0; j -= 1) { mapValue = getOwn(map, baseParts.slice(0, j).join('/')); //baseName segment has config, find if it has one for //this name. if (mapValue) { mapValue = getOwn(mapValue, nameSegment); if (mapValue) { //Match, update name to the new value. foundMap = mapValue; foundI = i; break; } } } } if (foundMap) { break; } //Check for a star map match, but just hold on to it, //if there is a shorter segment match later in a matching //config, then favor over this star map. if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { foundStarMap = getOwn(starMap, nameSegment); starI = i; } } if (!foundMap && foundStarMap) { foundMap = foundStarMap; foundI = starI; } if (foundMap) { nameParts.splice(0, foundI, foundMap); name = nameParts.join('/'); } } return name; } function removeScript(name) { if (isBrowser) { each(scripts(), function (scriptNode) { if (scriptNode.getAttribute('data-requiremodule') === name && scriptNode.getAttribute('data-requirecontext') === context.contextName) { scriptNode.parentNode.removeChild(scriptNode); return true; } }); } } function hasPathFallback(id) { var pathConfig = getOwn(config.paths, id); if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { //Pop off the first array value, since it failed, and //retry pathConfig.shift(); context.require.undef(id); context.require([id]); return true; } } //Turns a plugin!resource to [plugin, resource] //with the plugin being undefined if the name //did not have a plugin prefix. function splitPrefix(name) { var prefix, index = name ? name.indexOf('!') : -1; if (index > -1) { prefix = name.substring(0, index); name = name.substring(index + 1, name.length); } return [prefix, name]; } /** * Creates a module mapping that includes plugin prefix, module * name, and path. If parentModuleMap is provided it will * also normalize the name via require.normalize() * * @param {String} name the module name * @param {String} [parentModuleMap] parent module map * for the module name, used to resolve relative names. * @param {Boolean} isNormalized: is the ID already normalized. * This is true if this call is done for a define() module ID. * @param {Boolean} applyMap: apply the map config to the ID. * Should only be true if this map is for a dependency. * * @returns {Object} */ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { var url, pluginModule, suffix, nameParts, prefix = null, parentName = parentModuleMap ? parentModuleMap.name : null, originalName = name, isDefine = true, normalizedName = ''; //If no name, then it means it is a require call, generate an //internal name. if (!name) { isDefine = false; name = '_@r' + (requireCounter += 1); } nameParts = splitPrefix(name); prefix = nameParts[0]; name = nameParts[1]; if (prefix) { prefix = normalize(prefix, parentName, applyMap); pluginModule = getOwn(defined, prefix); } //Account for relative paths if there is a base name. if (name) { if (prefix) { if (pluginModule && pluginModule.normalize) { //Plugin is loaded, use its normalize method. normalizedName = pluginModule.normalize(name, function (name) { return normalize(name, parentName, applyMap); }); } else { normalizedName = normalize(name, parentName, applyMap); } } else { //A regular module. normalizedName = normalize(name, parentName, applyMap); //Normalized name may be a plugin ID due to map config //application in normalize. The map config values must //already be normalized, so do not need to redo that part. nameParts = splitPrefix(normalizedName); prefix = nameParts[0]; normalizedName = nameParts[1]; isNormalized = true; url = context.nameToUrl(normalizedName); } } //If the id is a plugin id that cannot be determined if it needs //normalization, stamp it with a unique ID so two matching relative //ids that may conflict can be separate. suffix = prefix && !pluginModule && !isNormalized ? '_unnormalized' + (unnormalizedCounter += 1) : ''; return { prefix: prefix, name: normalizedName, parentMap: parentModuleMap, unnormalized: !!suffix, url: url, originalName: originalName, isDefine: isDefine, id: (prefix ? prefix + '!' + normalizedName : normalizedName) + suffix }; } function getModule(depMap) { var id = depMap.id, mod = getOwn(registry, id); if (!mod) { mod = registry[id] = new context.Module(depMap); } return mod; } function on(depMap, name, fn) { var id = depMap.id, mod = getOwn(registry, id); if (hasProp(defined, id) && (!mod || mod.defineEmitComplete)) { if (name === 'defined') { fn(defined[id]); } } else { mod = getModule(depMap); if (mod.error && name === 'error') { fn(mod.error); } else { mod.on(name, fn); } } } function onError(err, errback) { var ids = err.requireModules, notified = false; if (errback) { errback(err); } else { each(ids, function (id) { var mod = getOwn(registry, id); if (mod) { //Set error on module, so it skips timeout checks. mod.error = err; if (mod.events.error) { notified = true; mod.emit('error', err); } } }); if (!notified) { req.onError(err); } } } /** * Internal method to transfer globalQueue items to this context's * defQueue. */ function takeGlobalQueue() { //Push all the globalDefQueue items into the context's defQueue if (globalDefQueue.length) { //Array splice in the values since the context code has a //local var ref to defQueue, so cannot just reassign the one //on context. apsp.apply(defQueue, [defQueue.length - 1, 0].concat(globalDefQueue)); globalDefQueue = []; } } handlers = { 'require': function (mod) { if (mod.require) { return mod.require; } else { return (mod.require = context.makeRequire(mod.map)); } }, 'exports': function (mod) { mod.usingExports = true; if (mod.map.isDefine) { if (mod.exports) { return mod.exports; } else { return (mod.exports = defined[mod.map.id] = {}); } } }, 'module': function (mod) { if (mod.module) { return mod.module; } else { return (mod.module = { id: mod.map.id, uri: mod.map.url, config: function () { var c, pkg = getOwn(config.pkgs, mod.map.id); // For packages, only support config targeted // at the main module. c = pkg ? getOwn(config.config, mod.map.id + '/' + pkg.main) : getOwn(config.config, mod.map.id); return c || {}; }, exports: defined[mod.map.id] }); } } }; function cleanRegistry(id) { //Clean up machinery used for waiting modules. delete registry[id]; delete enabledRegistry[id]; } function breakCycle(mod, traced, processed) { var id = mod.map.id; if (mod.error) { mod.emit('error', mod.error); } else { traced[id] = true; each(mod.depMaps, function (depMap, i) { var depId = depMap.id, dep = getOwn(registry, depId); //Only force things that have not completed //being defined, so still in the registry, //and only if it has not been matched up //in the module already. if (dep && !mod.depMatched[i] && !processed[depId]) { if (getOwn(traced, depId)) { mod.defineDep(i, defined[depId]); mod.check(); //pass false? } else { breakCycle(dep, traced, processed); } } }); processed[id] = true; } } function checkLoaded() { var map, modId, err, usingPathFallback, waitInterval = config.waitSeconds * 1000, //It is possible to disable the wait interval by using waitSeconds of 0. expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), noLoads = [], reqCalls = [], stillLoading = false, needCycleCheck = true; //Do not bother if this call was a result of a cycle break. if (inCheckLoaded) { return; } inCheckLoaded = true; //Figure out the state of all the modules. eachProp(enabledRegistry, function (mod) { map = mod.map; modId = map.id; //Skip things that are not enabled or in error state. if (!mod.enabled) { return; } if (!map.isDefine) { reqCalls.push(mod); } if (!mod.error) { //If the module should be executed, and it has not //been inited and time is up, remember it. if (!mod.inited && expired) { if (hasPathFallback(modId)) { usingPathFallback = true; stillLoading = true; } else { noLoads.push(modId); removeScript(modId); } } else if (!mod.inited && mod.fetched && map.isDefine) { stillLoading = true; if (!map.prefix) { //No reason to keep looking for unfinished //loading. If the only stillLoading is a //plugin resource though, keep going, //because it may be that a plugin resource //is waiting on a non-plugin cycle. return (needCycleCheck = false); } } } }); if (expired && noLoads.length) { //If wait time expired, throw error of unloaded modules. err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); err.contextName = context.contextName; return onError(err); } //Not expired, check for a cycle. if (needCycleCheck) { each(reqCalls, function (mod) { breakCycle(mod, {}, {}); }); } //If still waiting on loads, and the waiting load is something //other than a plugin resource, or there are still outstanding //scripts, then just try back later. if ((!expired || usingPathFallback) && stillLoading) { //Something is still waiting to load. Wait for it, but only //if a timeout is not already in effect. if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { checkLoadedTimeoutId = setTimeout(function () { checkLoadedTimeoutId = 0; checkLoaded(); }, 50); } } inCheckLoaded = false; } Module = function (map) { this.events = getOwn(undefEvents, map.id) || {}; this.map = map; this.shim = getOwn(config.shim, map.id); this.depExports = []; this.depMaps = []; this.depMatched = []; this.pluginMaps = {}; this.depCount = 0; /* this.exports this.factory this.depMaps = [], this.enabled, this.fetched */ }; Module.prototype = { init: function (depMaps, factory, errback, options) { options = options || {}; //Do not do more inits if already done. Can happen if there //are multiple define calls for the same module. That is not //a normal, common case, but it is also not unexpected. if (this.inited) { return; } this.factory = factory; if (errback) { //Register for errors on this module. this.on('error', errback); } else if (this.events.error) { //If no errback already, but there are error listeners //on this module, set up an errback to pass to the deps. errback = bind(this, function (err) { this.emit('error', err); }); } //Do a copy of the dependency array, so that //source inputs are not modified. For example //"shim" deps are passed in here directly, and //doing a direct modification of the depMaps array //would affect that config. this.depMaps = depMaps && depMaps.slice(0); this.errback = errback; //Indicate this module has be initialized this.inited = true; this.ignore = options.ignore; //Could have option to init this module in enabled mode, //or could have been previously marked as enabled. However, //the dependencies are not known until init is called. So //if enabled previously, now trigger dependencies as enabled. if (options.enabled || this.enabled) { //Enable this module and dependencies. //Will call this.check() this.enable(); } else { this.check(); } }, defineDep: function (i, depExports) { //Because of cycles, defined callback for a given //export can be called more than once. if (!this.depMatched[i]) { this.depMatched[i] = true; this.depCount -= 1; this.depExports[i] = depExports; } }, fetch: function () { if (this.fetched) { return; } this.fetched = true; context.startTime = (new Date()).getTime(); var map = this.map; //If the manager is for a plugin managed resource, //ask the plugin to load it now. if (this.shim) { context.makeRequire(this.map, { enableBuildCallback: true })(this.shim.deps || [], bind(this, function () { return map.prefix ? this.callPlugin() : this.load(); })); } else { //Regular dependency. return map.prefix ? this.callPlugin() : this.load(); } }, load: function () { var url = this.map.url; //Regular dependency. if (!urlFetched[url]) { urlFetched[url] = true; context.load(this.map.id, url); } }, /** * Checks if the module is ready to define itself, and if so, * define it. */ check: function () { if (!this.enabled || this.enabling) { return; } var err, cjsModule, id = this.map.id, depExports = this.depExports, exports = this.exports, factory = this.factory; if (!this.inited) { this.fetch(); } else if (this.error) { this.emit('error', this.error); } else if (!this.defining) { //The factory could trigger another require call //that would result in checking this module to //define itself again. If already in the process //of doing that, skip this work. this.defining = true; if (this.depCount < 1 && !this.defined) { if (isFunction(factory)) { //If there is an error listener, favor passing //to that instead of throwing an error. However, //only do it for define()'d modules. require //errbacks should not be called for failures in //their callbacks (#699). However if a global //onError is set, use that. if ((this.events.error && this.map.isDefine) || req.onError !== defaultOnError) { try { exports = context.execCb(id, factory, depExports, exports); } catch (e) { err = e; } } else { exports = context.execCb(id, factory, depExports, exports); } if (this.map.isDefine) { //If setting exports via 'module' is in play, //favor that over return value and exports. After that, //favor a non-undefined return value over exports use. cjsModule = this.module; if (cjsModule && cjsModule.exports !== undefined && //Make sure it is not already the exports value cjsModule.exports !== this.exports) { exports = cjsModule.exports; } else if (exports === undefined && this.usingExports) { //exports already set the defined value. exports = this.exports; } } if (err) { err.requireMap = this.map; err.requireModules = this.map.isDefine ? [this.map.id] : null; err.requireType = this.map.isDefine ? 'define' : 'require'; return onError((this.error = err)); } } else { //Just a literal value exports = factory; } this.exports = exports; if (this.map.isDefine && !this.ignore) { defined[id] = exports; if (req.onResourceLoad) { req.onResourceLoad(context, this.map, this.depMaps); } } //Clean up cleanRegistry(id); this.defined = true; } //Finished the define stage. Allow calling check again //to allow define notifications below in the case of a //cycle. this.defining = false; if (this.defined && !this.defineEmitted) { this.defineEmitted = true; this.emit('defined', this.exports); this.defineEmitComplete = true; } } }, callPlugin: function () { var map = this.map, id = map.id, //Map already normalized the prefix. pluginMap = makeModuleMap(map.prefix); //Mark this as a dependency for this plugin, so it //can be traced for cycles. this.depMaps.push(pluginMap); on(pluginMap, 'defined', bind(this, function (plugin) { var load, normalizedMap, normalizedMod, name = this.map.name, parentName = this.map.parentMap ? this.map.parentMap.name : null, localRequire = context.makeRequire(map.parentMap, { enableBuildCallback: true }); //If current map is not normalized, wait for that //normalized name to load instead of continuing. if (this.map.unnormalized) { //Normalize the ID if the plugin allows it. if (plugin.normalize) { name = plugin.normalize(name, function (name) { return normalize(name, parentName, true); }) || ''; } //prefix and name should already be normalized, no need //for applying map config again either. normalizedMap = makeModuleMap(map.prefix + '!' + name, this.map.parentMap); on(normalizedMap, 'defined', bind(this, function (value) { this.init([], function () { return value; }, null, { enabled: true, ignore: true }); })); normalizedMod = getOwn(registry, normalizedMap.id); if (normalizedMod) { //Mark this as a dependency for this plugin, so it //can be traced for cycles. this.depMaps.push(normalizedMap); if (this.events.error) { normalizedMod.on('error', bind(this, function (err) { this.emit('error', err); })); } normalizedMod.enable(); } return; } load = bind(this, function (value) { this.init([], function () { return value; }, null, { enabled: true }); }); load.error = bind(this, function (err) { this.inited = true; this.error = err; err.requireModules = [id]; //Remove temp unnormalized modules for this module, //since they will never be resolved otherwise now. eachProp(registry, function (mod) { if (mod.map.id.indexOf(id + '_unnormalized') === 0) { cleanRegistry(mod.map.id); } }); onError(err); }); //Allow plugins to load other code without having to know the //context or how to 'complete' the load. load.fromText = bind(this, function (text, textAlt) { /*jslint evil: true */ var moduleName = map.name, moduleMap = makeModuleMap(moduleName), hasInteractive = useInteractive; //As of 2.1.0, support just passing the text, to reinforce //fromText only being called once per resource. Still //support old style of passing moduleName but discard //that moduleName in favor of the internal ref. if (textAlt) { text = textAlt; } //Turn off interactive script matching for IE for any define //calls in the text, then turn it back on at the end. if (hasInteractive) { useInteractive = false; } //Prime the system by creating a module instance for //it. getModule(moduleMap); //Transfer any config to this other module. if (hasProp(config.config, id)) { config.config[moduleName] = config.config[id]; } try { req.exec(text); } catch (e) { return onError(makeError('fromtexteval', 'fromText eval for ' + id + ' failed: ' + e, e, [id])); } if (hasInteractive) { useInteractive = true; } //Mark this as a dependency for the plugin //resource this.depMaps.push(moduleMap); //Support anonymous modules. context.completeLoad(moduleName); //Bind the value of that module to the value for this //resource ID. localRequire([moduleName], load); }); //Use parentName here since the plugin's name is not reliable, //could be some weird string with no path that actually wants to //reference the parentName's path. plugin.load(map.name, localRequire, load, config); })); context.enable(pluginMap, this); this.pluginMaps[pluginMap.id] = pluginMap; }, enable: function () { enabledRegistry[this.map.id] = this; this.enabled = true; //Set flag mentioning that the module is enabling, //so that immediate calls to the defined callbacks //for dependencies do not trigger inadvertent load //with the depCount still being zero. this.enabling = true; //Enable each dependency each(this.depMaps, bind(this, function (depMap, i) { var id, mod, handler; if (typeof depMap === 'string') { //Dependency needs to be converted to a depMap //and wired up to this module. depMap = makeModuleMap(depMap, (this.map.isDefine ? this.map : this.map.parentMap), false, !this.skipMap); this.depMaps[i] = depMap; handler = getOwn(handlers, depMap.id); if (handler) { this.depExports[i] = handler(this); return; } this.depCount += 1; on(depMap, 'defined', bind(this, function (depExports) { this.defineDep(i, depExports); this.check(); })); if (this.errback) { on(depMap, 'error', bind(this, this.errback)); } } id = depMap.id; mod = registry[id]; //Skip special modules like 'require', 'exports', 'module' //Also, don't call enable if it is already enabled, //important in circular dependency cases. if (!hasProp(handlers, id) && mod && !mod.enabled) { context.enable(depMap, this); } })); //Enable each plugin that is used in //a dependency eachProp(this.pluginMaps, bind(this, function (pluginMap) { var mod = getOwn(registry, pluginMap.id); if (mod && !mod.enabled) { context.enable(pluginMap, this); } })); this.enabling = false; this.check(); }, on: function (name, cb) { var cbs = this.events[name]; if (!cbs) { cbs = this.events[name] = []; } cbs.push(cb); }, emit: function (name, evt) { each(this.events[name], function (cb) { cb(evt); }); if (name === 'error') { //Now that the error handler was triggered, remove //the listeners, since this broken Module instance //can stay around for a while in the registry. delete this.events[name]; } } }; function callGetModule(args) { //Skip modules already defined. if (!hasProp(defined, args[0])) { getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); } } function removeListener(node, func, name, ieName) { //Favor detachEvent because of IE9 //issue, see attachEvent/addEventListener comment elsewhere //in this file. if (node.detachEvent && !isOpera) { //Probably IE. If not it will throw an error, which will be //useful to know. if (ieName) { node.detachEvent(ieName, func); } } else { node.removeEventListener(name, func, false); } } /** * Given an event from a script node, get the requirejs info from it, * and then removes the event listeners on the node. * @param {Event} evt * @returns {Object} */ function getScriptData(evt) { //Using currentTarget instead of target for Firefox 2.0's sake. Not //all old browsers will be supported, but this one was easy enough //to support and still makes sense. var node = evt.currentTarget || evt.srcElement; //Remove the listeners once here. removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); removeListener(node, context.onScriptError, 'error'); return { node: node, id: node && node.getAttribute('data-requiremodule') }; } function intakeDefines() { var args; //Any defined modules in the global queue, intake them now. takeGlobalQueue(); //Make sure any remaining defQueue items get properly processed. while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); } else { //args are id, deps, factory. Should be normalized by the //define() function. callGetModule(args); } } } context = { config: config, contextName: contextName, registry: registry, defined: defined, urlFetched: urlFetched, defQueue: defQueue, Module: Module, makeModuleMap: makeModuleMap, nextTick: req.nextTick, onError: onError, /** * Set a configuration for the context. * @param {Object} cfg config object to integrate. */ configure: function (cfg) { //Make sure the baseUrl ends in a slash. if (cfg.baseUrl) { if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { cfg.baseUrl += '/'; } } //Save off the paths and packages since they require special processing, //they are additive. var pkgs = config.pkgs, shim = config.shim, objs = { paths: true, config: true, map: true }; eachProp(cfg, function (value, prop) { if (objs[prop]) { if (prop === 'map') { if (!config.map) { config.map = {}; } mixin(config[prop], value, true, true); } else { mixin(config[prop], value, true); } } else { config[prop] = value; } }); //Merge shim if (cfg.shim) { eachProp(cfg.shim, function (value, id) { //Normalize the structure if (isArray(value)) { value = { deps: value }; } if ((value.exports || value.init) && !value.exportsFn) { value.exportsFn = context.makeShimExports(value); } shim[id] = value; }); config.shim = shim; } //Adjust packages if necessary. if (cfg.packages) { each(cfg.packages, function (pkgObj) { var location; pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; location = pkgObj.location; //Create a brand new object on pkgs, since currentPackages can //be passed in again, and config.pkgs is the internal transformed //state for all package configs. pkgs[pkgObj.name] = { name: pkgObj.name, location: location || pkgObj.name, //Remove leading dot in main, so main paths are normalized, //and remove any trailing .js, since different package //envs have different conventions: some use a module name, //some use a file name. main: (pkgObj.main || 'main') .replace(currDirRegExp, '') .replace(jsSuffixRegExp, '') }; }); //Done with modifications, assing packages back to context config config.pkgs = pkgs; } //If there are any "waiting to execute" modules in the registry, //update the maps for them, since their info, like URLs to load, //may have changed. eachProp(registry, function (mod, id) { //If module already has init called, since it is too //late to modify them, and ignore unnormalized ones //since they are transient. if (!mod.inited && !mod.map.unnormalized) { mod.map = makeModuleMap(id); } }); //If a deps array or a config callback is specified, then call //require with those args. This is useful when require is defined as a //config object before require.js is loaded. if (cfg.deps || cfg.callback) { context.require(cfg.deps || [], cfg.callback); } }, makeShimExports: function (value) { function fn() { var ret; if (value.init) { ret = value.init.apply(global, arguments); } return ret || (value.exports && getGlobal(value.exports)); } return fn; }, makeRequire: function (relMap, options) { options = options || {}; function localRequire(deps, callback, errback) { var id, map, requireMod; if (options.enableBuildCallback && callback && isFunction(callback)) { callback.__requireJsBuild = true; } if (typeof deps === 'string') { if (isFunction(callback)) { //Invalid call return onError(makeError('requireargs', 'Invalid require call'), errback); } //If require|exports|module are requested, get the //value for them from the special handlers. Caveat: //this only works while module is being defined. if (relMap && hasProp(handlers, deps)) { return handlers[deps](registry[relMap.id]); } //Synchronous access to one module. If require.get is //available (as in the Node adapter), prefer that. if (req.get) { return req.get(context, deps, relMap, localRequire); } //Normalize module name, if it contains . or .. map = makeModuleMap(deps, relMap, false, true); id = map.id; if (!hasProp(defined, id)) { return onError(makeError('notloaded', 'Module name "' + id + '" has not been loaded yet for context: ' + contextName + (relMap ? '' : '. Use require([])'))); } return defined[id]; } //Grab defines waiting in the global queue. intakeDefines(); //Mark all the dependencies as needing to be loaded. context.nextTick(function () { //Some defines could have been added since the //require call, collect them. intakeDefines(); requireMod = getModule(makeModuleMap(null, relMap)); //Store if map config should be applied to this require //call for dependencies. requireMod.skipMap = options.skipMap; requireMod.init(deps, callback, errback, { enabled: true }); checkLoaded(); }); return localRequire; } mixin(localRequire, { isBrowser: isBrowser, /** * Converts a module name + .extension into an URL path. * *Requires* the use of a module name. It does not support using * plain URLs like nameToUrl. */ toUrl: function (moduleNamePlusExt) { var ext, index = moduleNamePlusExt.lastIndexOf('.'), segment = moduleNamePlusExt.split('/')[0], isRelative = segment === '.' || segment === '..'; //Have a file extension alias, and it is not the //dots from a relative path. if (index !== -1 && (!isRelative || index > 1)) { ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); moduleNamePlusExt = moduleNamePlusExt.substring(0, index); } return context.nameToUrl(normalize(moduleNamePlusExt, relMap && relMap.id, true), ext, true); }, defined: function (id) { return hasProp(defined, makeModuleMap(id, relMap, false, true).id); }, specified: function (id) { id = makeModuleMap(id, relMap, false, true).id; return hasProp(defined, id) || hasProp(registry, id); } }); //Only allow undef on top level require calls if (!relMap) { localRequire.undef = function (id) { //Bind any waiting define() calls to this context, //fix for #408 takeGlobalQueue(); var map = makeModuleMap(id, relMap, true), mod = getOwn(registry, id); removeScript(id); delete defined[id]; delete urlFetched[map.url]; delete undefEvents[id]; if (mod) { //Hold on to listeners in case the //module will be attempted to be reloaded //using a different config. if (mod.events.defined) { undefEvents[id] = mod.events; } cleanRegistry(id); } }; } return localRequire; }, /** * Called to enable a module if it is still in the registry * awaiting enablement. A second arg, parent, the parent module, * is passed in for context, when this method is overriden by * the optimizer. Not shown here to keep code compact. */ enable: function (depMap) { var mod = getOwn(registry, depMap.id); if (mod) { getModule(depMap).enable(); } }, /** * Internal method used by environment adapters to complete a load event. * A load event could be a script load or just a load pass from a synchronous * load call. * @param {String} moduleName the name of the module to potentially complete. */ completeLoad: function (moduleName) { var found, args, mod, shim = getOwn(config.shim, moduleName) || {}, shExports = shim.exports; takeGlobalQueue(); while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { args[0] = moduleName; //If already found an anonymous module and bound it //to this name, then this is some other anon module //waiting for its completeLoad to fire. if (found) { break; } found = true; } else if (args[0] === moduleName) { //Found matching define call for this script! found = true; } callGetModule(args); } //Do this after the cycle of callGetModule in case the result //of those calls/init calls changes the registry. mod = getOwn(registry, moduleName); if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { if (hasPathFallback(moduleName)) { return; } else { return onError(makeError('nodefine', 'No define call for ' + moduleName, null, [moduleName])); } } else { //A script that does not call define(), so just simulate //the call for it. callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); } } checkLoaded(); }, /** * Converts a module name to a file path. Supports cases where * moduleName may actually be just an URL. * Note that it **does not** call normalize on the moduleName, * it is assumed to have already been normalized. This is an * internal API, not a public one. Use toUrl for the public API. */ nameToUrl: function (moduleName, ext, skipExt) { var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url, parentPath; //If a colon is in the URL, it indicates a protocol is used and it is just //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) //or ends with .js, then assume the user meant to use an url and not a module id. //The slash is important for protocol-less URLs as well as full paths. if (req.jsExtRegExp.test(moduleName)) { //Just a plain path, not module name lookup, so just return it. //Add extension if it is included. This is a bit wonky, only non-.js things pass //an extension, this method probably needs to be reworked. url = moduleName + (ext || ''); } else { //A module that needs to be converted to a path. paths = config.paths; pkgs = config.pkgs; syms = moduleName.split('/'); //For each module name segment, see if there is a path //registered for it. Start with most specific name //and work up from it. for (i = syms.length; i > 0; i -= 1) { parentModule = syms.slice(0, i).join('/'); pkg = getOwn(pkgs, parentModule); parentPath = getOwn(paths, parentModule); if (parentPath) { //If an array, it means there are a few choices, //Choose the one that is desired if (isArray(parentPath)) { parentPath = parentPath[0]; } syms.splice(0, i, parentPath); break; } else if (pkg) { //If module name is just the package name, then looking //for the main module. if (moduleName === pkg.name) { pkgPath = pkg.location + '/' + pkg.main; } else { pkgPath = pkg.location; } syms.splice(0, i, pkgPath); break; } } //Join the path parts together, then figure out if baseUrl is needed. url = syms.join('/'); url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js')); url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; } return config.urlArgs ? url + ((url.indexOf('?') === -1 ? '?' : '&') + config.urlArgs) : url; }, //Delegates to req.load. Broken out as a separate function to //allow overriding in the optimizer. load: function (id, url) { req.load(context, id, url); }, /** * Executes a module callback function. Broken out as a separate function * solely to allow the build system to sequence the files in the built * layer in the right sequence. * * @private */ execCb: function (name, callback, args, exports) { return callback.apply(exports, args); }, /** * callback for script loads, used to check status of loading. * * @param {Event} evt the event from the browser for the script * that was loaded. */ onScriptLoad: function (evt) { //Using currentTarget instead of target for Firefox 2.0's sake. Not //all old browsers will be supported, but this one was easy enough //to support and still makes sense. if (evt.type === 'load' || (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { //Reset interactive script so a script node is not held onto for //to long. interactiveScript = null; //Pull out the name of the module and the context. var data = getScriptData(evt); context.completeLoad(data.id); } }, /** * Callback for script errors. */ onScriptError: function (evt) { var data = getScriptData(evt); if (!hasPathFallback(data.id)) { return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id])); } } }; context.require = context.makeRequire(); return context; } /** * Main entry point. * * If the only argument to require is a string, then the module that * is represented by that string is fetched for the appropriate context. * * If the first argument is an array, then it will be treated as an array * of dependency string names to fetch. An optional function callback can * be specified to execute when all of those dependencies are available. * * Make a local req variable to help Caja compliance (it assumes things * on a require that are not standardized), and to give a short * name for minification/local scope use. */ req = requirejs = function (deps, callback, errback, optional) { //Find the right context, use default var context, config, contextName = defContextName; // Determine if have config object in the call. if (!isArray(deps) && typeof deps !== 'string') { // deps is a config object config = deps; if (isArray(callback)) { // Adjust args if there are dependencies deps = callback; callback = errback; errback = optional; } else { deps = []; } } if (config && config.context) { contextName = config.context; } context = getOwn(contexts, contextName); if (!context) { context = contexts[contextName] = req.s.newContext(contextName); } if (config) { context.configure(config); } return context.require(deps, callback, errback); }; /** * Support require.config() to make it easier to cooperate with other * AMD loaders on globally agreed names. */ req.config = function (config) { return req(config); }; /** * Execute something after the current tick * of the event loop. Override for other envs * that have a better solution than setTimeout. * @param {Function} fn function to execute later. */ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { setTimeout(fn, 4); } : function (fn) { fn(); }; /** * Export require as a global, but only if it does not already exist. */ if (!require) { require = req; } req.version = version; //Used to filter out dependencies that are already paths. req.jsExtRegExp = /^\/|:|\?|\.js$/; req.isBrowser = isBrowser; s = req.s = { contexts: contexts, newContext: newContext }; //Create default context. req({}); //Exports some context-sensitive methods on global require. each([ 'toUrl', 'undef', 'defined', 'specified' ], function (prop) { //Reference from contexts instead of early binding to default context, //so that during builds, the latest instance of the default context //with its config gets used. req[prop] = function () { var ctx = contexts[defContextName]; return ctx.require[prop].apply(ctx, arguments); }; }); if (isBrowser) { head = s.head = document.getElementsByTagName('head')[0]; //If BASE tag is in play, using appendChild is a problem for IE6. //When that browser dies, this can be removed. Details in this jQuery bug: //http://dev.jquery.com/ticket/2709 baseElement = document.getElementsByTagName('base')[0]; if (baseElement) { head = s.head = baseElement.parentNode; } } /** * Any errors that require explicitly generates will be passed to this * function. Intercept/override it if you want custom error handling. * @param {Error} err the error object. */ req.onError = defaultOnError; /** * Creates the node for the load command. Only used in browser envs. */ req.createNode = function (config, moduleName, url) { var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script'); node.type = config.scriptType || 'text/javascript'; node.charset = 'utf-8'; node.async = true; return node; }; /** * Does the request to load a module for the browser case. * Make this a separate function to allow other environments * to override it. * * @param {Object} context the require context to find state. * @param {String} moduleName the name of the module. * @param {Object} url the URL to the module. */ req.load = function (context, moduleName, url) { var config = (context && context.config) || {}, node; if (isBrowser) { //In the browser so use a script tag node = req.createNode(config, moduleName, url); node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); //Set up load listener. Test attachEvent first because IE9 has //a subtle issue in its addEventListener and script onload firings //that do not match the behavior of all other browsers with //addEventListener support, which fire the onload event for a //script right after the script execution. See: //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution //UNFORTUNATELY Opera implements attachEvent but does not follow the script //script execution mode. if (node.attachEvent && //Check if node.attachEvent is artificially added by custom script or //natively supported by browser //read https://github.com/jrburke/requirejs/issues/187 //if we can NOT find [native code] then it must NOT natively supported. //in IE8, node.attachEvent does not have toString() //Note the test for "[native code" with no closing brace, see: //https://github.com/jrburke/requirejs/issues/273 !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) { //Probably IE. IE (at least 6-8) do not fire //script onload right after executing the script, so //we cannot tie the anonymous define call to a name. //However, IE reports the script as being in 'interactive' //readyState at the time of the define call. useInteractive = true; node.attachEvent('onreadystatechange', context.onScriptLoad); //It would be great to add an error handler here to catch //404s in IE9+. However, onreadystatechange will fire before //the error handler, so that does not help. If addEventListener //is used, then IE will fire error before load, but we cannot //use that pathway given the connect.microsoft.com issue //mentioned above about not doing the 'script execute, //then fire the script load event listener before execute //next script' that other browsers do. //Best hope: IE10 fixes the issues, //and then destroys all installs of IE 6-9. //node.attachEvent('onerror', context.onScriptError); } else { node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false); } node.src = url; //For some cache cases in IE 6-8, the script executes before the end //of the appendChild execution, so to tie an anonymous define //call to the module name (which is stored on the node), hold on //to a reference to this node, but clear after the DOM insertion. currentlyAddingScript = node; if (baseElement) { head.insertBefore(node, baseElement); } else { head.appendChild(node); } currentlyAddingScript = null; return node; } else if (isWebWorker) { try { //In a web worker, use importScripts. This is not a very //efficient use of importScripts, importScripts will block until //its script is downloaded and evaluated. However, if web workers //are in play, the expectation that a build has been done so that //only one script needs to be loaded anyway. This may need to be //reevaluated if other use cases become common. importScripts(url); //Account for anonymous modules context.completeLoad(moduleName); } catch (e) { context.onError(makeError('importscripts', 'importScripts failed for ' + moduleName + ' at ' + url, e, [moduleName])); } } }; function getInteractiveScript() { if (interactiveScript && interactiveScript.readyState === 'interactive') { return interactiveScript; } eachReverse(scripts(), function (script) { if (script.readyState === 'interactive') { return (interactiveScript = script); } }); return interactiveScript; } //Look for a data-main script attribute, which could also adjust the baseUrl. if (isBrowser && !cfg.skipDataMain) { //Figure out baseUrl. Get it from the script tag with require.js in it. eachReverse(scripts(), function (script) { //Set the 'head' where we can append children by //using the script's parent. if (!head) { head = script.parentNode; } //Look for a data-main attribute to set main script for the page //to load. If it is there, the path to data main becomes the //baseUrl, if it is not already set. dataMain = script.getAttribute('data-main'); if (dataMain) { //Preserve dataMain in case it is a path (i.e. contains '?') mainScript = dataMain; //Set final baseUrl if there is not already an explicit one. if (!cfg.baseUrl) { //Pull off the directory of data-main for use as the //baseUrl. src = mainScript.split('/'); mainScript = src.pop(); subPath = src.length ? src.join('/') + '/' : './'; cfg.baseUrl = subPath; } //Strip off any trailing .js since mainScript is now //like a module name. mainScript = mainScript.replace(jsSuffixRegExp, ''); //If mainScript is still a path, fall back to dataMain if (req.jsExtRegExp.test(mainScript)) { mainScript = dataMain; } //Put the data-main script in the files to load. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; return true; } }); } /** * The function that handles definitions of modules. Differs from * require() in that a string for the module should be the first argument, * and the function to execute after dependencies are loaded should * return a value to define the module corresponding to the first argument's * name. */ define = function (name, deps, callback) { var node, context; //Allow for anonymous modules if (typeof name !== 'string') { //Adjust args appropriately callback = deps; deps = name; name = null; } //This module may not have dependencies if (!isArray(deps)) { callback = deps; deps = null; } //If no name, and callback is a function, then figure out if it a //CommonJS thing with dependencies. if (!deps && isFunction(callback)) { deps = []; //Remove comments from the callback string, //look for require calls, and pull them into the dependencies, //but only if there are function args. if (callback.length) { callback .toString() .replace(commentRegExp, '') .replace(cjsRequireRegExp, function (match, dep) { deps.push(dep); }); //May be a CommonJS thing even without require calls, but still //could use exports, and module. Avoid doing exports and module //work though if it just needs require. //REQUIRES the function to expect the CommonJS variables in the //order listed below. deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); } } //If in IE 6-8 and hit an anonymous define() call, do the interactive //work. if (useInteractive) { node = currentlyAddingScript || getInteractiveScript(); if (node) { if (!name) { name = node.getAttribute('data-requiremodule'); } context = contexts[node.getAttribute('data-requirecontext')]; } } //Always save off evaluating the def call until the script onload handler. //This allows multiple modules to be in a file without prematurely //tracing dependencies, and allows for anonymous module support, //where the module name is not known until the script onload event //occurs. If no context, use the global queue, and get it processed //in the onscript load callback. (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); }; define.amd = { jQuery: true }; /** * Executes the text. Normally just uses eval, but can be modified * to use a better, environment-specific call. Only used for transpiling * loader plugins, not for plain JS modules. * @param {String} text the text to execute/evaluate. */ req.exec = function (text) { /*jslint evil: true */ return eval(text); }; //Set up with config info. req(cfg); }(this)); ================================================ FILE: test/vendor/underscore.js ================================================ // Underscore.js 1.8.3 // http://underscorejs.org // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { // Baseline setup // -------------- // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Save the previous value of the `_` variable. var previousUnderscore = root._; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. var push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind, nativeCreate = Object.create; // Naked function reference for surrogate-prototype-swapping. var Ctor = function(){}; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; }; // Export the Underscore object for **Node.js**, with // backwards-compatibility for the old `require()` API. If we're in // the browser, add `_` as a global object. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; } // Current version. _.VERSION = '1.8.3'; // Internal function that returns an efficient (for current engines) version // of the passed-in callback, to be repeatedly applied in other Underscore // functions. var optimizeCb = function(func, context, argCount) { if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { case 1: return function(value) { return func.call(context, value); }; case 2: return function(value, other) { return func.call(context, value, other); }; case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); }; }; // A mostly-internal function to generate callbacks that can be applied // to each element in a collection, returning the desired result — either // identity, an arbitrary callback, a property matcher, or a property accessor. var cb = function(value, context, argCount) { if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value)) return _.matcher(value); return _.property(value); }; _.iteratee = function(value, context) { return cb(value, context, Infinity); }; // An internal function for creating assigner functions. var createAssigner = function(keysFunc, undefinedOnly) { return function(obj) { var length = arguments.length; if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; }; // An internal function for creating a new object that inherits from another. var baseCreate = function(prototype) { if (!_.isObject(prototype)) return {}; if (nativeCreate) return nativeCreate(prototype); Ctor.prototype = prototype; var result = new Ctor; Ctor.prototype = null; return result; }; var property = function(key) { return function(obj) { return obj == null ? void 0 : obj[key]; }; }; // Helper for collection methods to determine whether a collection // should be iterated as an array or as an object // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; var getLength = property('length'); var isArrayLike = function(collection) { var length = getLength(collection); return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; }; // Collection Functions // -------------------- // The cornerstone, an `each` implementation, aka `forEach`. // Handles raw objects in addition to array-likes. Treats all // sparse array-likes as if they were dense. _.each = _.forEach = function(obj, iteratee, context) { iteratee = optimizeCb(iteratee, context); var i, length; if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } } else { var keys = _.keys(obj); for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } } return obj; }; // Return the results of applying the iteratee to each element. _.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; }; // Create a reducing function iterating left or right. function createReduce(dir) { // Optimized iterator function as using arguments.length // in the main function will deoptimize the, see #1991. function iterator(obj, iteratee, memo, keys, index, length) { for (; index >= 0 && index < length; index += dir) { var currentKey = keys ? keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; } return function(obj, iteratee, memo, context) { iteratee = optimizeCb(iteratee, context, 4); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, index = dir > 0 ? 0 : length - 1; // Determine the initial value if none is provided. if (arguments.length < 3) { memo = obj[keys ? keys[index] : index]; index += dir; } return iterator(obj, iteratee, memo, keys, index, length); }; } // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. _.reduce = _.foldl = _.inject = createReduce(1); // The right-associative version of reduce, also known as `foldr`. _.reduceRight = _.foldr = createReduce(-1); // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, predicate, context) { var key; if (isArrayLike(obj)) { key = _.findIndex(obj, predicate, context); } else { key = _.findKey(obj, predicate, context); } if (key !== void 0 && key !== -1) return obj[key]; }; // Return all the elements that pass a truth test. // Aliased as `select`. _.filter = _.select = function(obj, predicate, context) { var results = []; predicate = cb(predicate, context); _.each(obj, function(value, index, list) { if (predicate(value, index, list)) results.push(value); }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, predicate, context) { return _.filter(obj, _.negate(cb(predicate)), context); }; // Determine whether all of the elements match a truth test. // Aliased as `all`. _.every = _.all = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; if (!predicate(obj[currentKey], currentKey, obj)) return false; } return true; }; // Determine if at least one element in the object matches a truth test. // Aliased as `any`. _.some = _.any = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; if (predicate(obj[currentKey], currentKey, obj)) return true; } return false; }; // Determine if the array or object contains a given item (using `===`). // Aliased as `includes` and `include`. _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { if (!isArrayLike(obj)) obj = _.values(obj); if (typeof fromIndex != 'number' || guard) fromIndex = 0; return _.indexOf(obj, item, fromIndex) >= 0; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); var isFunc = _.isFunction(method); return _.map(obj, function(value) { var func = isFunc ? method : value[method]; return func == null ? func : func.apply(value, args); }); }; // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, _.property(key)); }; // Convenience version of a common use case of `filter`: selecting only objects // containing specific `key:value` pairs. _.where = function(obj, attrs) { return _.filter(obj, _.matcher(attrs)); }; // Convenience version of a common use case of `find`: getting the first object // containing specific `key:value` pairs. _.findWhere = function(obj, attrs) { return _.find(obj, _.matcher(attrs)); }; // Return the maximum element (or element-based computation). _.max = function(obj, iteratee, context) { var result = -Infinity, lastComputed = -Infinity, value, computed; if (iteratee == null && obj != null) { obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value > result) { result = value; } } } else { iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed > lastComputed || computed === -Infinity && result === -Infinity) { result = value; lastComputed = computed; } }); } return result; }; // Return the minimum element (or element-based computation). _.min = function(obj, iteratee, context) { var result = Infinity, lastComputed = Infinity, value, computed; if (iteratee == null && obj != null) { obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value < result) { result = value; } } } else { iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed < lastComputed || computed === Infinity && result === Infinity) { result = value; lastComputed = computed; } }); } return result; }; // Shuffle a collection, using the modern version of the // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { var set = isArrayLike(obj) ? obj : _.values(obj); var length = set.length; var shuffled = Array(length); for (var index = 0, rand; index < length; index++) { rand = _.random(0, index); if (rand !== index) shuffled[index] = shuffled[rand]; shuffled[rand] = set[index]; } return shuffled; }; // Sample **n** random values from a collection. // If **n** is not specified, returns a single random element. // The internal `guard` argument allows it to work with `map`. _.sample = function(obj, n, guard) { if (n == null || guard) { if (!isArrayLike(obj)) obj = _.values(obj); return obj[_.random(obj.length - 1)]; } return _.shuffle(obj).slice(0, Math.max(0, n)); }; // Sort the object's values by a criterion produced by an iteratee. _.sortBy = function(obj, iteratee, context) { iteratee = cb(iteratee, context); return _.pluck(_.map(obj, function(value, index, list) { return { value: value, index: index, criteria: iteratee(value, index, list) }; }).sort(function(left, right) { var a = left.criteria; var b = right.criteria; if (a !== b) { if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } return left.index - right.index; }), 'value'); }; // An internal function used for aggregate "group by" operations. var group = function(behavior) { return function(obj, iteratee, context) { var result = {}; iteratee = cb(iteratee, context); _.each(obj, function(value, index) { var key = iteratee(value, index, obj); behavior(result, value, key); }); return result; }; }; // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = group(function(result, value, key) { if (_.has(result, key)) result[key].push(value); else result[key] = [value]; }); // Indexes the object's values by a criterion, similar to `groupBy`, but for // when you know that your index values will be unique. _.indexBy = group(function(result, value, key) { result[key] = value; }); // Counts instances of an object that group by a certain criterion. Pass // either a string attribute to count by, or a function that returns the // criterion. _.countBy = group(function(result, value, key) { if (_.has(result, key)) result[key]++; else result[key] = 1; }); // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); if (isArrayLike(obj)) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { if (obj == null) return 0; return isArrayLike(obj) ? obj.length : _.keys(obj).length; }; // Split a collection into two arrays: one whose elements all satisfy the given // predicate, and one whose elements all do not satisfy the predicate. _.partition = function(obj, predicate, context) { predicate = cb(predicate, context); var pass = [], fail = []; _.each(obj, function(value, key, obj) { (predicate(value, key, obj) ? pass : fail).push(value); }); return [pass, fail]; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[0]; return _.initial(array, array.length - n); }; // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. _.initial = function(array, n, guard) { return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); }; // Get the last element of an array. Passing **n** will return the last N // values in the array. _.last = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[array.length - 1]; return _.rest(array, Math.max(0, array.length - n)); }; // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Especially useful on the arguments object. Passing an **n** will return // the rest N values in the array. _.rest = _.tail = _.drop = function(array, n, guard) { return slice.call(array, n == null || guard ? 1 : n); }; // Trim out all falsy values from an array. _.compact = function(array) { return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. var flatten = function(input, shallow, strict, startIndex) { var output = [], idx = 0; for (var i = startIndex || 0, length = getLength(input); i < length; i++) { var value = input[i]; if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { //flatten current level of array or arguments object if (!shallow) value = flatten(value, shallow, strict); var j = 0, len = value.length; output.length += len; while (j < len) { output[idx++] = value[j++]; } } else if (!strict) { output[idx++] = value; } } return output; }; // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { return flatten(array, shallow, false); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { return _.difference(array, slice.call(arguments, 1)); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iteratee, context) { if (!_.isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false; } if (iteratee != null) iteratee = cb(iteratee, context); var result = []; var seen = []; for (var i = 0, length = getLength(array); i < length; i++) { var value = array[i], computed = iteratee ? iteratee(value, i, array) : value; if (isSorted) { if (!i || seen !== computed) result.push(value); seen = computed; } else if (iteratee) { if (!_.contains(seen, computed)) { seen.push(computed); result.push(value); } } else if (!_.contains(result, value)) { result.push(value); } } return result; }; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { return _.uniq(flatten(arguments, true, true)); }; // Produce an array that contains every item shared between all the // passed-in arrays. _.intersection = function(array) { var result = []; var argsLength = arguments.length; for (var i = 0, length = getLength(array); i < length; i++) { var item = array[i]; if (_.contains(result, item)) continue; for (var j = 1; j < argsLength; j++) { if (!_.contains(arguments[j], item)) break; } if (j === argsLength) result.push(item); } return result; }; // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { var rest = flatten(arguments, true, true, 1); return _.filter(array, function(value){ return !_.contains(rest, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { return _.unzip(arguments); }; // Complement of _.zip. Unzip accepts an array of arrays and groups // each array's elements on shared indices _.unzip = function(array) { var length = array && _.max(array, getLength).length || 0; var result = Array(length); for (var index = 0; index < length; index++) { result[index] = _.pluck(array, index); } return result; }; // Converts lists into objects. Pass either a single array of `[key, value]` // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { var result = {}; for (var i = 0, length = getLength(list); i < length; i++) { if (values) { result[list[i]] = values[i]; } else { result[list[i][0]] = list[i][1]; } } return result; }; // Generator function to create the findIndex and findLastIndex functions function createPredicateIndexFinder(dir) { return function(array, predicate, context) { predicate = cb(predicate, context); var length = getLength(array); var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (predicate(array[index], index, array)) return index; } return -1; }; } // Returns the first index on an array-like that passes a predicate test _.findIndex = createPredicateIndexFinder(1); _.findLastIndex = createPredicateIndexFinder(-1); // Use a comparator function to figure out the smallest index at which // an object should be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iteratee, context) { iteratee = cb(iteratee, context, 1); var value = iteratee(obj); var low = 0, high = getLength(array); while (low < high) { var mid = Math.floor((low + high) / 2); if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; } return low; }; // Generator function to create the indexOf and lastIndexOf functions function createIndexFinder(dir, predicateFind, sortedIndex) { return function(array, item, idx) { var i = 0, length = getLength(array); if (typeof idx == 'number') { if (dir > 0) { i = idx >= 0 ? idx : Math.max(idx + length, i); } else { length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } } else if (sortedIndex && idx && length) { idx = sortedIndex(array, item); return array[idx] === item ? idx : -1; } if (item !== item) { idx = predicateFind(slice.call(array, i, length), _.isNaN); return idx >= 0 ? idx + i : -1; } for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { if (array[idx] === item) return idx; } return -1; }; } // Return the position of the first occurrence of an item in an array, // or -1 if the item is not included in the array. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { if (stop == null) { stop = start || 0; start = 0; } step = step || 1; var length = Math.max(Math.ceil((stop - start) / step), 0); var range = Array(length); for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start; } return range; }; // Function (ahem) Functions // ------------------ // Determines whether to execute a function as a constructor // or a normal function with the provided arguments var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); var self = baseCreate(sourceFunc.prototype); var result = sourceFunc.apply(self, args); if (_.isObject(result)) return result; return self; }; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = function(func, context) { if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); var args = slice.call(arguments, 2); var bound = function() { return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); }; return bound; }; // Partially apply a function by creating a version that has had some of its // arguments pre-filled, without changing its dynamic `this` context. _ acts // as a placeholder, allowing any combination of arguments to be pre-filled. _.partial = function(func) { var boundArgs = slice.call(arguments, 1); var bound = function() { var position = 0, length = boundArgs.length; var args = Array(length); for (var i = 0; i < length; i++) { args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; } while (position < arguments.length) args.push(arguments[position++]); return executeBound(func, bound, this, this, args); }; return bound; }; // Bind a number of an object's methods to that object. Remaining arguments // are the method names to be bound. Useful for ensuring that all callbacks // defined on an object belong to it. _.bindAll = function(obj) { var i, length = arguments.length, key; if (length <= 1) throw new Error('bindAll must be passed function names'); for (i = 1; i < length; i++) { key = arguments[i]; obj[key] = _.bind(obj[key], obj); } return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; memoize.cache = {}; return memoize; }; // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(null, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = _.partial(_.delay, _, 1); // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : _.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _.now(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { var last = _.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; timestamp = _.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; }; // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. _.wrap = function(func, wrapper) { return _.partial(wrapper, func); }; // Returns a negated version of the passed-in predicate. _.negate = function(predicate) { return function() { return !predicate.apply(this, arguments); }; }; // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i--) result = args[i].call(this, result); return result; }; }; // Returns a function that will only be executed on and after the Nth call. _.after = function(times, func) { return function() { if (--times < 1) { return func.apply(this, arguments); } }; }; // Returns a function that will only be executed up to (but not including) the Nth call. _.before = function(times, func) { var memo; return function() { if (--times > 0) { memo = func.apply(this, arguments); } if (times <= 1) func = null; return memo; }; }; // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. _.once = _.partial(_.before, 2); // Object Functions // ---------------- // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; function collectNonEnumProps(obj, keys) { var nonEnumIdx = nonEnumerableProps.length; var constructor = obj.constructor; var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; // Constructor is a special case. var prop = 'constructor'; if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); while (nonEnumIdx--) { prop = nonEnumerableProps[nonEnumIdx]; if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { keys.push(prop); } } } // Retrieve the names of an object's own properties. // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = function(obj) { if (!_.isObject(obj)) return []; if (nativeKeys) return nativeKeys(obj); var keys = []; for (var key in obj) if (_.has(obj, key)) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; }; // Retrieve all the property names of an object. _.allKeys = function(obj) { if (!_.isObject(obj)) return []; var keys = []; for (var key in obj) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { var keys = _.keys(obj); var length = keys.length; var values = Array(length); for (var i = 0; i < length; i++) { values[i] = obj[keys[i]]; } return values; }; // Returns the results of applying the iteratee to each element of the object // In contrast to _.map it returns an object _.mapObject = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = _.keys(obj), length = keys.length, results = {}, currentKey; for (var index = 0; index < length; index++) { currentKey = keys[index]; results[currentKey] = iteratee(obj[currentKey], currentKey, obj); } return results; }; // Convert an object into a list of `[key, value]` pairs. _.pairs = function(obj) { var keys = _.keys(obj); var length = keys.length; var pairs = Array(length); for (var i = 0; i < length; i++) { pairs[i] = [keys[i], obj[keys[i]]]; } return pairs; }; // Invert the keys and values of an object. The values must be serializable. _.invert = function(obj) { var result = {}; var keys = _.keys(obj); for (var i = 0, length = keys.length; i < length; i++) { result[obj[keys[i]]] = keys[i]; } return result; }; // Return a sorted list of the function names available on the object. // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = createAssigner(_.allKeys); // Assigns a given object with all the own properties in the passed-in object(s) // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) _.extendOwn = _.assign = createAssigner(_.keys); // Returns the first key on an object that passes a predicate test _.findKey = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = _.keys(obj), key; for (var i = 0, length = keys.length; i < length; i++) { key = keys[i]; if (predicate(obj[key], key, obj)) return key; } }; // Return a copy of the object only containing the allowed properties. _.pick = function(object, oiteratee, context) { var result = {}, obj = object, iteratee, keys; if (obj == null) return result; if (_.isFunction(oiteratee)) { keys = _.allKeys(obj); iteratee = optimizeCb(oiteratee, context); } else { keys = flatten(arguments, false, false, 1); iteratee = function(value, key, obj) { return key in obj; }; obj = Object(obj); } for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; var value = obj[key]; if (iteratee(value, key, obj)) result[key] = value; } return result; }; // Return a copy of the object without the disallowed properties. _.omit = function(obj, iteratee, context) { if (_.isFunction(iteratee)) { iteratee = _.negate(iteratee); } else { var keys = _.map(flatten(arguments, false, false, 1), String); iteratee = function(value, key) { return !_.contains(keys, key); }; } return _.pick(obj, iteratee, context); }; // Fill in a given object with default properties. _.defaults = createAssigner(_.allKeys, true); // Creates an object that inherits from the given prototype object. // If additional properties are provided then they will be added to the // created object. _.create = function(prototype, props) { var result = baseCreate(prototype); if (props) _.extendOwn(result, props); return result; }; // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; }; // Returns whether an object has a given set of `key:value` pairs. _.isMatch = function(object, attrs) { var keys = _.keys(attrs), length = keys.length; if (object == null) return !length; var obj = Object(object); for (var i = 0; i < length; i++) { var key = keys[i]; if (attrs[key] !== obj[key] || !(key in obj)) return false; } return true; }; // Internal recursive comparison function for `isEqual`. var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a === 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return '' + a === '' + b; case '[object Number]': // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; } var areArrays = className === '[object Array]'; if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); // Recursively compare objects and arrays. if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; if (length !== b.length) return false; // Deep compare the contents, ignoring non-numeric properties. while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. var keys = _.keys(a), key; length = keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. if (_.keys(b).length !== length) return false; while (length--) { // Deep compare each member key = keys[length]; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); return true; }; // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { return eq(a, b); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (obj == null) return true; if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; return _.keys(obj).length === 0; }; // Is a given value a DOM element? _.isElement = function(obj) { return !!(obj && obj.nodeType === 1); }; // Is a given value an array? // Delegates to ECMA5's native Array.isArray _.isArray = nativeIsArray || function(obj) { return toString.call(obj) === '[object Array]'; }; // Is a given variable an object? _.isObject = function(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; }; // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { _['is' + name] = function(obj) { return toString.call(obj) === '[object ' + name + ']'; }; }); // Define a fallback version of the method in browsers (ahem, IE < 9), where // there isn't any inspectable "Arguments" type. if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return _.has(obj, 'callee'); }; } // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, // IE 11 (#1621), and in Safari 8 (#1929). if (typeof /./ != 'function' && typeof Int8Array != 'object') { _.isFunction = function(obj) { return typeof obj == 'function' || false; }; } // Is a given object a finite number? _.isFinite = function(obj) { return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). _.isNaN = function(obj) { return _.isNumber(obj) && obj !== +obj; }; // Is a given value a boolean? _.isBoolean = function(obj) { return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; }; // Is a given value equal to null? _.isNull = function(obj) { return obj === null; }; // Is a given variable undefined? _.isUndefined = function(obj) { return obj === void 0; }; // Shortcut function for checking if an object has a given property directly // on itself (in other words, not on a prototype). _.has = function(obj, key) { return obj != null && hasOwnProperty.call(obj, key); }; // Utility Functions // ----------------- // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; return this; }; // Keep the identity function around for default iteratees. _.identity = function(value) { return value; }; // Predicate-generating functions. Often useful outside of Underscore. _.constant = function(value) { return function() { return value; }; }; _.noop = function(){}; _.property = property; // Generates a function for a given object that returns a given property. _.propertyOf = function(obj) { return obj == null ? function(){} : function(key) { return obj[key]; }; }; // Returns a predicate for checking whether an object has a given set of // `key:value` pairs. _.matcher = _.matches = function(attrs) { attrs = _.extendOwn({}, attrs); return function(obj) { return _.isMatch(obj, attrs); }; }; // Run a function **n** times. _.times = function(n, iteratee, context) { var accum = Array(Math.max(0, n)); iteratee = optimizeCb(iteratee, context, 1); for (var i = 0; i < n; i++) accum[i] = iteratee(i); return accum; }; // Return a random integer between min and max (inclusive). _.random = function(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); }; // A (possibly faster) way to get the current timestamp as an integer. _.now = Date.now || function() { return new Date().getTime(); }; // List of HTML entities for escaping. var escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }; var unescapeMap = _.invert(escapeMap); // Functions for escaping and unescaping strings to/from HTML interpolation. var createEscaper = function(map) { var escaper = function(match) { return map[match]; }; // Regexes for identifying a key that needs to be escaped var source = '(?:' + _.keys(map).join('|') + ')'; var testRegexp = RegExp(source); var replaceRegexp = RegExp(source, 'g'); return function(string) { string = string == null ? '' : '' + string; return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; }; }; _.escape = createEscaper(escapeMap); _.unescape = createEscaper(unescapeMap); // If the value of the named `property` is a function then invoke it with the // `object` as context; otherwise, return it. _.result = function(object, property, fallback) { var value = object == null ? void 0 : object[property]; if (value === void 0) { value = fallback; } return _.isFunction(value) ? value.call(object) : value; }; // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { var id = ++idCounter + ''; return prefix ? prefix + id : id; }; // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g, escape : /<%-([\s\S]+?)%>/g }; // When customizing `templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. var noMatch = /(.)^/; // Certain characters need to be escaped so that they can be put into a // string literal. var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escaper = /\\|'|\r|\n|\u2028|\u2029/g; var escapeChar = function(match) { return '\\' + escapes[match]; }; // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. // NB: `oldSettings` only exists for backwards compatibility. _.template = function(text, settings, oldSettings) { if (!settings && oldSettings) settings = oldSettings; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escaper, escapeChar); index = offset + match.length; if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } else if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } // Adobe VMs need the match returned to produce the correct offest. return match; }); source += "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n'; try { var render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } var template = function(data) { return render.call(this, data, _); }; // Provide the compiled source as a convenience for precompilation. var argument = settings.variable || 'obj'; template.source = 'function(' + argument + '){\n' + source + '}'; return template; }; // Add a "chain" function. Start chaining a wrapped Underscore object. _.chain = function(obj) { var instance = _(obj); instance._chain = true; return instance; }; // OOP // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. // Helper function to continue chaining intermediate results. var result = function(instance, obj) { return instance._chain ? _(obj).chain() : obj; }; // Add your own custom functions to the Underscore object. _.mixin = function(obj) { _.each(_.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return result(this, func.apply(_, args)); }; }); }; // Add all of the Underscore functions to the wrapper object. _.mixin(_); // Add all mutator Array functions to the wrapper. _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; method.apply(obj, arguments); if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; return result(this, obj); }; }); // Add all accessor Array functions to the wrapper. _.each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { return result(this, method.apply(this._wrapped, arguments)); }; }); // Extracts the result from a wrapped and chained object. _.prototype.value = function() { return this._wrapped; }; // Provide unwrapping proxy for some methods used in engine operations // such as arithmetic and JSON stringification. _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; _.prototype.toString = function() { return '' + this._wrapped; }; // AMD registration happens at the end for compatibility with AMD loaders // that may not enforce next-turn semantics on modules. Even though general // practice for AMD registration is to be anonymous, underscore registers // as a named module because, like jQuery, it is a base library that is // popular enough to be bundled in a third party lib, but not be part of // an AMD load request. Those cases could generate an error when an // anonymous define() is called outside of a loader request. if (typeof define === 'function' && define.amd) { define('underscore', [], function() { return _; }); } }.call(this)); ================================================ FILE: test/view.js ================================================ (function(QUnit) { var view; QUnit.module('Backbone.View', { beforeEach: function() { $('#qunit-fixture').append( '

          Test

          ' ); view = new Backbone.View({ id: 'test-view', className: 'test-view', other: 'non-special-option' }); }, afterEach: function() { $('#testElement').remove(); $('#test-view').remove(); } }); QUnit.test('constructor', function(assert) { assert.expect(3); assert.equal(view.el.id, 'test-view'); assert.equal(view.el.className, 'test-view'); assert.equal(view.el.other, void 0); }); QUnit.test('$', function(assert) { assert.expect(2); var myView = new Backbone.View; myView.setElement('

          test

          '); var result = myView.$('a b'); assert.strictEqual(result[0].innerHTML, 'test'); assert.ok(result.length === +result.length); }); QUnit.test('$el', function(assert) { assert.expect(3); var myView = new Backbone.View; myView.setElement('

          test

          '); assert.strictEqual(myView.el.nodeType, 1); assert.ok(myView.$el instanceof Backbone.$); assert.strictEqual(myView.$el[0], myView.el); }); QUnit.test('initialize', function(assert) { assert.expect(1); var View = Backbone.View.extend({ initialize: function() { this.one = 1; } }); assert.strictEqual(new View().one, 1); }); QUnit.test('preinitialize', function(assert) { assert.expect(1); var View = Backbone.View.extend({ preinitialize: function() { this.one = 1; } }); assert.strictEqual(new View().one, 1); }); QUnit.test('preinitialize occurs before the view is set up', function(assert) { assert.expect(2); var View = Backbone.View.extend({ preinitialize: function() { assert.equal(this.el, undefined); } }); var _view = new View({}); assert.notEqual(_view.el, undefined); }); QUnit.test('render', function(assert) { assert.expect(1); var myView = new Backbone.View; assert.equal(myView.render(), myView, '#render returns the view instance'); }); QUnit.test('delegateEvents', function(assert) { assert.expect(6); var counter1 = 0, counter2 = 0; var myView = new Backbone.View({el: '#testElement'}); myView.increment = function() { counter1++; }; myView.$el.on('click', function() { counter2++; }); var events = {'click h1': 'increment'}; myView.delegateEvents(events); myView.$('h1').trigger('click'); assert.equal(counter1, 1); assert.equal(counter2, 1); myView.$('h1').trigger('click'); assert.equal(counter1, 2); assert.equal(counter2, 2); myView.delegateEvents(events); myView.$('h1').trigger('click'); assert.equal(counter1, 3); assert.equal(counter2, 3); }); QUnit.test('delegate', function(assert) { assert.expect(3); var myView = new Backbone.View({el: '#testElement'}); myView.delegate('click', 'h1', function() { assert.ok(true); }); myView.delegate('click', function() { assert.ok(true); }); myView.$('h1').trigger('click'); assert.equal(myView.delegate(), myView, '#delegate returns the view instance'); }); QUnit.test('delegateEvents allows functions for callbacks', function(assert) { assert.expect(3); var myView = new Backbone.View({el: '

          '}); myView.counter = 0; var events = { click: function() { this.counter++; } }; myView.delegateEvents(events); myView.$el.trigger('click'); assert.equal(myView.counter, 1); myView.$el.trigger('click'); assert.equal(myView.counter, 2); myView.delegateEvents(events); myView.$el.trigger('click'); assert.equal(myView.counter, 3); }); QUnit.test('delegateEvents ignore undefined methods', function(assert) { assert.expect(0); var myView = new Backbone.View({el: '

          '}); myView.delegateEvents({click: 'undefinedMethod'}); myView.$el.trigger('click'); }); QUnit.test('undelegateEvents', function(assert) { assert.expect(7); var counter1 = 0, counter2 = 0; var myView = new Backbone.View({el: '#testElement'}); myView.increment = function() { counter1++; }; myView.$el.on('click', function() { counter2++; }); var events = {'click h1': 'increment'}; myView.delegateEvents(events); myView.$('h1').trigger('click'); assert.equal(counter1, 1); assert.equal(counter2, 1); myView.undelegateEvents(); myView.$('h1').trigger('click'); assert.equal(counter1, 1); assert.equal(counter2, 2); myView.delegateEvents(events); myView.$('h1').trigger('click'); assert.equal(counter1, 2); assert.equal(counter2, 3); assert.equal(myView.undelegateEvents(), myView, '#undelegateEvents returns the view instance'); }); QUnit.test('undelegate', function(assert) { assert.expect(1); var myView = new Backbone.View({el: '#testElement'}); myView.delegate('click', function() { assert.ok(false); }); myView.delegate('click', 'h1', function() { assert.ok(false); }); myView.undelegate('click'); myView.$('h1').trigger('click'); myView.$el.trigger('click'); assert.equal(myView.undelegate(), myView, '#undelegate returns the view instance'); }); QUnit.test('undelegate with passed handler', function(assert) { assert.expect(1); var myView = new Backbone.View({el: '#testElement'}); var listener = function() { assert.ok(false); }; myView.delegate('click', listener); myView.delegate('click', function() { assert.ok(true); }); myView.undelegate('click', listener); myView.$el.trigger('click'); }); QUnit.test('undelegate with selector', function(assert) { assert.expect(2); var myView = new Backbone.View({el: '#testElement'}); myView.delegate('click', function() { assert.ok(true); }); myView.delegate('click', 'h1', function() { assert.ok(false); }); myView.undelegate('click', 'h1'); myView.$('h1').trigger('click'); myView.$el.trigger('click'); }); QUnit.test('undelegate with handler and selector', function(assert) { assert.expect(2); var myView = new Backbone.View({el: '#testElement'}); myView.delegate('click', function() { assert.ok(true); }); var handler = function() { assert.ok(false); }; myView.delegate('click', 'h1', handler); myView.undelegate('click', 'h1', handler); myView.$('h1').trigger('click'); myView.$el.trigger('click'); }); QUnit.test('tagName can be provided as a string', function(assert) { assert.expect(1); var View = Backbone.View.extend({ tagName: 'span' }); assert.equal(new View().el.tagName, 'SPAN'); }); QUnit.test('tagName can be provided as a function', function(assert) { assert.expect(1); var View = Backbone.View.extend({ tagName: function() { return 'p'; } }); assert.ok(new View().$el.is('p')); }); QUnit.test('_ensureElement with DOM node el', function(assert) { assert.expect(1); var View = Backbone.View.extend({ el: document.body }); assert.equal(new View().el, document.body); }); QUnit.test('_ensureElement with string el', function(assert) { assert.expect(3); var View = Backbone.View.extend({ el: 'body' }); assert.strictEqual(new View().el, document.body); View = Backbone.View.extend({ el: '#testElement > h1' }); assert.strictEqual(new View().el, $('#testElement > h1').get(0)); View = Backbone.View.extend({ el: '#nonexistent' }); assert.ok(!new View().el); }); QUnit.test('with className and id functions', function(assert) { assert.expect(2); var View = Backbone.View.extend({ className: function() { return 'className'; }, id: function() { return 'id'; } }); assert.strictEqual(new View().el.className, 'className'); assert.strictEqual(new View().el.id, 'id'); }); QUnit.test('with attributes', function(assert) { assert.expect(2); var View = Backbone.View.extend({ attributes: { 'id': 'id', 'class': 'class' } }); assert.strictEqual(new View().el.className, 'class'); assert.strictEqual(new View().el.id, 'id'); }); QUnit.test('with attributes as a function', function(assert) { assert.expect(1); var View = Backbone.View.extend({ attributes: function() { return {'class': 'dynamic'}; } }); assert.strictEqual(new View().el.className, 'dynamic'); }); QUnit.test('should default to className/id properties', function(assert) { assert.expect(4); var View = Backbone.View.extend({ className: 'backboneClass', id: 'backboneId', attributes: { 'class': 'attributeClass', 'id': 'attributeId' } }); var myView = new View; assert.strictEqual(myView.el.className, 'backboneClass'); assert.strictEqual(myView.el.id, 'backboneId'); assert.strictEqual(myView.$el.attr('class'), 'backboneClass'); assert.strictEqual(myView.$el.attr('id'), 'backboneId'); }); QUnit.test('multiple views per element', function(assert) { assert.expect(3); var count = 0; var $el = $('

          '); var View = Backbone.View.extend({ el: $el, events: { click: function() { count++; } } }); var view1 = new View; $el.trigger('click'); assert.equal(1, count); var view2 = new View; $el.trigger('click'); assert.equal(3, count); view1.delegateEvents(); $el.trigger('click'); assert.equal(5, count); }); QUnit.test('custom events', function(assert) { assert.expect(2); var View = Backbone.View.extend({ el: $('body'), events: { fake$event: function() { assert.ok(true); } } }); var myView = new View; $('body').trigger('fake$event').trigger('fake$event'); $('body').off('fake$event'); $('body').trigger('fake$event'); }); QUnit.test('#1048 - setElement uses provided object.', function(assert) { assert.expect(2); var $el = $('body'); var myView = new Backbone.View({el: $el}); assert.ok(myView.$el === $el); myView.setElement($el = $($el)); assert.ok(myView.$el === $el); }); QUnit.test('#986 - Undelegate before changing element.', function(assert) { assert.expect(1); var button1 = $(''); var button2 = $(''); var View = Backbone.View.extend({ events: { click: function(e) { assert.ok(myView.el === e.target); } } }); var myView = new View({el: button1}); myView.setElement(button2); button1.trigger('click'); button2.trigger('click'); }); QUnit.test('#1172 - Clone attributes object', function(assert) { assert.expect(2); var View = Backbone.View.extend({ attributes: {foo: 'bar'} }); var view1 = new View({id: 'foo'}); assert.strictEqual(view1.el.id, 'foo'); var view2 = new View(); assert.ok(!view2.el.id); }); QUnit.test('views stopListening', function(assert) { assert.expect(0); var View = Backbone.View.extend({ initialize: function() { this.listenTo(this.model, 'all x', function() { assert.ok(false); }); this.listenTo(this.collection, 'all x', function() { assert.ok(false); }); } }); var myView = new View({ model: new Backbone.Model, collection: new Backbone.Collection }); myView.stopListening(); myView.model.trigger('x'); myView.collection.trigger('x'); }); QUnit.test('Provide function for el.', function(assert) { assert.expect(2); var View = Backbone.View.extend({ el: function() { return '

          '; } }); var myView = new View; assert.ok(myView.$el.is('p')); assert.ok(myView.$el.has('a')); }); QUnit.test('events passed in options', function(assert) { assert.expect(1); var counter = 0; var View = Backbone.View.extend({ el: '#testElement', increment: function() { counter++; } }); var myView = new View({ events: { 'click h1': 'increment' } }); myView.$('h1').trigger('click').trigger('click'); assert.equal(counter, 2); }); QUnit.test('remove', function(assert) { assert.expect(2); var myView = new Backbone.View; document.body.appendChild(view.el); myView.delegate('click', function() { assert.ok(false); }); myView.listenTo(myView, 'all x', function() { assert.ok(false); }); assert.equal(myView.remove(), myView, '#remove returns the view instance'); myView.$el.trigger('click'); myView.trigger('x'); // In IE8 and below, parentNode still exists but is not document.body. assert.notEqual(myView.el.parentNode, document.body); }); QUnit.test('setElement', function(assert) { assert.expect(3); var myView = new Backbone.View({ events: { click: function() { assert.ok(false); } } }); myView.events = { click: function() { assert.ok(true); } }; var oldEl = myView.el; var $oldEl = myView.$el; myView.setElement(document.createElement('div')); $oldEl.click(); myView.$el.click(); assert.notEqual(oldEl, myView.el); assert.notEqual($oldEl, myView.$el); }); })(QUnit);