├── CHANGELOG.md ├── manual ├── faq.md ├── overview.md ├── tutorial.md └── installation.md ├── .babelrc ├── .eslintignore ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── config.js ├── .eslintrc ├── .esdocrc ├── gulpfile.babel.js ├── config ├── bundle-config-travis.json └── bundle-config.json ├── src ├── GlobalRuntime.js ├── BackboneProxy.js ├── Debug.js ├── GlobalInclusiveRuntime.js ├── ModuleRuntime.js ├── extend.js ├── sync.js ├── Backbone.js ├── Utils.js ├── Router.js ├── View.js ├── History.js ├── Model.js └── Collection.js ├── package.json ├── README.md ├── LICENSE └── dist └── amd └── backbone.min.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /manual/faq.md: -------------------------------------------------------------------------------- 1 | FAQ! -------------------------------------------------------------------------------- /manual/overview.md: -------------------------------------------------------------------------------- 1 | Overview! -------------------------------------------------------------------------------- /manual/tutorial.md: -------------------------------------------------------------------------------- 1 | Tutorial! -------------------------------------------------------------------------------- /manual/installation.md: -------------------------------------------------------------------------------- 1 | Installation! -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config/ 2 | coverage/ 3 | dist/ 4 | docs/ 5 | jspm_packages/ 6 | node_modules/ 7 | config.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | npm-debug.log 4 | coverage/ 5 | docs/ 6 | node_modules/ 7 | jspm_packages/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "5.8.0" 5 | 6 | before_script: 7 | - npm install -g gulp 8 | - npm install -g jspm 9 | - jspm install 10 | 11 | script: gulp jspm-test-basic 12 | 13 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ####Corporate Copyright Statements 2 | ----- 3 | 4 | Copyright (c) 2015-present TyphonRT Inc. [@typhonrt](https://github.com/typhonrt) 5 | 6 | ####Contributors 7 | ----- 8 | 9 | Michael Leahy [@typhonrt](https://github.com/typhonrt) 10 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | defaultJSExtensions: true, 3 | transpiler: "babel", 4 | babelOptions: { 5 | "optional": [ 6 | "runtime", 7 | "optimisation.modules.system" 8 | ] 9 | }, 10 | paths: {}, 11 | map: {} 12 | }); -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads https://github.com/typhonjs/typhonjs-config-eslint/blob/master/2.0/es6/browser/.eslintrc 3 | * NPM: https://www.npmjs.com/package/typhonjs-config-eslint 4 | */ 5 | { 6 | "extends": "./node_modules/typhonjs-config-eslint/2.0/es6/browser/.eslintrc" 7 | } -------------------------------------------------------------------------------- /.esdocrc: -------------------------------------------------------------------------------- 1 | { 2 | "title": "backbone-es6", 3 | "source": "src", 4 | "destination": "docs", 5 | "plugins": [ { "name": "esdoc-plugin-jspm" } ], 6 | "manual": 7 | { 8 | "overview": ["./manual/overview.md"], 9 | "installation": ["./manual/installation.md"], 10 | "tutorial": ["./manual/tutorial.md"], 11 | "faq": ["./manual/faq.md"], 12 | "changelog": ["./CHANGELOG.md"] 13 | } 14 | } -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Please see `typhonjs-core-gulptasks` (https://www.npmjs.com/package/typhonjs-core-gulptasks) 3 | */ 4 | import gulp from 'gulp'; 5 | import gulpTasks from 'typhonjs-core-gulptasks'; 6 | 7 | // Import all tasks and set `rootPath` to the base project path and `srcGlob` to all JS sources in `./src`. 8 | gulpTasks(gulp, 9 | { 10 | rootPath: __dirname, 11 | srcGlob: ['./src/**/*.js'] 12 | }); 13 | -------------------------------------------------------------------------------- /config/bundle-config-travis.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": 3 | [ 4 | { 5 | "inMemoryBuild": true, 6 | "formats": ["amd"], 7 | "mangle": false, 8 | "minify": false, 9 | "src": "src/ModuleRuntime.js", 10 | "extraConfig": 11 | { 12 | "meta": 13 | { 14 | "jquery": { "build": false }, 15 | "underscore": { "build": false } 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/GlobalRuntime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GlobalRuntime.js -- Initializes Backbone and sets it to "root".Backbone if it exists. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import Backbone from './ModuleRuntime.js'; 8 | 9 | // Establish the root object, `window` (`self`) in the browser, or `global` on the server. 10 | // We use `self` instead of `window` for `WebWorker` support. 11 | const root = (typeof self === 'object' && self.self === self && self) || 12 | (typeof global === 'object' && global.global === global && global); 13 | 14 | if (typeof root !== 'undefined' && root !== null) 15 | { 16 | root.Backbone = Backbone; 17 | } 18 | else 19 | { 20 | throw new Error('Could not find a valid global object.'); 21 | } 22 | 23 | export default Backbone; -------------------------------------------------------------------------------- /src/BackboneProxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BackboneProxy -- Provides a proxy for the actual created Backbone instance. This is initialized in the constructor 3 | * for Backbone (backbone-es6/src/Backbone.js). Anywhere a reference is needed for the composed Backbone instance 4 | * import BackboneProxy and access it by "BackboneProxy.backbone". 5 | * 6 | * @example 7 | * import BackboneProxy from 'backbone-es6/src/BackboneProxy.js'; 8 | * 9 | * BackboneProxy.backbone.sync(...) 10 | */ 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * Defines a proxy Object to hold a reference of the Backbone object instantiated. 16 | * 17 | * @type {{backbone: null}} 18 | */ 19 | const BackboneProxy = 20 | { 21 | backbone: null 22 | }; 23 | 24 | export default BackboneProxy; -------------------------------------------------------------------------------- /src/Debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const s_DEBUG_LOG = false; 4 | const s_DEBUG_TRACE = false; 5 | 6 | /* eslint-disable no-console */ 7 | 8 | /** 9 | * Debug.js - Provides basic logging functionality that can be turned on via setting s_DEBUG_LOG = true; 10 | * 11 | * This is temporary until stability is fully tested. 12 | */ 13 | export default class Debug 14 | { 15 | /** 16 | * Posts a log message to console. 17 | * 18 | * @param {string} message - A message to log 19 | * @param {boolean} trace - A boolean indicating whether to also log `console.trace()` 20 | */ 21 | static log(message, trace = s_DEBUG_TRACE) 22 | { 23 | if (s_DEBUG_LOG) 24 | { 25 | console.log(message); 26 | } 27 | 28 | if (s_DEBUG_LOG && trace) 29 | { 30 | console.trace(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/GlobalInclusiveRuntime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GlobalInclusiveRuntime.js (Backbone) -- Initializes jQuery and Underscore setting them to "root.$", "root.jQuery" 3 | * and "root._" respectively before delegating to GlobalRuntime.js to initialize Backbone. "root" is defined as self in 4 | * the browser or global if on the server. 5 | * 6 | * Note: We use CJS here as ES6 imports are hoisted. We must set root.$ before initializing Backbone. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /* eslint-disable no-var */ 12 | 13 | // Establish the root object, `window` (`self`) in the browser, or `global` on the server. 14 | // We use `self` instead of `window` for `WebWorker` support. 15 | var root = (typeof self === 'object' && self.self === self && self) || 16 | (typeof global === 'object' && global.global === global && global); 17 | 18 | if (typeof root !== 'undefined' && root !== null) 19 | { 20 | root.$ = root.jQuery = require('jquery'); 21 | root._ = require('underscore'); 22 | } 23 | else 24 | { 25 | throw new Error('Could not find a valid global object.'); 26 | } 27 | 28 | require('./GlobalRuntime.js'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-es6", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/typhonjs-backbone/backbone-es6", 5 | "description": "A fork of Backbone converting it to ES6.", 6 | "license": "MPL-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/typhonjs-backbone/backbone-es6.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/typhonjs-backbone/backbone-es6/issues" 13 | }, 14 | "jspm": { 15 | "main": "src/ModuleRuntime.js", 16 | "dependencies": { 17 | "typhonjs-core-backbone-events": "github:typhonjs-backbone/typhonjs-core-backbone-events@master", 18 | "underscore": "npm:underscore@^1.0.0" 19 | }, 20 | "devDependencies": { 21 | "babel": "npm:babel-core@^5.0.0", 22 | "babel-runtime": "npm:babel-runtime@^5.0.0", 23 | "core-js": "npm:core-js@^1.0.0", 24 | "jquery": "npm:jquery@^2.0.0" 25 | } 26 | }, 27 | "devDependencies": { 28 | "gulp": "^3.0.0", 29 | "jspm": "^0.16.0", 30 | "typhonjs-config-eslint": "^0.4.0", 31 | "typhonjs-core-gulptasks": "^0.6.0", 32 | "typhonjs-npm-build-test": "^0.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ModuleRuntime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ModuleRuntime.js -- Provides the standard / default configuration that is the same as Backbone 1.2.3 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import Backbone from './Backbone.js'; 8 | import Collection from './Collection.js'; 9 | import Events from 'typhonjs-core-backbone-events/src/Events.js'; 10 | import History from './History.js'; 11 | import Model from './Model.js'; 12 | import Router from './Router.js'; 13 | import View from './View.js'; 14 | 15 | import extend from './extend.js'; 16 | import sync from './sync.js'; 17 | 18 | const options = 19 | { 20 | // Current version of the library. Keep in sync with Backbone version supported. 21 | VERSION: '1.3.3', 22 | 23 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will fake `"PATCH"`, `"PUT"` and 24 | // `"DELETE"` requests via the `_method` parameter and set a `X-Http-Method-Override` header. 25 | emulateHTTP: false, 26 | 27 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct `application/json` requests ... this 28 | // will encode the body as `application/x-www-form-urlencoded` instead and will send the model in a form param 29 | // named `model`. 30 | emulateJSON: false 31 | }; 32 | 33 | const backbone = new Backbone(Collection, Events, History, Model, Router, View, sync, options); 34 | 35 | // Set up older extends inheritance support for the model, collection, router, view and history. 36 | backbone.Model.extend = backbone.Collection.extend = backbone.Router.extend = backbone.View.extend = 37 | backbone.History.extend = extend; 38 | 39 | export default backbone; -------------------------------------------------------------------------------- /src/extend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | 5 | /** 6 | * Provides older "extend" functionality for Backbone. While it is still accessible it is recommended 7 | * to adopt the new Backbone-ES6 patterns and ES6 sub-classing via "extends". 8 | * 9 | * Helper function to correctly set up the prototype chain for subclasses. Similar to `goog.inherits`, but uses a hash 10 | * of prototype properties and class properties to be extended. 11 | * 12 | * @see http://backbonejs.org/#Collection-extend 13 | * @see http://backbonejs.org/#Model-extend 14 | * @see http://backbonejs.org/#Router-extend 15 | * @see http://backbonejs.org/#View-extend 16 | * 17 | * @param {object} protoProps - instance properties 18 | * @param {object} staticProps - class properties 19 | * @returns {*} Subclass of parent class. 20 | */ 21 | export default function extend(protoProps, staticProps) 22 | { 23 | const parent = this; 24 | let child; 25 | 26 | // The constructor function for the new subclass is either defined by you (the "constructor" property in your 27 | // `extend` definition), or defaulted by us to simply call the parent constructor. 28 | if (protoProps && _.has(protoProps, 'constructor')) 29 | { 30 | child = protoProps.constructor; 31 | } 32 | else 33 | { 34 | child = function() 35 | { 36 | return parent.apply(this, arguments); 37 | }; 38 | } 39 | 40 | // Add static properties to the constructor function, if supplied. 41 | _.extend(child, parent, staticProps); 42 | 43 | // Set the prototype chain to inherit from `parent`, without calling 44 | // `parent`'s constructor function and add the prototype properties. 45 | child.prototype = _.create(parent.prototype, protoProps); 46 | child.prototype.constructor = child; 47 | 48 | // backbone-es6 addition: Because View defines a getter for tagName we must actually redefine this getter 49 | // from the `protoProps.tagName` if it exists. 50 | if (protoProps && protoProps.tagName) 51 | { 52 | Object.defineProperty(child.prototype, 'tagName', { get: () => { return protoProps.tagName; } }); 53 | } 54 | 55 | // Set a convenience property in case the parent's prototype is needed later. 56 | child.__super__ = parent.prototype; 57 | 58 | return child; 59 | } -------------------------------------------------------------------------------- /config/bundle-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": 3 | [ 4 | { 5 | "destBaseDir": "./dist", 6 | "destFilename": "backbone.js", 7 | "formats": ["amd", "cjs"], 8 | "mangle": false, 9 | "minify": false, 10 | "src": "src/ModuleRuntime.js", 11 | "extraConfig": 12 | { 13 | "meta": 14 | { 15 | "jquery": { "build": false }, 16 | "underscore": { "build": false } 17 | } 18 | } 19 | }, 20 | 21 | { 22 | "destBaseDir": "./dist", 23 | "destFilename": "backbone.js", 24 | "formats": ["umd", "global"], 25 | "mangle": false, 26 | "minify": false, 27 | "src": "src/GlobalRuntime.js", 28 | "extraConfig": 29 | { 30 | "meta": 31 | { 32 | "jquery": { "build": false }, 33 | "underscore": { "build": false } 34 | } 35 | }, 36 | "builderOptions": 37 | { 38 | "globalDeps": 39 | { 40 | "jquery": "$", 41 | "underscore": "_" 42 | } 43 | } 44 | }, 45 | 46 | { 47 | "destBaseDir": "./dist", 48 | "destFilename": "backbone.min.js", 49 | "formats": ["amd", "cjs"], 50 | "mangle": true, 51 | "minify": true, 52 | "src": "src/ModuleRuntime.js", 53 | "extraConfig": 54 | { 55 | "meta": 56 | { 57 | "jquery": { "build": false }, 58 | "underscore": { "build": false } 59 | } 60 | } 61 | }, 62 | 63 | { 64 | "destBaseDir": "./dist", 65 | "destFilename": "backbone.min.js", 66 | "formats": ["umd", "global"], 67 | "mangle": true, 68 | "minify": true, 69 | "src": "src/GlobalRuntime.js", 70 | "extraConfig": 71 | { 72 | "meta": 73 | { 74 | "jquery": { "build": false }, 75 | "underscore": { "build": false } 76 | } 77 | }, 78 | "builderOptions": 79 | { 80 | "globalName": "Backbone", 81 | "globalDeps": 82 | { 83 | "jquery": "$", 84 | "underscore": "_" 85 | } 86 | } 87 | }, 88 | 89 | { 90 | "destBaseDir": "./dist", 91 | "destFilename": "backbone-inclusive.js", 92 | "formats": ["global"], 93 | "mangle": false, 94 | "minify": false, 95 | "src": "src/GlobalInclusiveRuntime.js", 96 | "extraConfig": 97 | { 98 | "meta": 99 | { 100 | "src/GlobalInclusiveRuntime.js": { "format": "cjs" } 101 | } 102 | } 103 | }, 104 | 105 | { 106 | "destBaseDir": "./dist", 107 | "destFilename": "backbone-inclusive.min.js", 108 | "formats": ["global"], 109 | "mangle": true, 110 | "minify": true, 111 | "src": "src/GlobalInclusiveRuntime.js", 112 | "extraConfig": 113 | { 114 | "meta": 115 | { 116 | "src/GlobalInclusiveRuntime.js": { "format": "cjs" } 117 | } 118 | } 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![backbone-es6](https://i.imgur.com/KKkgP8P.png) 2 | 3 | [![Backbone](https://img.shields.io/badge/backbone-1.3.3-yellowgreen.svg?style=flat)](https://github.com/jashkenas/backbone) 4 | [![Documentation](http://docs.typhonjs.io/typhonjs-backbone/backbone-es6/badge.svg)](http://docs.typhonjs.io/typhonjs-backbone/backbone-es6/) 5 | [![Code Style](https://img.shields.io/badge/code%20style-allman-yellowgreen.svg?style=flat)](https://en.wikipedia.org/wiki/Indent_style#Allman_style) 6 | [![License](https://img.shields.io/badge/license-MPLv2-yellowgreen.svg?style=flat)](https://github.com/typhonjs-backbone/backbone-es6/blob/master/LICENSE) 7 | [![Gitter](https://img.shields.io/gitter/room/typhonjs/TyphonJS.svg)](https://gitter.im/typhonjs/TyphonJS) 8 | 9 | [![Build Status](https://travis-ci.org/typhonjs-backbone/backbone-es6.svg?branch=master)](https://travis-ci.org/typhonjs-backbone/backbone-es6) 10 | [![Dependency Status](https://www.versioneye.com/user/projects/56eb95004e714c003625c72a/badge.svg?style=flat)](https://www.versioneye.com/user/projects/56eb95004e714c003625c72a) 11 | 12 | Backbone supplies structure to JavaScript-heavy applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface. 13 | 14 | backbone-es6 is a fork of Backbone (https://github.com/jashkenas/backbone) converting and modularizing it into idiomatic ES6. The impetus for this fork is to experiment with modernizing and making Backbone easier to modify in a granular fashion. In particular the [Parse JS SDK](http://www.parse.com) previously also was a fork of Backbone, but with the 1.6+ SDK release the Backbone API was unceremoniously removed. backbone-es6 provides the base for [backbone-parse-es6](https://github.com/typhonjs-backbone-parse/backbone-parse-es6) which provides a solution for Backbone dependent Parse users. 15 | 16 | Another reason for backbone-es6 is supporting end to end documentation via ESDoc for ES6 frameworks and apps built on top of backbone-es6. An integrated build and testing NPM module [typhonjs-npm-build-test](https://github.com/typhonjs-node-npm-scripts/typhonjs-npm-build-test) including several plugins for ESDoc along with a complete integrated set of Gulp tasks, [typhonjs-core-gulptasks](https://github.com/typhonjs-node-gulp/typhonjs-core-gulptasks) provide documentation generation across multiple modules / source roots via JSPM along with ESLint and several JSPM & NPM tasks. 17 | 18 | backbone-es6 uses [JSPM](http://www.jspm.io) / [SystemJS](https://github.com/systemjs/systemjs) for dependency management and bundling distributions. For an example of using JSPM / SystemJS directly with backbone-es6 & Backbone.localStorage including typhonjs-core-gulptasks support please see these demo repos: 19 | 20 | - https://github.com/typhonjs-demos/backbone-es6-localstorage-todos 21 | - https://github.com/typhonjs-demos/electron-backbone-es6-localstorage-todos (Electron desktop version) 22 | 23 | Update (05/12/16): backbone-es6 has been updated with the latest Backbone 1.3.3 changes. After Backbone is updated to 1.4 and backbone-es6 is updated a full test suite will also be created including Nightmare JS / browser testing. The final planned upgrade is to then modify backbone-es6 completing full modularization by splitting it into several separate repos which will still be collected and exposed through this repo. This final separation will allow each component of Backbone to potentially be used independently with minimal dependencies including server side usage. 24 | 25 | This repository contains several pre-packed downloads in the `dist/` directory. There are AMD, CJS, and Global distributions. The "global-inclusive" bundle includes the latest jQuery (2.2.3) and Underscore (1.8.3) libraries. 26 | 27 | API documentation can be generated locally and is also found online here: 28 | http://docs.typhonjs.io/typhonjs-backbone/backbone-es6/ 29 | 30 | For original Backbone Docs, License, Tests, pre-packed downloads, see: 31 | http://backbonejs.org 32 | 33 | To suggest a feature or report a bug: 34 | https://github.com/typhonjs-backbone/backbone-es6/issues 35 | 36 | Many thanks to DocumentCloud & all Backbone contributors. 37 | 38 | Backbone (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 39 | 40 | backbone-es6 (c) 2015-present Michael Leahy, TyphonRT Inc. 41 | 42 | backbone-es6 may be freely distributed under the MPL v2.0 license. 43 | -------------------------------------------------------------------------------- /src/sync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import BackboneProxy from './BackboneProxy.js'; 5 | import Utils from './Utils.js'; 6 | 7 | /** 8 | * Map from CRUD to HTTP for our default `Backbone.sync` implementation. 9 | * @type {{create: string, update: string, patch: string, delete: string, read: string}} 10 | */ 11 | const s_METHOD_MAP = 12 | { 13 | 'create': 'POST', 14 | 'update': 'PUT', 15 | 'patch': 'PATCH', 16 | 'delete': 'DELETE', 17 | 'read': 'GET' 18 | }; 19 | 20 | /** 21 | * Backbone.sync - Persists models to the server. (http://backbonejs.org/#Sync) 22 | * ------------- 23 | * 24 | * Override this function to change the manner in which Backbone persists models to the server. You will be passed the 25 | * type of request, and the model in question. By default, makes a RESTful Ajax request to the model's `url()`. Some 26 | * possible customizations could be: 27 | * 28 | * Use `setTimeout` to batch rapid-fire updates into a single request. 29 | * Send up the models as XML instead of JSON. 30 | * Persist models via WebSockets instead of Ajax. 31 | * 32 | * Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests as `POST`, with a `_method` parameter 33 | * containing the true HTTP method, as well as all requests with the body as `application/x-www-form-urlencoded` 34 | * instead of `application/json` with the model in a param named `model`. Useful when interfacing with server-side 35 | * languages like **PHP** that make it difficult to read the body of `PUT` requests. 36 | * 37 | * @param {string} method - A string that defines the synchronization action to perform. 38 | * @param {Model|Collection} model - The model or collection instance to synchronize. 39 | * @param {object} options - Optional parameters 40 | * @returns {XMLHttpRequest} An XMLHttpRequest 41 | */ 42 | export default function sync(method, model, options = {}) 43 | { 44 | const type = s_METHOD_MAP[method]; 45 | 46 | // Default options, unless specified. 47 | _.defaults(options, 48 | { 49 | emulateHTTP: BackboneProxy.backbone.emulateHTTP, 50 | emulateJSON: BackboneProxy.backbone.emulateJSON 51 | }); 52 | 53 | // Default JSON-request options. 54 | const params = { type, dataType: 'json' }; 55 | 56 | // Ensure that we have a URL. 57 | if (!options.url) 58 | { 59 | params.url = _.result(model, 'url') || Utils.urlError(); 60 | } 61 | 62 | // Ensure that we have the appropriate request data. 63 | if (Utils.isNullOrUndef(options.data) && model && (method === 'create' || method === 'update' || method === 'patch')) 64 | { 65 | params.contentType = 'application/json'; 66 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 67 | } 68 | 69 | // For older servers, emulate JSON by encoding the request into an HTML-form. 70 | if (options.emulateJSON) 71 | { 72 | params.contentType = 'application/x-www-form-urlencoded'; 73 | params.data = params.data ? { model: params.data } : {}; 74 | } 75 | 76 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 77 | // And an `X-HTTP-Method-Override` header. 78 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) 79 | { 80 | params.type = 'POST'; 81 | 82 | if (options.emulateJSON) { params.data._method = type; } 83 | 84 | const beforeSend = options.beforeSend; 85 | 86 | options.beforeSend = function(xhr) 87 | { 88 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 89 | if (beforeSend) { return beforeSend.apply(this, arguments); } 90 | }; 91 | } 92 | 93 | // Don't process data on a non-GET request. 94 | if (params.type !== 'GET' && !options.emulateJSON) 95 | { 96 | params.processData = false; 97 | } 98 | 99 | // Pass along `textStatus` and `errorThrown` from jQuery. 100 | const error = options.error; 101 | 102 | options.error = function(xhr, textStatus, errorThrown) 103 | { 104 | options.textStatus = textStatus; 105 | options.errorThrown = errorThrown; 106 | if (error) { error.call(options.context, xhr, textStatus, errorThrown); } 107 | }; 108 | 109 | // Make the request, allowing the user to override any Ajax options. 110 | const xhr = options.xhr = BackboneProxy.backbone.ajax(_.extend(params, options)); 111 | 112 | model.trigger('request', model, xhr, options); 113 | 114 | return xhr; 115 | } -------------------------------------------------------------------------------- /src/Backbone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | import _ from 'underscore'; 5 | import BackboneProxy from './BackboneProxy.js'; 6 | 7 | /** 8 | * Backbone.js 9 | * 10 | * (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 11 | * Backbone may be freely distributed under the MIT license. 12 | * 13 | * For all details and documentation: 14 | * http://backbonejs.org 15 | * 16 | * --------- 17 | * 18 | * backbone-es6 19 | * https://github.com/typhonjs/backbone-es6 20 | * (c) 2015 Michael Leahy 21 | * backbone-es6 may be freely distributed under the MPLv2 license. 22 | * 23 | * This fork of Backbone converts it to ES6 and provides extension through constructor injection for easy modification. 24 | * The only major difference from Backbone is that Backbone itself is not a global Events instance anymore. Please 25 | * see @link{Events.js} for documentation on easily setting up an ES6 event module for global usage. 26 | * 27 | * @see http://backbonejs.org 28 | * @see https://github.com/typhonjs/backbone-es6 29 | * @author Michael Leahy 30 | * @version 1.3.3 31 | * @copyright Michael Leahy 2015 32 | */ 33 | export default class Backbone 34 | { 35 | /** 36 | * Initializes Backbone by constructor injection. You may provide variations on any component below by passing 37 | * in a different version. The "runtime" initializing Backbone is responsible for further modification like 38 | * supporting the older "extend" support. See backbone-es6/src/ModuleRuntime.js and backbone-es6/src/extend.js 39 | * for an example on composing Backbone for usage. 40 | * 41 | * @param {Collection} Collection - A class defining Backbone.Collection. 42 | * @param {Events} Events - A class defining Backbone.Events. 43 | * @param {History} History - A class defining Backbone.History. 44 | * @param {Model} Model - A class defining Backbone.Model. 45 | * @param {Router} Router - A class defining Backbone.Router. 46 | * @param {View} View - A class defining Backbone.View. 47 | * @param {function} sync - A function defining synchronization for Collection & Model. 48 | * @param {object} options - Options to mixin to Backbone. 49 | * @constructor 50 | */ 51 | constructor(Collection, Events, History, Model, Router, View, sync, options = {}) 52 | { 53 | /** 54 | * Establish the root object, `window` (`self`) in the browser, or `global` on the server. 55 | * We use `self` instead of `window` for `WebWorker` support. 56 | * 57 | * @type {object|global} 58 | */ 59 | const root = (typeof self === 'object' && self.self === self && self) || 60 | (typeof global === 'object' && global.global === global && global); 61 | 62 | /** 63 | * jQuery or equivalent 64 | * @type {*} 65 | */ 66 | this.$ = ($ || root.jQuery || root.Zepto || root.ender || root.$); 67 | 68 | if (typeof this.$ === 'undefined') 69 | { 70 | throw new Error("Backbone - ctor - could not locate global '$' (jQuery or equivalent)."); 71 | } 72 | 73 | /** 74 | * Initial setup. Mixin options and set the BackboneProxy instance to this. 75 | */ 76 | if (_.isObject(options)) 77 | { 78 | _.extend(this, options); 79 | } 80 | 81 | BackboneProxy.backbone = this; 82 | 83 | /** 84 | * A public reference of the Collection class. 85 | * @class 86 | */ 87 | this.Collection = Collection; 88 | 89 | /** 90 | * A public reference of the Events class. 91 | * @class 92 | */ 93 | this.Events = Events; 94 | 95 | /** 96 | * A public reference of the History class. 97 | * @class 98 | */ 99 | this.History = History; 100 | 101 | /** 102 | * A public reference of the Model class. 103 | * @class 104 | */ 105 | this.Model = Model; 106 | 107 | /** 108 | * A public reference of the Router class. 109 | * @class 110 | */ 111 | this.Router = Router; 112 | 113 | /** 114 | * A public reference of the View class. 115 | * @class 116 | */ 117 | this.View = View; 118 | 119 | /** 120 | * A public instance of History. 121 | * @instance 122 | */ 123 | this.history = new History(); 124 | 125 | /** 126 | * A public instance of the sync function. 127 | * @instance 128 | */ 129 | this.sync = sync; 130 | 131 | /** 132 | * Set the default implementation of `Backbone.ajax` to proxy through to `$`. 133 | * Override this if you'd like to use a different library. 134 | * 135 | * @returns {XMLHttpRequest} XMLHttpRequest 136 | */ 137 | this.ajax = () => 138 | { 139 | return this.$.ajax(...arguments); 140 | }; 141 | } 142 | } -------------------------------------------------------------------------------- /src/Utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import BackboneProxy from './BackboneProxy.js'; 5 | 6 | /** 7 | * Provides static utility functions. 8 | * -------- 9 | * 10 | * Proxy Backbone class methods to Underscore functions, wrapping the model's `attributes` object or collection's 11 | * `models` array behind the scenes. 12 | * 13 | * `Function#apply` can be slow so we use the method's arg count, if we know it. 14 | * 15 | * @example 16 | * collection.filter(function(model) { return model.get('age') > 10 }); 17 | * collection.each(this.addView); 18 | */ 19 | export default class Utils 20 | { 21 | /** 22 | * Adds Underscore methods if they exist from keys of the `methods` hash to `Class` running against the variable 23 | * defined by `attribute` 24 | * 25 | * @param {Class} Class - Class to add Underscore methods to. 26 | * @param {object} methods - Hash with keys as method names and values as argument length. 27 | * @param {string} attribute - The variable to run Underscore methods against. Often "attributes" 28 | */ 29 | static addUnderscoreMethods(Class, methods, attribute) 30 | { 31 | _.each(methods, (length, method) => 32 | { 33 | if (_[method]) { Class.prototype[method] = s_ADD_METHOD(length, method, attribute); } 34 | }); 35 | } 36 | 37 | /** 38 | * Method for checking whether an unknown variable is an instance of `Backbone.Model`. 39 | * 40 | * @param {*} unknown - Variable to test. 41 | * @returns {boolean} 42 | */ 43 | static isModel(unknown) 44 | { 45 | return unknown instanceof BackboneProxy.backbone.Model; 46 | } 47 | 48 | /** 49 | * Method for checking whether a variable is undefined or null. 50 | * 51 | * @param {*} unknown - Variable to test. 52 | * @returns {boolean} 53 | */ 54 | static isNullOrUndef(unknown) 55 | { 56 | return unknown === null || typeof unknown === 'undefined'; 57 | } 58 | 59 | /** 60 | * Throw an error when a URL is needed, and none is supplied. 61 | */ 62 | static urlError() 63 | { 64 | throw new Error('A "url" property or function must be specified'); 65 | } 66 | 67 | /** 68 | * Wrap an optional error callback with a fallback error event. 69 | * 70 | * @param {Model|Collection} model - Model or Collection target to construct and error callback against. 71 | * @param {object} options - Options hash to store error callback inside. 72 | */ 73 | static wrapError(model, options) 74 | { 75 | const error = options.error; 76 | options.error = (resp) => 77 | { 78 | if (error) { error.call(options.context, model, resp, options); } 79 | model.trigger('error', model, resp, options); 80 | }; 81 | } 82 | } 83 | 84 | // Private / internal methods --------------------------------------------------------------------------------------- 85 | 86 | /** 87 | * Creates an optimized function that dispatches to an associated Underscore function. 88 | * 89 | * @param {number} length - Length of variables for given Underscore method to dispatch. 90 | * @param {string} method - Function name of Underscore to invoke. 91 | * @param {string} attribute - Attribute to associate with the Underscore function invoked. 92 | * @returns {Function} 93 | */ 94 | const s_ADD_METHOD = (length, method, attribute) => 95 | { 96 | switch (length) 97 | { 98 | case 1: 99 | return function() 100 | { 101 | return _[method](this[attribute]); 102 | }; 103 | case 2: 104 | return function(value) 105 | { 106 | return _[method](this[attribute], value); 107 | }; 108 | case 3: 109 | return function(iteratee, context) 110 | { 111 | return _[method](this[attribute], s_CB(iteratee), context); 112 | }; 113 | case 4: 114 | return function(iteratee, defaultVal, context) 115 | { 116 | return _[method](this[attribute], s_CB(iteratee), defaultVal, context); 117 | }; 118 | default: 119 | return function() 120 | { 121 | const args = Array.prototype.slice.call(arguments); 122 | args.unshift(this[attribute]); 123 | return _[method](...args); 124 | }; 125 | } 126 | }; 127 | 128 | /** 129 | * Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. 130 | * 131 | * @param {*} iteratee - 132 | * @returns {*} 133 | */ 134 | const s_CB = (iteratee) => 135 | { 136 | if (_.isFunction(iteratee)) { return iteratee; } 137 | if (_.isObject(iteratee) && !Utils.isModel(iteratee)) { return s_MODEL_MATCHER(iteratee); } 138 | if (_.isString(iteratee)) 139 | { 140 | return function(model) 141 | { 142 | return model.get(iteratee); 143 | }; 144 | } 145 | return iteratee; 146 | }; 147 | 148 | /** 149 | * Creates a matching function against `attrs`. 150 | * 151 | * @param {*} attrs - 152 | * @returns {Function} 153 | */ 154 | const s_MODEL_MATCHER = (attrs) => 155 | { 156 | const matcher = _.matches(attrs); 157 | return (model) => 158 | { 159 | return matcher(model.attributes); 160 | }; 161 | }; -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import BackboneProxy from './BackboneProxy.js'; 5 | import Events from 'typhonjs-core-backbone-events/src/Events.js'; 6 | 7 | /** 8 | * Backbone.Router - Provides methods for routing client-side pages, and connecting them to actions and events. 9 | * (http://backbonejs.org/#Router) 10 | * --------------- 11 | * Web applications often provide linkable, bookmarkable, shareable URLs for important locations in the app. Until 12 | * recently, hash fragments (#page) were used to provide these permalinks, but with the arrival of the History API, 13 | * it's now possible to use standard URLs (/page). Backbone.Router provides methods for routing client-side pages, and 14 | * connecting them to actions and events. For browsers which don't yet support the History API, the Router handles 15 | * graceful fallback and transparent translation to the fragment version of the URL. 16 | * 17 | * During page load, after your application has finished creating all of its routers, be sure to call 18 | * Backbone.history.start() or Backbone.history.start({pushState: true}) to route the initial URL. 19 | * 20 | * routes - router.routes 21 | * The routes hash maps URLs with parameters to functions on your router (or just direct function definitions, if you 22 | * prefer), similar to the View's events hash. Routes can contain parameter parts, :param, which match a single URL 23 | * component between slashes; and splat parts *splat, which can match any number of URL components. Part of a route can 24 | * be made optional by surrounding it in parentheses (/:optional). 25 | * 26 | * For example, a route of "search/:query/p:page" will match a fragment of #search/obama/p2, passing "obama" and "2" to 27 | * the action. 28 | * 29 | * A route of "file/*path" will match #file/folder/file.txt, passing "folder/file.txt" to the action. 30 | * 31 | * A route of "docs/:section(/:subsection)" will match #docs/faq and #docs/faq/installing, passing "faq" to the action 32 | * in the first case, and passing "faq" and "installing" to the action in the second. 33 | * 34 | * A nested optional route of "docs(/:section)(/:subsection)" will match #docs, #docs/faq, and #docs/faq/installing, 35 | * passing "faq" to the action in the second case, and passing "faq" and "installing" to the action in the third. 36 | * 37 | * Trailing slashes are treated as part of the URL, and (correctly) treated as a unique route when accessed. docs and 38 | * docs/ will fire different callbacks. If you can't avoid generating both types of URLs, you can define a "docs(/)" 39 | * matcher to capture both cases. 40 | * 41 | * When the visitor presses the back button, or enters a URL, and a particular route is matched, the name of the action 42 | * will be fired as an event, so that other objects can listen to the router, and be notified. In the following example, 43 | * visiting #help/uploading will fire a route:help event from the router. 44 | * 45 | * @example 46 | * routes: { 47 | * "help/:page": "help", 48 | * "download/*path": "download", 49 | * "folder/:name": "openFolder", 50 | * "folder/:name-:mode": "openFolder" 51 | * } 52 | * 53 | * router.on("route:help", function(page) { 54 | * ... 55 | * }); 56 | * 57 | * @example 58 | * Old extend - Backbone.Router.extend(properties, [classProperties]) 59 | * Get started by creating a custom router class. Define actions that are triggered when certain URL fragments are 60 | * matched, and provide a routes hash that pairs routes to actions. Note that you'll want to avoid using a leading 61 | * slash in your route definitions: 62 | * 63 | * var Workspace = Backbone.Router.extend({ 64 | * routes: { 65 | * "help": "help", // #help 66 | * "search/:query": "search", // #search/kiwis 67 | * "search/:query/p:page": "search" // #search/kiwis/p7 68 | * }, 69 | * 70 | * help: function() { 71 | * ... 72 | * }, 73 | * 74 | * search: function(query, page) { 75 | * ... 76 | * } 77 | * }); 78 | * 79 | * @example 80 | * Converting the above example to ES6 using a getter method for `routes`: 81 | * class Workspace extends Backbone.Router { 82 | * get routes() { 83 | * return { 84 | * "help": "help", // #help 85 | * "search/:query": "search", // #search/kiwis 86 | * "search/:query/p:page": "search" // #search/kiwis/p7 87 | * }; 88 | * } 89 | * 90 | * help() { 91 | * ... 92 | * }, 93 | * 94 | * search(query, page) { 95 | * ... 96 | * } 97 | * } 98 | * 99 | * @example 100 | * Basic default "no route router": 101 | * new Backbone.Router({ routes: { '*actions': 'defaultRoute' } }); 102 | */ 103 | export default class Router extends Events 104 | { 105 | /** 106 | * When creating a new router, you may pass its routes hash directly as an option, if you choose. All options will 107 | * also be passed to your initialize function, if defined. 108 | * 109 | * @see http://backbonejs.org/#Router-constructor 110 | * 111 | * @param {object} options - Optional parameters which may contain a "routes" object literal. 112 | */ 113 | constructor(options = {}) 114 | { 115 | super(); 116 | 117 | // Must detect if there are any getters defined in order to skip setting this value. 118 | const hasRoutesGetter = !_.isUndefined(this.routes); 119 | 120 | if (!hasRoutesGetter && options.routes) 121 | { 122 | /** 123 | * Stores the routes hash. 124 | * @type {object} 125 | */ 126 | this.routes = options.routes; 127 | } 128 | 129 | s_BIND_ROUTES(this); 130 | 131 | this.initialize(...arguments); 132 | } 133 | 134 | /* eslint-disable no-unused-vars */ 135 | /** 136 | * Execute a route handler with the provided parameters. This is an excellent place to do pre-route setup or 137 | * post-route cleanup. 138 | * 139 | * @see http://backbonejs.org/#Router-execute 140 | * 141 | * @param {function} callback - Callback function to execute. 142 | * @param {*[]} args - Arguments to apply to callback. 143 | * @param {string} name - Named route. 144 | */ 145 | execute(callback, args, name) 146 | { 147 | /* eslint-enable no-unused-vars */ 148 | if (callback) { callback.apply(this, args); } 149 | } 150 | 151 | /** 152 | * Initialize is an empty function by default. Override it with your own initialization logic. 153 | * 154 | * @see http://backbonejs.org/#Router-constructor 155 | * @abstract 156 | */ 157 | initialize() 158 | { 159 | } 160 | 161 | /** 162 | * Simple proxy to `Backbone.history` to save a fragment into the history. 163 | * 164 | * @see http://backbonejs.org/#Router-navigate 165 | * @see History 166 | * 167 | * @param {string} fragment - String representing an URL fragment. 168 | * @param {object} options - Optional hash containing parameters for navigate. 169 | * @returns {Router} 170 | */ 171 | navigate(fragment, options) 172 | { 173 | BackboneProxy.backbone.history.navigate(fragment, options); 174 | return this; 175 | } 176 | 177 | /** 178 | * Manually bind a single named route to a callback. For example: 179 | * 180 | * @example 181 | * this.route('search/:query/p:num', 'search', function(query, num) 182 | * { 183 | * ... 184 | * }); 185 | * 186 | * @see http://backbonejs.org/#Router-route 187 | * 188 | * @param {string|RegExp} route - A route string or regex. 189 | * @param {string} name - A name for the route. 190 | * @param {function} callback - A function to invoke when the route is matched. 191 | * @returns {Router} 192 | */ 193 | route(route, name, callback) 194 | { 195 | if (!_.isRegExp(route)) { route = s_ROUTE_TO_REGEX(route); } 196 | if (_.isFunction(name)) 197 | { 198 | callback = name; 199 | name = ''; 200 | } 201 | if (!callback) { callback = this[name]; } 202 | 203 | BackboneProxy.backbone.history.route(route, (fragment) => 204 | { 205 | const args = s_EXTRACT_PARAMETERS(route, fragment); 206 | 207 | if (this.execute(callback, args, name) !== false) 208 | { 209 | this.trigger(...([`route:${name}`].concat(args))); 210 | this.trigger('route', name, args); 211 | BackboneProxy.backbone.history.trigger('route', this, name, args); 212 | } 213 | }); 214 | 215 | return this; 216 | } 217 | } 218 | 219 | // Private / internal methods --------------------------------------------------------------------------------------- 220 | 221 | /** 222 | * Cached regular expressions for matching named param parts and splatted parts of route strings. 223 | * @type {RegExp} 224 | */ 225 | const s_ESCAPE_REGEX = /[\-{}\[\]+?.,\\\^$|#\s]/g; 226 | const s_NAMED_PARAM = /(\(\?)?:\w+/g; 227 | const s_OPTIONAL_PARAM = /\((.*?)\)/g; 228 | const s_SPLAT_PARAM = /\*\w+/g; 229 | 230 | /** 231 | * Bind all defined routes to `Backbone.history`. We have to reverse the order of the routes here to support behavior 232 | * where the most general routes can be defined at the bottom of the route map. 233 | * 234 | * @param {Router} router - Instance of `Backbone.Router`. 235 | */ 236 | const s_BIND_ROUTES = (router) => 237 | { 238 | if (!router.routes) { return; } 239 | 240 | router.routes = _.result(router, 'routes'); 241 | 242 | _.each(_.keys(router.routes), (route) => 243 | { 244 | router.route(route, router.routes[route]); 245 | }); 246 | }; 247 | 248 | /** 249 | * Given a route, and a URL fragment that it matches, return the array of extracted decoded parameters. Empty or 250 | * unmatched parameters will be treated as `null` to normalize cross-browser behavior. 251 | * 252 | * @param {string} route - A route string or regex. 253 | * @param {string} fragment - URL fragment. 254 | * @returns {*} 255 | */ 256 | const s_EXTRACT_PARAMETERS = (route, fragment) => 257 | { 258 | const params = route.exec(fragment).slice(1); 259 | 260 | return _.map(params, (param, i) => 261 | { 262 | // Don't decode the search params. 263 | if (i === params.length - 1) { return param || null; } 264 | return param ? decodeURIComponent(param) : null; 265 | }); 266 | }; 267 | 268 | /** 269 | * Convert a route string into a regular expression, suitable for matching against the current location hash. 270 | * 271 | * @param {string} route - A route string or regex. 272 | * @returns {RegExp} 273 | */ 274 | const s_ROUTE_TO_REGEX = (route) => 275 | { 276 | route = route.replace(s_ESCAPE_REGEX, '\\$&') 277 | .replace(s_OPTIONAL_PARAM, '(?:$1)?') 278 | .replace(s_NAMED_PARAM, (match, optional) => 279 | { 280 | return optional ? match : '([^/?]+)'; 281 | }) 282 | .replace(s_SPLAT_PARAM, '([^?]*?)'); 283 | return new RegExp(`^${route}(?:\\?([\\s\\S]*))?$`); 284 | }; -------------------------------------------------------------------------------- /src/View.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import BackboneProxy from './BackboneProxy.js'; 5 | import Events from 'typhonjs-core-backbone-events/src/Events.js'; 6 | 7 | /** 8 | * Backbone.View - Represents a logical chunk of UI in the DOM. (http://backbonejs.org/#View) 9 | * ------------- 10 | * 11 | * Backbone Views are almost more convention than they are actual code. A View is simply a JavaScript object that 12 | * represents a logical chunk of UI in the DOM. This might be a single item, an entire list, a sidebar or panel, or 13 | * even the surrounding frame which wraps your whole app. Defining a chunk of UI as a **View** allows you to define 14 | * your DOM events declaratively, without having to worry about render order ... and makes it easy for the view to 15 | * react to specific changes in the state of your models. 16 | * 17 | * Creating a Backbone.View creates its initial element outside of the DOM, if an existing element is not provided... 18 | * 19 | * Example if working with Backbone as ES6 source: 20 | * @example 21 | * 22 | * import Backbone from 'backbone'; 23 | * 24 | * export default class MyView extends Backbone.View 25 | * { 26 | * constructor(options) 27 | * { 28 | * super(options); 29 | * ... 30 | * } 31 | * 32 | * initialize() 33 | * { 34 | * ... 35 | * } 36 | * ... 37 | * } 38 | * 39 | * @example 40 | * 41 | * To use a custom $el / element define it by a getter method: 42 | * 43 | * get el() { return 'my-element'; } 44 | * 45 | * Likewise with events define it by a getter method: 46 | * 47 | * get events() 48 | * { 49 | * return { 50 | * 'submit form.login-form': 'logIn', 51 | * 'click .sign-up': 'signUp', 52 | * 'click .forgot-password': 'forgotPassword' 53 | * } 54 | * } 55 | */ 56 | export default class View extends Events 57 | { 58 | /** 59 | * The default `tagName` of a View's element is `"div"`. 60 | * 61 | * @returns {string} 62 | */ 63 | get tagName() { return 'div'; } 64 | 65 | /** 66 | * There are several special options that, if passed, will be attached directly to the view: model, collection, el, 67 | * id, className, tagName, attributes and events. If the view defines an initialize function, it will be called when 68 | * the view is first created. If you'd like to create a view that references an element already in the DOM, pass in 69 | * the element as an option: new View({el: existingElement}) 70 | * 71 | * @see http://backbonejs.org/#View-constructor 72 | * 73 | * @param {object} options - Default options which are mixed into this class as properties via `_.pick` against 74 | * s_VIEW_OPTIONS. Options also is passed onto the `initialize()` function. 75 | */ 76 | constructor(options) 77 | { 78 | super(); 79 | 80 | /** 81 | * Client ID 82 | * @type {number} 83 | */ 84 | this.cid = _.uniqueId('view'); 85 | 86 | _.extend(this, _.pick(options, s_VIEW_OPTIONS)); 87 | 88 | this._ensureElement(); 89 | this.initialize(...arguments); 90 | } 91 | 92 | /** 93 | * If jQuery is included on the page, each view has a $ function that runs queries scoped within the view's element. 94 | * If you use this scoped jQuery function, you don't have to use model ids as part of your query to pull out specific 95 | * elements in a list, and can rely much more on HTML class attributes. It's equivalent to running: 96 | * view.$el.find(selector) 97 | * 98 | * @see https://api.jquery.com/find/ 99 | * 100 | * @example 101 | * class Chapter extends Backbone.View { 102 | * serialize() { 103 | * return { 104 | * title: this.$(".title").text(), 105 | * start: this.$(".start-page").text(), 106 | * end: this.$(".end-page").text() 107 | * }; 108 | * } 109 | * } 110 | * 111 | * @see http://backbonejs.org/#View-dollar 112 | * @see https://api.jquery.com/find/ 113 | * 114 | * @param {string} selector - A string containing a selector expression to match elements against. 115 | * @returns {Element|$} 116 | */ 117 | $(selector) 118 | { 119 | return this.$el.find(selector); 120 | } 121 | 122 | /** 123 | * Produces a DOM element to be assigned to your view. Exposed for subclasses using an alternative DOM 124 | * manipulation API. 125 | * 126 | * @protected 127 | * @param {string} tagName - Name of the tag element to create. 128 | * @returns {Element} 129 | * 130 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement 131 | */ 132 | _createElement(tagName) 133 | { 134 | return document.createElement(tagName); 135 | } 136 | 137 | /** 138 | * Add a single event listener to the view's element (or a child element using `selector`). This only works for 139 | * delegate-able events: not `focus`, `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. 140 | * 141 | * @see http://backbonejs.org/#View-delegateEvents 142 | * @see http://api.jquery.com/on/ 143 | * 144 | * @param {string} eventName - One or more space-separated event types and optional namespaces. 145 | * @param {string} selector - A selector string to filter the descendants of the selected elements that trigger 146 | * the event. 147 | * @param {function} listener - A function to execute when the event is triggered. 148 | * @returns {View} 149 | */ 150 | delegate(eventName, selector, listener) 151 | { 152 | this.$el.on(`${eventName}.delegateEvents${this.cid}`, selector, listener); 153 | return this; 154 | } 155 | 156 | /** 157 | * Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not 158 | * passed directly, uses this.events as the source. Events are written in the format {"event selector": "callback"}. 159 | * The callback may be either the name of a method on the view, or a direct function body. Omitting the selector 160 | * causes the event to be bound to the view's root element (this.el). By default, delegateEvents is called within 161 | * the View's constructor for you, so if you have a simple events hash, all of your DOM events will always already 162 | * be connected, and you will never have to call this function yourself. 163 | * 164 | * The events property may also be defined as a function that returns an events hash, to make it easier to 165 | * programmatically define your events, as well as inherit them from parent views. 166 | * 167 | * Using delegateEvents provides a number of advantages over manually using jQuery to bind events to child elements 168 | * during render. All attached callbacks are bound to the view before being handed off to jQuery, so when the 169 | * callbacks are invoked, this continues to refer to the view object. When delegateEvents is run again, perhaps with 170 | * a different events hash, all callbacks are removed and delegated afresh — useful for views which need to behave 171 | * differently when in different modes. 172 | * 173 | * A single-event version of delegateEvents is available as delegate. In fact, delegateEvents is simply a multi-event 174 | * wrapper around delegate. A counterpart to undelegateEvents is available as undelegate. 175 | * 176 | * Callbacks will be bound to the view, with `this` set properly. Uses event delegation for efficiency. 177 | * Omitting the selector binds the event to `this.el`. 178 | * 179 | * @example 180 | * Older `extend` example: 181 | * var DocumentView = Backbone.View.extend({ 182 | * events: { 183 | * "dblclick" : "open", 184 | * "click .icon.doc" : "select", 185 | * "contextmenu .icon.doc" : "showMenu", 186 | * "click .show_notes" : "toggleNotes", 187 | * "click .title .lock" : "editAccessLevel", 188 | * "mouseover .title .date" : "showTooltip" 189 | * }, 190 | * 191 | * render: function() { 192 | * this.$el.html(this.template(this.model.attributes)); 193 | * return this; 194 | * }, 195 | * 196 | * open: function() { 197 | * window.open(this.model.get("viewer_url")); 198 | * }, 199 | * 200 | * select: function() { 201 | * this.model.set({selected: true}); 202 | * }, 203 | * 204 | * ... 205 | * }); 206 | * 207 | * @example 208 | * Converting the above `extend` example to ES6: 209 | * class DocumentView extends Backbone.View { 210 | * get events() { 211 | * return { 212 | * "dblclick" : "open", 213 | * "click .icon.doc" : "select", 214 | * "contextmenu .icon.doc" : "showMenu", 215 | * "click .show_notes" : "toggleNotes", 216 | * "click .title .lock" : "editAccessLevel", 217 | * "mouseover .title .date" : "showTooltip" 218 | * }; 219 | * } 220 | * 221 | * render() { 222 | * this.$el.html(this.template(this.model.attributes)); 223 | * return this; 224 | * } 225 | * 226 | * open() { 227 | * window.open(this.model.get("viewer_url")); 228 | * } 229 | * 230 | * select() { 231 | * this.model.set({selected: true}); 232 | * } 233 | * ... 234 | * } 235 | * 236 | * @see http://backbonejs.org/#View-delegateEvents 237 | * @see http://api.jquery.com/on/ 238 | * 239 | * @param {object} events - hash of event descriptions to bind. 240 | * @returns {View} 241 | */ 242 | delegateEvents(events) 243 | { 244 | events = events || _.result(this, 'events'); 245 | if (!events) { return this; } 246 | this.undelegateEvents(); 247 | for (const key in events) 248 | { 249 | let method = events[key]; 250 | if (!_.isFunction(method)) { method = this[method]; } 251 | if (!method) { continue; } 252 | const match = key.match(s_DELEGATE_EVENT_SPLITTER); 253 | this.delegate(match[1], match[2], _.bind(method, this)); 254 | } 255 | return this; 256 | } 257 | 258 | /** 259 | * Ensure that the View has a DOM element to render into. If `this.el` is a string, pass it through `$()`, take 260 | * the first matching element, and re-assign it to `el`. Otherwise, create an element from the `id`, `className` 261 | * and `tagName` properties. 262 | * 263 | * @protected 264 | */ 265 | _ensureElement() 266 | { 267 | if (!this.el) 268 | { 269 | const attrs = _.extend({}, _.result(this, 'attributes')); 270 | if (this.id) { attrs.id = _.result(this, 'id'); } 271 | if (this.className) { attrs['class'] = _.result(this, 'className'); } 272 | this.setElement(this._createElement(_.result(this, 'tagName'))); 273 | this._setAttributes(attrs); 274 | } 275 | else 276 | { 277 | this.setElement(_.result(this, 'el')); 278 | } 279 | } 280 | 281 | /** 282 | * Initialize is an empty function by default. Override it with your own initialization logic. 283 | * 284 | * @see http://backbonejs.org/#View-constructor 285 | * @abstract 286 | */ 287 | initialize() 288 | { 289 | } 290 | 291 | /** 292 | * Removes a view and its el from the DOM, and calls stopListening to remove any bound events that the view has 293 | * listenTo'd. 294 | * 295 | * @see http://backbonejs.org/#View-remove 296 | * @see {@link _removeElement} 297 | * @see {@link stopListening} 298 | * 299 | * @returns {View} 300 | */ 301 | remove() 302 | { 303 | this._removeElement(); 304 | this.stopListening(); 305 | return this; 306 | } 307 | 308 | /** 309 | * Remove this view's element from the document and all event listeners attached to it. Exposed for subclasses 310 | * using an alternative DOM manipulation API. 311 | * 312 | * @protected 313 | * @see https://api.jquery.com/remove/ 314 | */ 315 | _removeElement() 316 | { 317 | this.$el.remove(); 318 | } 319 | 320 | /** 321 | * The default implementation of render is a no-op. Override this function with your code that renders the view 322 | * template from model data, and updates this.el with the new HTML. A good convention is to return this at the end 323 | * of render to enable chained calls. 324 | * 325 | * Backbone is agnostic with respect to your preferred method of HTML templating. Your render function could even 326 | * munge together an HTML string, or use document.createElement to generate a DOM tree. However, we suggest choosing 327 | * a nice JavaScript templating library. Mustache.js, Haml-js, and Eco are all fine alternatives. Because 328 | * Underscore.js is already on the page, _.template is available, and is an excellent choice if you prefer simple 329 | * interpolated-JavaScript style templates. 330 | * 331 | * Whatever templating strategy you end up with, it's nice if you never have to put strings of HTML in your 332 | * JavaScript. At DocumentCloud, we use Jammit in order to package up JavaScript templates stored in /app/views as 333 | * part of our main core.js asset package. 334 | * 335 | * @example 336 | * class Bookmark extends Backbone.View { 337 | * get template() { return _.template(...); } 338 | * 339 | * render() { 340 | * this.$el.html(this.template(this.model.attributes)); 341 | * return this; 342 | * } 343 | * } 344 | * 345 | * @see http://backbonejs.org/#View-render 346 | * 347 | * @abstract 348 | * @returns {View} 349 | */ 350 | render() 351 | { 352 | return this; 353 | } 354 | 355 | /** 356 | * Set attributes from a hash on this view's element. Exposed for subclasses using an alternative DOM 357 | * manipulation API. 358 | * 359 | * @protected 360 | * @param {object} attributes - An object defining attributes to associate with `this.$el`. 361 | */ 362 | _setAttributes(attributes) 363 | { 364 | this.$el.attr(attributes); 365 | } 366 | 367 | /** 368 | * Creates the `this.el` and `this.$el` references for this view using the given `el`. `el` can be a CSS selector 369 | * or an HTML string, a jQuery context or an element. Subclasses can override this to utilize an alternative DOM 370 | * manipulation API and are only required to set the `this.el` property. 371 | * 372 | * @protected 373 | * @param {string|object} el - A CSS selector or an HTML string, a jQuery context or an element. 374 | */ 375 | _setElement(el) 376 | { 377 | /** 378 | * Cached jQuery context for element. 379 | * @type {object} 380 | */ 381 | this.$el = el instanceof BackboneProxy.backbone.$ ? el : BackboneProxy.backbone.$(el); 382 | 383 | /** 384 | * Cached element 385 | * @type {Element} 386 | */ 387 | this.el = this.$el[0]; 388 | } 389 | 390 | /** 391 | * If you'd like to apply a Backbone view to a different DOM element, use setElement, which will also create the 392 | * cached $el reference and move the view's delegated events from the old element to the new one. 393 | * 394 | * @see http://backbonejs.org/#View-setElement 395 | * @see {@link undelegateEvents} 396 | * @see {@link _setElement} 397 | * @see {@link delegateEvents} 398 | * 399 | * @param {string|object} element - A CSS selector or an HTML string, a jQuery context or an element. 400 | * @returns {View} 401 | */ 402 | setElement(element) 403 | { 404 | this.undelegateEvents(); 405 | this._setElement(element); 406 | this.delegateEvents(); 407 | return this; 408 | } 409 | 410 | /** 411 | * A finer-grained `undelegateEvents` for removing a single delegated event. `selector` and `listener` are 412 | * both optional. 413 | * 414 | * @see http://backbonejs.org/#View-undelegateEvents 415 | * @see http://api.jquery.com/off/ 416 | * 417 | * @param {string} eventName - One or more space-separated event types and optional namespaces. 418 | * @param {string} selector - A selector which should match the one originally passed to `.delegate()`. 419 | * @param {function} listener - A handler function previously attached for the event(s). 420 | * @returns {View} 421 | */ 422 | undelegate(eventName, selector, listener) 423 | { 424 | this.$el.off(`${eventName}.delegateEvents${this.cid}`, selector, listener); 425 | return this; 426 | } 427 | 428 | /** 429 | * Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM 430 | * temporarily. 431 | * 432 | * @see http://backbonejs.org/#View-undelegateEvents 433 | * @see http://api.jquery.com/off/ 434 | * 435 | * @returns {View} 436 | */ 437 | undelegateEvents() 438 | { 439 | if (this.$el) { this.$el.off(`.delegateEvents${this.cid}`); } 440 | return this; 441 | } 442 | } 443 | 444 | // Private / internal methods --------------------------------------------------------------------------------------- 445 | 446 | /** 447 | * Cached regex to split keys for `delegate`. 448 | * @type {RegExp} 449 | */ 450 | const s_DELEGATE_EVENT_SPLITTER = /^(\S+)\s*(.*)$/; 451 | 452 | /** 453 | * List of view options to be set as properties. 454 | * @type {string[]} 455 | */ 456 | const s_VIEW_OPTIONS = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/History.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'underscore'; 4 | import Events from 'typhonjs-core-backbone-events/src/Events.js'; 5 | import Utils from './Utils.js'; 6 | 7 | /** 8 | * Backbone.History - History serves as a global router. (http://backbonejs.org/#History) 9 | * ---------------- 10 | * 11 | * History serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, 12 | * and trigger callbacks. You shouldn't ever have to create one of these yourself since Backbone.history already 13 | * contains one. 14 | * 15 | * pushState support exists on a purely opt-in basis in Backbone. Older browsers that don't support pushState will 16 | * continue to use hash-based URL fragments, and if a hash URL is visited by a pushState-capable browser, it will be 17 | * transparently upgraded to the true URL. Note that using real URLs requires your web server to be able to correctly 18 | * render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, 19 | * your web server must be able to serve that page, if the browser visits that URL directly. For full search-engine 20 | * crawlability, it's best to have the server generate the complete HTML for the page ... but if it's a web application, 21 | * just rendering the same content you would have for the root URL, and filling in the rest with Backbone Views and 22 | * JavaScript works fine. 23 | * 24 | * Handles cross-browser history management, based on either [pushState](http://diveintohtml5.info/history.html) and 25 | * real URLs, or [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) and URL fragments. 26 | * If the browser supports neither (old IE, natch), falls back to polling. 27 | */ 28 | export default class History extends Events 29 | { 30 | /** */ 31 | constructor() 32 | { 33 | super(); 34 | 35 | /** 36 | * Stores route / callback pairs for validation. 37 | * @type {Array>} 38 | */ 39 | this.handlers = []; 40 | this.checkUrl = _.bind(this.checkUrl, this); 41 | 42 | // Ensure that `History` can be used outside of the browser. 43 | if (typeof window !== 'undefined') 44 | { 45 | /** 46 | * Browser Location or URL string. 47 | * @type {Location|String} 48 | */ 49 | this.location = window.location; 50 | 51 | /** 52 | * Browser history 53 | * @type {History} 54 | */ 55 | this.history = window.history; 56 | } 57 | 58 | /** 59 | * Has the history handling already been started? 60 | * @type {boolean} 61 | */ 62 | this.started = false; 63 | 64 | /** 65 | * The default interval to poll for hash changes, if necessary, is twenty times a second. 66 | * @type {number} 67 | */ 68 | this.interval = 50; 69 | } 70 | 71 | /** 72 | * Are we at the app root? 73 | * 74 | * @returns {boolean} 75 | */ 76 | atRoot() 77 | { 78 | const path = this.location.pathname.replace(/[^\/]$/, '$&/'); 79 | return path === this.root && !this.getSearch(); 80 | } 81 | 82 | /** 83 | * Checks the current URL to see if it has changed, and if it has, calls `loadUrl`, normalizing across the 84 | * hidden iframe. 85 | * 86 | * @returns {boolean} 87 | */ 88 | checkUrl() 89 | { 90 | let current = this.getFragment(); 91 | 92 | // If the user pressed the back button, the iframe's hash will have changed and we should use that for comparison. 93 | if (current === this.fragment && this.iframe) 94 | { 95 | current = this.getHash(this.iframe.contentWindow); 96 | } 97 | 98 | if (current === this.fragment) { return false; } 99 | if (this.iframe) { this.navigate(current); } 100 | this.loadUrl(); 101 | } 102 | 103 | /** 104 | * Unicode characters in `location.pathname` are percent encoded so they're decoded for comparison. `%25` should 105 | * not be decoded since it may be part of an encoded parameter. 106 | * 107 | * @param {string} fragment - URL fragment 108 | * @return {string} 109 | */ 110 | decodeFragment(fragment) 111 | { 112 | return decodeURI(fragment.replace(/%25/g, '%2525')); 113 | } 114 | 115 | /** 116 | * Get the cross-browser normalized URL fragment from the path or hash. 117 | * 118 | * @param {string} fragment -- URL fragment 119 | * @returns {*|void|string|XML} 120 | */ 121 | getFragment(fragment) 122 | { 123 | if (Utils.isNullOrUndef(fragment)) 124 | { 125 | if (this._usePushState || !this._wantsHashChange) 126 | { 127 | fragment = this.getPath(); 128 | } 129 | else 130 | { 131 | fragment = this.getHash(); 132 | } 133 | } 134 | return fragment.replace(s_ROUTE_STRIPPER, ''); 135 | } 136 | 137 | /** 138 | * Gets the true hash value. Cannot use location.hash directly due to bug in Firefox where location.hash will 139 | * always be decoded. 140 | * 141 | * @param {object} window - Browser `window` 142 | * @returns {*} 143 | */ 144 | getHash(window) 145 | { 146 | const match = (window || this).location.href.match(/#(.*)$/); 147 | return match ? match[1] : ''; 148 | } 149 | 150 | /** 151 | * Get the pathname and search params, without the root. 152 | * 153 | * @returns {*} 154 | */ 155 | getPath() 156 | { 157 | const path = this.decodeFragment(this.location.pathname + this.getSearch()).slice(this.root.length - 1); 158 | return path.charAt(0) === '/' ? path.slice(1) : path; 159 | } 160 | 161 | /** 162 | * In IE6, the hash fragment and search params are incorrect if the fragment contains `?`. 163 | * 164 | * @returns {string} 165 | */ 166 | getSearch() 167 | { 168 | const match = this.location.href.replace(/#.*/, '').match(/\?.+/); 169 | return match ? match[0] : ''; 170 | } 171 | 172 | /** 173 | * Attempt to load the current URL fragment. If a route succeeds with a match, returns `true`. If no defined routes 174 | * matches the fragment, returns `false`. 175 | * 176 | * @param {string} fragment - URL fragment 177 | * @returns {boolean} 178 | */ 179 | loadUrl(fragment) 180 | { 181 | // If the root doesn't match, no routes can match either. 182 | if (!this.matchRoot()) { return false; } 183 | fragment = this.fragment = this.getFragment(fragment); 184 | return _.some(this.handlers, (handler) => 185 | { 186 | if (handler.route.test(fragment)) 187 | { 188 | handler.callback(fragment); 189 | return true; 190 | } 191 | }); 192 | } 193 | 194 | /** 195 | * Does the pathname match the root? 196 | * 197 | * @returns {boolean} 198 | */ 199 | matchRoot() 200 | { 201 | const path = this.decodeFragment(this.location.pathname); 202 | const rootPath = `${path.slice(0, this.root.length - 1)}/`; 203 | return rootPath === this.root; 204 | } 205 | 206 | /** 207 | * Save a fragment into the hash history, or replace the URL state if the 'replace' option is passed. You are 208 | * responsible for properly URL-encoding the fragment in advance. 209 | * 210 | * The options object can contain `trigger: true` if you wish to have the route callback be fired (not usually 211 | * desirable), or `replace: true`, if you wish to modify the current URL without adding an entry to the history. 212 | * 213 | * @param {string} fragment - String representing an URL fragment. 214 | * @param {object} options - Optional hash containing parameters for navigate. 215 | * @returns {*} 216 | */ 217 | navigate(fragment, options) 218 | { 219 | if (!History.started) { return false; } 220 | if (!options || options === true) { options = { trigger: !!options }; } 221 | 222 | // Normalize the fragment. 223 | fragment = this.getFragment(fragment || ''); 224 | 225 | // Don't include a trailing slash on the root. 226 | let rootPath = this.root; 227 | 228 | if (fragment === '' || fragment.charAt(0) === '?') 229 | { 230 | rootPath = rootPath.slice(0, -1) || '/'; 231 | } 232 | 233 | const url = rootPath + fragment; 234 | 235 | // Strip the hash and decode for matching. 236 | fragment = this.decodeFragment(fragment.replace(s_PATH_STRIPPER, '')); 237 | 238 | if (this.fragment === fragment) { return; } 239 | 240 | /** 241 | * URL fragment 242 | * @type {*|void|string|XML} 243 | */ 244 | this.fragment = fragment; 245 | 246 | // If pushState is available, we use it to set the fragment as a real URL. 247 | if (this._usePushState) 248 | { 249 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 250 | 251 | // If hash changes haven't been explicitly disabled, update the hash fragment to store history. 252 | } 253 | else if (this._wantsHashChange) 254 | { 255 | s_UPDATE_HASH(this.location, fragment, options.replace); 256 | 257 | if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) 258 | { 259 | const iWindow = this.iframe.contentWindow; 260 | 261 | // Opening and closing the iframe tricks IE7 and earlier to push a history 262 | // entry on hash-tag change. When replace is true, we don't want this. 263 | if (!options.replace) 264 | { 265 | iWindow.document.open(); 266 | iWindow.document.close(); 267 | } 268 | 269 | s_UPDATE_HASH(iWindow.location, fragment, options.replace); 270 | } 271 | 272 | // If you've told us that you explicitly don't want fallback hashchange- 273 | // based history, then `navigate` becomes a page refresh. 274 | } 275 | else 276 | { 277 | return this.location.assign(url); 278 | } 279 | 280 | if (options.trigger) { return this.loadUrl(fragment); } 281 | } 282 | 283 | /** 284 | * When all of your Routers have been created, and all of the routes are set up properly, call 285 | * Backbone.history.start() to begin monitoring hashchange events, and dispatching routes. Subsequent calls to 286 | * Backbone.history.start() will throw an error, and Backbone.History.started is a boolean value indicating whether 287 | * it has already been called. 288 | * 289 | * To indicate that you'd like to use HTML5 pushState support in your application, use 290 | * Backbone.history.start({pushState: true}). If you'd like to use pushState, but have browsers that don't support 291 | * it natively use full page refreshes instead, you can add {hashChange: false} to the options. 292 | * 293 | * If your application is not being served from the root url / of your domain, be sure to tell History where the 294 | * root really is, as an option: Backbone.history.start({pushState: true, root: "/public/search/"}) 295 | * 296 | * When called, if a route succeeds with a match for the current URL, Backbone.history.start() returns true. If no 297 | * defined route matches the current URL, it returns false. 298 | * 299 | * If the server has already rendered the entire page, and you don't want the initial route to trigger when starting 300 | * History, pass silent: true. 301 | * 302 | * Because hash-based history in Internet Explorer relies on an