├── fileapi ├── __init__.py ├── tests │ ├── __init__.py │ ├── static │ │ ├── index.html │ │ ├── tests │ │ │ ├── test_models.js │ │ │ └── test_views.js │ │ └── libs │ │ │ └── qunit.css │ ├── test_qunit.py │ └── test_functional.py ├── urls.py ├── wsgi.py ├── static │ ├── css │ │ └── site.css │ ├── js │ │ ├── models.js │ │ ├── main.js │ │ └── views.js │ └── libs │ │ ├── underscore.js │ │ └── backbone.js ├── templates │ └── index.html ├── views.py └── settings.py ├── runtime.txt ├── Procfile ├── manage.py ├── requirements.txt ├── LICENSE └── README.rst /fileapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fileapi/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.5 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn fileapi.wsgi --log-file=- 2 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fileapi.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Base requirements 2 | Django==1.8.4 3 | PyJWT==1.4.0 4 | django-jwt-auth==0.0.1 5 | 6 | # Deployment requirements 7 | gunicorn>=19.1,<19.2 8 | dj-database-url==0.3.0 9 | dj-static==0.0.6 10 | static3==0.5.1 11 | psycopg2-binary==2.7.4 12 | 13 | # Testing requirements 14 | selenium==2.44.0 15 | coverage==3.7.1 16 | -------------------------------------------------------------------------------- /fileapi/urls.py: -------------------------------------------------------------------------------- 1 | import jwt_auth.views 2 | 3 | from django.conf.urls import url 4 | 5 | from .views import FileListView, FileDetailView, IndexView 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^uploads/$', FileListView.as_view(), name='upload-list'), 10 | url(r'^uploads/(?P.*)', FileDetailView.as_view(), name='upload-detail'), 11 | url(r'^api-token/', jwt_auth.views.obtain_jwt_token, name='api-token'), 12 | url(r'^$', IndexView.as_view(), name='homepage') 13 | ] 14 | -------------------------------------------------------------------------------- /fileapi/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for fileapi project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "fileapi.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | from dj_static import Cling 15 | 16 | application = Cling(get_wsgi_application()) 17 | -------------------------------------------------------------------------------- /fileapi/static/css/site.css: -------------------------------------------------------------------------------- 1 | #content { 2 | width: 80%; 3 | margin: 0 auto; 4 | padding: 100px 0; 5 | } 6 | 7 | #content > h1 { 8 | text-align: center; 9 | } 10 | 11 | #files { 12 | display: none; 13 | margin: 20px auto; 14 | border: 1px solid black; 15 | } 16 | 17 | #files .file { 18 | padding: 5px; 19 | } 20 | 21 | #files .delete { 22 | float: right; 23 | } 24 | 25 | #upload { 26 | display: none; 27 | text-align: center; 28 | padding: 25px; 29 | border: 2px dashed black; 30 | } 31 | 32 | #upload.hover { 33 | background: #ccc; 34 | border: 2px solid black; 35 | } 36 | 37 | #login { 38 | text-align: center; 39 | } 40 | 41 | #login input { 42 | display: block; 43 | margin: 10px auto; 44 | } 45 | -------------------------------------------------------------------------------- /fileapi/static/js/models.js: -------------------------------------------------------------------------------- 1 | (function ($, Backbone, _, config) { 2 | 3 | var Upload = Backbone.Model.extend({ 4 | url: function () { 5 | var links = this.get('links'), 6 | url = links && links.self; 7 | if (!url) { 8 | url = Backbone.Model.prototype.url.call(this); 9 | } 10 | return url; 11 | }, 12 | idAttribute: 'name' 13 | }); 14 | 15 | config.collections = {}; 16 | 17 | config.collections.UploadCollection = Backbone.Collection.extend({ 18 | model: Upload, 19 | url: config.api, 20 | parse: function (response) { 21 | this._count = response.count; 22 | return response.files || []; 23 | } 24 | }); 25 | 26 | })(jQuery, Backbone, _, config); 27 | -------------------------------------------------------------------------------- /fileapi/static/js/main.js: -------------------------------------------------------------------------------- 1 | var config = (function ($, Backbone, _) { 2 | 3 | $(document).ready(function () { 4 | var uploads = new config.collections.UploadCollection(), 5 | listView = new config.views.UploadListingView(), 6 | newView = new config.views.NewUploadView({uploads: uploads}), 7 | loginView = new config.views.LoginView(); 8 | 9 | uploads.on('add', listView.addFile, listView); 10 | loginView.on('login', function (token) { 11 | $.ajaxPrefilter(function (settings, options, xhr) { 12 | xhr.setRequestHeader('Authorization', 'Bearer ' + token); 13 | }); 14 | newView.render(); 15 | listView.render(); 16 | uploads.fetch(); 17 | }); 18 | }); 19 | 20 | return JSON.parse($('#config').text()); 21 | 22 | })(jQuery, Backbone, _); 23 | -------------------------------------------------------------------------------- /fileapi/tests/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 |
10 |
11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mark Lavin 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /fileapi/tests/test_qunit.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 5 | from django.test.utils import modify_settings 6 | 7 | from selenium import webdriver 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.support import expected_conditions 10 | from selenium.webdriver.support.ui import WebDriverWait 11 | 12 | 13 | @modify_settings(STATICFILES_DIRS={ 14 | 'append': os.path.join(os.path.dirname(__file__), 'static')}) 15 | class QunitTests(StaticLiveServerTestCase): 16 | """Iteractive tests with selenium.""" 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | cls.browser = webdriver.PhantomJS() 21 | super().setUpClass() 22 | 23 | @classmethod 24 | def tearDownClass(cls): 25 | cls.browser.quit() 26 | super().tearDownClass() 27 | 28 | def test_qunit(self): 29 | """Load the QUnit tests and check for failures.""" 30 | 31 | self.browser.get(self.live_server_url + settings.STATIC_URL + 'index.html') 32 | results = WebDriverWait(self.browser, 5).until( 33 | expected_conditions.visibility_of_element_located((By.ID, 'qunit-testresult'))) 34 | total = int(results.find_element_by_class_name('total').text) 35 | failed = int(results.find_element_by_class_name('failed').text) 36 | self.assertTrue(total and not failed, results.text) 37 | -------------------------------------------------------------------------------- /fileapi/tests/static/tests/test_models.js: -------------------------------------------------------------------------------- 1 | QUnit.test('Config Populated', function (assert) { 2 | assert.ok(config.collections); 3 | assert.ok(config.collections.UploadCollection); 4 | }); 5 | 6 | QUnit.test('Parse Empty API response', function (assert) { 7 | var uploads = new config.collections.UploadCollection(), 8 | response = { 9 | count: 0, 10 | files: [] 11 | }; 12 | assert.deepEqual(uploads.parse(response), []); 13 | }); 14 | 15 | QUnit.test('Parse Populated API response', function (assert) { 16 | var uploads = new config.collections.UploadCollection(), 17 | file = { 18 | name: 'test.png', 19 | size: 200, 20 | created: '2015-01-14T01:21:56.870', 21 | links: { 22 | self: '/uploads/test.png' 23 | } 24 | }, 25 | response = { 26 | count: 1, 27 | files: [file] 28 | }; 29 | assert.deepEqual(uploads.parse(response), [file]); 30 | }); 31 | 32 | QUnit.test('Model Provided URL', function (assert) { 33 | var uploads = new config.collections.UploadCollection(), 34 | file = { 35 | name: 'test.png', 36 | size: 200, 37 | created: '2015-01-14T01:21:56.870', 38 | links: { 39 | self: '/uploads/test.png' 40 | } 41 | }, 42 | model = uploads.push(file); 43 | assert.equal(model.url(), '/uploads/test.png'); 44 | }); 45 | 46 | QUnit.test('Model Construct URL', function (assert) { 47 | var uploads = new config.collections.UploadCollection(), 48 | model = uploads.push({name: 'test.png'}); 49 | assert.equal(model.url(), '/uploads/test.png'); 50 | }); 51 | -------------------------------------------------------------------------------- /fileapi/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load staticfiles %} 5 | 6 | 7 | File Upload Example 8 | 9 | 10 | 11 |
12 | Fork me on GitHub 13 |

File Upload API Example

14 |
Drag and Drop to Upload
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /fileapi/views.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.files.storage import FileSystemStorage 3 | from django.core.urlresolvers import reverse 4 | from django.http import JsonResponse, HttpResponseNotFound, HttpResponse 5 | from django.views.generic import View, TemplateView 6 | 7 | from jwt_auth.mixins import JSONWebTokenAuthMixin 8 | 9 | 10 | storage = FileSystemStorage() 11 | 12 | 13 | class UploadForm(forms.Form): 14 | name = forms.FileField() 15 | 16 | 17 | def file_info(name): 18 | return { 19 | 'name': name, 20 | 'size': storage.size(name), 21 | 'created': storage.created_time(name), 22 | 'links': { 23 | 'self': reverse('upload-detail', kwargs={'name': name}) 24 | } 25 | } 26 | 27 | 28 | class FileListView(JSONWebTokenAuthMixin, View): 29 | """Get a list of all available files or create a new file.""" 30 | 31 | def get(self, request): 32 | """List all files.""" 33 | _, files = storage.listdir('') 34 | info = [] 35 | for name in files: 36 | info.append(file_info(name)) 37 | result = { 38 | 'files': info, 39 | 'count': len(info), 40 | } 41 | return JsonResponse(result) 42 | 43 | def post(self, request): 44 | """Add a new file.""" 45 | form = UploadForm(request.POST, request.FILES) 46 | if form.is_valid(): 47 | upload = form.cleaned_data['name'] 48 | storage.save(upload.name, upload) 49 | result = file_info(upload.name) 50 | return JsonResponse(result, status=201) 51 | else: 52 | return HttpResponse(form.errors.as_json(), status=400, content_type='application/json') 53 | 54 | 55 | class FileDetailView(JSONWebTokenAuthMixin, View): 56 | """Get details for a single file or delete the file.""" 57 | 58 | def get(self, request, name): 59 | """Get details for a file.""" 60 | if storage.exists(name): 61 | result = file_info(name) 62 | return JsonResponse(result) 63 | else: 64 | return HttpResponseNotFound() 65 | 66 | def delete(self, request, name): 67 | """Delete a file.""" 68 | if storage.exists(name): 69 | storage.delete(name) 70 | return HttpResponse(status=204) 71 | else: 72 | return HttpResponse(status=410) 73 | 74 | 75 | class IndexView(TemplateView): 76 | template_name = 'index.html' 77 | -------------------------------------------------------------------------------- /fileapi/tests/static/tests/test_views.js: -------------------------------------------------------------------------------- 1 | QUnit.test('Config Populated', function (assert) { 2 | assert.ok(config.views); 3 | assert.ok(config.views.UploadListingView); 4 | assert.ok(config.views.NewUploadView); 5 | assert.ok(config.views.LoginView); 6 | }); 7 | 8 | QUnit.module('UploadListingView Tests', { 9 | beforeEach: function () { 10 | $('#qunit-fixture').append($('
', {id: 'files'})); 11 | this.view = new config.views.UploadListingView(); 12 | }, 13 | afterEach: function () { 14 | this.view.remove(); 15 | } 16 | }); 17 | 18 | QUnit.test('Add Upload', function (assert) { 19 | var uploads = new config.collections.UploadCollection(), 20 | upload = uploads.push({ 21 | name: 'test.png', 22 | size: 200, 23 | created: '2015-01-14T01:21:56.870', 24 | links: { 25 | self: '/uploads/test.png' 26 | } 27 | }); 28 | this.view.addFile(upload); 29 | assert.equal($('.file', this.view.$el).length, 1); 30 | }); 31 | 32 | QUnit.module('NewUploadView Tests', { 33 | beforeEach: function () { 34 | $('#qunit-fixture').append($('
', {id: 'upload'})); 35 | this.uploads = new config.collections.UploadCollection(); 36 | this.view = new config.views.NewUploadView({uploads: this.uploads}); 37 | sinon.stub(this.uploads, 'create'); 38 | }, 39 | afterEach: function () { 40 | this.view.remove(); 41 | this.uploads.create.restore(); 42 | } 43 | }); 44 | 45 | QUnit.test('Drag Enter', function (assert) { 46 | var e = $.Event('dragenter'); 47 | this.view.enter(e); 48 | assert.equal(this.view.enter(e), false); 49 | assert.ok(this.view.$el.hasClass('hover')); 50 | }); 51 | 52 | QUnit.test('Drag Over', function (assert) { 53 | var e = $.Event('dragover'); 54 | assert.equal(this.view.over(e), false); 55 | assert.ok(this.view.$el.hasClass('hover')); 56 | }); 57 | 58 | QUnit.test('Drag End', function (assert) { 59 | var e = $.Event('dragend'); 60 | this.view.$el.addClass('hover'); 61 | this.view.end(e); 62 | assert.equal(this.view.$el.hasClass('hover'), false); 63 | }); 64 | 65 | QUnit.test('Drop File', function (assert) { 66 | var e = $.Event('drop'), 67 | file = {'name': 'test.txt'}; 68 | e.originalEvent = { 69 | dataTransfer: {files: [file, ]} 70 | }; 71 | this.view.$el.addClass('hover'); 72 | this.view.drop(e); 73 | assert.ok(this.uploads.create.calledOnce); 74 | assert.equal(this.view.$el.hasClass('hover'), false); 75 | }); 76 | -------------------------------------------------------------------------------- /fileapi/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for fileapi project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | import dj_database_url 14 | 15 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | DEFAULT_SECRET_KEY = '7clb$a&c(*ter7=+bo-vn=43d3jrjxo#l(n+ysw^6#4t&5t&fj' 23 | 24 | SECRET_KEY = os.environ.get('SECRET_KEY', DEFAULT_SECRET_KEY) 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = os.environ.get('DEBUG', 'on') == 'on' 28 | 29 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(';') 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.staticfiles', 37 | 'fileapi', 38 | ) 39 | 40 | MIDDLEWARE_CLASSES = ( 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 44 | ) 45 | 46 | ROOT_URLCONF = 'fileapi.urls' 47 | 48 | TEMPLATES = [ 49 | { 50 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 51 | 'DIRS': [], 52 | 'APP_DIRS': True, 53 | 'OPTIONS': { 54 | 'context_processors': [ 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.template.context_processors.debug', 57 | 'django.template.context_processors.i18n', 58 | 'django.template.context_processors.tz', 59 | 'django.template.context_processors.request', 60 | 'django.contrib.messages.context_processors.messages', 61 | ], 62 | }, 63 | }, 64 | ] 65 | 66 | WSGI_APPLICATION = 'fileapi.wsgi.application' 67 | 68 | 69 | # Database 70 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 71 | 72 | DATABASES = { 73 | 'default': dj_database_url.config(default='sqlite:///%s' % os.path.join(BASE_DIR, 'db.sqlite3')) 74 | } 75 | 76 | # Internationalization 77 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 78 | 79 | LANGUAGE_CODE = 'en-us' 80 | 81 | TIME_ZONE = 'UTC' 82 | 83 | USE_I18N = True 84 | 85 | USE_L10N = True 86 | 87 | USE_TZ = True 88 | 89 | 90 | # Static files (CSS, JavaScript, Images) 91 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 92 | 93 | STATIC_URL = '/static/' 94 | 95 | MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') 96 | 97 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 98 | 99 | 100 | def jwt_decode_handler(token): 101 | """Customized handler to fix compatibility with pyJWT 1.0+""" 102 | import jwt 103 | from jwt_auth import settings as jwt_settings 104 | options = { 105 | 'verify_exp': jwt_settings.JWT_VERIFY_EXPIRATION, 106 | } 107 | return jwt.decode( 108 | token, 109 | key=jwt_settings.JWT_SECRET_KEY, 110 | verify=jwt_settings.JWT_VERIFY, 111 | algorithms=[jwt_settings.JWT_ALGORITHM, ], 112 | leeway=jwt_settings.JWT_LEEWAY, 113 | options=options) 114 | 115 | JWT_DECODE_HANDLER = jwt_decode_handler 116 | 117 | X_FRAME_OPTIONS = 'DENY' 118 | 119 | SECURE_CONTENT_TYPE_NOSNIFF = True 120 | 121 | SECURE_BROWSER_XSS_FILTER = True 122 | 123 | SSL_ENABLED = bool(os.environ.get('SSL_ENABLED', False)) 124 | 125 | SECURE_SSL_REDIRECT = SSL_ENABLED 126 | 127 | SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365 if SSL_ENABLED else 0 128 | -------------------------------------------------------------------------------- /fileapi/static/js/views.js: -------------------------------------------------------------------------------- 1 | (function ($, Backbone, _, config) { 2 | 3 | var UploadView = Backbone.View.extend({ 4 | className: 'file', 5 | template: _.template('<%- name %> X'), 6 | events: { 7 | 'click .delete': 'delete' 8 | }, 9 | initialize: function () { 10 | this.listenTo(this.model, 'change', this.render); 11 | this.listenTo(this.model, 'destroy', this.remove); 12 | }, 13 | render: function () { 14 | this.$el.html(this.template(this.model.toJSON())); 15 | }, 16 | delete: function (e) { 17 | e.preventDefault(); 18 | this.model.destroy(); 19 | } 20 | }); 21 | 22 | config.views = {}; 23 | 24 | config.views.UploadListingView = Backbone.View.extend({ 25 | el: '#files', 26 | render: function () { 27 | this.$el.show(); 28 | }, 29 | addFile: function (file) { 30 | var view = new UploadView({model: file}); 31 | this.$el.append(view.$el); 32 | view.render(); 33 | } 34 | }); 35 | 36 | config.views.NewUploadView = Backbone.View.extend({ 37 | el: '#upload', 38 | events: { 39 | 'dragenter': 'enter', 40 | 'dragover': 'over', 41 | 'dragend': 'end', 42 | 'dragleave': 'end', 43 | 'drop': 'drop' 44 | }, 45 | initialize: function (options) { 46 | this.uploads = options.uploads; 47 | }, 48 | enter: function (e) { 49 | e.stopPropagation(); 50 | e.preventDefault(); 51 | this.$el.addClass('hover'); 52 | return false; 53 | }, 54 | over: function (e) { 55 | e.stopPropagation(); 56 | e.preventDefault(); 57 | this.$el.addClass('hover'); 58 | return false; 59 | }, 60 | end: function (e) { 61 | this.$el.removeClass('hover'); 62 | }, 63 | drop: function (e) { 64 | var formData = new FormData(), 65 | file; 66 | e.stopPropagation(); 67 | e.preventDefault(); 68 | if (e.originalEvent.dataTransfer.files.length === 0) { 69 | return; 70 | } 71 | file = e.originalEvent.dataTransfer.files[0]; 72 | formData.append('name', file); 73 | this.uploads.create({}, { 74 | error: $.proxy(this.fail, this), 75 | wait: true, 76 | data: formData, 77 | processData: false, 78 | contentType: false 79 | }); 80 | this.end(); 81 | }, 82 | fail: function (_model, response) { 83 | var errors = response.responseJSON.name, 84 | errorTemplate = _.template('

<%- message %>

'); 85 | _.map(errors, function (error) { 86 | this.$el.before(errorTemplate(error)); 87 | }, this); 88 | setTimeout($.proxy(function() { 89 | var errors = $('.error'); 90 | errors.fadeOut('slow', function () { 91 | errors.remove(); 92 | }); 93 | }, this), 2500); 94 | }, 95 | render: function () { 96 | this.$el.show(); 97 | } 98 | }); 99 | 100 | config.views.LoginView = Backbone.View.extend({ 101 | el: '#login', 102 | events: { 103 | 'submit': 'submit' 104 | }, 105 | submit: function (e) { 106 | e.preventDefault(); 107 | var data = JSON.stringify({ 108 | username: $(':input[name="username"]', this.$el).val(), 109 | password: $(':input[name="password"]', this.$el).val(), 110 | }); 111 | $('.error', this.$el).remove(); 112 | $.post(config.login, data) 113 | .done($.proxy(this.login, this)) 114 | .fail($.proxy(this.fail, this)); 115 | }, 116 | login: function (result) { 117 | this.trigger('login', result.token); 118 | this.$el.hide(); 119 | }, 120 | fail: function () { 121 | this.$el.prepend('

Invalid username/password

'); 122 | } 123 | }); 124 | 125 | })(jQuery, Backbone, _, config); 126 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django File API Example 2 | ======================= 3 | 4 | This is an example project created for the "Creating Enriching Web Applications with Django and Backbone.js" 5 | webcast http://www.oreilly.com/pub/e/3154 and extended by a follow up webcast 6 | "Testing Client-Side Applications with Django" http://www.oreilly.com/pub/e/3302 7 | 8 | It demonstrates a basic REST API using Django and a JS client application using Backbone. This 9 | application is tested using integration tests written in Python using Selenium and using 10 | Javascript unit tests using QUnit. 11 | 12 | You can find a demo application running at http://fileapi.mlavin.org (Login: guest/guest) 13 | Uploaded files must be less than 1 MB and the directory is cleared every 5 mins. 14 | 15 | 16 | Project Requirements 17 | -------------------- 18 | 19 | This project was written and tested using Python 3.3+. The Python dependencies 20 | are listed in the `requirements.txt `_ file. 21 | These can easily be installed with `pip `_:: 22 | 23 | pip install -r requirements.txt 24 | 25 | Using `virtualenv `_ is recommended. 26 | 27 | 28 | How to Use this Repo 29 | -------------------- 30 | 31 | The commits in the repo follow the creation of the API and client from start to finish. Branches 32 | have been created to note stopping points along the way. To see the project evolve throughout 33 | this process you should step through the branches in order. 34 | 35 | Building the application: 36 | 37 | 1. `Fresh Project `_ 38 | 2. `Read Only API `_ (`Diff `_) 39 | 3. `Write API `_ (`Diff `_) 40 | 4. `Template Layout `_ (`Diff `_) 41 | 5. `File Listing `_ (`Diff `_) 42 | 6. `File Delete `_ (`Diff `_) 43 | 7. `File Upload `_ (`Diff `_) 44 | 8. `Token Authentication `_ (`Diff `_) 45 | 9. `Configuration `_ (`Diff `_) 46 | 47 | Testing the application: 48 | 49 | 1. `Working Application `_ 50 | 2. `Selenium Intro `_ (`Diff `_) 51 | 3. `Selenium Timeouts `_ (`Diff `_) 52 | 4. `Selenium File Interactions `_ (`Diff `_) 53 | 5. `Client Refactor `_ (`Diff `_) 54 | 6. `QUnit Setup `_ (`Diff `_) 55 | 7. `QUnit Fixtures `_ (`Diff `_) 56 | 8. `Sinon Mocks `_ (`Diff `_) 57 | 9. `Django Integration `_ (`Diff `_) 58 | 59 | License 60 | ------- 61 | 62 | The project content is released under the BSD License. See the 63 | `LICENSE `_ file for more details. 64 | -------------------------------------------------------------------------------- /fileapi/tests/test_functional.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | from django.contrib.auth.models import User 6 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 7 | from django.core.files.storage import FileSystemStorage 8 | 9 | from selenium import webdriver 10 | from selenium.common.exceptions import NoSuchElementException 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.support import expected_conditions 13 | from selenium.webdriver.support.ui import WebDriverWait 14 | 15 | from .. import views 16 | 17 | 18 | class FunctionalTests(StaticLiveServerTestCase): 19 | """Iteractive tests with selenium.""" 20 | 21 | @classmethod 22 | def setUpClass(cls): 23 | cls.browser = webdriver.PhantomJS() 24 | super().setUpClass() 25 | 26 | @classmethod 27 | def tearDownClass(cls): 28 | cls.browser.quit() 29 | super().tearDownClass() 30 | 31 | def setUp(self): 32 | self.temp_dir = tempfile.mkdtemp() 33 | self._orig_storage = views.storage 34 | views.storage = FileSystemStorage(self.temp_dir) 35 | self.username = 'test' 36 | self.password = 'test' 37 | User.objects.create_user(self.username, '', self.password) 38 | 39 | def tearDown(self): 40 | shutil.rmtree(self.temp_dir) 41 | views.storage = self._orig_storage 42 | 43 | def test_show_login(self): 44 | """The login should be shown on page load.""" 45 | 46 | self.browser.get(self.live_server_url) 47 | form = self.browser.find_element_by_id('login') 48 | upload = self.browser.find_element_by_id('upload') 49 | self.assertTrue(form.is_displayed(), 'Login form should be visible.') 50 | self.assertFalse(upload.is_displayed(), 'Upload area should not be visible.') 51 | 52 | def login(self, username, password): 53 | """Helper for login form submission.""" 54 | 55 | self.browser.get(self.live_server_url) 56 | form = self.browser.find_element_by_id('login') 57 | username_input = form.find_element_by_name('username') 58 | username_input.send_keys(username) 59 | password_input = form.find_element_by_name('password') 60 | password_input.send_keys(password) 61 | form.submit() 62 | 63 | def test_login(self): 64 | """Submit the login form with a valid login.""" 65 | 66 | self.login(self.username, self.password) 67 | WebDriverWait(self.browser, 5).until( 68 | expected_conditions.visibility_of_element_located((By.ID, 'upload'))) 69 | form = self.browser.find_element_by_id('login') 70 | self.assertFalse(form.is_displayed(), 'Login form should no longer be visible.') 71 | 72 | def test_invalid_login(self): 73 | """Submit the login form with an invalid login.""" 74 | 75 | self.login(self.username, self.password[1:]) 76 | error = WebDriverWait(self.browser, 5).until( 77 | expected_conditions.presence_of_element_located((By.CLASS_NAME, 'error'))) 78 | self.assertEqual('Invalid username/password', error.text) 79 | form = self.browser.find_element_by_id('login') 80 | self.assertTrue(form.is_displayed(), 'Login form should still be visible.') 81 | 82 | def test_browse_files(self): 83 | """Browse existing uploads.""" 84 | 85 | _, test_file = tempfile.mkstemp(dir=self.temp_dir) 86 | self.login(self.username, self.password) 87 | element = WebDriverWait(self.browser, 5).until( 88 | expected_conditions.presence_of_element_located((By.CLASS_NAME, 'file'))) 89 | self.assertIn(os.path.basename(test_file), element.text) 90 | 91 | def test_file_delete(self): 92 | """Delete uploaded file.""" 93 | 94 | _, test_file = tempfile.mkstemp(dir=self.temp_dir) 95 | self.login(self.username, self.password) 96 | element = WebDriverWait(self.browser, 5).until( 97 | expected_conditions.presence_of_element_located((By.CLASS_NAME, 'file'))) 98 | element.find_element_by_class_name('delete').click() 99 | self.browser.implicitly_wait(1) 100 | with self.assertRaises(NoSuchElementException): 101 | self.browser.find_element_by_class_name('file') 102 | self.assertFalse(os.path.exists(test_file)) 103 | 104 | def upload(self, filepath): 105 | # Create file input for fake drag and drop 106 | self.browser.execute_script(''' 107 | input = $('', {id: 'seleniumUpload', type: 'file'}).appendTo('body'); 108 | '''); 109 | self.browser.find_element_by_id('seleniumUpload').send_keys(filepath) 110 | # Fake a file drag and drop event 111 | self.browser.execute_script(''' 112 | event = $.Event('drop'); 113 | event.originalEvent = { 114 | dataTransfer: {files: input.get(0).files} 115 | }; 116 | $('#upload').trigger(event); 117 | '''); 118 | 119 | def test_file_upload(self): 120 | """Drag and drop a new file upload.""" 121 | 122 | self.login(self.username, self.password) 123 | _, test_file = tempfile.mkstemp() 124 | with open(test_file, 'w') as f: 125 | f.write('XXX') 126 | self.upload(test_file) 127 | element = WebDriverWait(self.browser, 5).until( 128 | expected_conditions.presence_of_element_located((By.CLASS_NAME, 'file'))) 129 | self.assertIn(os.path.basename(test_file), element.text) 130 | 131 | def test_invalid_file(self): 132 | """Drag and drop a new file upload.""" 133 | 134 | self.login(self.username, self.password) 135 | _, test_file = tempfile.mkstemp() 136 | self.upload(test_file) 137 | element = WebDriverWait(self.browser, 5).until( 138 | expected_conditions.presence_of_element_located((By.CLASS_NAME, 'error'))) 139 | self.assertEqual('The submitted file is empty.', element.text) -------------------------------------------------------------------------------- /fileapi/tests/static/libs/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.16.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2006, 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-12-03T16:32Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #2B81AF; 74 | color: #FFF; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | #qunit-modulefilter-container { 79 | float: right; 80 | } 81 | 82 | /** Tests: Pass/Fail */ 83 | 84 | #qunit-tests { 85 | list-style-position: inside; 86 | } 87 | 88 | #qunit-tests li { 89 | padding: 0.4em 1em 0.4em 1em; 90 | border-bottom: 1px solid #FFF; 91 | list-style-position: inside; 92 | } 93 | 94 | #qunit-tests > li { 95 | display: none; 96 | } 97 | 98 | #qunit-tests li.pass, #qunit-tests li.running, #qunit-tests li.fail { 99 | display: list-item; 100 | } 101 | 102 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 103 | display: none; 104 | } 105 | 106 | #qunit-tests li strong { 107 | cursor: pointer; 108 | } 109 | 110 | #qunit-tests li.skipped strong { 111 | cursor: default; 112 | } 113 | 114 | #qunit-tests li a { 115 | padding: 0.5em; 116 | color: #C2CCD1; 117 | text-decoration: none; 118 | } 119 | #qunit-tests li a:hover, 120 | #qunit-tests li a:focus { 121 | color: #000; 122 | } 123 | 124 | #qunit-tests li .runtime { 125 | float: right; 126 | font-size: smaller; 127 | } 128 | 129 | .qunit-assert-list { 130 | margin-top: 0.5em; 131 | padding: 0.5em; 132 | 133 | background-color: #FFF; 134 | 135 | border-radius: 5px; 136 | } 137 | 138 | .qunit-collapsed { 139 | display: none; 140 | } 141 | 142 | #qunit-tests table { 143 | border-collapse: collapse; 144 | margin-top: 0.2em; 145 | } 146 | 147 | #qunit-tests th { 148 | text-align: right; 149 | vertical-align: top; 150 | padding: 0 0.5em 0 0; 151 | } 152 | 153 | #qunit-tests td { 154 | vertical-align: top; 155 | } 156 | 157 | #qunit-tests pre { 158 | margin: 0; 159 | white-space: pre-wrap; 160 | word-wrap: break-word; 161 | } 162 | 163 | #qunit-tests del { 164 | background-color: #E0F2BE; 165 | color: #374E0C; 166 | text-decoration: none; 167 | } 168 | 169 | #qunit-tests ins { 170 | background-color: #FFCACA; 171 | color: #500; 172 | text-decoration: none; 173 | } 174 | 175 | /*** Test Counts */ 176 | 177 | #qunit-tests b.counts { color: #000; } 178 | #qunit-tests b.passed { color: #5E740B; } 179 | #qunit-tests b.failed { color: #710909; } 180 | 181 | #qunit-tests li li { 182 | padding: 5px; 183 | background-color: #FFF; 184 | border-bottom: none; 185 | list-style-position: inside; 186 | } 187 | 188 | /*** Passing Styles */ 189 | 190 | #qunit-tests li li.pass { 191 | color: #3C510C; 192 | background-color: #FFF; 193 | border-left: 10px solid #C6E746; 194 | } 195 | 196 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 197 | #qunit-tests .pass .test-name { color: #366097; } 198 | 199 | #qunit-tests .pass .test-actual, 200 | #qunit-tests .pass .test-expected { color: #999; } 201 | 202 | #qunit-banner.qunit-pass { background-color: #C6E746; } 203 | 204 | /*** Failing Styles */ 205 | 206 | #qunit-tests li li.fail { 207 | color: #710909; 208 | background-color: #FFF; 209 | border-left: 10px solid #EE5757; 210 | white-space: pre; 211 | } 212 | 213 | #qunit-tests > li:last-child { 214 | border-radius: 0 0 5px 5px; 215 | } 216 | 217 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 218 | #qunit-tests .fail .test-name, 219 | #qunit-tests .fail .module-name { color: #000; } 220 | 221 | #qunit-tests .fail .test-actual { color: #EE5757; } 222 | #qunit-tests .fail .test-expected { color: #008000; } 223 | 224 | #qunit-banner.qunit-fail { background-color: #EE5757; } 225 | 226 | /*** Skipped tests */ 227 | 228 | #qunit-tests .skipped { 229 | background-color: #EBECE9; 230 | } 231 | 232 | #qunit-tests .qunit-skipped-label { 233 | background-color: #F4FF77; 234 | display: inline-block; 235 | font-style: normal; 236 | color: #366097; 237 | line-height: 1.8em; 238 | padding: 0 0.5em; 239 | margin: -0.4em 0.4em -0.4em 0; 240 | } 241 | 242 | /** Result */ 243 | 244 | #qunit-testresult { 245 | padding: 0.5em 1em 0.5em 1em; 246 | 247 | color: #2B81AF; 248 | background-color: #D2E0E6; 249 | 250 | border-bottom: 1px solid #FFF; 251 | } 252 | #qunit-testresult .module-name { 253 | font-weight: 700; 254 | } 255 | 256 | /** Fixture */ 257 | 258 | #qunit-fixture { 259 | position: absolute; 260 | top: -10000px; 261 | left: -10000px; 262 | width: 1000px; 263 | height: 1000px; 264 | } 265 | -------------------------------------------------------------------------------- /fileapi/static/libs/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.7.0 2 | // http://underscorejs.org 3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Save bytes in the minified (but not gzipped) version: 18 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 19 | 20 | // Create quick reference variables for speed access to core prototypes. 21 | var 22 | push = ArrayProto.push, 23 | slice = ArrayProto.slice, 24 | concat = ArrayProto.concat, 25 | toString = ObjProto.toString, 26 | hasOwnProperty = ObjProto.hasOwnProperty; 27 | 28 | // All **ECMAScript 5** native function implementations that we hope to use 29 | // are declared here. 30 | var 31 | nativeIsArray = Array.isArray, 32 | nativeKeys = Object.keys, 33 | nativeBind = FuncProto.bind; 34 | 35 | // Create a safe reference to the Underscore object for use below. 36 | var _ = function(obj) { 37 | if (obj instanceof _) return obj; 38 | if (!(this instanceof _)) return new _(obj); 39 | this._wrapped = obj; 40 | }; 41 | 42 | // Export the Underscore object for **Node.js**, with 43 | // backwards-compatibility for the old `require()` API. If we're in 44 | // the browser, add `_` as a global object. 45 | if (typeof exports !== 'undefined') { 46 | if (typeof module !== 'undefined' && module.exports) { 47 | exports = module.exports = _; 48 | } 49 | exports._ = _; 50 | } else { 51 | root._ = _; 52 | } 53 | 54 | // Current version. 55 | _.VERSION = '1.7.0'; 56 | 57 | // Internal function that returns an efficient (for current engines) version 58 | // of the passed-in callback, to be repeatedly applied in other Underscore 59 | // functions. 60 | var createCallback = function(func, context, argCount) { 61 | if (context === void 0) return func; 62 | switch (argCount == null ? 3 : argCount) { 63 | case 1: return function(value) { 64 | return func.call(context, value); 65 | }; 66 | case 2: return function(value, other) { 67 | return func.call(context, value, other); 68 | }; 69 | case 3: return function(value, index, collection) { 70 | return func.call(context, value, index, collection); 71 | }; 72 | case 4: return function(accumulator, value, index, collection) { 73 | return func.call(context, accumulator, value, index, collection); 74 | }; 75 | } 76 | return function() { 77 | return func.apply(context, arguments); 78 | }; 79 | }; 80 | 81 | // A mostly-internal function to generate callbacks that can be applied 82 | // to each element in a collection, returning the desired result — either 83 | // identity, an arbitrary callback, a property matcher, or a property accessor. 84 | _.iteratee = function(value, context, argCount) { 85 | if (value == null) return _.identity; 86 | if (_.isFunction(value)) return createCallback(value, context, argCount); 87 | if (_.isObject(value)) return _.matches(value); 88 | return _.property(value); 89 | }; 90 | 91 | // Collection Functions 92 | // -------------------- 93 | 94 | // The cornerstone, an `each` implementation, aka `forEach`. 95 | // Handles raw objects in addition to array-likes. Treats all 96 | // sparse array-likes as if they were dense. 97 | _.each = _.forEach = function(obj, iteratee, context) { 98 | if (obj == null) return obj; 99 | iteratee = createCallback(iteratee, context); 100 | var i, length = obj.length; 101 | if (length === +length) { 102 | for (i = 0; i < length; i++) { 103 | iteratee(obj[i], i, obj); 104 | } 105 | } else { 106 | var keys = _.keys(obj); 107 | for (i = 0, length = keys.length; i < length; i++) { 108 | iteratee(obj[keys[i]], keys[i], obj); 109 | } 110 | } 111 | return obj; 112 | }; 113 | 114 | // Return the results of applying the iteratee to each element. 115 | _.map = _.collect = function(obj, iteratee, context) { 116 | if (obj == null) return []; 117 | iteratee = _.iteratee(iteratee, context); 118 | var keys = obj.length !== +obj.length && _.keys(obj), 119 | length = (keys || obj).length, 120 | results = Array(length), 121 | currentKey; 122 | for (var index = 0; index < length; index++) { 123 | currentKey = keys ? keys[index] : index; 124 | results[index] = iteratee(obj[currentKey], currentKey, obj); 125 | } 126 | return results; 127 | }; 128 | 129 | var reduceError = 'Reduce of empty array with no initial value'; 130 | 131 | // **Reduce** builds up a single result from a list of values, aka `inject`, 132 | // or `foldl`. 133 | _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { 134 | if (obj == null) obj = []; 135 | iteratee = createCallback(iteratee, context, 4); 136 | var keys = obj.length !== +obj.length && _.keys(obj), 137 | length = (keys || obj).length, 138 | index = 0, currentKey; 139 | if (arguments.length < 3) { 140 | if (!length) throw new TypeError(reduceError); 141 | memo = obj[keys ? keys[index++] : index++]; 142 | } 143 | for (; index < length; index++) { 144 | currentKey = keys ? keys[index] : index; 145 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 146 | } 147 | return memo; 148 | }; 149 | 150 | // The right-associative version of reduce, also known as `foldr`. 151 | _.reduceRight = _.foldr = function(obj, iteratee, memo, context) { 152 | if (obj == null) obj = []; 153 | iteratee = createCallback(iteratee, context, 4); 154 | var keys = obj.length !== + obj.length && _.keys(obj), 155 | index = (keys || obj).length, 156 | currentKey; 157 | if (arguments.length < 3) { 158 | if (!index) throw new TypeError(reduceError); 159 | memo = obj[keys ? keys[--index] : --index]; 160 | } 161 | while (index--) { 162 | currentKey = keys ? keys[index] : index; 163 | memo = iteratee(memo, obj[currentKey], currentKey, obj); 164 | } 165 | return memo; 166 | }; 167 | 168 | // Return the first value which passes a truth test. Aliased as `detect`. 169 | _.find = _.detect = function(obj, predicate, context) { 170 | var result; 171 | predicate = _.iteratee(predicate, context); 172 | _.some(obj, function(value, index, list) { 173 | if (predicate(value, index, list)) { 174 | result = value; 175 | return true; 176 | } 177 | }); 178 | return result; 179 | }; 180 | 181 | // Return all the elements that pass a truth test. 182 | // Aliased as `select`. 183 | _.filter = _.select = function(obj, predicate, context) { 184 | var results = []; 185 | if (obj == null) return results; 186 | predicate = _.iteratee(predicate, context); 187 | _.each(obj, function(value, index, list) { 188 | if (predicate(value, index, list)) results.push(value); 189 | }); 190 | return results; 191 | }; 192 | 193 | // Return all the elements for which a truth test fails. 194 | _.reject = function(obj, predicate, context) { 195 | return _.filter(obj, _.negate(_.iteratee(predicate)), context); 196 | }; 197 | 198 | // Determine whether all of the elements match a truth test. 199 | // Aliased as `all`. 200 | _.every = _.all = function(obj, predicate, context) { 201 | if (obj == null) return true; 202 | predicate = _.iteratee(predicate, context); 203 | var keys = obj.length !== +obj.length && _.keys(obj), 204 | length = (keys || obj).length, 205 | index, currentKey; 206 | for (index = 0; index < length; index++) { 207 | currentKey = keys ? keys[index] : index; 208 | if (!predicate(obj[currentKey], currentKey, obj)) return false; 209 | } 210 | return true; 211 | }; 212 | 213 | // Determine if at least one element in the object matches a truth test. 214 | // Aliased as `any`. 215 | _.some = _.any = function(obj, predicate, context) { 216 | if (obj == null) return false; 217 | predicate = _.iteratee(predicate, context); 218 | var keys = obj.length !== +obj.length && _.keys(obj), 219 | length = (keys || obj).length, 220 | index, currentKey; 221 | for (index = 0; index < length; index++) { 222 | currentKey = keys ? keys[index] : index; 223 | if (predicate(obj[currentKey], currentKey, obj)) return true; 224 | } 225 | return false; 226 | }; 227 | 228 | // Determine if the array or object contains a given value (using `===`). 229 | // Aliased as `include`. 230 | _.contains = _.include = function(obj, target) { 231 | if (obj == null) return false; 232 | if (obj.length !== +obj.length) obj = _.values(obj); 233 | return _.indexOf(obj, target) >= 0; 234 | }; 235 | 236 | // Invoke a method (with arguments) on every item in a collection. 237 | _.invoke = function(obj, method) { 238 | var args = slice.call(arguments, 2); 239 | var isFunc = _.isFunction(method); 240 | return _.map(obj, function(value) { 241 | return (isFunc ? method : value[method]).apply(value, args); 242 | }); 243 | }; 244 | 245 | // Convenience version of a common use case of `map`: fetching a property. 246 | _.pluck = function(obj, key) { 247 | return _.map(obj, _.property(key)); 248 | }; 249 | 250 | // Convenience version of a common use case of `filter`: selecting only objects 251 | // containing specific `key:value` pairs. 252 | _.where = function(obj, attrs) { 253 | return _.filter(obj, _.matches(attrs)); 254 | }; 255 | 256 | // Convenience version of a common use case of `find`: getting the first object 257 | // containing specific `key:value` pairs. 258 | _.findWhere = function(obj, attrs) { 259 | return _.find(obj, _.matches(attrs)); 260 | }; 261 | 262 | // Return the maximum element (or element-based computation). 263 | _.max = function(obj, iteratee, context) { 264 | var result = -Infinity, lastComputed = -Infinity, 265 | value, computed; 266 | if (iteratee == null && obj != null) { 267 | obj = obj.length === +obj.length ? obj : _.values(obj); 268 | for (var i = 0, length = obj.length; i < length; i++) { 269 | value = obj[i]; 270 | if (value > result) { 271 | result = value; 272 | } 273 | } 274 | } else { 275 | iteratee = _.iteratee(iteratee, context); 276 | _.each(obj, function(value, index, list) { 277 | computed = iteratee(value, index, list); 278 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) { 279 | result = value; 280 | lastComputed = computed; 281 | } 282 | }); 283 | } 284 | return result; 285 | }; 286 | 287 | // Return the minimum element (or element-based computation). 288 | _.min = function(obj, iteratee, context) { 289 | var result = Infinity, lastComputed = Infinity, 290 | value, computed; 291 | if (iteratee == null && obj != null) { 292 | obj = obj.length === +obj.length ? obj : _.values(obj); 293 | for (var i = 0, length = obj.length; i < length; i++) { 294 | value = obj[i]; 295 | if (value < result) { 296 | result = value; 297 | } 298 | } 299 | } else { 300 | iteratee = _.iteratee(iteratee, context); 301 | _.each(obj, function(value, index, list) { 302 | computed = iteratee(value, index, list); 303 | if (computed < lastComputed || computed === Infinity && result === Infinity) { 304 | result = value; 305 | lastComputed = computed; 306 | } 307 | }); 308 | } 309 | return result; 310 | }; 311 | 312 | // Shuffle a collection, using the modern version of the 313 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 314 | _.shuffle = function(obj) { 315 | var set = obj && obj.length === +obj.length ? obj : _.values(obj); 316 | var length = set.length; 317 | var shuffled = Array(length); 318 | for (var index = 0, rand; index < length; index++) { 319 | rand = _.random(0, index); 320 | if (rand !== index) shuffled[index] = shuffled[rand]; 321 | shuffled[rand] = set[index]; 322 | } 323 | return shuffled; 324 | }; 325 | 326 | // Sample **n** random values from a collection. 327 | // If **n** is not specified, returns a single random element. 328 | // The internal `guard` argument allows it to work with `map`. 329 | _.sample = function(obj, n, guard) { 330 | if (n == null || guard) { 331 | if (obj.length !== +obj.length) obj = _.values(obj); 332 | return obj[_.random(obj.length - 1)]; 333 | } 334 | return _.shuffle(obj).slice(0, Math.max(0, n)); 335 | }; 336 | 337 | // Sort the object's values by a criterion produced by an iteratee. 338 | _.sortBy = function(obj, iteratee, context) { 339 | iteratee = _.iteratee(iteratee, context); 340 | return _.pluck(_.map(obj, function(value, index, list) { 341 | return { 342 | value: value, 343 | index: index, 344 | criteria: iteratee(value, index, list) 345 | }; 346 | }).sort(function(left, right) { 347 | var a = left.criteria; 348 | var b = right.criteria; 349 | if (a !== b) { 350 | if (a > b || a === void 0) return 1; 351 | if (a < b || b === void 0) return -1; 352 | } 353 | return left.index - right.index; 354 | }), 'value'); 355 | }; 356 | 357 | // An internal function used for aggregate "group by" operations. 358 | var group = function(behavior) { 359 | return function(obj, iteratee, context) { 360 | var result = {}; 361 | iteratee = _.iteratee(iteratee, context); 362 | _.each(obj, function(value, index) { 363 | var key = iteratee(value, index, obj); 364 | behavior(result, value, key); 365 | }); 366 | return result; 367 | }; 368 | }; 369 | 370 | // Groups the object's values by a criterion. Pass either a string attribute 371 | // to group by, or a function that returns the criterion. 372 | _.groupBy = group(function(result, value, key) { 373 | if (_.has(result, key)) result[key].push(value); else result[key] = [value]; 374 | }); 375 | 376 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 377 | // when you know that your index values will be unique. 378 | _.indexBy = group(function(result, value, key) { 379 | result[key] = value; 380 | }); 381 | 382 | // Counts instances of an object that group by a certain criterion. Pass 383 | // either a string attribute to count by, or a function that returns the 384 | // criterion. 385 | _.countBy = group(function(result, value, key) { 386 | if (_.has(result, key)) result[key]++; else result[key] = 1; 387 | }); 388 | 389 | // Use a comparator function to figure out the smallest index at which 390 | // an object should be inserted so as to maintain order. Uses binary search. 391 | _.sortedIndex = function(array, obj, iteratee, context) { 392 | iteratee = _.iteratee(iteratee, context, 1); 393 | var value = iteratee(obj); 394 | var low = 0, high = array.length; 395 | while (low < high) { 396 | var mid = low + high >>> 1; 397 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; 398 | } 399 | return low; 400 | }; 401 | 402 | // Safely create a real, live array from anything iterable. 403 | _.toArray = function(obj) { 404 | if (!obj) return []; 405 | if (_.isArray(obj)) return slice.call(obj); 406 | if (obj.length === +obj.length) return _.map(obj, _.identity); 407 | return _.values(obj); 408 | }; 409 | 410 | // Return the number of elements in an object. 411 | _.size = function(obj) { 412 | if (obj == null) return 0; 413 | return obj.length === +obj.length ? obj.length : _.keys(obj).length; 414 | }; 415 | 416 | // Split a collection into two arrays: one whose elements all satisfy the given 417 | // predicate, and one whose elements all do not satisfy the predicate. 418 | _.partition = function(obj, predicate, context) { 419 | predicate = _.iteratee(predicate, context); 420 | var pass = [], fail = []; 421 | _.each(obj, function(value, key, obj) { 422 | (predicate(value, key, obj) ? pass : fail).push(value); 423 | }); 424 | return [pass, fail]; 425 | }; 426 | 427 | // Array Functions 428 | // --------------- 429 | 430 | // Get the first element of an array. Passing **n** will return the first N 431 | // values in the array. Aliased as `head` and `take`. The **guard** check 432 | // allows it to work with `_.map`. 433 | _.first = _.head = _.take = function(array, n, guard) { 434 | if (array == null) return void 0; 435 | if (n == null || guard) return array[0]; 436 | if (n < 0) return []; 437 | return slice.call(array, 0, n); 438 | }; 439 | 440 | // Returns everything but the last entry of the array. Especially useful on 441 | // the arguments object. Passing **n** will return all the values in 442 | // the array, excluding the last N. The **guard** check allows it to work with 443 | // `_.map`. 444 | _.initial = function(array, n, guard) { 445 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); 446 | }; 447 | 448 | // Get the last element of an array. Passing **n** will return the last N 449 | // values in the array. The **guard** check allows it to work with `_.map`. 450 | _.last = function(array, n, guard) { 451 | if (array == null) return void 0; 452 | if (n == null || guard) return array[array.length - 1]; 453 | return slice.call(array, Math.max(array.length - n, 0)); 454 | }; 455 | 456 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 457 | // Especially useful on the arguments object. Passing an **n** will return 458 | // the rest N values in the array. The **guard** 459 | // check allows it to work with `_.map`. 460 | _.rest = _.tail = _.drop = function(array, n, guard) { 461 | return slice.call(array, n == null || guard ? 1 : n); 462 | }; 463 | 464 | // Trim out all falsy values from an array. 465 | _.compact = function(array) { 466 | return _.filter(array, _.identity); 467 | }; 468 | 469 | // Internal implementation of a recursive `flatten` function. 470 | var flatten = function(input, shallow, strict, output) { 471 | if (shallow && _.every(input, _.isArray)) { 472 | return concat.apply(output, input); 473 | } 474 | for (var i = 0, length = input.length; i < length; i++) { 475 | var value = input[i]; 476 | if (!_.isArray(value) && !_.isArguments(value)) { 477 | if (!strict) output.push(value); 478 | } else if (shallow) { 479 | push.apply(output, value); 480 | } else { 481 | flatten(value, shallow, strict, output); 482 | } 483 | } 484 | return output; 485 | }; 486 | 487 | // Flatten out an array, either recursively (by default), or just one level. 488 | _.flatten = function(array, shallow) { 489 | return flatten(array, shallow, false, []); 490 | }; 491 | 492 | // Return a version of the array that does not contain the specified value(s). 493 | _.without = function(array) { 494 | return _.difference(array, slice.call(arguments, 1)); 495 | }; 496 | 497 | // Produce a duplicate-free version of the array. If the array has already 498 | // been sorted, you have the option of using a faster algorithm. 499 | // Aliased as `unique`. 500 | _.uniq = _.unique = function(array, isSorted, iteratee, context) { 501 | if (array == null) return []; 502 | if (!_.isBoolean(isSorted)) { 503 | context = iteratee; 504 | iteratee = isSorted; 505 | isSorted = false; 506 | } 507 | if (iteratee != null) iteratee = _.iteratee(iteratee, context); 508 | var result = []; 509 | var seen = []; 510 | for (var i = 0, length = array.length; i < length; i++) { 511 | var value = array[i]; 512 | if (isSorted) { 513 | if (!i || seen !== value) result.push(value); 514 | seen = value; 515 | } else if (iteratee) { 516 | var computed = iteratee(value, i, array); 517 | if (_.indexOf(seen, computed) < 0) { 518 | seen.push(computed); 519 | result.push(value); 520 | } 521 | } else if (_.indexOf(result, value) < 0) { 522 | result.push(value); 523 | } 524 | } 525 | return result; 526 | }; 527 | 528 | // Produce an array that contains the union: each distinct element from all of 529 | // the passed-in arrays. 530 | _.union = function() { 531 | return _.uniq(flatten(arguments, true, true, [])); 532 | }; 533 | 534 | // Produce an array that contains every item shared between all the 535 | // passed-in arrays. 536 | _.intersection = function(array) { 537 | if (array == null) return []; 538 | var result = []; 539 | var argsLength = arguments.length; 540 | for (var i = 0, length = array.length; i < length; i++) { 541 | var item = array[i]; 542 | if (_.contains(result, item)) continue; 543 | for (var j = 1; j < argsLength; j++) { 544 | if (!_.contains(arguments[j], item)) break; 545 | } 546 | if (j === argsLength) result.push(item); 547 | } 548 | return result; 549 | }; 550 | 551 | // Take the difference between one array and a number of other arrays. 552 | // Only the elements present in just the first array will remain. 553 | _.difference = function(array) { 554 | var rest = flatten(slice.call(arguments, 1), true, true, []); 555 | return _.filter(array, function(value){ 556 | return !_.contains(rest, value); 557 | }); 558 | }; 559 | 560 | // Zip together multiple lists into a single array -- elements that share 561 | // an index go together. 562 | _.zip = function(array) { 563 | if (array == null) return []; 564 | var length = _.max(arguments, 'length').length; 565 | var results = Array(length); 566 | for (var i = 0; i < length; i++) { 567 | results[i] = _.pluck(arguments, i); 568 | } 569 | return results; 570 | }; 571 | 572 | // Converts lists into objects. Pass either a single array of `[key, value]` 573 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 574 | // the corresponding values. 575 | _.object = function(list, values) { 576 | if (list == null) return {}; 577 | var result = {}; 578 | for (var i = 0, length = list.length; i < length; i++) { 579 | if (values) { 580 | result[list[i]] = values[i]; 581 | } else { 582 | result[list[i][0]] = list[i][1]; 583 | } 584 | } 585 | return result; 586 | }; 587 | 588 | // Return the position of the first occurrence of an item in an array, 589 | // or -1 if the item is not included in the array. 590 | // If the array is large and already in sort order, pass `true` 591 | // for **isSorted** to use binary search. 592 | _.indexOf = function(array, item, isSorted) { 593 | if (array == null) return -1; 594 | var i = 0, length = array.length; 595 | if (isSorted) { 596 | if (typeof isSorted == 'number') { 597 | i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; 598 | } else { 599 | i = _.sortedIndex(array, item); 600 | return array[i] === item ? i : -1; 601 | } 602 | } 603 | for (; i < length; i++) if (array[i] === item) return i; 604 | return -1; 605 | }; 606 | 607 | _.lastIndexOf = function(array, item, from) { 608 | if (array == null) return -1; 609 | var idx = array.length; 610 | if (typeof from == 'number') { 611 | idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); 612 | } 613 | while (--idx >= 0) if (array[idx] === item) return idx; 614 | return -1; 615 | }; 616 | 617 | // Generate an integer Array containing an arithmetic progression. A port of 618 | // the native Python `range()` function. See 619 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 620 | _.range = function(start, stop, step) { 621 | if (arguments.length <= 1) { 622 | stop = start || 0; 623 | start = 0; 624 | } 625 | step = step || 1; 626 | 627 | var length = Math.max(Math.ceil((stop - start) / step), 0); 628 | var range = Array(length); 629 | 630 | for (var idx = 0; idx < length; idx++, start += step) { 631 | range[idx] = start; 632 | } 633 | 634 | return range; 635 | }; 636 | 637 | // Function (ahem) Functions 638 | // ------------------ 639 | 640 | // Reusable constructor function for prototype setting. 641 | var Ctor = function(){}; 642 | 643 | // Create a function bound to a given object (assigning `this`, and arguments, 644 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 645 | // available. 646 | _.bind = function(func, context) { 647 | var args, bound; 648 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 649 | if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); 650 | args = slice.call(arguments, 2); 651 | bound = function() { 652 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 653 | Ctor.prototype = func.prototype; 654 | var self = new Ctor; 655 | Ctor.prototype = null; 656 | var result = func.apply(self, args.concat(slice.call(arguments))); 657 | if (_.isObject(result)) return result; 658 | return self; 659 | }; 660 | return bound; 661 | }; 662 | 663 | // Partially apply a function by creating a version that has had some of its 664 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 665 | // as a placeholder, allowing any combination of arguments to be pre-filled. 666 | _.partial = function(func) { 667 | var boundArgs = slice.call(arguments, 1); 668 | return function() { 669 | var position = 0; 670 | var args = boundArgs.slice(); 671 | for (var i = 0, length = args.length; i < length; i++) { 672 | if (args[i] === _) args[i] = arguments[position++]; 673 | } 674 | while (position < arguments.length) args.push(arguments[position++]); 675 | return func.apply(this, args); 676 | }; 677 | }; 678 | 679 | // Bind a number of an object's methods to that object. Remaining arguments 680 | // are the method names to be bound. Useful for ensuring that all callbacks 681 | // defined on an object belong to it. 682 | _.bindAll = function(obj) { 683 | var i, length = arguments.length, key; 684 | if (length <= 1) throw new Error('bindAll must be passed function names'); 685 | for (i = 1; i < length; i++) { 686 | key = arguments[i]; 687 | obj[key] = _.bind(obj[key], obj); 688 | } 689 | return obj; 690 | }; 691 | 692 | // Memoize an expensive function by storing its results. 693 | _.memoize = function(func, hasher) { 694 | var memoize = function(key) { 695 | var cache = memoize.cache; 696 | var address = hasher ? hasher.apply(this, arguments) : key; 697 | if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 698 | return cache[address]; 699 | }; 700 | memoize.cache = {}; 701 | return memoize; 702 | }; 703 | 704 | // Delays a function for the given number of milliseconds, and then calls 705 | // it with the arguments supplied. 706 | _.delay = function(func, wait) { 707 | var args = slice.call(arguments, 2); 708 | return setTimeout(function(){ 709 | return func.apply(null, args); 710 | }, wait); 711 | }; 712 | 713 | // Defers a function, scheduling it to run after the current call stack has 714 | // cleared. 715 | _.defer = function(func) { 716 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 717 | }; 718 | 719 | // Returns a function, that, when invoked, will only be triggered at most once 720 | // during a given window of time. Normally, the throttled function will run 721 | // as much as it can, without ever going more than once per `wait` duration; 722 | // but if you'd like to disable the execution on the leading edge, pass 723 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 724 | _.throttle = function(func, wait, options) { 725 | var context, args, result; 726 | var timeout = null; 727 | var previous = 0; 728 | if (!options) options = {}; 729 | var later = function() { 730 | previous = options.leading === false ? 0 : _.now(); 731 | timeout = null; 732 | result = func.apply(context, args); 733 | if (!timeout) context = args = null; 734 | }; 735 | return function() { 736 | var now = _.now(); 737 | if (!previous && options.leading === false) previous = now; 738 | var remaining = wait - (now - previous); 739 | context = this; 740 | args = arguments; 741 | if (remaining <= 0 || remaining > wait) { 742 | clearTimeout(timeout); 743 | timeout = null; 744 | previous = now; 745 | result = func.apply(context, args); 746 | if (!timeout) context = args = null; 747 | } else if (!timeout && options.trailing !== false) { 748 | timeout = setTimeout(later, remaining); 749 | } 750 | return result; 751 | }; 752 | }; 753 | 754 | // Returns a function, that, as long as it continues to be invoked, will not 755 | // be triggered. The function will be called after it stops being called for 756 | // N milliseconds. If `immediate` is passed, trigger the function on the 757 | // leading edge, instead of the trailing. 758 | _.debounce = function(func, wait, immediate) { 759 | var timeout, args, context, timestamp, result; 760 | 761 | var later = function() { 762 | var last = _.now() - timestamp; 763 | 764 | if (last < wait && last > 0) { 765 | timeout = setTimeout(later, wait - last); 766 | } else { 767 | timeout = null; 768 | if (!immediate) { 769 | result = func.apply(context, args); 770 | if (!timeout) context = args = null; 771 | } 772 | } 773 | }; 774 | 775 | return function() { 776 | context = this; 777 | args = arguments; 778 | timestamp = _.now(); 779 | var callNow = immediate && !timeout; 780 | if (!timeout) timeout = setTimeout(later, wait); 781 | if (callNow) { 782 | result = func.apply(context, args); 783 | context = args = null; 784 | } 785 | 786 | return result; 787 | }; 788 | }; 789 | 790 | // Returns the first function passed as an argument to the second, 791 | // allowing you to adjust arguments, run code before and after, and 792 | // conditionally execute the original function. 793 | _.wrap = function(func, wrapper) { 794 | return _.partial(wrapper, func); 795 | }; 796 | 797 | // Returns a negated version of the passed-in predicate. 798 | _.negate = function(predicate) { 799 | return function() { 800 | return !predicate.apply(this, arguments); 801 | }; 802 | }; 803 | 804 | // Returns a function that is the composition of a list of functions, each 805 | // consuming the return value of the function that follows. 806 | _.compose = function() { 807 | var args = arguments; 808 | var start = args.length - 1; 809 | return function() { 810 | var i = start; 811 | var result = args[start].apply(this, arguments); 812 | while (i--) result = args[i].call(this, result); 813 | return result; 814 | }; 815 | }; 816 | 817 | // Returns a function that will only be executed after being called N times. 818 | _.after = function(times, func) { 819 | return function() { 820 | if (--times < 1) { 821 | return func.apply(this, arguments); 822 | } 823 | }; 824 | }; 825 | 826 | // Returns a function that will only be executed before being called N times. 827 | _.before = function(times, func) { 828 | var memo; 829 | return function() { 830 | if (--times > 0) { 831 | memo = func.apply(this, arguments); 832 | } else { 833 | func = null; 834 | } 835 | return memo; 836 | }; 837 | }; 838 | 839 | // Returns a function that will be executed at most one time, no matter how 840 | // often you call it. Useful for lazy initialization. 841 | _.once = _.partial(_.before, 2); 842 | 843 | // Object Functions 844 | // ---------------- 845 | 846 | // Retrieve the names of an object's properties. 847 | // Delegates to **ECMAScript 5**'s native `Object.keys` 848 | _.keys = function(obj) { 849 | if (!_.isObject(obj)) return []; 850 | if (nativeKeys) return nativeKeys(obj); 851 | var keys = []; 852 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 853 | return keys; 854 | }; 855 | 856 | // Retrieve the values of an object's properties. 857 | _.values = function(obj) { 858 | var keys = _.keys(obj); 859 | var length = keys.length; 860 | var values = Array(length); 861 | for (var i = 0; i < length; i++) { 862 | values[i] = obj[keys[i]]; 863 | } 864 | return values; 865 | }; 866 | 867 | // Convert an object into a list of `[key, value]` pairs. 868 | _.pairs = function(obj) { 869 | var keys = _.keys(obj); 870 | var length = keys.length; 871 | var pairs = Array(length); 872 | for (var i = 0; i < length; i++) { 873 | pairs[i] = [keys[i], obj[keys[i]]]; 874 | } 875 | return pairs; 876 | }; 877 | 878 | // Invert the keys and values of an object. The values must be serializable. 879 | _.invert = function(obj) { 880 | var result = {}; 881 | var keys = _.keys(obj); 882 | for (var i = 0, length = keys.length; i < length; i++) { 883 | result[obj[keys[i]]] = keys[i]; 884 | } 885 | return result; 886 | }; 887 | 888 | // Return a sorted list of the function names available on the object. 889 | // Aliased as `methods` 890 | _.functions = _.methods = function(obj) { 891 | var names = []; 892 | for (var key in obj) { 893 | if (_.isFunction(obj[key])) names.push(key); 894 | } 895 | return names.sort(); 896 | }; 897 | 898 | // Extend a given object with all the properties in passed-in object(s). 899 | _.extend = function(obj) { 900 | if (!_.isObject(obj)) return obj; 901 | var source, prop; 902 | for (var i = 1, length = arguments.length; i < length; i++) { 903 | source = arguments[i]; 904 | for (prop in source) { 905 | if (hasOwnProperty.call(source, prop)) { 906 | obj[prop] = source[prop]; 907 | } 908 | } 909 | } 910 | return obj; 911 | }; 912 | 913 | // Return a copy of the object only containing the whitelisted properties. 914 | _.pick = function(obj, iteratee, context) { 915 | var result = {}, key; 916 | if (obj == null) return result; 917 | if (_.isFunction(iteratee)) { 918 | iteratee = createCallback(iteratee, context); 919 | for (key in obj) { 920 | var value = obj[key]; 921 | if (iteratee(value, key, obj)) result[key] = value; 922 | } 923 | } else { 924 | var keys = concat.apply([], slice.call(arguments, 1)); 925 | obj = new Object(obj); 926 | for (var i = 0, length = keys.length; i < length; i++) { 927 | key = keys[i]; 928 | if (key in obj) result[key] = obj[key]; 929 | } 930 | } 931 | return result; 932 | }; 933 | 934 | // Return a copy of the object without the blacklisted properties. 935 | _.omit = function(obj, iteratee, context) { 936 | if (_.isFunction(iteratee)) { 937 | iteratee = _.negate(iteratee); 938 | } else { 939 | var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); 940 | iteratee = function(value, key) { 941 | return !_.contains(keys, key); 942 | }; 943 | } 944 | return _.pick(obj, iteratee, context); 945 | }; 946 | 947 | // Fill in a given object with default properties. 948 | _.defaults = function(obj) { 949 | if (!_.isObject(obj)) return obj; 950 | for (var i = 1, length = arguments.length; i < length; i++) { 951 | var source = arguments[i]; 952 | for (var prop in source) { 953 | if (obj[prop] === void 0) obj[prop] = source[prop]; 954 | } 955 | } 956 | return obj; 957 | }; 958 | 959 | // Create a (shallow-cloned) duplicate of an object. 960 | _.clone = function(obj) { 961 | if (!_.isObject(obj)) return obj; 962 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 963 | }; 964 | 965 | // Invokes interceptor with the obj, and then returns obj. 966 | // The primary purpose of this method is to "tap into" a method chain, in 967 | // order to perform operations on intermediate results within the chain. 968 | _.tap = function(obj, interceptor) { 969 | interceptor(obj); 970 | return obj; 971 | }; 972 | 973 | // Internal recursive comparison function for `isEqual`. 974 | var eq = function(a, b, aStack, bStack) { 975 | // Identical objects are equal. `0 === -0`, but they aren't identical. 976 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 977 | if (a === b) return a !== 0 || 1 / a === 1 / b; 978 | // A strict comparison is necessary because `null == undefined`. 979 | if (a == null || b == null) return a === b; 980 | // Unwrap any wrapped objects. 981 | if (a instanceof _) a = a._wrapped; 982 | if (b instanceof _) b = b._wrapped; 983 | // Compare `[[Class]]` names. 984 | var className = toString.call(a); 985 | if (className !== toString.call(b)) return false; 986 | switch (className) { 987 | // Strings, numbers, regular expressions, dates, and booleans are compared by value. 988 | case '[object RegExp]': 989 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') 990 | case '[object String]': 991 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 992 | // equivalent to `new String("5")`. 993 | return '' + a === '' + b; 994 | case '[object Number]': 995 | // `NaN`s are equivalent, but non-reflexive. 996 | // Object(NaN) is equivalent to NaN 997 | if (+a !== +a) return +b !== +b; 998 | // An `egal` comparison is performed for other numeric values. 999 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 1000 | case '[object Date]': 1001 | case '[object Boolean]': 1002 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 1003 | // millisecond representations. Note that invalid dates with millisecond representations 1004 | // of `NaN` are not equivalent. 1005 | return +a === +b; 1006 | } 1007 | if (typeof a != 'object' || typeof b != 'object') return false; 1008 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 1009 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 1010 | var length = aStack.length; 1011 | while (length--) { 1012 | // Linear search. Performance is inversely proportional to the number of 1013 | // unique nested structures. 1014 | if (aStack[length] === a) return bStack[length] === b; 1015 | } 1016 | // Objects with different constructors are not equivalent, but `Object`s 1017 | // from different frames are. 1018 | var aCtor = a.constructor, bCtor = b.constructor; 1019 | if ( 1020 | aCtor !== bCtor && 1021 | // Handle Object.create(x) cases 1022 | 'constructor' in a && 'constructor' in b && 1023 | !(_.isFunction(aCtor) && aCtor instanceof aCtor && 1024 | _.isFunction(bCtor) && bCtor instanceof bCtor) 1025 | ) { 1026 | return false; 1027 | } 1028 | // Add the first object to the stack of traversed objects. 1029 | aStack.push(a); 1030 | bStack.push(b); 1031 | var size, result; 1032 | // Recursively compare objects and arrays. 1033 | if (className === '[object Array]') { 1034 | // Compare array lengths to determine if a deep comparison is necessary. 1035 | size = a.length; 1036 | result = size === b.length; 1037 | if (result) { 1038 | // Deep compare the contents, ignoring non-numeric properties. 1039 | while (size--) { 1040 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 1041 | } 1042 | } 1043 | } else { 1044 | // Deep compare objects. 1045 | var keys = _.keys(a), key; 1046 | size = keys.length; 1047 | // Ensure that both objects contain the same number of properties before comparing deep equality. 1048 | result = _.keys(b).length === size; 1049 | if (result) { 1050 | while (size--) { 1051 | // Deep compare each member 1052 | key = keys[size]; 1053 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 1054 | } 1055 | } 1056 | } 1057 | // Remove the first object from the stack of traversed objects. 1058 | aStack.pop(); 1059 | bStack.pop(); 1060 | return result; 1061 | }; 1062 | 1063 | // Perform a deep comparison to check if two objects are equal. 1064 | _.isEqual = function(a, b) { 1065 | return eq(a, b, [], []); 1066 | }; 1067 | 1068 | // Is a given array, string, or object empty? 1069 | // An "empty" object has no enumerable own-properties. 1070 | _.isEmpty = function(obj) { 1071 | if (obj == null) return true; 1072 | if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; 1073 | for (var key in obj) if (_.has(obj, key)) return false; 1074 | return true; 1075 | }; 1076 | 1077 | // Is a given value a DOM element? 1078 | _.isElement = function(obj) { 1079 | return !!(obj && obj.nodeType === 1); 1080 | }; 1081 | 1082 | // Is a given value an array? 1083 | // Delegates to ECMA5's native Array.isArray 1084 | _.isArray = nativeIsArray || function(obj) { 1085 | return toString.call(obj) === '[object Array]'; 1086 | }; 1087 | 1088 | // Is a given variable an object? 1089 | _.isObject = function(obj) { 1090 | var type = typeof obj; 1091 | return type === 'function' || type === 'object' && !!obj; 1092 | }; 1093 | 1094 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 1095 | _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 1096 | _['is' + name] = function(obj) { 1097 | return toString.call(obj) === '[object ' + name + ']'; 1098 | }; 1099 | }); 1100 | 1101 | // Define a fallback version of the method in browsers (ahem, IE), where 1102 | // there isn't any inspectable "Arguments" type. 1103 | if (!_.isArguments(arguments)) { 1104 | _.isArguments = function(obj) { 1105 | return _.has(obj, 'callee'); 1106 | }; 1107 | } 1108 | 1109 | // Optimize `isFunction` if appropriate. Work around an IE 11 bug. 1110 | if (typeof /./ !== 'function') { 1111 | _.isFunction = function(obj) { 1112 | return typeof obj == 'function' || false; 1113 | }; 1114 | } 1115 | 1116 | // Is a given object a finite number? 1117 | _.isFinite = function(obj) { 1118 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1119 | }; 1120 | 1121 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1122 | _.isNaN = function(obj) { 1123 | return _.isNumber(obj) && obj !== +obj; 1124 | }; 1125 | 1126 | // Is a given value a boolean? 1127 | _.isBoolean = function(obj) { 1128 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; 1129 | }; 1130 | 1131 | // Is a given value equal to null? 1132 | _.isNull = function(obj) { 1133 | return obj === null; 1134 | }; 1135 | 1136 | // Is a given variable undefined? 1137 | _.isUndefined = function(obj) { 1138 | return obj === void 0; 1139 | }; 1140 | 1141 | // Shortcut function for checking if an object has a given property directly 1142 | // on itself (in other words, not on a prototype). 1143 | _.has = function(obj, key) { 1144 | return obj != null && hasOwnProperty.call(obj, key); 1145 | }; 1146 | 1147 | // Utility Functions 1148 | // ----------------- 1149 | 1150 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1151 | // previous owner. Returns a reference to the Underscore object. 1152 | _.noConflict = function() { 1153 | root._ = previousUnderscore; 1154 | return this; 1155 | }; 1156 | 1157 | // Keep the identity function around for default iteratees. 1158 | _.identity = function(value) { 1159 | return value; 1160 | }; 1161 | 1162 | // Predicate-generating functions. Often useful outside of Underscore. 1163 | _.constant = function(value) { 1164 | return function() { 1165 | return value; 1166 | }; 1167 | }; 1168 | 1169 | _.noop = function(){}; 1170 | 1171 | _.property = function(key) { 1172 | return function(obj) { 1173 | return obj[key]; 1174 | }; 1175 | }; 1176 | 1177 | // Returns a predicate for checking whether an object has a given set of `key:value` pairs. 1178 | _.matches = function(attrs) { 1179 | var pairs = _.pairs(attrs), length = pairs.length; 1180 | return function(obj) { 1181 | if (obj == null) return !length; 1182 | obj = new Object(obj); 1183 | for (var i = 0; i < length; i++) { 1184 | var pair = pairs[i], key = pair[0]; 1185 | if (pair[1] !== obj[key] || !(key in obj)) return false; 1186 | } 1187 | return true; 1188 | }; 1189 | }; 1190 | 1191 | // Run a function **n** times. 1192 | _.times = function(n, iteratee, context) { 1193 | var accum = Array(Math.max(0, n)); 1194 | iteratee = createCallback(iteratee, context, 1); 1195 | for (var i = 0; i < n; i++) accum[i] = iteratee(i); 1196 | return accum; 1197 | }; 1198 | 1199 | // Return a random integer between min and max (inclusive). 1200 | _.random = function(min, max) { 1201 | if (max == null) { 1202 | max = min; 1203 | min = 0; 1204 | } 1205 | return min + Math.floor(Math.random() * (max - min + 1)); 1206 | }; 1207 | 1208 | // A (possibly faster) way to get the current timestamp as an integer. 1209 | _.now = Date.now || function() { 1210 | return new Date().getTime(); 1211 | }; 1212 | 1213 | // List of HTML entities for escaping. 1214 | var escapeMap = { 1215 | '&': '&', 1216 | '<': '<', 1217 | '>': '>', 1218 | '"': '"', 1219 | "'": ''', 1220 | '`': '`' 1221 | }; 1222 | var unescapeMap = _.invert(escapeMap); 1223 | 1224 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1225 | var createEscaper = function(map) { 1226 | var escaper = function(match) { 1227 | return map[match]; 1228 | }; 1229 | // Regexes for identifying a key that needs to be escaped 1230 | var source = '(?:' + _.keys(map).join('|') + ')'; 1231 | var testRegexp = RegExp(source); 1232 | var replaceRegexp = RegExp(source, 'g'); 1233 | return function(string) { 1234 | string = string == null ? '' : '' + string; 1235 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; 1236 | }; 1237 | }; 1238 | _.escape = createEscaper(escapeMap); 1239 | _.unescape = createEscaper(unescapeMap); 1240 | 1241 | // If the value of the named `property` is a function then invoke it with the 1242 | // `object` as context; otherwise, return it. 1243 | _.result = function(object, property) { 1244 | if (object == null) return void 0; 1245 | var value = object[property]; 1246 | return _.isFunction(value) ? object[property]() : value; 1247 | }; 1248 | 1249 | // Generate a unique integer id (unique within the entire client session). 1250 | // Useful for temporary DOM ids. 1251 | var idCounter = 0; 1252 | _.uniqueId = function(prefix) { 1253 | var id = ++idCounter + ''; 1254 | return prefix ? prefix + id : id; 1255 | }; 1256 | 1257 | // By default, Underscore uses ERB-style template delimiters, change the 1258 | // following template settings to use alternative delimiters. 1259 | _.templateSettings = { 1260 | evaluate : /<%([\s\S]+?)%>/g, 1261 | interpolate : /<%=([\s\S]+?)%>/g, 1262 | escape : /<%-([\s\S]+?)%>/g 1263 | }; 1264 | 1265 | // When customizing `templateSettings`, if you don't want to define an 1266 | // interpolation, evaluation or escaping regex, we need one that is 1267 | // guaranteed not to match. 1268 | var noMatch = /(.)^/; 1269 | 1270 | // Certain characters need to be escaped so that they can be put into a 1271 | // string literal. 1272 | var escapes = { 1273 | "'": "'", 1274 | '\\': '\\', 1275 | '\r': 'r', 1276 | '\n': 'n', 1277 | '\u2028': 'u2028', 1278 | '\u2029': 'u2029' 1279 | }; 1280 | 1281 | var escaper = /\\|'|\r|\n|\u2028|\u2029/g; 1282 | 1283 | var escapeChar = function(match) { 1284 | return '\\' + escapes[match]; 1285 | }; 1286 | 1287 | // JavaScript micro-templating, similar to John Resig's implementation. 1288 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1289 | // and correctly escapes quotes within interpolated code. 1290 | // NB: `oldSettings` only exists for backwards compatibility. 1291 | _.template = function(text, settings, oldSettings) { 1292 | if (!settings && oldSettings) settings = oldSettings; 1293 | settings = _.defaults({}, settings, _.templateSettings); 1294 | 1295 | // Combine delimiters into one regular expression via alternation. 1296 | var matcher = RegExp([ 1297 | (settings.escape || noMatch).source, 1298 | (settings.interpolate || noMatch).source, 1299 | (settings.evaluate || noMatch).source 1300 | ].join('|') + '|$', 'g'); 1301 | 1302 | // Compile the template source, escaping string literals appropriately. 1303 | var index = 0; 1304 | var source = "__p+='"; 1305 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1306 | source += text.slice(index, offset).replace(escaper, escapeChar); 1307 | index = offset + match.length; 1308 | 1309 | if (escape) { 1310 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1311 | } else if (interpolate) { 1312 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1313 | } else if (evaluate) { 1314 | source += "';\n" + evaluate + "\n__p+='"; 1315 | } 1316 | 1317 | // Adobe VMs need the match returned to produce the correct offest. 1318 | return match; 1319 | }); 1320 | source += "';\n"; 1321 | 1322 | // If a variable is not specified, place data values in local scope. 1323 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1324 | 1325 | source = "var __t,__p='',__j=Array.prototype.join," + 1326 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1327 | source + 'return __p;\n'; 1328 | 1329 | try { 1330 | var render = new Function(settings.variable || 'obj', '_', source); 1331 | } catch (e) { 1332 | e.source = source; 1333 | throw e; 1334 | } 1335 | 1336 | var template = function(data) { 1337 | return render.call(this, data, _); 1338 | }; 1339 | 1340 | // Provide the compiled source as a convenience for precompilation. 1341 | var argument = settings.variable || 'obj'; 1342 | template.source = 'function(' + argument + '){\n' + source + '}'; 1343 | 1344 | return template; 1345 | }; 1346 | 1347 | // Add a "chain" function. Start chaining a wrapped Underscore object. 1348 | _.chain = function(obj) { 1349 | var instance = _(obj); 1350 | instance._chain = true; 1351 | return instance; 1352 | }; 1353 | 1354 | // OOP 1355 | // --------------- 1356 | // If Underscore is called as a function, it returns a wrapped object that 1357 | // can be used OO-style. This wrapper holds altered versions of all the 1358 | // underscore functions. Wrapped objects may be chained. 1359 | 1360 | // Helper function to continue chaining intermediate results. 1361 | var result = function(obj) { 1362 | return this._chain ? _(obj).chain() : obj; 1363 | }; 1364 | 1365 | // Add your own custom functions to the Underscore object. 1366 | _.mixin = function(obj) { 1367 | _.each(_.functions(obj), function(name) { 1368 | var func = _[name] = obj[name]; 1369 | _.prototype[name] = function() { 1370 | var args = [this._wrapped]; 1371 | push.apply(args, arguments); 1372 | return result.call(this, func.apply(_, args)); 1373 | }; 1374 | }); 1375 | }; 1376 | 1377 | // Add all of the Underscore functions to the wrapper object. 1378 | _.mixin(_); 1379 | 1380 | // Add all mutator Array functions to the wrapper. 1381 | _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1382 | var method = ArrayProto[name]; 1383 | _.prototype[name] = function() { 1384 | var obj = this._wrapped; 1385 | method.apply(obj, arguments); 1386 | if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; 1387 | return result.call(this, obj); 1388 | }; 1389 | }); 1390 | 1391 | // Add all accessor Array functions to the wrapper. 1392 | _.each(['concat', 'join', 'slice'], function(name) { 1393 | var method = ArrayProto[name]; 1394 | _.prototype[name] = function() { 1395 | return result.call(this, method.apply(this._wrapped, arguments)); 1396 | }; 1397 | }); 1398 | 1399 | // Extracts the result from a wrapped and chained object. 1400 | _.prototype.value = function() { 1401 | return this._wrapped; 1402 | }; 1403 | 1404 | // AMD registration happens at the end for compatibility with AMD loaders 1405 | // that may not enforce next-turn semantics on modules. Even though general 1406 | // practice for AMD registration is to be anonymous, underscore registers 1407 | // as a named module because, like jQuery, it is a base library that is 1408 | // popular enough to be bundled in a third party lib, but not be part of 1409 | // an AMD load request. Those cases could generate an error when an 1410 | // anonymous define() is called outside of a loader request. 1411 | if (typeof define === 'function' && define.amd) { 1412 | define('underscore', [], function() { 1413 | return _; 1414 | }); 1415 | } 1416 | }.call(this)); 1417 | -------------------------------------------------------------------------------- /fileapi/static/libs/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.1.2 2 | 3 | // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(root, factory) { 9 | 10 | // Set up Backbone appropriately for the environment. Start with AMD. 11 | if (typeof define === 'function' && define.amd) { 12 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) { 13 | // Export global even in AMD case in case this script is loaded with 14 | // others that may still expect a global Backbone. 15 | root.Backbone = factory(root, exports, _, $); 16 | }); 17 | 18 | // Next for Node.js or CommonJS. jQuery may not be needed as a module. 19 | } else if (typeof exports !== 'undefined') { 20 | var _ = require('underscore'); 21 | factory(root, exports, _); 22 | 23 | // Finally, as a browser global. 24 | } else { 25 | root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); 26 | } 27 | 28 | }(this, function(root, Backbone, _, $) { 29 | 30 | // Initial Setup 31 | // ------------- 32 | 33 | // Save the previous value of the `Backbone` variable, so that it can be 34 | // restored later on, if `noConflict` is used. 35 | var previousBackbone = root.Backbone; 36 | 37 | // Create local references to array methods we'll want to use later. 38 | var array = []; 39 | var push = array.push; 40 | var slice = array.slice; 41 | var splice = array.splice; 42 | 43 | // Current version of the library. Keep in sync with `package.json`. 44 | Backbone.VERSION = '1.1.2'; 45 | 46 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 47 | // the `$` variable. 48 | Backbone.$ = $; 49 | 50 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 51 | // to its previous owner. Returns a reference to this Backbone object. 52 | Backbone.noConflict = function() { 53 | root.Backbone = previousBackbone; 54 | return this; 55 | }; 56 | 57 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 58 | // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and 59 | // set a `X-Http-Method-Override` header. 60 | Backbone.emulateHTTP = false; 61 | 62 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 63 | // `application/json` requests ... will encode the body as 64 | // `application/x-www-form-urlencoded` instead and will send the model in a 65 | // form param named `model`. 66 | Backbone.emulateJSON = false; 67 | 68 | // Backbone.Events 69 | // --------------- 70 | 71 | // A module that can be mixed in to *any object* in order to provide it with 72 | // custom events. You may bind with `on` or remove with `off` callback 73 | // functions to an event; `trigger`-ing an event fires all callbacks in 74 | // succession. 75 | // 76 | // var object = {}; 77 | // _.extend(object, Backbone.Events); 78 | // object.on('expand', function(){ alert('expanded'); }); 79 | // object.trigger('expand'); 80 | // 81 | var Events = Backbone.Events = { 82 | 83 | // Bind an event to a `callback` function. Passing `"all"` will bind 84 | // the callback to all events fired. 85 | on: function(name, callback, context) { 86 | if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 87 | this._events || (this._events = {}); 88 | var events = this._events[name] || (this._events[name] = []); 89 | events.push({callback: callback, context: context, ctx: context || this}); 90 | return this; 91 | }, 92 | 93 | // Bind an event to only be triggered a single time. After the first time 94 | // the callback is invoked, it will be removed. 95 | once: function(name, callback, context) { 96 | if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 97 | var self = this; 98 | var once = _.once(function() { 99 | self.off(name, once); 100 | callback.apply(this, arguments); 101 | }); 102 | once._callback = callback; 103 | return this.on(name, once, context); 104 | }, 105 | 106 | // Remove one or many callbacks. If `context` is null, removes all 107 | // callbacks with that function. If `callback` is null, removes all 108 | // callbacks for the event. If `name` is null, removes all bound 109 | // callbacks for all events. 110 | off: function(name, callback, context) { 111 | var retain, ev, events, names, i, l, j, k; 112 | if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 113 | if (!name && !callback && !context) { 114 | this._events = void 0; 115 | return this; 116 | } 117 | names = name ? [name] : _.keys(this._events); 118 | for (i = 0, l = names.length; i < l; i++) { 119 | name = names[i]; 120 | if (events = this._events[name]) { 121 | this._events[name] = retain = []; 122 | if (callback || context) { 123 | for (j = 0, k = events.length; j < k; j++) { 124 | ev = events[j]; 125 | if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 126 | (context && context !== ev.context)) { 127 | retain.push(ev); 128 | } 129 | } 130 | } 131 | if (!retain.length) delete this._events[name]; 132 | } 133 | } 134 | 135 | return this; 136 | }, 137 | 138 | // Trigger one or many events, firing all bound callbacks. Callbacks are 139 | // passed the same arguments as `trigger` is, apart from the event name 140 | // (unless you're listening on `"all"`, which will cause your callback to 141 | // receive the true name of the event as the first argument). 142 | trigger: function(name) { 143 | if (!this._events) return this; 144 | var args = slice.call(arguments, 1); 145 | if (!eventsApi(this, 'trigger', name, args)) return this; 146 | var events = this._events[name]; 147 | var allEvents = this._events.all; 148 | if (events) triggerEvents(events, args); 149 | if (allEvents) triggerEvents(allEvents, arguments); 150 | return this; 151 | }, 152 | 153 | // Tell this object to stop listening to either specific events ... or 154 | // to every object it's currently listening to. 155 | stopListening: function(obj, name, callback) { 156 | var listeningTo = this._listeningTo; 157 | if (!listeningTo) return this; 158 | var remove = !name && !callback; 159 | if (!callback && typeof name === 'object') callback = this; 160 | if (obj) (listeningTo = {})[obj._listenId] = obj; 161 | for (var id in listeningTo) { 162 | obj = listeningTo[id]; 163 | obj.off(name, callback, this); 164 | if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; 165 | } 166 | return this; 167 | } 168 | 169 | }; 170 | 171 | // Regular expression used to split event strings. 172 | var eventSplitter = /\s+/; 173 | 174 | // Implement fancy features of the Events API such as multiple event 175 | // names `"change blur"` and jQuery-style event maps `{change: action}` 176 | // in terms of the existing API. 177 | var eventsApi = function(obj, action, name, rest) { 178 | if (!name) return true; 179 | 180 | // Handle event maps. 181 | if (typeof name === 'object') { 182 | for (var key in name) { 183 | obj[action].apply(obj, [key, name[key]].concat(rest)); 184 | } 185 | return false; 186 | } 187 | 188 | // Handle space separated event names. 189 | if (eventSplitter.test(name)) { 190 | var names = name.split(eventSplitter); 191 | for (var i = 0, l = names.length; i < l; i++) { 192 | obj[action].apply(obj, [names[i]].concat(rest)); 193 | } 194 | return false; 195 | } 196 | 197 | return true; 198 | }; 199 | 200 | // A difficult-to-believe, but optimized internal dispatch function for 201 | // triggering events. Tries to keep the usual cases speedy (most internal 202 | // Backbone events have 3 arguments). 203 | var triggerEvents = function(events, args) { 204 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 205 | switch (args.length) { 206 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 207 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 208 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 209 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 210 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 211 | } 212 | }; 213 | 214 | var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 215 | 216 | // Inversion-of-control versions of `on` and `once`. Tell *this* object to 217 | // listen to an event in another object ... keeping track of what it's 218 | // listening to. 219 | _.each(listenMethods, function(implementation, method) { 220 | Events[method] = function(obj, name, callback) { 221 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 222 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 223 | listeningTo[id] = obj; 224 | if (!callback && typeof name === 'object') callback = this; 225 | obj[implementation](name, callback, this); 226 | return this; 227 | }; 228 | }); 229 | 230 | // Aliases for backwards compatibility. 231 | Events.bind = Events.on; 232 | Events.unbind = Events.off; 233 | 234 | // Allow the `Backbone` object to serve as a global event bus, for folks who 235 | // want global "pubsub" in a convenient place. 236 | _.extend(Backbone, Events); 237 | 238 | // Backbone.Model 239 | // -------------- 240 | 241 | // Backbone **Models** are the basic data object in the framework -- 242 | // frequently representing a row in a table in a database on your server. 243 | // A discrete chunk of data and a bunch of useful, related methods for 244 | // performing computations and transformations on that data. 245 | 246 | // Create a new model with the specified attributes. A client id (`cid`) 247 | // is automatically generated and assigned for you. 248 | var Model = Backbone.Model = function(attributes, options) { 249 | var attrs = attributes || {}; 250 | options || (options = {}); 251 | this.cid = _.uniqueId('c'); 252 | this.attributes = {}; 253 | if (options.collection) this.collection = options.collection; 254 | if (options.parse) attrs = this.parse(attrs, options) || {}; 255 | attrs = _.defaults({}, attrs, _.result(this, 'defaults')); 256 | this.set(attrs, options); 257 | this.changed = {}; 258 | this.initialize.apply(this, arguments); 259 | }; 260 | 261 | // Attach all inheritable methods to the Model prototype. 262 | _.extend(Model.prototype, Events, { 263 | 264 | // A hash of attributes whose current and previous value differ. 265 | changed: null, 266 | 267 | // The value returned during the last failed validation. 268 | validationError: null, 269 | 270 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 271 | // CouchDB users may want to set this to `"_id"`. 272 | idAttribute: 'id', 273 | 274 | // Initialize is an empty function by default. Override it with your own 275 | // initialization logic. 276 | initialize: function(){}, 277 | 278 | // Return a copy of the model's `attributes` object. 279 | toJSON: function(options) { 280 | return _.clone(this.attributes); 281 | }, 282 | 283 | // Proxy `Backbone.sync` by default -- but override this if you need 284 | // custom syncing semantics for *this* particular model. 285 | sync: function() { 286 | return Backbone.sync.apply(this, arguments); 287 | }, 288 | 289 | // Get the value of an attribute. 290 | get: function(attr) { 291 | return this.attributes[attr]; 292 | }, 293 | 294 | // Get the HTML-escaped value of an attribute. 295 | escape: function(attr) { 296 | return _.escape(this.get(attr)); 297 | }, 298 | 299 | // Returns `true` if the attribute contains a value that is not null 300 | // or undefined. 301 | has: function(attr) { 302 | return this.get(attr) != null; 303 | }, 304 | 305 | // Set a hash of model attributes on the object, firing `"change"`. This is 306 | // the core primitive operation of a model, updating the data and notifying 307 | // anyone who needs to know about the change in state. The heart of the beast. 308 | set: function(key, val, options) { 309 | var attr, attrs, unset, changes, silent, changing, prev, current; 310 | if (key == null) return this; 311 | 312 | // Handle both `"key", value` and `{key: value}` -style arguments. 313 | if (typeof key === 'object') { 314 | attrs = key; 315 | options = val; 316 | } else { 317 | (attrs = {})[key] = val; 318 | } 319 | 320 | options || (options = {}); 321 | 322 | // Run validation. 323 | if (!this._validate(attrs, options)) return false; 324 | 325 | // Extract attributes and options. 326 | unset = options.unset; 327 | silent = options.silent; 328 | changes = []; 329 | changing = this._changing; 330 | this._changing = true; 331 | 332 | if (!changing) { 333 | this._previousAttributes = _.clone(this.attributes); 334 | this.changed = {}; 335 | } 336 | current = this.attributes, prev = this._previousAttributes; 337 | 338 | // Check for changes of `id`. 339 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 340 | 341 | // For each `set` attribute, update or delete the current value. 342 | for (attr in attrs) { 343 | val = attrs[attr]; 344 | if (!_.isEqual(current[attr], val)) changes.push(attr); 345 | if (!_.isEqual(prev[attr], val)) { 346 | this.changed[attr] = val; 347 | } else { 348 | delete this.changed[attr]; 349 | } 350 | unset ? delete current[attr] : current[attr] = val; 351 | } 352 | 353 | // Trigger all relevant attribute changes. 354 | if (!silent) { 355 | if (changes.length) this._pending = options; 356 | for (var i = 0, l = changes.length; i < l; i++) { 357 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 358 | } 359 | } 360 | 361 | // You might be wondering why there's a `while` loop here. Changes can 362 | // be recursively nested within `"change"` events. 363 | if (changing) return this; 364 | if (!silent) { 365 | while (this._pending) { 366 | options = this._pending; 367 | this._pending = false; 368 | this.trigger('change', this, options); 369 | } 370 | } 371 | this._pending = false; 372 | this._changing = false; 373 | return this; 374 | }, 375 | 376 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 377 | // if the attribute doesn't exist. 378 | unset: function(attr, options) { 379 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 380 | }, 381 | 382 | // Clear all attributes on the model, firing `"change"`. 383 | clear: function(options) { 384 | var attrs = {}; 385 | for (var key in this.attributes) attrs[key] = void 0; 386 | return this.set(attrs, _.extend({}, options, {unset: true})); 387 | }, 388 | 389 | // Determine if the model has changed since the last `"change"` event. 390 | // If you specify an attribute name, determine if that attribute has changed. 391 | hasChanged: function(attr) { 392 | if (attr == null) return !_.isEmpty(this.changed); 393 | return _.has(this.changed, attr); 394 | }, 395 | 396 | // Return an object containing all the attributes that have changed, or 397 | // false if there are no changed attributes. Useful for determining what 398 | // parts of a view need to be updated and/or what attributes need to be 399 | // persisted to the server. Unset attributes will be set to undefined. 400 | // You can also pass an attributes object to diff against the model, 401 | // determining if there *would be* a change. 402 | changedAttributes: function(diff) { 403 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 404 | var val, changed = false; 405 | var old = this._changing ? this._previousAttributes : this.attributes; 406 | for (var attr in diff) { 407 | if (_.isEqual(old[attr], (val = diff[attr]))) continue; 408 | (changed || (changed = {}))[attr] = val; 409 | } 410 | return changed; 411 | }, 412 | 413 | // Get the previous value of an attribute, recorded at the time the last 414 | // `"change"` event was fired. 415 | previous: function(attr) { 416 | if (attr == null || !this._previousAttributes) return null; 417 | return this._previousAttributes[attr]; 418 | }, 419 | 420 | // Get all of the attributes of the model at the time of the previous 421 | // `"change"` event. 422 | previousAttributes: function() { 423 | return _.clone(this._previousAttributes); 424 | }, 425 | 426 | // Fetch the model from the server. If the server's representation of the 427 | // model differs from its current attributes, they will be overridden, 428 | // triggering a `"change"` event. 429 | fetch: function(options) { 430 | options = options ? _.clone(options) : {}; 431 | if (options.parse === void 0) options.parse = true; 432 | var model = this; 433 | var success = options.success; 434 | options.success = function(resp) { 435 | if (!model.set(model.parse(resp, options), options)) return false; 436 | if (success) success(model, resp, options); 437 | model.trigger('sync', model, resp, options); 438 | }; 439 | wrapError(this, options); 440 | return this.sync('read', this, options); 441 | }, 442 | 443 | // Set a hash of model attributes, and sync the model to the server. 444 | // If the server returns an attributes hash that differs, the model's 445 | // state will be `set` again. 446 | save: function(key, val, options) { 447 | var attrs, method, xhr, attributes = this.attributes; 448 | 449 | // Handle both `"key", value` and `{key: value}` -style arguments. 450 | if (key == null || typeof key === 'object') { 451 | attrs = key; 452 | options = val; 453 | } else { 454 | (attrs = {})[key] = val; 455 | } 456 | 457 | options = _.extend({validate: true}, options); 458 | 459 | // If we're not waiting and attributes exist, save acts as 460 | // `set(attr).save(null, opts)` with validation. Otherwise, check if 461 | // the model will be valid when the attributes, if any, are set. 462 | if (attrs && !options.wait) { 463 | if (!this.set(attrs, options)) return false; 464 | } else { 465 | if (!this._validate(attrs, options)) return false; 466 | } 467 | 468 | // Set temporary attributes if `{wait: true}`. 469 | if (attrs && options.wait) { 470 | this.attributes = _.extend({}, attributes, attrs); 471 | } 472 | 473 | // After a successful server-side save, the client is (optionally) 474 | // updated with the server-side state. 475 | if (options.parse === void 0) options.parse = true; 476 | var model = this; 477 | var success = options.success; 478 | options.success = function(resp) { 479 | // Ensure attributes are restored during synchronous saves. 480 | model.attributes = attributes; 481 | var serverAttrs = model.parse(resp, options); 482 | if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 483 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 484 | return false; 485 | } 486 | if (success) success(model, resp, options); 487 | model.trigger('sync', model, resp, options); 488 | }; 489 | wrapError(this, options); 490 | 491 | method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 492 | if (method === 'patch') options.attrs = attrs; 493 | xhr = this.sync(method, this, options); 494 | 495 | // Restore attributes. 496 | if (attrs && options.wait) this.attributes = attributes; 497 | 498 | return xhr; 499 | }, 500 | 501 | // Destroy this model on the server if it was already persisted. 502 | // Optimistically removes the model from its collection, if it has one. 503 | // If `wait: true` is passed, waits for the server to respond before removal. 504 | destroy: function(options) { 505 | options = options ? _.clone(options) : {}; 506 | var model = this; 507 | var success = options.success; 508 | 509 | var destroy = function() { 510 | model.trigger('destroy', model, model.collection, options); 511 | }; 512 | 513 | options.success = function(resp) { 514 | if (options.wait || model.isNew()) destroy(); 515 | if (success) success(model, resp, options); 516 | if (!model.isNew()) model.trigger('sync', model, resp, options); 517 | }; 518 | 519 | if (this.isNew()) { 520 | options.success(); 521 | return false; 522 | } 523 | wrapError(this, options); 524 | 525 | var xhr = this.sync('delete', this, options); 526 | if (!options.wait) destroy(); 527 | return xhr; 528 | }, 529 | 530 | // Default URL for the model's representation on the server -- if you're 531 | // using Backbone's restful methods, override this to change the endpoint 532 | // that will be called. 533 | url: function() { 534 | var base = 535 | _.result(this, 'urlRoot') || 536 | _.result(this.collection, 'url') || 537 | urlError(); 538 | if (this.isNew()) return base; 539 | return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); 540 | }, 541 | 542 | // **parse** converts a response into the hash of attributes to be `set` on 543 | // the model. The default implementation is just to pass the response along. 544 | parse: function(resp, options) { 545 | return resp; 546 | }, 547 | 548 | // Create a new model with identical attributes to this one. 549 | clone: function() { 550 | return new this.constructor(this.attributes); 551 | }, 552 | 553 | // A model is new if it has never been saved to the server, and lacks an id. 554 | isNew: function() { 555 | return !this.has(this.idAttribute); 556 | }, 557 | 558 | // Check if the model is currently in a valid state. 559 | isValid: function(options) { 560 | return this._validate({}, _.extend(options || {}, { validate: true })); 561 | }, 562 | 563 | // Run validation against the next complete set of model attributes, 564 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 565 | _validate: function(attrs, options) { 566 | if (!options.validate || !this.validate) return true; 567 | attrs = _.extend({}, this.attributes, attrs); 568 | var error = this.validationError = this.validate(attrs, options) || null; 569 | if (!error) return true; 570 | this.trigger('invalid', this, error, _.extend(options, {validationError: error})); 571 | return false; 572 | } 573 | 574 | }); 575 | 576 | // Underscore methods that we want to implement on the Model. 577 | var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; 578 | 579 | // Mix in each Underscore method as a proxy to `Model#attributes`. 580 | _.each(modelMethods, function(method) { 581 | Model.prototype[method] = function() { 582 | var args = slice.call(arguments); 583 | args.unshift(this.attributes); 584 | return _[method].apply(_, args); 585 | }; 586 | }); 587 | 588 | // Backbone.Collection 589 | // ------------------- 590 | 591 | // If models tend to represent a single row of data, a Backbone Collection is 592 | // more analagous to a table full of data ... or a small slice or page of that 593 | // table, or a collection of rows that belong together for a particular reason 594 | // -- all of the messages in this particular folder, all of the documents 595 | // belonging to this particular author, and so on. Collections maintain 596 | // indexes of their models, both in order, and for lookup by `id`. 597 | 598 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 599 | // If a `comparator` is specified, the Collection will maintain 600 | // its models in sort order, as they're added and removed. 601 | var Collection = Backbone.Collection = function(models, options) { 602 | options || (options = {}); 603 | if (options.model) this.model = options.model; 604 | if (options.comparator !== void 0) this.comparator = options.comparator; 605 | this._reset(); 606 | this.initialize.apply(this, arguments); 607 | if (models) this.reset(models, _.extend({silent: true}, options)); 608 | }; 609 | 610 | // Default options for `Collection#set`. 611 | var setOptions = {add: true, remove: true, merge: true}; 612 | var addOptions = {add: true, remove: false}; 613 | 614 | // Define the Collection's inheritable methods. 615 | _.extend(Collection.prototype, Events, { 616 | 617 | // The default model for a collection is just a **Backbone.Model**. 618 | // This should be overridden in most cases. 619 | model: Model, 620 | 621 | // Initialize is an empty function by default. Override it with your own 622 | // initialization logic. 623 | initialize: function(){}, 624 | 625 | // The JSON representation of a Collection is an array of the 626 | // models' attributes. 627 | toJSON: function(options) { 628 | return this.map(function(model){ return model.toJSON(options); }); 629 | }, 630 | 631 | // Proxy `Backbone.sync` by default. 632 | sync: function() { 633 | return Backbone.sync.apply(this, arguments); 634 | }, 635 | 636 | // Add a model, or list of models to the set. 637 | add: function(models, options) { 638 | return this.set(models, _.extend({merge: false}, options, addOptions)); 639 | }, 640 | 641 | // Remove a model, or a list of models from the set. 642 | remove: function(models, options) { 643 | var singular = !_.isArray(models); 644 | models = singular ? [models] : _.clone(models); 645 | options || (options = {}); 646 | var i, l, index, model; 647 | for (i = 0, l = models.length; i < l; i++) { 648 | model = models[i] = this.get(models[i]); 649 | if (!model) continue; 650 | delete this._byId[model.id]; 651 | delete this._byId[model.cid]; 652 | index = this.indexOf(model); 653 | this.models.splice(index, 1); 654 | this.length--; 655 | if (!options.silent) { 656 | options.index = index; 657 | model.trigger('remove', model, this, options); 658 | } 659 | this._removeReference(model, options); 660 | } 661 | return singular ? models[0] : models; 662 | }, 663 | 664 | // Update a collection by `set`-ing a new list of models, adding new ones, 665 | // removing models that are no longer present, and merging models that 666 | // already exist in the collection, as necessary. Similar to **Model#set**, 667 | // the core operation for updating the data contained by the collection. 668 | set: function(models, options) { 669 | options = _.defaults({}, options, setOptions); 670 | if (options.parse) models = this.parse(models, options); 671 | var singular = !_.isArray(models); 672 | models = singular ? (models ? [models] : []) : _.clone(models); 673 | var i, l, id, model, attrs, existing, sort; 674 | var at = options.at; 675 | var targetModel = this.model; 676 | var sortable = this.comparator && (at == null) && options.sort !== false; 677 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 678 | var toAdd = [], toRemove = [], modelMap = {}; 679 | var add = options.add, merge = options.merge, remove = options.remove; 680 | var order = !sortable && add && remove ? [] : false; 681 | 682 | // Turn bare objects into model references, and prevent invalid models 683 | // from being added. 684 | for (i = 0, l = models.length; i < l; i++) { 685 | attrs = models[i] || {}; 686 | if (attrs instanceof Model) { 687 | id = model = attrs; 688 | } else { 689 | id = attrs[targetModel.prototype.idAttribute || 'id']; 690 | } 691 | 692 | // If a duplicate is found, prevent it from being added and 693 | // optionally merge it into the existing model. 694 | if (existing = this.get(id)) { 695 | if (remove) modelMap[existing.cid] = true; 696 | if (merge) { 697 | attrs = attrs === model ? model.attributes : attrs; 698 | if (options.parse) attrs = existing.parse(attrs, options); 699 | existing.set(attrs, options); 700 | if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 701 | } 702 | models[i] = existing; 703 | 704 | // If this is a new, valid model, push it to the `toAdd` list. 705 | } else if (add) { 706 | model = models[i] = this._prepareModel(attrs, options); 707 | if (!model) continue; 708 | toAdd.push(model); 709 | this._addReference(model, options); 710 | } 711 | 712 | // Do not add multiple models with the same `id`. 713 | model = existing || model; 714 | if (order && (model.isNew() || !modelMap[model.id])) order.push(model); 715 | modelMap[model.id] = true; 716 | } 717 | 718 | // Remove nonexistent models if appropriate. 719 | if (remove) { 720 | for (i = 0, l = this.length; i < l; ++i) { 721 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 722 | } 723 | if (toRemove.length) this.remove(toRemove, options); 724 | } 725 | 726 | // See if sorting is needed, update `length` and splice in new models. 727 | if (toAdd.length || (order && order.length)) { 728 | if (sortable) sort = true; 729 | this.length += toAdd.length; 730 | if (at != null) { 731 | for (i = 0, l = toAdd.length; i < l; i++) { 732 | this.models.splice(at + i, 0, toAdd[i]); 733 | } 734 | } else { 735 | if (order) this.models.length = 0; 736 | var orderedModels = order || toAdd; 737 | for (i = 0, l = orderedModels.length; i < l; i++) { 738 | this.models.push(orderedModels[i]); 739 | } 740 | } 741 | } 742 | 743 | // Silently sort the collection if appropriate. 744 | if (sort) this.sort({silent: true}); 745 | 746 | // Unless silenced, it's time to fire all appropriate add/sort events. 747 | if (!options.silent) { 748 | for (i = 0, l = toAdd.length; i < l; i++) { 749 | (model = toAdd[i]).trigger('add', model, this, options); 750 | } 751 | if (sort || (order && order.length)) this.trigger('sort', this, options); 752 | } 753 | 754 | // Return the added (or merged) model (or models). 755 | return singular ? models[0] : models; 756 | }, 757 | 758 | // When you have more items than you want to add or remove individually, 759 | // you can reset the entire set with a new list of models, without firing 760 | // any granular `add` or `remove` events. Fires `reset` when finished. 761 | // Useful for bulk operations and optimizations. 762 | reset: function(models, options) { 763 | options || (options = {}); 764 | for (var i = 0, l = this.models.length; i < l; i++) { 765 | this._removeReference(this.models[i], options); 766 | } 767 | options.previousModels = this.models; 768 | this._reset(); 769 | models = this.add(models, _.extend({silent: true}, options)); 770 | if (!options.silent) this.trigger('reset', this, options); 771 | return models; 772 | }, 773 | 774 | // Add a model to the end of the collection. 775 | push: function(model, options) { 776 | return this.add(model, _.extend({at: this.length}, options)); 777 | }, 778 | 779 | // Remove a model from the end of the collection. 780 | pop: function(options) { 781 | var model = this.at(this.length - 1); 782 | this.remove(model, options); 783 | return model; 784 | }, 785 | 786 | // Add a model to the beginning of the collection. 787 | unshift: function(model, options) { 788 | return this.add(model, _.extend({at: 0}, options)); 789 | }, 790 | 791 | // Remove a model from the beginning of the collection. 792 | shift: function(options) { 793 | var model = this.at(0); 794 | this.remove(model, options); 795 | return model; 796 | }, 797 | 798 | // Slice out a sub-array of models from the collection. 799 | slice: function() { 800 | return slice.apply(this.models, arguments); 801 | }, 802 | 803 | // Get a model from the set by id. 804 | get: function(obj) { 805 | if (obj == null) return void 0; 806 | return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; 807 | }, 808 | 809 | // Get the model at the given index. 810 | at: function(index) { 811 | return this.models[index]; 812 | }, 813 | 814 | // Return models with matching attributes. Useful for simple cases of 815 | // `filter`. 816 | where: function(attrs, first) { 817 | if (_.isEmpty(attrs)) return first ? void 0 : []; 818 | return this[first ? 'find' : 'filter'](function(model) { 819 | for (var key in attrs) { 820 | if (attrs[key] !== model.get(key)) return false; 821 | } 822 | return true; 823 | }); 824 | }, 825 | 826 | // Return the first model with matching attributes. Useful for simple cases 827 | // of `find`. 828 | findWhere: function(attrs) { 829 | return this.where(attrs, true); 830 | }, 831 | 832 | // Force the collection to re-sort itself. You don't need to call this under 833 | // normal circumstances, as the set will maintain sort order as each item 834 | // is added. 835 | sort: function(options) { 836 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 837 | options || (options = {}); 838 | 839 | // Run sort based on type of `comparator`. 840 | if (_.isString(this.comparator) || this.comparator.length === 1) { 841 | this.models = this.sortBy(this.comparator, this); 842 | } else { 843 | this.models.sort(_.bind(this.comparator, this)); 844 | } 845 | 846 | if (!options.silent) this.trigger('sort', this, options); 847 | return this; 848 | }, 849 | 850 | // Pluck an attribute from each model in the collection. 851 | pluck: function(attr) { 852 | return _.invoke(this.models, 'get', attr); 853 | }, 854 | 855 | // Fetch the default set of models for this collection, resetting the 856 | // collection when they arrive. If `reset: true` is passed, the response 857 | // data will be passed through the `reset` method instead of `set`. 858 | fetch: function(options) { 859 | options = options ? _.clone(options) : {}; 860 | if (options.parse === void 0) options.parse = true; 861 | var success = options.success; 862 | var collection = this; 863 | options.success = function(resp) { 864 | var method = options.reset ? 'reset' : 'set'; 865 | collection[method](resp, options); 866 | if (success) success(collection, resp, options); 867 | collection.trigger('sync', collection, resp, options); 868 | }; 869 | wrapError(this, options); 870 | return this.sync('read', this, options); 871 | }, 872 | 873 | // Create a new instance of a model in this collection. Add the model to the 874 | // collection immediately, unless `wait: true` is passed, in which case we 875 | // wait for the server to agree. 876 | create: function(model, options) { 877 | options = options ? _.clone(options) : {}; 878 | if (!(model = this._prepareModel(model, options))) return false; 879 | if (!options.wait) this.add(model, options); 880 | var collection = this; 881 | var success = options.success; 882 | options.success = function(model, resp) { 883 | if (options.wait) collection.add(model, options); 884 | if (success) success(model, resp, options); 885 | }; 886 | model.save(null, options); 887 | return model; 888 | }, 889 | 890 | // **parse** converts a response into a list of models to be added to the 891 | // collection. The default implementation is just to pass it through. 892 | parse: function(resp, options) { 893 | return resp; 894 | }, 895 | 896 | // Create a new collection with an identical list of models as this one. 897 | clone: function() { 898 | return new this.constructor(this.models); 899 | }, 900 | 901 | // Private method to reset all internal state. Called when the collection 902 | // is first initialized or reset. 903 | _reset: function() { 904 | this.length = 0; 905 | this.models = []; 906 | this._byId = {}; 907 | }, 908 | 909 | // Prepare a hash of attributes (or other model) to be added to this 910 | // collection. 911 | _prepareModel: function(attrs, options) { 912 | if (attrs instanceof Model) return attrs; 913 | options = options ? _.clone(options) : {}; 914 | options.collection = this; 915 | var model = new this.model(attrs, options); 916 | if (!model.validationError) return model; 917 | this.trigger('invalid', this, model.validationError, options); 918 | return false; 919 | }, 920 | 921 | // Internal method to create a model's ties to a collection. 922 | _addReference: function(model, options) { 923 | this._byId[model.cid] = model; 924 | if (model.id != null) this._byId[model.id] = model; 925 | if (!model.collection) model.collection = this; 926 | model.on('all', this._onModelEvent, this); 927 | }, 928 | 929 | // Internal method to sever a model's ties to a collection. 930 | _removeReference: function(model, options) { 931 | if (this === model.collection) delete model.collection; 932 | model.off('all', this._onModelEvent, this); 933 | }, 934 | 935 | // Internal method called every time a model in the set fires an event. 936 | // Sets need to update their indexes when models change ids. All other 937 | // events simply proxy through. "add" and "remove" events that originate 938 | // in other collections are ignored. 939 | _onModelEvent: function(event, model, collection, options) { 940 | if ((event === 'add' || event === 'remove') && collection !== this) return; 941 | if (event === 'destroy') this.remove(model, options); 942 | if (model && event === 'change:' + model.idAttribute) { 943 | delete this._byId[model.previous(model.idAttribute)]; 944 | if (model.id != null) this._byId[model.id] = model; 945 | } 946 | this.trigger.apply(this, arguments); 947 | } 948 | 949 | }); 950 | 951 | // Underscore methods that we want to implement on the Collection. 952 | // 90% of the core usefulness of Backbone Collections is actually implemented 953 | // right here: 954 | var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 955 | 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 956 | 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 957 | 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 958 | 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 959 | 'lastIndexOf', 'isEmpty', 'chain', 'sample']; 960 | 961 | // Mix in each Underscore method as a proxy to `Collection#models`. 962 | _.each(methods, function(method) { 963 | Collection.prototype[method] = function() { 964 | var args = slice.call(arguments); 965 | args.unshift(this.models); 966 | return _[method].apply(_, args); 967 | }; 968 | }); 969 | 970 | // Underscore methods that take a property name as an argument. 971 | var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; 972 | 973 | // Use attributes instead of properties. 974 | _.each(attributeMethods, function(method) { 975 | Collection.prototype[method] = function(value, context) { 976 | var iterator = _.isFunction(value) ? value : function(model) { 977 | return model.get(value); 978 | }; 979 | return _[method](this.models, iterator, context); 980 | }; 981 | }); 982 | 983 | // Backbone.View 984 | // ------------- 985 | 986 | // Backbone Views are almost more convention than they are actual code. A View 987 | // is simply a JavaScript object that represents a logical chunk of UI in the 988 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 989 | // even the surrounding frame which wraps your whole app. Defining a chunk of 990 | // UI as a **View** allows you to define your DOM events declaratively, without 991 | // having to worry about render order ... and makes it easy for the view to 992 | // react to specific changes in the state of your models. 993 | 994 | // Creating a Backbone.View creates its initial element outside of the DOM, 995 | // if an existing element is not provided... 996 | var View = Backbone.View = function(options) { 997 | this.cid = _.uniqueId('view'); 998 | options || (options = {}); 999 | _.extend(this, _.pick(options, viewOptions)); 1000 | this._ensureElement(); 1001 | this.initialize.apply(this, arguments); 1002 | this.delegateEvents(); 1003 | }; 1004 | 1005 | // Cached regex to split keys for `delegate`. 1006 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1007 | 1008 | // List of view options to be merged as properties. 1009 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1010 | 1011 | // Set up all inheritable **Backbone.View** properties and methods. 1012 | _.extend(View.prototype, Events, { 1013 | 1014 | // The default `tagName` of a View's element is `"div"`. 1015 | tagName: 'div', 1016 | 1017 | // jQuery delegate for element lookup, scoped to DOM elements within the 1018 | // current view. This should be preferred to global lookups where possible. 1019 | $: function(selector) { 1020 | return this.$el.find(selector); 1021 | }, 1022 | 1023 | // Initialize is an empty function by default. Override it with your own 1024 | // initialization logic. 1025 | initialize: function(){}, 1026 | 1027 | // **render** is the core function that your view should override, in order 1028 | // to populate its element (`this.el`), with the appropriate HTML. The 1029 | // convention is for **render** to always return `this`. 1030 | render: function() { 1031 | return this; 1032 | }, 1033 | 1034 | // Remove this view by taking the element out of the DOM, and removing any 1035 | // applicable Backbone.Events listeners. 1036 | remove: function() { 1037 | this.$el.remove(); 1038 | this.stopListening(); 1039 | return this; 1040 | }, 1041 | 1042 | // Change the view's element (`this.el` property), including event 1043 | // re-delegation. 1044 | setElement: function(element, delegate) { 1045 | if (this.$el) this.undelegateEvents(); 1046 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); 1047 | this.el = this.$el[0]; 1048 | if (delegate !== false) this.delegateEvents(); 1049 | return this; 1050 | }, 1051 | 1052 | // Set callbacks, where `this.events` is a hash of 1053 | // 1054 | // *{"event selector": "callback"}* 1055 | // 1056 | // { 1057 | // 'mousedown .title': 'edit', 1058 | // 'click .button': 'save', 1059 | // 'click .open': function(e) { ... } 1060 | // } 1061 | // 1062 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1063 | // Uses event delegation for efficiency. 1064 | // Omitting the selector binds the event to `this.el`. 1065 | // This only works for delegate-able events: not `focus`, `blur`, and 1066 | // not `change`, `submit`, and `reset` in Internet Explorer. 1067 | delegateEvents: function(events) { 1068 | if (!(events || (events = _.result(this, 'events')))) return this; 1069 | this.undelegateEvents(); 1070 | for (var key in events) { 1071 | var method = events[key]; 1072 | if (!_.isFunction(method)) method = this[events[key]]; 1073 | if (!method) continue; 1074 | 1075 | var match = key.match(delegateEventSplitter); 1076 | var eventName = match[1], selector = match[2]; 1077 | method = _.bind(method, this); 1078 | eventName += '.delegateEvents' + this.cid; 1079 | if (selector === '') { 1080 | this.$el.on(eventName, method); 1081 | } else { 1082 | this.$el.on(eventName, selector, method); 1083 | } 1084 | } 1085 | return this; 1086 | }, 1087 | 1088 | // Clears all callbacks previously bound to the view with `delegateEvents`. 1089 | // You usually don't need to use this, but may wish to if you have multiple 1090 | // Backbone views attached to the same DOM element. 1091 | undelegateEvents: function() { 1092 | this.$el.off('.delegateEvents' + this.cid); 1093 | return this; 1094 | }, 1095 | 1096 | // Ensure that the View has a DOM element to render into. 1097 | // If `this.el` is a string, pass it through `$()`, take the first 1098 | // matching element, and re-assign it to `el`. Otherwise, create 1099 | // an element from the `id`, `className` and `tagName` properties. 1100 | _ensureElement: function() { 1101 | if (!this.el) { 1102 | var attrs = _.extend({}, _.result(this, 'attributes')); 1103 | if (this.id) attrs.id = _.result(this, 'id'); 1104 | if (this.className) attrs['class'] = _.result(this, 'className'); 1105 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); 1106 | this.setElement($el, false); 1107 | } else { 1108 | this.setElement(_.result(this, 'el'), false); 1109 | } 1110 | } 1111 | 1112 | }); 1113 | 1114 | // Backbone.sync 1115 | // ------------- 1116 | 1117 | // Override this function to change the manner in which Backbone persists 1118 | // models to the server. You will be passed the type of request, and the 1119 | // model in question. By default, makes a RESTful Ajax request 1120 | // to the model's `url()`. Some possible customizations could be: 1121 | // 1122 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1123 | // * Send up the models as XML instead of JSON. 1124 | // * Persist models via WebSockets instead of Ajax. 1125 | // 1126 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1127 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1128 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1129 | // instead of `application/json` with the model in a param named `model`. 1130 | // Useful when interfacing with server-side languages like **PHP** that make 1131 | // it difficult to read the body of `PUT` requests. 1132 | Backbone.sync = function(method, model, options) { 1133 | var type = methodMap[method]; 1134 | 1135 | // Default options, unless specified. 1136 | _.defaults(options || (options = {}), { 1137 | emulateHTTP: Backbone.emulateHTTP, 1138 | emulateJSON: Backbone.emulateJSON 1139 | }); 1140 | 1141 | // Default JSON-request options. 1142 | var params = {type: type, dataType: 'json'}; 1143 | 1144 | // Ensure that we have a URL. 1145 | if (!options.url) { 1146 | params.url = _.result(model, 'url') || urlError(); 1147 | } 1148 | 1149 | // Ensure that we have the appropriate request data. 1150 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1151 | params.contentType = 'application/json'; 1152 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1153 | } 1154 | 1155 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1156 | if (options.emulateJSON) { 1157 | params.contentType = 'application/x-www-form-urlencoded'; 1158 | params.data = params.data ? {model: params.data} : {}; 1159 | } 1160 | 1161 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1162 | // And an `X-HTTP-Method-Override` header. 1163 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1164 | params.type = 'POST'; 1165 | if (options.emulateJSON) params.data._method = type; 1166 | var beforeSend = options.beforeSend; 1167 | options.beforeSend = function(xhr) { 1168 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1169 | if (beforeSend) return beforeSend.apply(this, arguments); 1170 | }; 1171 | } 1172 | 1173 | // Don't process data on a non-GET request. 1174 | if (params.type !== 'GET' && !options.emulateJSON) { 1175 | params.processData = false; 1176 | } 1177 | 1178 | // If we're sending a `PATCH` request, and we're in an old Internet Explorer 1179 | // that still has ActiveX enabled by default, override jQuery to use that 1180 | // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. 1181 | if (params.type === 'PATCH' && noXhrPatch) { 1182 | params.xhr = function() { 1183 | return new ActiveXObject("Microsoft.XMLHTTP"); 1184 | }; 1185 | } 1186 | 1187 | // Make the request, allowing the user to override any Ajax options. 1188 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1189 | model.trigger('request', model, xhr, options); 1190 | return xhr; 1191 | }; 1192 | 1193 | var noXhrPatch = 1194 | typeof window !== 'undefined' && !!window.ActiveXObject && 1195 | !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); 1196 | 1197 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1198 | var methodMap = { 1199 | 'create': 'POST', 1200 | 'update': 'PUT', 1201 | 'patch': 'PATCH', 1202 | 'delete': 'DELETE', 1203 | 'read': 'GET' 1204 | }; 1205 | 1206 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1207 | // Override this if you'd like to use a different library. 1208 | Backbone.ajax = function() { 1209 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1210 | }; 1211 | 1212 | // Backbone.Router 1213 | // --------------- 1214 | 1215 | // Routers map faux-URLs to actions, and fire events when routes are 1216 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1217 | var Router = Backbone.Router = function(options) { 1218 | options || (options = {}); 1219 | if (options.routes) this.routes = options.routes; 1220 | this._bindRoutes(); 1221 | this.initialize.apply(this, arguments); 1222 | }; 1223 | 1224 | // Cached regular expressions for matching named param parts and splatted 1225 | // parts of route strings. 1226 | var optionalParam = /\((.*?)\)/g; 1227 | var namedParam = /(\(\?)?:\w+/g; 1228 | var splatParam = /\*\w+/g; 1229 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1230 | 1231 | // Set up all inheritable **Backbone.Router** properties and methods. 1232 | _.extend(Router.prototype, Events, { 1233 | 1234 | // Initialize is an empty function by default. Override it with your own 1235 | // initialization logic. 1236 | initialize: function(){}, 1237 | 1238 | // Manually bind a single named route to a callback. For example: 1239 | // 1240 | // this.route('search/:query/p:num', 'search', function(query, num) { 1241 | // ... 1242 | // }); 1243 | // 1244 | route: function(route, name, callback) { 1245 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1246 | if (_.isFunction(name)) { 1247 | callback = name; 1248 | name = ''; 1249 | } 1250 | if (!callback) callback = this[name]; 1251 | var router = this; 1252 | Backbone.history.route(route, function(fragment) { 1253 | var args = router._extractParameters(route, fragment); 1254 | router.execute(callback, args); 1255 | router.trigger.apply(router, ['route:' + name].concat(args)); 1256 | router.trigger('route', name, args); 1257 | Backbone.history.trigger('route', router, name, args); 1258 | }); 1259 | return this; 1260 | }, 1261 | 1262 | // Execute a route handler with the provided parameters. This is an 1263 | // excellent place to do pre-route setup or post-route cleanup. 1264 | execute: function(callback, args) { 1265 | if (callback) callback.apply(this, args); 1266 | }, 1267 | 1268 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1269 | navigate: function(fragment, options) { 1270 | Backbone.history.navigate(fragment, options); 1271 | return this; 1272 | }, 1273 | 1274 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1275 | // order of the routes here to support behavior where the most general 1276 | // routes can be defined at the bottom of the route map. 1277 | _bindRoutes: function() { 1278 | if (!this.routes) return; 1279 | this.routes = _.result(this, 'routes'); 1280 | var route, routes = _.keys(this.routes); 1281 | while ((route = routes.pop()) != null) { 1282 | this.route(route, this.routes[route]); 1283 | } 1284 | }, 1285 | 1286 | // Convert a route string into a regular expression, suitable for matching 1287 | // against the current location hash. 1288 | _routeToRegExp: function(route) { 1289 | route = route.replace(escapeRegExp, '\\$&') 1290 | .replace(optionalParam, '(?:$1)?') 1291 | .replace(namedParam, function(match, optional) { 1292 | return optional ? match : '([^/?]+)'; 1293 | }) 1294 | .replace(splatParam, '([^?]*?)'); 1295 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); 1296 | }, 1297 | 1298 | // Given a route, and a URL fragment that it matches, return the array of 1299 | // extracted decoded parameters. Empty or unmatched parameters will be 1300 | // treated as `null` to normalize cross-browser behavior. 1301 | _extractParameters: function(route, fragment) { 1302 | var params = route.exec(fragment).slice(1); 1303 | return _.map(params, function(param, i) { 1304 | // Don't decode the search params. 1305 | if (i === params.length - 1) return param || null; 1306 | return param ? decodeURIComponent(param) : null; 1307 | }); 1308 | } 1309 | 1310 | }); 1311 | 1312 | // Backbone.History 1313 | // ---------------- 1314 | 1315 | // Handles cross-browser history management, based on either 1316 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1317 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1318 | // and URL fragments. If the browser supports neither (old IE, natch), 1319 | // falls back to polling. 1320 | var History = Backbone.History = function() { 1321 | this.handlers = []; 1322 | _.bindAll(this, 'checkUrl'); 1323 | 1324 | // Ensure that `History` can be used outside of the browser. 1325 | if (typeof window !== 'undefined') { 1326 | this.location = window.location; 1327 | this.history = window.history; 1328 | } 1329 | }; 1330 | 1331 | // Cached regex for stripping a leading hash/slash and trailing space. 1332 | var routeStripper = /^[#\/]|\s+$/g; 1333 | 1334 | // Cached regex for stripping leading and trailing slashes. 1335 | var rootStripper = /^\/+|\/+$/g; 1336 | 1337 | // Cached regex for detecting MSIE. 1338 | var isExplorer = /msie [\w.]+/; 1339 | 1340 | // Cached regex for removing a trailing slash. 1341 | var trailingSlash = /\/$/; 1342 | 1343 | // Cached regex for stripping urls of hash. 1344 | var pathStripper = /#.*$/; 1345 | 1346 | // Has the history handling already been started? 1347 | History.started = false; 1348 | 1349 | // Set up all inheritable **Backbone.History** properties and methods. 1350 | _.extend(History.prototype, Events, { 1351 | 1352 | // The default interval to poll for hash changes, if necessary, is 1353 | // twenty times a second. 1354 | interval: 50, 1355 | 1356 | // Are we at the app root? 1357 | atRoot: function() { 1358 | return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; 1359 | }, 1360 | 1361 | // Gets the true hash value. Cannot use location.hash directly due to bug 1362 | // in Firefox where location.hash will always be decoded. 1363 | getHash: function(window) { 1364 | var match = (window || this).location.href.match(/#(.*)$/); 1365 | return match ? match[1] : ''; 1366 | }, 1367 | 1368 | // Get the cross-browser normalized URL fragment, either from the URL, 1369 | // the hash, or the override. 1370 | getFragment: function(fragment, forcePushState) { 1371 | if (fragment == null) { 1372 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 1373 | fragment = decodeURI(this.location.pathname + this.location.search); 1374 | var root = this.root.replace(trailingSlash, ''); 1375 | if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); 1376 | } else { 1377 | fragment = this.getHash(); 1378 | } 1379 | } 1380 | return fragment.replace(routeStripper, ''); 1381 | }, 1382 | 1383 | // Start the hash change handling, returning `true` if the current URL matches 1384 | // an existing route, and `false` otherwise. 1385 | start: function(options) { 1386 | if (History.started) throw new Error("Backbone.history has already been started"); 1387 | History.started = true; 1388 | 1389 | // Figure out the initial configuration. Do we need an iframe? 1390 | // Is pushState desired ... is it available? 1391 | this.options = _.extend({root: '/'}, this.options, options); 1392 | this.root = this.options.root; 1393 | this._wantsHashChange = this.options.hashChange !== false; 1394 | this._wantsPushState = !!this.options.pushState; 1395 | this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); 1396 | var fragment = this.getFragment(); 1397 | var docMode = document.documentMode; 1398 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 1399 | 1400 | // Normalize root to always include a leading and trailing slash. 1401 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1402 | 1403 | if (oldIE && this._wantsHashChange) { 1404 | var frame = Backbone.$('