├── .editorconfig ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── react-mini-router.js └── react-mini-router.min.js ├── example ├── .gitignore ├── app │ ├── actions.js │ ├── components │ │ ├── app.jsx │ │ ├── createtodo.jsx │ │ ├── createtodolist.jsx │ │ ├── todoform.jsx │ │ └── todolist.jsx │ ├── constants.js │ ├── main.js │ ├── todo-service.js │ └── todo-store.js ├── gulpfile.js ├── package.json ├── public │ ├── css │ │ └── app.css │ └── index.html ├── server.js └── webpack.config.js ├── index.js ├── karma.conf.js ├── karma.saucelabs.conf.js ├── lib ├── RouterMixin.js ├── detect.js ├── event.js ├── getEventTarget.js ├── navigate.js └── replaceNavigate.js ├── package.json ├── test ├── client │ ├── detect.test.js │ └── history.test.js ├── server │ └── RouterMixin.test.js └── vendor │ ├── es5-sham.js │ ├── es5-shim.js │ ├── jquery-1.11.3.js │ └── jquery-2.1.1.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | [*.js] 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [{package.json,bower.json}] 10 | indent_style = space 11 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | msvs-version=2013 2 | python=python3.1 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | sudo: false 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.1 2 | 3 | * Updated react to 15.5. 4 | * Upated to use create-react-class & prop-types libraries to avoid error logs. 5 | 6 | ## 2.2.0 7 | 8 | * Added `replaceNavigate` api. 9 | 10 | ## 2.1.1 11 | 12 | * Updated dependencies. 13 | 14 | ## 2.1.0 15 | 16 | * Support React 15.4.0. 17 | 18 | ## 2.0.0 19 | 20 | * Supports React 0.14. Removed support for React <=0.13 due to introduction of react-dom. 21 | 22 | ## 1.1.7 23 | 24 | * Includes bug fix from issue #37. Intercepting clicks on links with routes urls now works correctly in IE 8. 25 | 26 | ## 1.1.6 27 | 28 | * Includes bug fix from issue #34. `navigate` now cross browser compatible for older versions of IE. 29 | 30 | ## 1.1.5 31 | 32 | * New build using webpack instead of browserify. No new functionality or bug fixes. 33 | 34 | ## 1.1.4 35 | 36 | * Includes bug fix from issue #29. 37 | 38 | ## 1.1.3 39 | 40 | * Includes bug fix from issue #26. 41 | 42 | ## 1.1.2 43 | 44 | * Includes bug fix from issue #24. 45 | 46 | ## 1.1.1 47 | 48 | * Includes bug fix from issue #22. 49 | 50 | ## 1.1.0 51 | 52 | * `path` state variable is always initialized when mixin is created. 53 | * Nested router support. 54 | * Example app updated to demonstrate nester routers. 55 | * Locked path-to-regexp to 1.0.1 and urllite to 0.5.0 to prevent dependency breakage. 56 | * Bug fixes relating to event propagation. 57 | 58 | ## 1.0.0 59 | 60 | * First stable API release. Supports basic routing functionality. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <2015> Larry Myers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This Project is Looking for Active Maintainers** 2 | [See Issue #68 to Volunteer](https://github.com/larrymyers/react-mini-router/issues/68) 3 | 4 | # React Mini Router 5 | 6 | [![Build Status](https://travis-ci.org/larrymyers/react-mini-router.svg?branch=master)](https://travis-ci.org/larrymyers/react-mini-router) 7 | 8 | A minimal URL router for [React.js](http://facebook.github.io/react/). 9 | 10 | The router provides a small React.js mixin that is easy to integrate into a root level component. 11 | It makes little to no demands on how you structure your application. 12 | 13 | Routes call methods instead of creating components directly. This makes async data loading outside of 14 | the child components straight forward (allowing them to remain stateless). This also makes server 15 | side rendering straight forward. 16 | 17 | The Router supports the HTML5 History API and Hash URLs. It requires no special components or markup. 18 | You can use regular anchor tags in your html markup to trigger navigation, or use the [navigate](./lib/navigate.js) 19 | util method to programmatically trigger routes. 20 | 21 | Its only dependencies are [path-to-regexp](https://github.com/component/path-to-regexp), 22 | [urllite](https://github.com/hzdg/urllite.js) and React >= 0.14.0. 23 | 24 | The complete browser build is 10kb minified and 4kb minified and gzipped. 25 | 26 | See the [example](./example) app for a complete solution that includes server side rendering 27 | and integrates with [Fluxxor](https://github.com/BinaryMuse/fluxxor) for Store/Dispatch functionality. 28 | 29 | **IMPORTANT** If you require React 0.13 or earlier, please install version 1.1.7. Version 2.0.0 30 | requires React 0.14 or newer. 31 | 32 | ## Install 33 | 34 | If using CommonJS modules and browserify: 35 | 36 | npm install react-mini-router 37 | 38 | For all other browser environments: 39 | 40 | bower install react-mini-router 41 | 42 | The [dist/react-mini-router.js](./dist/react-mini-router.js) build exposes a global ReactMiniRouter variable. 43 | 44 | ## Usage 45 | 46 | var React = require('react'), 47 | createReactClass = require('create-react-class'), 48 | RouterMixin = require('react-mini-router').RouterMixin; 49 | 50 | var App = createReactClass({ 51 | 52 | mixins: [RouterMixin], 53 | 54 | routes: { 55 | '/': 'home', 56 | '/message/:text': 'message' 57 | }, 58 | 59 | render: function() { 60 | return this.renderCurrentRoute(); 61 | }, 62 | 63 | home: function() { 64 | return
Hello World
; 65 | }, 66 | 67 | message: function(text) { 68 | return
{text}
; 69 | }, 70 | 71 | notFound: function(path) { 72 | return
Page Not Found: {path}
; 73 | } 74 | 75 | }); 76 | 77 | module.exports = App; 78 | 79 | ### Configuration 80 | 81 | By default the RouterMixin will use hash urls for routes. To enable the HTML5 History API 82 | with pushState pass a "history" boolean property to the Component. If you're using server rendering 83 | and intend on focusing primarily on modern browsers it is recommended to enable the History API. 84 | 85 | If a browser doesn't support the History API it will automatically fall back to hash urls. 86 | 87 | **NOTE:** Hash urls will use the hashbang (i.e. #!) format in order to properly support 88 | the [ajax crawling](https://developers.google.com/webmasters/ajax-crawling/) Google spec. 89 | 90 | Example: 91 | 92 | React.render( 93 | App({ history: true }), 94 | document.getElementById('app') 95 | ); 96 | 97 | You can also mount the Router at a root path, and all routes will be matched relative to it: 98 | 99 | React.render( 100 | App({ root: '/some/path/to/app' }), 101 | document.getElementById('app') 102 | ); 103 | 104 | ### Route Definitions and Handler Methods 105 | 106 | The RouterMixin uses path-to-regexp for all route definitions. See the docs on [parameters](https://github.com/component/path-to-regexp#parameters) 107 | for the variations allowed when defining urls. 108 | 109 | When a url matches a route, the handler method is executed. The handler is called with the following arguments: 110 | 111 | 1. Each matched parameter, in the order it appears in the url. 112 | 2. An object of key/value pairs that represents the parsed url query string. 113 | 114 | Example: 115 | 116 | routes: { 117 | '/search/:searchQuery': 'searchResults' 118 | } 119 | 120 | function searchResults(searchQuery, params) { 121 | // logic for getting search results data and rendering component 122 | } 123 | 124 | "/search/giant%20robots?sort=ascending&size=20" => searchResults("giant robots", { "sort": "ascending", "size": "20" }) 125 | 126 | ### The 404 Not Found Route 127 | 128 | By default the RouterMixin will throw an Error if it can't match a route. To render a 404 Not Found 129 | page just define a 'notFound' method on the component. It takes a single argument, path, which is 130 | the url path that failed to match a route definition. Any unmatched route will call this route handler 131 | if it is defined. See the usage example above for a code example. 132 | 133 | ### Navigation 134 | 135 | Any child anchor elements will have their click events captured, and if their href matches a route 136 | the matched route handler will be called. 137 | 138 | To programmatically trigger navigation there is a provided navigate method: 139 | 140 | var navigate = require('react-mini-router').navigate; 141 | 142 | navigate('/foo'); 143 | 144 | If you want to update the address bar url, but not trigger routing: 145 | 146 | navigate('/foo', true); 147 | 148 | ## Server Rendering 149 | 150 | See the [example](./example) app for how to approach server rendering. The short answer 151 | is that for every url the server must render you should provide the necessary data 152 | to the root Component as props, including the `path` property. 153 | 154 | [React.renderToString](http://facebook.github.io/react/docs/top-level-api.html#react.rendertostring) 155 | does not trigger the Component lifecycle methods, so you must do all async data loading outside 156 | of the render process. 157 | 158 | ## Nested Routers 159 | 160 | Nested routers are supported, though it requires some manual work. The `root` property must 161 | be explicitly passed to the nested router, which sets the base url where it will be mounted. 162 | 163 | You also need to provide a wildcard param at the end of any routes that will call route handlers 164 | that contain a nested router. The [example](./example) app and tests show how to do this. 165 | 166 | ## Running the Example App 167 | 168 | The [example](./example) app demonstrates how to use react-mini-router for client and server 169 | side rendering. To run the app do the following: 170 | 171 | cd example 172 | npm install 173 | gulp serve 174 | 175 | Then open a new browser window to: 176 | 177 | http://localhost:4000 178 | 179 | ## Alternatives 180 | 181 | * [React Router](https://github.com/rackt/react-router) 182 | * [React Router Component](https://github.com/andreypopp/react-router-component) 183 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mini-router", 3 | "main": "dist/react-mini-router.js", 4 | "version": "1.1.6", 5 | "homepage": "https://github.com/larrymyers/react-mini-router", 6 | "authors": [ 7 | "Larry Myers " 8 | ], 9 | "description": "A minimal router for React.js", 10 | "dependencies": { 11 | "path-to-regexp": "pillarjs/path-to-regexp#1.0.1", 12 | "urllite": "hzdg/urllite.js#0.5.0" 13 | }, 14 | "moduleType": [ 15 | "amd", 16 | "globals", 17 | "node" 18 | ], 19 | "keywords": [ 20 | "react", 21 | "router" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "example", 26 | "node_modules", 27 | "test", 28 | "karma.conf.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /dist/react-mini-router.min.js: -------------------------------------------------------------------------------- 1 | /*! ReactMiniRouter 2.2.1 - https://github.com/larrymyers/react-mini-router */ 2 | var ReactMiniRouter=function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){t.exports={RouterMixin:n(8),navigate:n(10),replaceNavigate:n(11)}},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(f===clearTimeout)return clearTimeout(t);if((f===r||!f)&&clearTimeout)return f=clearTimeout,clearTimeout(t);try{return f(t)}catch(e){try{return f.call(null,t)}catch(e){return f.call(this,t)}}}function a(){v&&h&&(v=!1,h.length?d=h.concat(d):y=-1,d.length&&s())}function s(){if(!v){var t=o(a);v=!0;for(var e=d.length;e;){for(h=d,d=[];++y1)for(var n=1;n1?e-1:0),r=1;r2?r-2:0),i=2;i{this.renderCurrentRoute()}; 24 | }, 25 | 26 | viewAllLists: function() { 27 | return ( 28 |
29 |
    30 | {this.state.lists.map(function(list) { 31 | return ( 32 |
  • 33 | {list.name} 34 |
  • 35 | ); 36 | })} 37 |
38 | 39 |
40 | ); 41 | }, 42 | 43 | viewList: function(id) { 44 | var list = this.state.lists.reduce(function(found, list) { 45 | if (list.id == id) { return list; } 46 | return found; 47 | }); 48 | 49 | return ; 50 | }, 51 | 52 | notFound: function(path) { 53 | return
Uh oh. {path} doesn't exist.
; 54 | } 55 | 56 | }); 57 | 58 | module.exports = App; 59 | -------------------------------------------------------------------------------- /example/app/components/createtodo.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Fluxxor = require('fluxxor'); 3 | 4 | var CreateTodo = React.createClass({ 5 | 6 | mixins: [Fluxxor.FluxMixin(React)], 7 | 8 | render: function() { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }, 16 | 17 | createTodo: function(evt) { 18 | evt.preventDefault(); 19 | 20 | var input = this.refs.text, 21 | text = input.value; 22 | 23 | input.value = ''; 24 | this.getFlux().actions.addTodo(this.props.list, text); 25 | } 26 | }); 27 | 28 | module.exports = CreateTodo; 29 | -------------------------------------------------------------------------------- /example/app/components/createtodolist.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Fluxxor = require('fluxxor'); 3 | 4 | var CreateTodoList = React.createClass({ 5 | 6 | mixins: [Fluxxor.FluxMixin(React)], 7 | 8 | render: function() { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }, 16 | 17 | createList: function(evt) { 18 | evt.preventDefault(); 19 | 20 | var input = this.refs.listName, 21 | listName = input.value; 22 | 23 | input.value = ''; 24 | this.getFlux().actions.createList(listName); 25 | } 26 | }); 27 | 28 | module.exports = CreateTodoList; 29 | -------------------------------------------------------------------------------- /example/app/components/todoform.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | navigate = require('./../../../lib/navigate'), 3 | Fluxxor = require('fluxxor'); 4 | 5 | var TodoForm = React.createClass({ 6 | 7 | mixins: [Fluxxor.FluxMixin(React)], 8 | 9 | componentDidMount: function() { 10 | this.componentDidUpdate(); 11 | }, 12 | 13 | componentDidUpdate: function() { 14 | this.refs.textInput.focus(); 15 | }, 16 | 17 | render: function() { 18 | return ( 19 |
20 | 21 | 22 |
23 | ); 24 | }, 25 | 26 | submit: function(evt) { 27 | evt.preventDefault(); 28 | var text = this.refs.textInput.value; 29 | this.getFlux().actions.addTodo(text); 30 | navigate('/'); 31 | } 32 | }); 33 | 34 | module.exports = TodoForm; 35 | -------------------------------------------------------------------------------- /example/app/components/todolist.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Fluxxor = require('fluxxor'), 3 | RouterMixin = require('./../../../lib/RouterMixin'), 4 | CreateTodo = require('./createtodo'), 5 | _find = require('lodash/collection/find'), 6 | _map = require('lodash/collection/map'); 7 | 8 | var TodoList = React.createClass({ 9 | 10 | mixins: [RouterMixin, Fluxxor.FluxMixin(React)], 11 | 12 | routes: { 13 | '/': 'showAll', 14 | '/edit/:id': 'showAll' 15 | }, 16 | 17 | render: function () { 18 | var list = this.props.list; 19 | 20 | return ( 21 |
22 |

{list.name}

23 | {this.renderCurrentRoute()} 24 | 27 |
28 | ); 29 | }, 30 | 31 | showAll: function(id) { 32 | var self = this, 33 | list = self.props.list, 34 | editTodo; 35 | 36 | if (id) { 37 | editTodo = _find(list.todos, { id: parseInt(id) }); 38 | } 39 | 40 | return ( 41 |
42 |
    43 | {_map(list.todos, function(todo) { 44 | if (todo === editTodo) { 45 | return ( 46 |
  • 47 |
    48 | 49 | 50 |
    51 |
  • 52 | ); 53 | } 54 | 55 | return ( 56 |
  • 57 | {todo.text} 58 |
  • 59 | ); 60 | })} 61 |
62 | 63 |
64 | ); 65 | }, 66 | 67 | saveTodo: function(id, evt) { 68 | evt.preventDefault(); 69 | 70 | var list = this.props.list, 71 | todo = _find(list.todos, { id: id }); 72 | 73 | todo.text = this.refs.editInput.value; 74 | 75 | this.getFlux().actions.updateTodo(list, todo); 76 | } 77 | 78 | }); 79 | 80 | module.exports = TodoList; 81 | -------------------------------------------------------------------------------- /example/app/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ADD_LIST: 'ADD_LIST', 3 | DELETE_LIST: 'DELETE_LIST', 4 | ADD_TODO: 'ADD_TODO', 5 | UPDATE_TODO: 'UPDATE_TODO', 6 | REMOVE_TODO: 'REMOVE_TODO' 7 | }; 8 | -------------------------------------------------------------------------------- /example/app/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | Fluxxor = require('fluxxor'), 4 | TodoStore = require('./todo-store'), 5 | App = React.createFactory(require('./components/app')); 6 | 7 | var data = window.APP_DATA || {}, 8 | useHistory = data.history; 9 | 10 | delete data.history; 11 | 12 | var flux = new Fluxxor.Flux({ 13 | 'TodoStore': new TodoStore(data) 14 | }, require('./actions')); 15 | 16 | ReactDOM.render( 17 | App({ flux: flux, history: useHistory }), 18 | document.getElementById('app') 19 | ); 20 | -------------------------------------------------------------------------------- /example/app/todo-service.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | 3 | var host = typeof window === 'undefined' ? 'http://localhost:4000' : ''; 4 | 5 | module.exports = { 6 | 7 | createList: function(data, callback) { 8 | request.post(host + '/api/lists/').send(data).end(function(res) { 9 | callback(null, res.body); 10 | }); 11 | }, 12 | 13 | deleteList: function(data, callback) { 14 | 15 | }, 16 | 17 | createTodo: function(list, data, callback) { 18 | request.post(host + '/api/lists/' + list.id + '/todos/').send(data).end(function(res) { 19 | callback(null, res.body); 20 | }); 21 | }, 22 | 23 | updateTodo: function(todoId, data, callback) { 24 | request.put(host + '/api/todos/' + todoId).end(function(res) { 25 | callback(null, res.body); 26 | }); 27 | }, 28 | 29 | removeTodo: function(todoId, callback) { 30 | request.del(host + '/api/todos/' + todoId).end(function(res) { 31 | callback(null, res.body); 32 | }); 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /example/app/todo-store.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'), 2 | constants = require('./constants'), 3 | _remove = require('lodash/array/remove'), 4 | _find = require('lodash/collection/find'), 5 | _findIndex = require('lodash/array/findIndex'); 6 | 7 | module.exports = Fluxxor.createStore({ 8 | initialize: function(options) { 9 | this.lists = options.lists || []; 10 | 11 | this.bindActions( 12 | constants.ADD_LIST, this.onAddList, 13 | constants.DELETE_LIST, this.onDeleteList, 14 | constants.ADD_TODO, this.onAddTodo, 15 | constants.UPDATE_TODO, this.onUpdateTodo, 16 | constants.REMOVE_TODO, this.onRemoveTodo 17 | ); 18 | }, 19 | 20 | onAddList: function(list) { 21 | this.lists.push(list); 22 | this.emit('change'); 23 | }, 24 | 25 | onDeleteList: function(list) { 26 | 27 | }, 28 | 29 | onAddTodo: function(payload) { 30 | var list = _find(this.lists, { id: payload.listId }); 31 | list.todos.push(payload.todo); 32 | this.emit('change'); 33 | }, 34 | 35 | onUpdateTodo: function(payload) { 36 | var list = _find(this.lists, { id: payload.listId }), 37 | idx = _findIndex(list.todos, { id: payload.todo.id }); 38 | 39 | if (idx > -1) { 40 | list.todos[idx] = payload.todo; 41 | this.emit('change'); 42 | } 43 | }, 44 | 45 | onRemoveTodo: function(payload) { 46 | }, 47 | 48 | getState: function() { 49 | return { 50 | lists: this.lists 51 | }; 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /example/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | del = require('del'), 3 | changed = require('gulp-changed'), 4 | babel = require('gulp-babel'), 5 | source = require('vinyl-source-stream'), 6 | nodemon = require('gulp-nodemon'); 7 | 8 | gulp.task('clean', function(done) { 9 | del([ 10 | 'app/components/*.js', 11 | 'public/js/app.js' 12 | ], done); 13 | }); 14 | 15 | gulp.task('react', function() { 16 | return gulp.src('./app/components/*.jsx') 17 | .pipe(changed('./app/components/', { extension: '.js' })) 18 | .pipe(babel()) 19 | .pipe(gulp.dest('./app/components/')); 20 | }); 21 | 22 | gulp.task('watch', function() { 23 | gulp.watch(['app/components/**/*.jsx'], ['react']) 24 | .on('change', function(event) { 25 | console.log('React Transform: ' + event.path + ' (' + event.type + ')'); 26 | }); 27 | }); 28 | 29 | gulp.task('serve', ['react', 'watch'], function() { 30 | nodemon({ 31 | script: 'server.js', 32 | watch: ['app'] 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mini-router-example", 3 | "version": "0.0.1", 4 | "description": "A sample React app that uses the mini-router for client and server rendering.", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.5.2", 14 | "express": "^4.7.2", 15 | "fluxxor": "^1.5.0", 16 | "gulp-babel": "^6.1.0", 17 | "lodash": "^3.0.0", 18 | "plates": "^0.4.9", 19 | "react": "^0.14.0", 20 | "react-dom": "^0.14.0", 21 | "superagent": "^0.18.2" 22 | }, 23 | "devDependencies": { 24 | "babel-plugin-syntax-jsx": "^6.1.18", 25 | "babel-plugin-transform-react-jsx": "^6.1.18", 26 | "del": "^1.1.1", 27 | "gulp": "^3.8.10", 28 | "gulp-changed": "^1.1.0", 29 | "gulp-nodemon": "^1.0.5", 30 | "vinyl-source-stream": "^1.0.0", 31 | "webpack": "^1.8.11", 32 | "webpack-dev-middleware": "^1.0.11" 33 | }, 34 | "babel": { 35 | "plugins": [ 36 | "transform-react-jsx", 37 | "syntax-jsx" 38 | ] 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/public/css/app.css: -------------------------------------------------------------------------------- 1 | nav { 2 | padding: 10px 15px; 3 | } 4 | 5 | nav a { 6 | padding: 0 10px; 7 | } 8 | 9 | nav a:first-child { 10 | border-right: 1px solid #cccccc; 11 | } 12 | 13 | #app { 14 | margin-top: 25px; 15 | } 16 | 17 | .lists, .todolist { 18 | max-width: 400px; 19 | } 20 | 21 | .lists .list-group-item { 22 | padding: 0; 23 | } 24 | 25 | .lists .list-group-item a { 26 | display: block; 27 | padding: 10px 15px; 28 | text-decoration: none; 29 | } 30 | 31 | .create-list-input { 32 | margin-right: 15px; 33 | } 34 | 35 | .todolist footer { 36 | margin-top: 30px; 37 | } 38 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Mini Router Example 7 | 8 | 9 | 10 | 11 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | express = require('express'), 4 | bodyParser = require('body-parser'), 5 | React = require('react'), 6 | ReactDOMServer = require('react-dom/server'), 7 | plates = require('plates'), 8 | _find = require('lodash/collection/find'), 9 | _remove = require('lodash/array/remove'), 10 | _cloneDeep = require('lodash/lang/cloneDeep'), 11 | _extend = require('lodash/object/assign'), 12 | _defaults = require('lodash/object/defaults'), 13 | Fluxxor = require('fluxxor'), 14 | TodoStore = require('./app/todo-store'), 15 | actions = require('./app/actions'), 16 | App = React.createFactory(require('./app/components/app')); 17 | 18 | var app = express(), 19 | todoLists = []; 20 | 21 | try { 22 | todoLists = JSON.parse(fs.readFileSync('db.json', 'utf8')); 23 | } catch(e) { 24 | // no saved data yet, just continue 25 | } 26 | 27 | function saveDB(callback) { 28 | fs.writeFile('db.json', JSON.stringify(todoLists, null, 4), 'utf8', callback); 29 | } 30 | 31 | if (process.env.NODE_ENV !== 'production') { 32 | var webpackDevMiddleware = require('webpack-dev-middleware'), 33 | webpack = require('webpack'), 34 | webpackConfig = _cloneDeep(require('./webpack.config')); 35 | 36 | webpackConfig.debug = true; 37 | webpackConfig.devtool = 'inline-source-map'; 38 | 39 | app.use(webpackDevMiddleware(webpack(webpackConfig), { stats: false })); 40 | } 41 | 42 | app.use(bodyParser.json()); 43 | 44 | // PAGES 45 | 46 | app.get('/', renderApp); 47 | app.get('/lists*', renderApp); 48 | 49 | function renderApp(req, res) { 50 | var stores = { 51 | 'TodoStore': new TodoStore({ lists: todoLists }) 52 | }; 53 | 54 | var flux = new Fluxxor.Flux(stores, require('./app/actions')); 55 | var appHtml = ReactDOMServer.renderToString(App({ path: req.path, flux: flux })); 56 | 57 | fs.readFile( 58 | path.join(__dirname, 'public', 'index.html'), 59 | { encoding: 'utf-8'}, 60 | function(err, tmpl) { 61 | var html = plates.bind(tmpl, { 62 | app: appHtml, 63 | appData: 'APP_DATA=' + JSON.stringify({ lists: todoLists, history: req.query.mode !== 'hash' }) 64 | }); 65 | 66 | res.set('Content-Type', 'text/html'); 67 | res.send(html); 68 | } 69 | ); 70 | } 71 | 72 | // API ENDPOINTS 73 | 74 | app.post('/api/lists/', function createList(req, res) { 75 | var list = req.body; 76 | list.id = todoLists.length + 1; 77 | _defaults(list, { name: 'New List', todos: [] }); 78 | todoLists.push(list); 79 | 80 | saveDB(function() { 81 | res.status(201).send(list); 82 | }); 83 | }); 84 | 85 | app.delete('/api/lists/:listId', function deleteList(req, res) { 86 | var listId = req.params.listId, 87 | removedList = _remove(todoLists, { id: listId }); 88 | 89 | saveDB(function() { 90 | res.send(removedList); 91 | }); 92 | }); 93 | 94 | app.post('/api/lists/:listId/todos/', function createTodo(req, res) { 95 | var list = _find(todoLists, { id: parseInt(req.params.listId) }), 96 | todo = req.body; 97 | 98 | todo.id = list.todos.length + 1; 99 | list.todos.push(todo); 100 | 101 | saveDB(function() { 102 | res.send(todo); 103 | }); 104 | }); 105 | 106 | app.put('/api/lists/:listName/todos/:todoId', function updateTodo(req, res) { 107 | var list = _find(todoLists, { id: req.params.listId }), 108 | todoId = parseInt(req.params.id), 109 | props = req.body, 110 | todo = _find(list.todos, { id: todoId }); 111 | 112 | _extend(todo, props); 113 | 114 | saveDB(function() { 115 | res.send(todo); 116 | }); 117 | }); 118 | 119 | app.delete('/api/lists/:listName/todos/:todoId', function deleteTodo(req, res) { 120 | var list = _find(todoLists, { id: req.params.listId }), 121 | todoId = parseInt(req.params.id), 122 | removed = _remove(list.todos, { id: todoId }); 123 | 124 | saveDB(function() { 125 | res.send(removed); 126 | }); 127 | }); 128 | 129 | app.use(express.static(path.join(__dirname, 'public'))); 130 | app.listen(4000); 131 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | main: path.resolve('./app/main.js') 7 | }, 8 | output: { 9 | path: path.resolve('./public'), 10 | filename: 'js/app.js' 11 | }, 12 | resolve: { 13 | root: ["node_modules"] 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | RouterMixin: require('./lib/RouterMixin'), 3 | navigate: require('./lib/navigate'), 4 | replaceNavigate: require('./lib/replaceNavigate') 5 | }; 6 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.config'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | 6 | basePath: '', 7 | 8 | files: [ 9 | 'node_modules/console-polyfill/index.js', 10 | 'test/vendor/es5-shim.js', 11 | 'test/vendor/es5-sham.js', 12 | 'test/vendor/jquery-1.11.3.js', 13 | 'test/client/**/*.test.js' 14 | ], 15 | 16 | frameworks: ['mocha'], 17 | 18 | preprocessors: { 19 | 'test/client/**/*.test.js': ['webpack'] 20 | }, 21 | 22 | reporters: ['progress'], 23 | 24 | webpack: { 25 | debug: true, 26 | devtool: 'eval' 27 | }, 28 | 29 | webpackMiddleware: { 30 | stats: false 31 | }, 32 | 33 | autoWatch: true, 34 | browsers: ['PhantomJS'] 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /karma.saucelabs.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prerequistes: 3 | * 4 | * 1. Sauce Connect already running 5 | * 2. SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set 6 | */ 7 | module.exports = function(config) { 8 | 9 | require('./karma.conf')(config); 10 | 11 | var customLaunchers = { 12 | sl_chrome: { 13 | base: 'SauceLabs', 14 | browserName: 'chrome', 15 | platform: 'Windows 8.1' 16 | }, 17 | 18 | sl_ie_11: { 19 | base: 'SauceLabs', 20 | browserName: 'internet explorer', 21 | platform: 'Windows 8.1', 22 | version: '11' 23 | }, 24 | 25 | sl_ie_9: { 26 | base: 'SauceLabs', 27 | browserName: 'internet explorer', 28 | platform: 'Windows 7', 29 | version: '9' 30 | }, 31 | 32 | sl_ie_8: { 33 | base: 'SauceLabs', 34 | browserName: 'internet explorer', 35 | platform: 'Windows 7', 36 | version: '8' 37 | }, 38 | 39 | sl_firefox_35: { 40 | base: 'SauceLabs', 41 | browserName: 'firefox', 42 | platform: 'Windows 7' 43 | } 44 | }; 45 | 46 | config.reporters.push('saucelabs'); 47 | 48 | config.set({ 49 | 50 | sauceLabs: { 51 | testName: 'react-mini-router client tests', 52 | }, 53 | 54 | customLaunchers: customLaunchers, 55 | 56 | browsers: Object.keys(customLaunchers), 57 | 58 | singleRun: true 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/RouterMixin.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | PropTypes = require('prop-types'), 4 | EventListener = require('fbjs/lib/EventListener'), 5 | getEventTarget = require('./getEventTarget'), 6 | pathToRegexp = require('path-to-regexp'), 7 | urllite = require('urllite/lib/core'), 8 | detect = require('./detect'); 9 | 10 | var PropValidation = { 11 | path: PropTypes.string, 12 | root: PropTypes.string, 13 | useHistory: PropTypes.bool 14 | }; 15 | 16 | module.exports = { 17 | 18 | propTypes: PropValidation, 19 | 20 | contextTypes: PropValidation, 21 | 22 | childContextTypes: PropValidation, 23 | 24 | getChildContext: function() { 25 | return { 26 | path: this.state.path, 27 | root: this.state.root, 28 | useHistory: this.state.useHistory 29 | } 30 | }, 31 | 32 | getDefaultProps: function() { 33 | return { 34 | routes: {} 35 | }; 36 | }, 37 | 38 | getInitialState: function() { 39 | return { 40 | path: getInitialPath(this), 41 | root: this.props.root || this.context.path || '', 42 | useHistory: (this.props.history || this.context.useHistory) && detect.hasPushState 43 | }; 44 | }, 45 | 46 | componentWillMount: function() { 47 | this.setState({ _routes: processRoutes(this.state.root, this.routes, this) }); 48 | }, 49 | 50 | componentDidMount: function() { 51 | var _events = this._events = []; 52 | 53 | _events.push(EventListener.listen(ReactDOM.findDOMNode(this), 'click', this.handleClick)); 54 | 55 | if (this.state.useHistory) { 56 | _events.push(EventListener.listen(window, 'popstate', this.onPopState)); 57 | } else { 58 | if (window.location.hash.indexOf('#!') === -1) { 59 | window.location.hash = '#!/'; 60 | } 61 | 62 | _events.push(EventListener.listen(window, 'hashchange', this.onPopState)); 63 | } 64 | }, 65 | 66 | componentWillUnmount: function() { 67 | this._events.forEach(function(listener) { 68 | listener.remove(); 69 | }); 70 | }, 71 | 72 | onPopState: function() { 73 | var url = urllite(window.location.href), 74 | hash = url.hash || '', 75 | path = this.state.useHistory ? url.pathname : hash.slice(2); 76 | 77 | if (path.length === 0) path = '/'; 78 | 79 | this.setState({ path: path + url.search }); 80 | }, 81 | 82 | renderCurrentRoute: function() { 83 | var path = this.state.path, 84 | url = urllite(path), 85 | queryParams = parseSearch(url.search); 86 | 87 | var parsedPath = url.pathname; 88 | 89 | if (!parsedPath || parsedPath.length === 0) parsedPath = '/'; 90 | 91 | var matchedRoute = this.matchRoute(parsedPath); 92 | 93 | if (matchedRoute) { 94 | return matchedRoute.handler.apply(this, matchedRoute.params.concat(queryParams)); 95 | } else if (this.notFound) { 96 | return this.notFound(parsedPath, queryParams); 97 | } else { 98 | throw new Error('No route matched path: ' + parsedPath); 99 | } 100 | }, 101 | 102 | handleClick: function(evt) { 103 | var self = this, 104 | url = getHref(evt); 105 | 106 | if (url && self.matchRoute(url.pathname)) { 107 | if(evt.preventDefault) { 108 | evt.preventDefault(); 109 | } else { 110 | evt.returnValue = false; 111 | } 112 | 113 | // See: http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html 114 | // Give any component event listeners a chance to fire in the current event loop, 115 | // since they happen at the end of the bubbling phase. (Allows an onClick prop to 116 | // work correctly on the event target component.) 117 | setTimeout(function() { 118 | var pathWithSearch = url.pathname + (url.search || ''); 119 | if (pathWithSearch.length === 0) pathWithSearch = '/'; 120 | 121 | if (self.state.useHistory) { 122 | window.history.pushState({}, '', pathWithSearch); 123 | } else { 124 | window.location.hash = '!' + pathWithSearch; 125 | } 126 | 127 | self.setState({ path: pathWithSearch}); 128 | }, 0); 129 | } 130 | }, 131 | 132 | matchRoute: function(path) { 133 | if (!path) { 134 | return false; 135 | } 136 | 137 | var matchedRoute = {}; 138 | 139 | this.state._routes.some(function(route) { 140 | var matches = route.pattern.exec(path); 141 | 142 | if (matches) { 143 | matchedRoute.handler = route.handler; 144 | matchedRoute.params = matches.slice(1, route.params.length + 1); 145 | 146 | return true; 147 | } 148 | 149 | return false; 150 | }); 151 | 152 | return matchedRoute.handler ? matchedRoute : false; 153 | } 154 | 155 | }; 156 | 157 | function getInitialPath(component) { 158 | var path = component.props.path || component.context.path, 159 | hash, 160 | url; 161 | 162 | if (!path && detect.canUseDOM) { 163 | url = urllite(window.location.href); 164 | 165 | if (component.props.history) { 166 | path = url.pathname + url.search; 167 | } else if (url.hash) { 168 | hash = urllite(url.hash.slice(2)); 169 | path = hash.pathname + hash.search; 170 | } 171 | } 172 | 173 | return path || '/'; 174 | } 175 | 176 | function getHref(evt) { 177 | if (evt.defaultPrevented) { 178 | return; 179 | } 180 | 181 | if (evt.metaKey || evt.ctrlKey || evt.shiftKey) { 182 | return; 183 | } 184 | 185 | if (evt.button !== 0) { 186 | return; 187 | } 188 | 189 | var elt = getEventTarget(evt); 190 | 191 | // Since a click could originate from a child element of the tag, 192 | // walk back up the tree to find it. 193 | while (elt && elt.nodeName !== 'A') { 194 | elt = elt.parentNode; 195 | } 196 | 197 | if (!elt) { 198 | return; 199 | } 200 | 201 | if (elt.target && elt.target !== '_self') { 202 | return; 203 | } 204 | 205 | if (!!elt.attributes.download) { 206 | return; 207 | } 208 | 209 | var linkURL = urllite(elt.href); 210 | var windowURL = urllite(window.location.href); 211 | 212 | if (linkURL.protocol !== windowURL.protocol || linkURL.host !== windowURL.host) { 213 | return; 214 | } 215 | 216 | return linkURL; 217 | } 218 | 219 | function processRoutes(root, routes, component) { 220 | var patterns = [], 221 | path, pattern, keys, handler, handlerFn; 222 | 223 | for (path in routes) { 224 | if (routes.hasOwnProperty(path)) { 225 | keys = []; 226 | pattern = pathToRegexp(root + path, keys); 227 | handler = routes[path]; 228 | handlerFn = component[handler]; 229 | 230 | patterns.push({ pattern: pattern, params: keys, handler: handlerFn }); 231 | } 232 | } 233 | 234 | return patterns; 235 | } 236 | 237 | function parseSearch(str) { 238 | var parsed = {}; 239 | 240 | if (str.indexOf('?') === 0) str = str.slice(1); 241 | 242 | var pairs = str.split('&'); 243 | 244 | pairs.forEach(function(pair) { 245 | var keyVal = pair.split('='); 246 | 247 | parsed[decodeURIComponent(keyVal[0])] = decodeURIComponent(keyVal[1]); 248 | }); 249 | 250 | return parsed; 251 | } 252 | -------------------------------------------------------------------------------- /lib/detect.js: -------------------------------------------------------------------------------- 1 | var canUseDOM = !!( 2 | typeof window !== 'undefined' && 3 | window.document && 4 | window.document.createElement 5 | ); 6 | 7 | module.exports = { 8 | canUseDOM: canUseDOM, 9 | hasPushState: canUseDOM && window.history && 'pushState' in window.history, 10 | hasReplaceState: canUseDOM && window.history && 'replaceState' in window.history, 11 | hasHashbang: function() { 12 | return canUseDOM && window.location.hash.indexOf('#!') === 0; 13 | }, 14 | hasEventConstructor: function() { 15 | return typeof window.Event == "function"; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/event.js: -------------------------------------------------------------------------------- 1 | var detect = require('./detect'); 2 | 3 | module.exports = { 4 | createEvent: function(name) { 5 | if (detect.hasEventConstructor()) { 6 | return new window.Event(name); 7 | } else { 8 | var event = document.createEvent('Event'); 9 | event.initEvent(name, true, false); 10 | return event; 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/getEventTarget.js: -------------------------------------------------------------------------------- 1 | // addressing https://github.com/larrymyers/react-mini-router/issues/65 2 | // this code taken from https://raw.githubusercontent.com/facebook/react/v15.3.2/src/renderers/dom/client/utils/getEventTarget.js 3 | 'use strict'; 4 | 5 | /** 6 | * Gets the target node from a native browser event by accounting for 7 | * inconsistencies in browser DOM APIs. 8 | * 9 | * @param {object} nativeEvent Native browser event. 10 | * @return {DOMEventTarget} Target node. 11 | */ 12 | function getEventTarget(nativeEvent) { 13 | var target = nativeEvent.target || nativeEvent.srcElement || window; 14 | 15 | // Normalize SVG element events #4963 16 | if (target.correspondingUseElement) { 17 | target = target.correspondingUseElement; 18 | } 19 | 20 | // Safari may fire events on text nodes (Node.TEXT_NODE is 3). 21 | // @see http://www.quirksmode.org/js/events_properties.html 22 | return target.nodeType === 3 ? target.parentNode : target; 23 | } 24 | 25 | module.exports = getEventTarget; -------------------------------------------------------------------------------- /lib/navigate.js: -------------------------------------------------------------------------------- 1 | var detect = require('./detect'); 2 | var event = require('./event'); 3 | 4 | module.exports = function triggerUrl(url, silent) { 5 | if (detect.hasHashbang()) { 6 | window.location.hash = '#!' + url; 7 | } else if (detect.hasPushState) { 8 | window.history.pushState({}, '', url); 9 | if (!silent) { 10 | window.dispatchEvent(event.createEvent('popstate')); 11 | } 12 | } else { 13 | console.error("Browser does not support pushState, and hash is missing a hashbang prefix!"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/replaceNavigate.js: -------------------------------------------------------------------------------- 1 | var detect = require('./detect'); 2 | var event = require('./event'); 3 | 4 | module.exports = function triggerUrl(url, silent) { 5 | if (detect.hasHashbang()) { 6 | window.location.hash = '#!' + url; 7 | 8 | } else if (detect.hasReplaceState) { 9 | window.history.replaceState({}, '', url); 10 | if (!silent) { 11 | window.dispatchEvent(event.createEvent('popstate')); 12 | } 13 | } else { 14 | console.error("Browser does not support replaceState, and hash is missing a hashbang prefix!"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mini-router", 3 | "version": "2.2.1", 4 | "description": "A small url router for React apps.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "dist": "npm run build && webpack -p --output-file=react-mini-router.min.js", 9 | "test": "npm run test-client && npm run test-server", 10 | "test-client": "karma start --single-run", 11 | "test-server": "mocha test/server/**/*.test.js" 12 | }, 13 | "author": "Larry Myers ", 14 | "homepage": "https://github.com/larrymyers/react-mini-router", 15 | "keywords": [ 16 | "react", 17 | "router" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/larrymyers/react-mini-router" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/larrymyers/react-mini-router/issues" 25 | }, 26 | "license": "MIT", 27 | "peerDependencies": { 28 | "react": ">=15.4.1", 29 | "react-dom": ">=15.4.1" 30 | }, 31 | "dependencies": { 32 | "fbjs": "^0.3.2", 33 | "path-to-regexp": "^1.0.1", 34 | "urllite": "^0.5.0", 35 | "prop-types": "^15.5.0" 36 | }, 37 | "devDependencies": { 38 | "cheerio": "^0.18.0", 39 | "console-polyfill": "^0.2.1", 40 | "karma": "^1.3.0", 41 | "karma-chrome-launcher": "^0.1.7", 42 | "karma-cli": "1.0.1", 43 | "karma-mocha": "^0.1.10", 44 | "karma-phantomjs-launcher": "^1.0.2", 45 | "karma-sauce-launcher": "^0.2.10", 46 | "karma-webpack": "^1.7.0", 47 | "mocha": "^2.1.0", 48 | "react": "^15.5.4", 49 | "react-dom": "^15.5.4", 50 | "create-react-class": "^15.5.0", 51 | "uglify-js": "^2.4.15", 52 | "webpack": "^1.12.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/client/detect.test.js: -------------------------------------------------------------------------------- 1 | /* global describe:true, it:true */ 2 | 3 | var assert = require('assert'); 4 | 5 | describe('detect', function() { 6 | 7 | var detect = require('./../../lib/detect'); 8 | 9 | it('Should detect if it is running in a DOM environment.', function() { 10 | assert.ok(detect.canUseDOM); 11 | }); 12 | 13 | it('Should detect if HTML5 History API is available.', function() { 14 | var isIE9 = navigator.userAgent.indexOf('MSIE 9') > -1, 15 | isIE8 = navigator.userAgent.indexOf('MSIE 8') > -1; 16 | 17 | assert.ok(detect.hasPushState !== (isIE9 || isIE8)); 18 | }); 19 | 20 | it('Should detect if it is in hashbang url mode.', function() { 21 | window.location.hash = '#!/foo'; 22 | 23 | assert.ok(detect.hasHashbang()); 24 | 25 | window.location.hash = ''; 26 | 27 | assert.ok(!detect.hasHashbang()); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/client/history.test.js: -------------------------------------------------------------------------------- 1 | /* global describe:true, it:true, beforeEach:true, afterEach:true */ 2 | 3 | var assert = require('assert'), 4 | React = require('react'), 5 | ReactDOM = require('react-dom'), 6 | createReactClass = require('create-react-class') 7 | RouterMixin = require('./../../lib/RouterMixin'), 8 | navigate = require('./../../lib/navigate'); 9 | 10 | var App, NestedApp; 11 | 12 | describe('RouterMixin', function() { 13 | 14 | beforeEach(function() { 15 | $('body').append('
'); 16 | }); 17 | 18 | afterEach(function() { 19 | var $root = $('.app'); 20 | ReactDOM.unmountComponentAtNode($root.get(0)); 21 | $root.remove(); 22 | setHash(''); 23 | }); 24 | 25 | function setHash(url) { 26 | window.location.hash = '#!' + url; 27 | } 28 | 29 | it('Should set the initial path to the root when there is no hash and no History API.', function(done) { 30 | window.location.hash = ''; 31 | 32 | ReactDOM.render( 33 | App(), 34 | $('.app').get(0) 35 | ); 36 | 37 | assert.equal($('.home').length, 1); 38 | assert.equal($('.nested').length, 0); 39 | done(); 40 | }); 41 | 42 | it('Should set the initial path to the hash with query params.', function(done) { 43 | setHash('/search?q=1'); 44 | 45 | ReactDOM.render( 46 | App(), 47 | $('.app').get(0) 48 | ); 49 | 50 | assert.equal($('.search').length, 1); 51 | assert.equal($('.search').text(), '1'); 52 | done(); 53 | }); 54 | 55 | it('Should render the matched route when the hash url changes.', function(done) { 56 | setHash('/'); 57 | 58 | ReactDOM.render( 59 | App(), 60 | $('.app').get(0) 61 | ); 62 | 63 | assert.equal($('.home').length, 1); 64 | assert.equal($('.nested').length, 0); 65 | 66 | setHash('/nested/'); 67 | 68 | setTimeout(function() { 69 | assert.equal($('.home').length, 0); 70 | assert.equal($('.nested').length, 1); 71 | done(); 72 | }, 100); 73 | }); 74 | 75 | it('Should render the matched route when the url changes and history support is enabled.', function() { 76 | // TODO create a pushState test, but detect the functionality first since phantomjs won't 77 | // have history API support until 2.0. 78 | }); 79 | 80 | it('Should trigger a route when navigate is called with a routable url.', function() { 81 | 82 | }); 83 | 84 | it('Should trigger a route when a link is clicked on with a routable href.', function() { 85 | // TODO make sure propagation is stopped correctly so a handler matches 86 | // on the correct app (with nesting) 87 | }); 88 | 89 | }); 90 | 91 | var AppClass = createReactClass({ 92 | 93 | mixins: [RouterMixin], 94 | 95 | routes: { 96 | '/': 'home', 97 | '/nested/:path*': 'nestedApp', 98 | '/search': 'search' 99 | }, 100 | 101 | render: function() { 102 | return this.renderCurrentRoute(); 103 | }, 104 | 105 | home: function() { 106 | return React.DOM.div({ className: 'home' }, 'root home'); 107 | }, 108 | 109 | nestedApp: function() { 110 | return NestedApp({ root: '/nested' }); 111 | }, 112 | 113 | search: function(params) { 114 | return React.DOM.div({ className: 'search' }, params.q); 115 | } 116 | 117 | }); 118 | 119 | var NestedAppClass = createReactClass({ 120 | 121 | mixins: [RouterMixin], 122 | 123 | routes: { 124 | '/': 'home' 125 | }, 126 | 127 | render: function() { 128 | return this.renderCurrentRoute(); 129 | }, 130 | 131 | home: function() { 132 | return React.DOM.div({ className: 'nested' }, 'nested home'); 133 | } 134 | 135 | }); 136 | 137 | App = React.createFactory(AppClass); 138 | NestedApp = React.createFactory(NestedAppClass); 139 | -------------------------------------------------------------------------------- /test/server/RouterMixin.test.js: -------------------------------------------------------------------------------- 1 | /* global describe:true, it:true */ 2 | 3 | var assert = require('assert'), 4 | cheerio = require('cheerio'), 5 | React = require('react'), 6 | ReactDOMServer = require('react-dom/server'), 7 | createReactClass = require('create-react-class'), 8 | RouterMixin = require('./../../lib/RouterMixin'); 9 | 10 | describe('RouterMixin', function() { 11 | 12 | it('Should render the route that matches the path prop.', function() { 13 | var html = ReactDOMServer.renderToString(App({ path: '/search/foo' })), 14 | $ = cheerio.load(html); 15 | 16 | var $el = $('.search-results'); 17 | assert.equal($el.length, 1); 18 | assert.equal($el.text(), 'foo'); 19 | }); 20 | 21 | it('Should default path to the url root if it is not passed as a prop.', function() { 22 | var html = ReactDOMServer.renderToString(App()), 23 | $ = cheerio.load(html); 24 | 25 | assert.equal($('.home').length, 1); 26 | }); 27 | 28 | it('Should preprend an optional root to each route, and match on the resulting path.', function() { 29 | var html = ReactDOMServer.renderToString(App({ root: '/bar', path: '/bar/search/foo' })), 30 | $ = cheerio.load(html); 31 | 32 | var $el = $('.search-results'); 33 | assert.equal($el.length, 1); 34 | assert.equal($el.text(), 'foo'); 35 | }); 36 | 37 | it('Should render the notFound handler if no matching route exists.', function() { 38 | var html = ReactDOMServer.renderToString(App({ path: '/bogus' })), 39 | $ = cheerio.load(html); 40 | 41 | assert.equal($('.not-found').length, 1); 42 | }); 43 | 44 | it('Should throw an error if no route matches and a notFound handler does not exist.', function() { 45 | assert.throws( 46 | function() { 47 | ReactDOMServer.renderToString(AppWithoutNotFound({ path: '/bogus' })); 48 | }, 49 | /No route matched path: \/bogus/ 50 | ); 51 | }); 52 | 53 | it('Should pass matched params and the parsed query string to the router handler.', function() { 54 | 55 | }); 56 | 57 | it('Should render the nested app.', function() { 58 | var html = ReactDOMServer.renderToString(App({ path: '/nested' })), 59 | $ = cheerio.load(html); 60 | 61 | assert.equal($('.nested').length, 1); 62 | }); 63 | 64 | }); 65 | 66 | var AppClass = createReactClass({ 67 | 68 | mixins: [RouterMixin], 69 | 70 | routes: { 71 | '/': 'home', 72 | '/search/:searchQuery': 'searchResults', 73 | '/nested/:path*': 'nestedApp' 74 | }, 75 | 76 | render: function() { 77 | return this.renderCurrentRoute(); 78 | }, 79 | 80 | home: function() { 81 | return React.DOM.div({ className: 'home' }, 'home content'); 82 | }, 83 | 84 | searchResults: function(searchQuery, params) { 85 | return React.DOM.div({ className: 'search-results'}, searchQuery); 86 | }, 87 | 88 | nestedApp: function() { 89 | return NestedApp({ root: '/nested' }); 90 | }, 91 | 92 | notFound: function(path) { 93 | return React.DOM.div({ className: 'not-found'}, path); 94 | } 95 | 96 | }); 97 | 98 | var AppWithoutNotFoundClass = createReactClass({ 99 | 100 | mixins: [RouterMixin], 101 | 102 | routes: { 103 | '/': 'home' 104 | }, 105 | 106 | render: function() { 107 | return this.renderCurrentRoute(); 108 | }, 109 | 110 | home: function() { 111 | return React.DOM.div(null); 112 | } 113 | 114 | }); 115 | 116 | var NestedAppClass = createReactClass({ 117 | 118 | mixins: [RouterMixin], 119 | 120 | routes: { 121 | '/': 'home' 122 | }, 123 | 124 | render: function() { 125 | return this.renderCurrentRoute(); 126 | }, 127 | 128 | home: function() { 129 | return React.DOM.div({ className: 'nested' }, 'test'); 130 | } 131 | 132 | }); 133 | 134 | App = React.createFactory(AppClass); 135 | AppWithoutNotFound = React.createFactory(AppWithoutNotFoundClass); 136 | NestedApp = React.createFactory(NestedAppClass); 137 | -------------------------------------------------------------------------------- /test/vendor/es5-sham.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/es-shims/es5-shim 3 | * @license es5-shim Copyright 2009-2015 by contributors, MIT License 4 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 5 | */ 6 | 7 | // vim: ts=4 sts=4 sw=4 expandtab 8 | 9 | //Add semicolon to prevent IIFE from being passed as argument to concated code. 10 | ; 11 | 12 | // UMD (Universal Module Definition) 13 | // see https://github.com/umdjs/umd/blob/master/returnExports.js 14 | (function (root, factory) { 15 | 'use strict'; 16 | 17 | /*global define, exports, module */ 18 | if (typeof define === 'function' && define.amd) { 19 | // AMD. Register as an anonymous module. 20 | define(factory); 21 | } else if (typeof exports === 'object') { 22 | // Node. Does not work with strict CommonJS, but 23 | // only CommonJS-like enviroments that support module.exports, 24 | // like Node. 25 | module.exports = factory(); 26 | } else { 27 | // Browser globals (root is window) 28 | root.returnExports = factory(); 29 | } 30 | }(this, function () { 31 | 32 | var call = Function.prototype.call; 33 | var prototypeOfObject = Object.prototype; 34 | var owns = call.bind(prototypeOfObject.hasOwnProperty); 35 | 36 | // If JS engine supports accessors creating shortcuts. 37 | var defineGetter; 38 | var defineSetter; 39 | var lookupGetter; 40 | var lookupSetter; 41 | var supportsAccessors = owns(prototypeOfObject, '__defineGetter__'); 42 | if (supportsAccessors) { 43 | /*eslint-disable no-underscore-dangle */ 44 | defineGetter = call.bind(prototypeOfObject.__defineGetter__); 45 | defineSetter = call.bind(prototypeOfObject.__defineSetter__); 46 | lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); 47 | lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); 48 | /*eslint-enable no-underscore-dangle */ 49 | } 50 | 51 | // ES5 15.2.3.2 52 | // http://es5.github.com/#x15.2.3.2 53 | if (!Object.getPrototypeOf) { 54 | // https://github.com/es-shims/es5-shim/issues#issue/2 55 | // http://ejohn.org/blog/objectgetprototypeof/ 56 | // recommended by fschaefer on github 57 | // 58 | // sure, and webreflection says ^_^ 59 | // ... this will nerever possibly return null 60 | // ... Opera Mini breaks here with infinite loops 61 | Object.getPrototypeOf = function getPrototypeOf(object) { 62 | /*eslint-disable no-proto */ 63 | var proto = object.__proto__; 64 | /*eslint-enable no-proto */ 65 | if (proto || proto === null) { 66 | return proto; 67 | } else if (object.constructor) { 68 | return object.constructor.prototype; 69 | } else { 70 | return prototypeOfObject; 71 | } 72 | }; 73 | } 74 | 75 | //ES5 15.2.3.3 76 | //http://es5.github.com/#x15.2.3.3 77 | 78 | var doesGetOwnPropertyDescriptorWork = function doesGetOwnPropertyDescriptorWork(object) { 79 | try { 80 | object.sentinel = 0; 81 | return Object.getOwnPropertyDescriptor(object, 'sentinel').value === 0; 82 | } catch (exception) { 83 | return false; 84 | } 85 | }; 86 | 87 | //check whether getOwnPropertyDescriptor works if it's given. Otherwise, 88 | //shim partially. 89 | if (Object.defineProperty) { 90 | var getOwnPropertyDescriptorWorksOnObject = doesGetOwnPropertyDescriptorWork({}); 91 | var getOwnPropertyDescriptorWorksOnDom = typeof document === 'undefined' || 92 | doesGetOwnPropertyDescriptorWork(document.createElement('div')); 93 | if (!getOwnPropertyDescriptorWorksOnDom || !getOwnPropertyDescriptorWorksOnObject) { 94 | var getOwnPropertyDescriptorFallback = Object.getOwnPropertyDescriptor; 95 | } 96 | } 97 | 98 | if (!Object.getOwnPropertyDescriptor || getOwnPropertyDescriptorFallback) { 99 | var ERR_NON_OBJECT = 'Object.getOwnPropertyDescriptor called on a non-object: '; 100 | 101 | /*eslint-disable no-proto */ 102 | Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { 103 | if ((typeof object !== 'object' && typeof object !== 'function') || object === null) { 104 | throw new TypeError(ERR_NON_OBJECT + object); 105 | } 106 | 107 | // make a valiant attempt to use the real getOwnPropertyDescriptor 108 | // for I8's DOM elements. 109 | if (getOwnPropertyDescriptorFallback) { 110 | try { 111 | return getOwnPropertyDescriptorFallback.call(Object, object, property); 112 | } catch (exception) { 113 | // try the shim if the real one doesn't work 114 | } 115 | } 116 | 117 | var descriptor; 118 | 119 | // If object does not owns property return undefined immediately. 120 | if (!owns(object, property)) { 121 | return descriptor; 122 | } 123 | 124 | // If object has a property then it's for sure both `enumerable` and 125 | // `configurable`. 126 | descriptor = { enumerable: true, configurable: true }; 127 | 128 | // If JS engine supports accessor properties then property may be a 129 | // getter or setter. 130 | if (supportsAccessors) { 131 | // Unfortunately `__lookupGetter__` will return a getter even 132 | // if object has own non getter property along with a same named 133 | // inherited getter. To avoid misbehavior we temporary remove 134 | // `__proto__` so that `__lookupGetter__` will return getter only 135 | // if it's owned by an object. 136 | var prototype = object.__proto__; 137 | var notPrototypeOfObject = object !== prototypeOfObject; 138 | // avoid recursion problem, breaking in Opera Mini when 139 | // Object.getOwnPropertyDescriptor(Object.prototype, 'toString') 140 | // or any other Object.prototype accessor 141 | if (notPrototypeOfObject) { 142 | object.__proto__ = prototypeOfObject; 143 | } 144 | 145 | var getter = lookupGetter(object, property); 146 | var setter = lookupSetter(object, property); 147 | 148 | if (notPrototypeOfObject) { 149 | // Once we have getter and setter we can put values back. 150 | object.__proto__ = prototype; 151 | } 152 | 153 | if (getter || setter) { 154 | if (getter) { 155 | descriptor.get = getter; 156 | } 157 | if (setter) { 158 | descriptor.set = setter; 159 | } 160 | // If it was accessor property we're done and return here 161 | // in order to avoid adding `value` to the descriptor. 162 | return descriptor; 163 | } 164 | } 165 | 166 | // If we got this far we know that object has an own property that is 167 | // not an accessor so we set it as a value and return descriptor. 168 | descriptor.value = object[property]; 169 | descriptor.writable = true; 170 | return descriptor; 171 | }; 172 | /*eslint-enable no-proto */ 173 | } 174 | 175 | // ES5 15.2.3.4 176 | // http://es5.github.com/#x15.2.3.4 177 | if (!Object.getOwnPropertyNames) { 178 | Object.getOwnPropertyNames = function getOwnPropertyNames(object) { 179 | return Object.keys(object); 180 | }; 181 | } 182 | 183 | // ES5 15.2.3.5 184 | // http://es5.github.com/#x15.2.3.5 185 | if (!Object.create) { 186 | 187 | // Contributed by Brandon Benvie, October, 2012 188 | var createEmpty; 189 | var supportsProto = !({ __proto__: null } instanceof Object); 190 | // the following produces false positives 191 | // in Opera Mini => not a reliable check 192 | // Object.prototype.__proto__ === null 193 | /*global document */ 194 | if (supportsProto || typeof document === 'undefined') { 195 | createEmpty = function () { 196 | return { __proto__: null }; 197 | }; 198 | } else { 199 | // In old IE __proto__ can't be used to manually set `null`, nor does 200 | // any other method exist to make an object that inherits from nothing, 201 | // aside from Object.prototype itself. Instead, create a new global 202 | // object and *steal* its Object.prototype and strip it bare. This is 203 | // used as the prototype to create nullary objects. 204 | createEmpty = function () { 205 | var iframe = document.createElement('iframe'); 206 | var parent = document.body || document.documentElement; 207 | iframe.style.display = 'none'; 208 | parent.appendChild(iframe); 209 | /*eslint-disable no-script-url */ 210 | iframe.src = 'javascript:'; 211 | /*eslint-enable no-script-url */ 212 | var empty = iframe.contentWindow.Object.prototype; 213 | parent.removeChild(iframe); 214 | iframe = null; 215 | delete empty.constructor; 216 | delete empty.hasOwnProperty; 217 | delete empty.propertyIsEnumerable; 218 | delete empty.isPrototypeOf; 219 | delete empty.toLocaleString; 220 | delete empty.toString; 221 | delete empty.valueOf; 222 | /*eslint-disable no-proto */ 223 | empty.__proto__ = null; 224 | /*eslint-enable no-proto */ 225 | 226 | var Empty = function Empty() {}; 227 | Empty.prototype = empty; 228 | // short-circuit future calls 229 | createEmpty = function () { 230 | return new Empty(); 231 | }; 232 | return new Empty(); 233 | }; 234 | } 235 | 236 | Object.create = function create(prototype, properties) { 237 | 238 | var object; 239 | var Type = function Type() {}; // An empty constructor. 240 | 241 | if (prototype === null) { 242 | object = createEmpty(); 243 | } else { 244 | if (typeof prototype !== 'object' && typeof prototype !== 'function') { 245 | // In the native implementation `parent` can be `null` 246 | // OR *any* `instanceof Object` (Object|Function|Array|RegExp|etc) 247 | // Use `typeof` tho, b/c in old IE, DOM elements are not `instanceof Object` 248 | // like they are in modern browsers. Using `Object.create` on DOM elements 249 | // is...err...probably inappropriate, but the native version allows for it. 250 | throw new TypeError('Object prototype may only be an Object or null'); // same msg as Chrome 251 | } 252 | Type.prototype = prototype; 253 | object = new Type(); 254 | // IE has no built-in implementation of `Object.getPrototypeOf` 255 | // neither `__proto__`, but this manually setting `__proto__` will 256 | // guarantee that `Object.getPrototypeOf` will work as expected with 257 | // objects created using `Object.create` 258 | /*eslint-disable no-proto */ 259 | object.__proto__ = prototype; 260 | /*eslint-enable no-proto */ 261 | } 262 | 263 | if (properties !== void 0) { 264 | Object.defineProperties(object, properties); 265 | } 266 | 267 | return object; 268 | }; 269 | } 270 | 271 | // ES5 15.2.3.6 272 | // http://es5.github.com/#x15.2.3.6 273 | 274 | // Patch for WebKit and IE8 standard mode 275 | // Designed by hax 276 | // related issue: https://github.com/es-shims/es5-shim/issues#issue/5 277 | // IE8 Reference: 278 | // http://msdn.microsoft.com/en-us/library/dd282900.aspx 279 | // http://msdn.microsoft.com/en-us/library/dd229916.aspx 280 | // WebKit Bugs: 281 | // https://bugs.webkit.org/show_bug.cgi?id=36423 282 | 283 | var doesDefinePropertyWork = function doesDefinePropertyWork(object) { 284 | try { 285 | Object.defineProperty(object, 'sentinel', {}); 286 | return 'sentinel' in object; 287 | } catch (exception) { 288 | return false; 289 | } 290 | }; 291 | 292 | // check whether defineProperty works if it's given. Otherwise, 293 | // shim partially. 294 | if (Object.defineProperty) { 295 | var definePropertyWorksOnObject = doesDefinePropertyWork({}); 296 | var definePropertyWorksOnDom = typeof document === 'undefined' || 297 | doesDefinePropertyWork(document.createElement('div')); 298 | if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { 299 | var definePropertyFallback = Object.defineProperty, 300 | definePropertiesFallback = Object.defineProperties; 301 | } 302 | } 303 | 304 | if (!Object.defineProperty || definePropertyFallback) { 305 | var ERR_NON_OBJECT_DESCRIPTOR = 'Property description must be an object: '; 306 | var ERR_NON_OBJECT_TARGET = 'Object.defineProperty called on non-object: '; 307 | var ERR_ACCESSORS_NOT_SUPPORTED = 'getters & setters can not be defined on this javascript engine'; 308 | 309 | Object.defineProperty = function defineProperty(object, property, descriptor) { 310 | if ((typeof object !== 'object' && typeof object !== 'function') || object === null) { 311 | throw new TypeError(ERR_NON_OBJECT_TARGET + object); 312 | } 313 | if ((typeof descriptor !== 'object' && typeof descriptor !== 'function') || descriptor === null) { 314 | throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); 315 | } 316 | // make a valiant attempt to use the real defineProperty 317 | // for I8's DOM elements. 318 | if (definePropertyFallback) { 319 | try { 320 | return definePropertyFallback.call(Object, object, property, descriptor); 321 | } catch (exception) { 322 | // try the shim if the real one doesn't work 323 | } 324 | } 325 | 326 | // If it's a data property. 327 | if ('value' in descriptor) { 328 | // fail silently if 'writable', 'enumerable', or 'configurable' 329 | // are requested but not supported 330 | /* 331 | // alternate approach: 332 | if ( // can't implement these features; allow false but not true 333 | ('writable' in descriptor && !descriptor.writable) || 334 | ('enumerable' in descriptor && !descriptor.enumerable) || 335 | ('configurable' in descriptor && !descriptor.configurable) 336 | )) 337 | throw new RangeError( 338 | 'This implementation of Object.defineProperty does not support configurable, enumerable, or writable.' 339 | ); 340 | */ 341 | 342 | if (supportsAccessors && (lookupGetter(object, property) || lookupSetter(object, property))) { 343 | // As accessors are supported only on engines implementing 344 | // `__proto__` we can safely override `__proto__` while defining 345 | // a property to make sure that we don't hit an inherited 346 | // accessor. 347 | /*eslint-disable no-proto */ 348 | var prototype = object.__proto__; 349 | object.__proto__ = prototypeOfObject; 350 | // Deleting a property anyway since getter / setter may be 351 | // defined on object itself. 352 | delete object[property]; 353 | object[property] = descriptor.value; 354 | // Setting original `__proto__` back now. 355 | object.__proto__ = prototype; 356 | /*eslint-enable no-proto */ 357 | } else { 358 | object[property] = descriptor.value; 359 | } 360 | } else { 361 | if (!supportsAccessors) { 362 | throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); 363 | } 364 | // If we got that far then getters and setters can be defined !! 365 | if ('get' in descriptor) { 366 | defineGetter(object, property, descriptor.get); 367 | } 368 | if ('set' in descriptor) { 369 | defineSetter(object, property, descriptor.set); 370 | } 371 | } 372 | return object; 373 | }; 374 | } 375 | 376 | // ES5 15.2.3.7 377 | // http://es5.github.com/#x15.2.3.7 378 | if (!Object.defineProperties || definePropertiesFallback) { 379 | Object.defineProperties = function defineProperties(object, properties) { 380 | // make a valiant attempt to use the real defineProperties 381 | if (definePropertiesFallback) { 382 | try { 383 | return definePropertiesFallback.call(Object, object, properties); 384 | } catch (exception) { 385 | // try the shim if the real one doesn't work 386 | } 387 | } 388 | 389 | for (var property in properties) { 390 | if (owns(properties, property) && property !== '__proto__') { 391 | Object.defineProperty(object, property, properties[property]); 392 | } 393 | } 394 | return object; 395 | }; 396 | } 397 | 398 | // ES5 15.2.3.8 399 | // http://es5.github.com/#x15.2.3.8 400 | if (!Object.seal) { 401 | Object.seal = function seal(object) { 402 | if (Object(object) !== object) { 403 | throw new TypeError('Object.seal can only be called on Objects.'); 404 | } 405 | // this is misleading and breaks feature-detection, but 406 | // allows "securable" code to "gracefully" degrade to working 407 | // but insecure code. 408 | return object; 409 | }; 410 | } 411 | 412 | // ES5 15.2.3.9 413 | // http://es5.github.com/#x15.2.3.9 414 | if (!Object.freeze) { 415 | Object.freeze = function freeze(object) { 416 | if (Object(object) !== object) { 417 | throw new TypeError('Object.freeze can only be called on Objects.'); 418 | } 419 | // this is misleading and breaks feature-detection, but 420 | // allows "securable" code to "gracefully" degrade to working 421 | // but insecure code. 422 | return object; 423 | }; 424 | } 425 | 426 | // detect a Rhino bug and patch it 427 | try { 428 | Object.freeze(function () {}); 429 | } catch (exception) { 430 | Object.freeze = (function freeze(freezeObject) { 431 | return function freeze(object) { 432 | if (typeof object === 'function') { 433 | return object; 434 | } else { 435 | return freezeObject(object); 436 | } 437 | }; 438 | }(Object.freeze)); 439 | } 440 | 441 | // ES5 15.2.3.10 442 | // http://es5.github.com/#x15.2.3.10 443 | if (!Object.preventExtensions) { 444 | Object.preventExtensions = function preventExtensions(object) { 445 | if (Object(object) !== object) { 446 | throw new TypeError('Object.preventExtensions can only be called on Objects.'); 447 | } 448 | // this is misleading and breaks feature-detection, but 449 | // allows "securable" code to "gracefully" degrade to working 450 | // but insecure code. 451 | return object; 452 | }; 453 | } 454 | 455 | // ES5 15.2.3.11 456 | // http://es5.github.com/#x15.2.3.11 457 | if (!Object.isSealed) { 458 | Object.isSealed = function isSealed(object) { 459 | if (Object(object) !== object) { 460 | throw new TypeError('Object.isSealed can only be called on Objects.'); 461 | } 462 | return false; 463 | }; 464 | } 465 | 466 | // ES5 15.2.3.12 467 | // http://es5.github.com/#x15.2.3.12 468 | if (!Object.isFrozen) { 469 | Object.isFrozen = function isFrozen(object) { 470 | if (Object(object) !== object) { 471 | throw new TypeError('Object.isFrozen can only be called on Objects.'); 472 | } 473 | return false; 474 | }; 475 | } 476 | 477 | // ES5 15.2.3.13 478 | // http://es5.github.com/#x15.2.3.13 479 | if (!Object.isExtensible) { 480 | Object.isExtensible = function isExtensible(object) { 481 | // 1. If Type(O) is not Object throw a TypeError exception. 482 | if (Object(object) !== object) { 483 | throw new TypeError('Object.isExtensible can only be called on Objects.'); 484 | } 485 | // 2. Return the Boolean value of the [[Extensible]] internal property of O. 486 | var name = ''; 487 | while (owns(object, name)) { 488 | name += '?'; 489 | } 490 | object[name] = true; 491 | var returnValue = owns(object, name); 492 | delete object[name]; 493 | return returnValue; 494 | }; 495 | } 496 | 497 | })); 498 | -------------------------------------------------------------------------------- /test/vendor/es5-shim.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/es-shims/es5-shim 3 | * @license es5-shim Copyright 2009-2014 by contributors, MIT License 4 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 5 | */ 6 | 7 | // vim: ts=4 sts=4 sw=4 expandtab 8 | 9 | //Add semicolon to prevent IIFE from being passed as argument to concated code. 10 | ; 11 | 12 | // UMD (Universal Module Definition) 13 | // see https://github.com/umdjs/umd/blob/master/returnExports.js 14 | (function (root, factory) { 15 | if (typeof define === 'function' && define.amd) { 16 | // AMD. Register as an anonymous module. 17 | define(factory); 18 | } else if (typeof exports === 'object') { 19 | // Node. Does not work with strict CommonJS, but 20 | // only CommonJS-like enviroments that support module.exports, 21 | // like Node. 22 | module.exports = factory(); 23 | } else { 24 | // Browser globals (root is window) 25 | root.returnExports = factory(); 26 | } 27 | }(this, function () { 28 | 29 | /** 30 | * Brings an environment as close to ECMAScript 5 compliance 31 | * as is possible with the facilities of erstwhile engines. 32 | * 33 | * Annotated ES5: http://es5.github.com/ (specific links below) 34 | * ES5 Spec: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf 35 | * Required reading: http://javascriptweblog.wordpress.com/2011/12/05/extending-javascript-natives/ 36 | */ 37 | 38 | // Shortcut to an often accessed properties, in order to avoid multiple 39 | // dereference that costs universally. 40 | var ArrayPrototype = Array.prototype; 41 | var ObjectPrototype = Object.prototype; 42 | var FunctionPrototype = Function.prototype; 43 | var StringPrototype = String.prototype; 44 | var NumberPrototype = Number.prototype; 45 | var array_slice = ArrayPrototype.slice; 46 | var array_splice = ArrayPrototype.splice; 47 | var array_push = ArrayPrototype.push; 48 | var array_unshift = ArrayPrototype.unshift; 49 | var call = FunctionPrototype.call; 50 | 51 | // Having a toString local variable name breaks in Opera so use _toString. 52 | var _toString = ObjectPrototype.toString; 53 | 54 | var isFunction = function (val) { 55 | return ObjectPrototype.toString.call(val) === '[object Function]'; 56 | }; 57 | var isRegex = function (val) { 58 | return ObjectPrototype.toString.call(val) === '[object RegExp]'; 59 | }; 60 | var isArray = function isArray(obj) { 61 | return _toString.call(obj) === "[object Array]"; 62 | }; 63 | var isString = function isString(obj) { 64 | return _toString.call(obj) === "[object String]"; 65 | }; 66 | var isArguments = function isArguments(value) { 67 | var str = _toString.call(value); 68 | var isArgs = str === '[object Arguments]'; 69 | if (!isArgs) { 70 | isArgs = !isArray(value) 71 | && value !== null 72 | && typeof value === 'object' 73 | && typeof value.length === 'number' 74 | && value.length >= 0 75 | && isFunction(value.callee); 76 | } 77 | return isArgs; 78 | }; 79 | 80 | var supportsDescriptors = Object.defineProperty && (function () { 81 | try { 82 | Object.defineProperty({}, 'x', {}); 83 | return true; 84 | } catch (e) { /* this is ES3 */ 85 | return false; 86 | } 87 | }()); 88 | 89 | // Define configurable, writable and non-enumerable props 90 | // if they don't exist. 91 | var defineProperty; 92 | if (supportsDescriptors) { 93 | defineProperty = function (object, name, method, forceAssign) { 94 | if (!forceAssign && (name in object)) { return; } 95 | Object.defineProperty(object, name, { 96 | configurable: true, 97 | enumerable: false, 98 | writable: true, 99 | value: method 100 | }); 101 | }; 102 | } else { 103 | defineProperty = function (object, name, method, forceAssign) { 104 | if (!forceAssign && (name in object)) { return; } 105 | object[name] = method; 106 | }; 107 | } 108 | var defineProperties = function (object, map, forceAssign) { 109 | for (var name in map) { 110 | if (ObjectPrototype.hasOwnProperty.call(map, name)) { 111 | defineProperty(object, name, map[name], forceAssign); 112 | } 113 | } 114 | }; 115 | 116 | // 117 | // Util 118 | // ====== 119 | // 120 | 121 | // ES5 9.4 122 | // http://es5.github.com/#x9.4 123 | // http://jsperf.com/to-integer 124 | 125 | function toInteger(n) { 126 | n = +n; 127 | if (n !== n) { // isNaN 128 | n = 0; 129 | } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { 130 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 131 | } 132 | return n; 133 | } 134 | 135 | function isPrimitive(input) { 136 | var type = typeof input; 137 | return ( 138 | input === null || 139 | type === "undefined" || 140 | type === "boolean" || 141 | type === "number" || 142 | type === "string" 143 | ); 144 | } 145 | 146 | function toPrimitive(input) { 147 | var val, valueOf, toStr; 148 | if (isPrimitive(input)) { 149 | return input; 150 | } 151 | valueOf = input.valueOf; 152 | if (isFunction(valueOf)) { 153 | val = valueOf.call(input); 154 | if (isPrimitive(val)) { 155 | return val; 156 | } 157 | } 158 | toStr = input.toString; 159 | if (isFunction(toStr)) { 160 | val = toStr.call(input); 161 | if (isPrimitive(val)) { 162 | return val; 163 | } 164 | } 165 | throw new TypeError(); 166 | } 167 | 168 | // ES5 9.9 169 | // http://es5.github.com/#x9.9 170 | var toObject = function (o) { 171 | if (o == null) { // this matches both null and undefined 172 | throw new TypeError("can't convert " + o + " to object"); 173 | } 174 | return Object(o); 175 | }; 176 | 177 | var ToUint32 = function ToUint32(x) { 178 | return x >>> 0; 179 | }; 180 | 181 | // 182 | // Function 183 | // ======== 184 | // 185 | 186 | // ES-5 15.3.4.5 187 | // http://es5.github.com/#x15.3.4.5 188 | 189 | function Empty() {} 190 | 191 | defineProperties(FunctionPrototype, { 192 | bind: function bind(that) { // .length is 1 193 | // 1. Let Target be the this value. 194 | var target = this; 195 | // 2. If IsCallable(Target) is false, throw a TypeError exception. 196 | if (!isFunction(target)) { 197 | throw new TypeError("Function.prototype.bind called on incompatible " + target); 198 | } 199 | // 3. Let A be a new (possibly empty) internal list of all of the 200 | // argument values provided after thisArg (arg1, arg2 etc), in order. 201 | // XXX slicedArgs will stand in for "A" if used 202 | var args = array_slice.call(arguments, 1); // for normal call 203 | // 4. Let F be a new native ECMAScript object. 204 | // 11. Set the [[Prototype]] internal property of F to the standard 205 | // built-in Function prototype object as specified in 15.3.3.1. 206 | // 12. Set the [[Call]] internal property of F as described in 207 | // 15.3.4.5.1. 208 | // 13. Set the [[Construct]] internal property of F as described in 209 | // 15.3.4.5.2. 210 | // 14. Set the [[HasInstance]] internal property of F as described in 211 | // 15.3.4.5.3. 212 | var binder = function () { 213 | 214 | if (this instanceof bound) { 215 | // 15.3.4.5.2 [[Construct]] 216 | // When the [[Construct]] internal method of a function object, 217 | // F that was created using the bind function is called with a 218 | // list of arguments ExtraArgs, the following steps are taken: 219 | // 1. Let target be the value of F's [[TargetFunction]] 220 | // internal property. 221 | // 2. If target has no [[Construct]] internal method, a 222 | // TypeError exception is thrown. 223 | // 3. Let boundArgs be the value of F's [[BoundArgs]] internal 224 | // property. 225 | // 4. Let args be a new list containing the same values as the 226 | // list boundArgs in the same order followed by the same 227 | // values as the list ExtraArgs in the same order. 228 | // 5. Return the result of calling the [[Construct]] internal 229 | // method of target providing args as the arguments. 230 | 231 | var result = target.apply( 232 | this, 233 | args.concat(array_slice.call(arguments)) 234 | ); 235 | if (Object(result) === result) { 236 | return result; 237 | } 238 | return this; 239 | 240 | } else { 241 | // 15.3.4.5.1 [[Call]] 242 | // When the [[Call]] internal method of a function object, F, 243 | // which was created using the bind function is called with a 244 | // this value and a list of arguments ExtraArgs, the following 245 | // steps are taken: 246 | // 1. Let boundArgs be the value of F's [[BoundArgs]] internal 247 | // property. 248 | // 2. Let boundThis be the value of F's [[BoundThis]] internal 249 | // property. 250 | // 3. Let target be the value of F's [[TargetFunction]] internal 251 | // property. 252 | // 4. Let args be a new list containing the same values as the 253 | // list boundArgs in the same order followed by the same 254 | // values as the list ExtraArgs in the same order. 255 | // 5. Return the result of calling the [[Call]] internal method 256 | // of target providing boundThis as the this value and 257 | // providing args as the arguments. 258 | 259 | // equiv: target.call(this, ...boundArgs, ...args) 260 | return target.apply( 261 | that, 262 | args.concat(array_slice.call(arguments)) 263 | ); 264 | 265 | } 266 | 267 | }; 268 | 269 | // 15. If the [[Class]] internal property of Target is "Function", then 270 | // a. Let L be the length property of Target minus the length of A. 271 | // b. Set the length own property of F to either 0 or L, whichever is 272 | // larger. 273 | // 16. Else set the length own property of F to 0. 274 | 275 | var boundLength = Math.max(0, target.length - args.length); 276 | 277 | // 17. Set the attributes of the length own property of F to the values 278 | // specified in 15.3.5.1. 279 | var boundArgs = []; 280 | for (var i = 0; i < boundLength; i++) { 281 | boundArgs.push("$" + i); 282 | } 283 | 284 | // XXX Build a dynamic function with desired amount of arguments is the only 285 | // way to set the length property of a function. 286 | // In environments where Content Security Policies enabled (Chrome extensions, 287 | // for ex.) all use of eval or Function costructor throws an exception. 288 | // However in all of these environments Function.prototype.bind exists 289 | // and so this code will never be executed. 290 | var bound = Function("binder", "return function (" + boundArgs.join(",") + "){return binder.apply(this,arguments)}")(binder); 291 | 292 | if (target.prototype) { 293 | Empty.prototype = target.prototype; 294 | bound.prototype = new Empty(); 295 | // Clean up dangling references. 296 | Empty.prototype = null; 297 | } 298 | 299 | // TODO 300 | // 18. Set the [[Extensible]] internal property of F to true. 301 | 302 | // TODO 303 | // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). 304 | // 20. Call the [[DefineOwnProperty]] internal method of F with 305 | // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: 306 | // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and 307 | // false. 308 | // 21. Call the [[DefineOwnProperty]] internal method of F with 309 | // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, 310 | // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, 311 | // and false. 312 | 313 | // TODO 314 | // NOTE Function objects created using Function.prototype.bind do not 315 | // have a prototype property or the [[Code]], [[FormalParameters]], and 316 | // [[Scope]] internal properties. 317 | // XXX can't delete prototype in pure-js. 318 | 319 | // 22. Return F. 320 | return bound; 321 | } 322 | }); 323 | 324 | // _Please note: Shortcuts are defined after `Function.prototype.bind` as we 325 | // us it in defining shortcuts. 326 | var owns = call.bind(ObjectPrototype.hasOwnProperty); 327 | 328 | // If JS engine supports accessors creating shortcuts. 329 | var defineGetter; 330 | var defineSetter; 331 | var lookupGetter; 332 | var lookupSetter; 333 | var supportsAccessors; 334 | if ((supportsAccessors = owns(ObjectPrototype, "__defineGetter__"))) { 335 | defineGetter = call.bind(ObjectPrototype.__defineGetter__); 336 | defineSetter = call.bind(ObjectPrototype.__defineSetter__); 337 | lookupGetter = call.bind(ObjectPrototype.__lookupGetter__); 338 | lookupSetter = call.bind(ObjectPrototype.__lookupSetter__); 339 | } 340 | 341 | // 342 | // Array 343 | // ===== 344 | // 345 | 346 | // ES5 15.4.4.12 347 | // http://es5.github.com/#x15.4.4.12 348 | var spliceNoopReturnsEmptyArray = (function () { 349 | var a = [1, 2]; 350 | var result = a.splice(); 351 | return a.length === 2 && isArray(result) && result.length === 0; 352 | }()); 353 | defineProperties(ArrayPrototype, { 354 | // Safari 5.0 bug where .splice() returns undefined 355 | splice: function splice(start, deleteCount) { 356 | if (arguments.length === 0) { 357 | return []; 358 | } else { 359 | return array_splice.apply(this, arguments); 360 | } 361 | } 362 | }, spliceNoopReturnsEmptyArray); 363 | 364 | var spliceWorksWithEmptyObject = (function () { 365 | var obj = {}; 366 | ArrayPrototype.splice.call(obj, 0, 0, 1); 367 | return obj.length === 1; 368 | }()); 369 | defineProperties(ArrayPrototype, { 370 | splice: function splice(start, deleteCount) { 371 | if (arguments.length === 0) { return []; } 372 | var args = arguments; 373 | this.length = Math.max(toInteger(this.length), 0); 374 | if (arguments.length > 0 && typeof deleteCount !== 'number') { 375 | args = array_slice.call(arguments); 376 | if (args.length < 2) { 377 | args.push(this.length - start); 378 | } else { 379 | args[1] = toInteger(deleteCount); 380 | } 381 | } 382 | return array_splice.apply(this, args); 383 | } 384 | }, !spliceWorksWithEmptyObject); 385 | 386 | // ES5 15.4.4.12 387 | // http://es5.github.com/#x15.4.4.13 388 | // Return len+argCount. 389 | // [bugfix, ielt8] 390 | // IE < 8 bug: [].unshift(0) === undefined but should be "1" 391 | var hasUnshiftReturnValueBug = [].unshift(0) !== 1; 392 | defineProperties(ArrayPrototype, { 393 | unshift: function () { 394 | array_unshift.apply(this, arguments); 395 | return this.length; 396 | } 397 | }, hasUnshiftReturnValueBug); 398 | 399 | // ES5 15.4.3.2 400 | // http://es5.github.com/#x15.4.3.2 401 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/isArray 402 | defineProperties(Array, { isArray: isArray }); 403 | 404 | // The IsCallable() check in the Array functions 405 | // has been replaced with a strict check on the 406 | // internal class of the object to trap cases where 407 | // the provided function was actually a regular 408 | // expression literal, which in V8 and 409 | // JavaScriptCore is a typeof "function". Only in 410 | // V8 are regular expression literals permitted as 411 | // reduce parameters, so it is desirable in the 412 | // general case for the shim to match the more 413 | // strict and common behavior of rejecting regular 414 | // expressions. 415 | 416 | // ES5 15.4.4.18 417 | // http://es5.github.com/#x15.4.4.18 418 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach 419 | 420 | // Check failure of by-index access of string characters (IE < 9) 421 | // and failure of `0 in boxedString` (Rhino) 422 | var boxedString = Object("a"); 423 | var splitString = boxedString[0] !== "a" || !(0 in boxedString); 424 | 425 | var properlyBoxesContext = function properlyBoxed(method) { 426 | // Check node 0.6.21 bug where third parameter is not boxed 427 | var properlyBoxesNonStrict = true; 428 | var properlyBoxesStrict = true; 429 | if (method) { 430 | method.call('foo', function (_, __, context) { 431 | if (typeof context !== 'object') { properlyBoxesNonStrict = false; } 432 | }); 433 | 434 | method.call([1], function () { 435 | 'use strict'; 436 | properlyBoxesStrict = typeof this === 'string'; 437 | }, 'x'); 438 | } 439 | return !!method && properlyBoxesNonStrict && properlyBoxesStrict; 440 | }; 441 | 442 | defineProperties(ArrayPrototype, { 443 | forEach: function forEach(fun /*, thisp*/) { 444 | var object = toObject(this), 445 | self = splitString && isString(this) ? this.split('') : object, 446 | thisp = arguments[1], 447 | i = -1, 448 | length = self.length >>> 0; 449 | 450 | // If no callback function or if callback is not a callable function 451 | if (!isFunction(fun)) { 452 | throw new TypeError(); // TODO message 453 | } 454 | 455 | while (++i < length) { 456 | if (i in self) { 457 | // Invoke the callback function with call, passing arguments: 458 | // context, property value, property key, thisArg object 459 | // context 460 | fun.call(thisp, self[i], i, object); 461 | } 462 | } 463 | } 464 | }, !properlyBoxesContext(ArrayPrototype.forEach)); 465 | 466 | // ES5 15.4.4.19 467 | // http://es5.github.com/#x15.4.4.19 468 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map 469 | defineProperties(ArrayPrototype, { 470 | map: function map(fun /*, thisp*/) { 471 | var object = toObject(this), 472 | self = splitString && isString(this) ? this.split('') : object, 473 | length = self.length >>> 0, 474 | result = Array(length), 475 | thisp = arguments[1]; 476 | 477 | // If no callback function or if callback is not a callable function 478 | if (!isFunction(fun)) { 479 | throw new TypeError(fun + " is not a function"); 480 | } 481 | 482 | for (var i = 0; i < length; i++) { 483 | if (i in self) { 484 | result[i] = fun.call(thisp, self[i], i, object); 485 | } 486 | } 487 | return result; 488 | } 489 | }, !properlyBoxesContext(ArrayPrototype.map)); 490 | 491 | // ES5 15.4.4.20 492 | // http://es5.github.com/#x15.4.4.20 493 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter 494 | defineProperties(ArrayPrototype, { 495 | filter: function filter(fun /*, thisp */) { 496 | var object = toObject(this), 497 | self = splitString && isString(this) ? this.split('') : object, 498 | length = self.length >>> 0, 499 | result = [], 500 | value, 501 | thisp = arguments[1]; 502 | 503 | // If no callback function or if callback is not a callable function 504 | if (!isFunction(fun)) { 505 | throw new TypeError(fun + " is not a function"); 506 | } 507 | 508 | for (var i = 0; i < length; i++) { 509 | if (i in self) { 510 | value = self[i]; 511 | if (fun.call(thisp, value, i, object)) { 512 | result.push(value); 513 | } 514 | } 515 | } 516 | return result; 517 | } 518 | }, !properlyBoxesContext(ArrayPrototype.filter)); 519 | 520 | // ES5 15.4.4.16 521 | // http://es5.github.com/#x15.4.4.16 522 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every 523 | defineProperties(ArrayPrototype, { 524 | every: function every(fun /*, thisp */) { 525 | var object = toObject(this), 526 | self = splitString && isString(this) ? this.split('') : object, 527 | length = self.length >>> 0, 528 | thisp = arguments[1]; 529 | 530 | // If no callback function or if callback is not a callable function 531 | if (!isFunction(fun)) { 532 | throw new TypeError(fun + " is not a function"); 533 | } 534 | 535 | for (var i = 0; i < length; i++) { 536 | if (i in self && !fun.call(thisp, self[i], i, object)) { 537 | return false; 538 | } 539 | } 540 | return true; 541 | } 542 | }, !properlyBoxesContext(ArrayPrototype.every)); 543 | 544 | // ES5 15.4.4.17 545 | // http://es5.github.com/#x15.4.4.17 546 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some 547 | defineProperties(ArrayPrototype, { 548 | some: function some(fun /*, thisp */) { 549 | var object = toObject(this), 550 | self = splitString && isString(this) ? this.split('') : object, 551 | length = self.length >>> 0, 552 | thisp = arguments[1]; 553 | 554 | // If no callback function or if callback is not a callable function 555 | if (!isFunction(fun)) { 556 | throw new TypeError(fun + " is not a function"); 557 | } 558 | 559 | for (var i = 0; i < length; i++) { 560 | if (i in self && fun.call(thisp, self[i], i, object)) { 561 | return true; 562 | } 563 | } 564 | return false; 565 | } 566 | }, !properlyBoxesContext(ArrayPrototype.some)); 567 | 568 | // ES5 15.4.4.21 569 | // http://es5.github.com/#x15.4.4.21 570 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce 571 | var reduceCoercesToObject = false; 572 | if (ArrayPrototype.reduce) { 573 | reduceCoercesToObject = typeof ArrayPrototype.reduce.call('es5', function (_, __, ___, list) { return list; }) === 'object'; 574 | } 575 | defineProperties(ArrayPrototype, { 576 | reduce: function reduce(fun /*, initial*/) { 577 | var object = toObject(this), 578 | self = splitString && isString(this) ? this.split('') : object, 579 | length = self.length >>> 0; 580 | 581 | // If no callback function or if callback is not a callable function 582 | if (!isFunction(fun)) { 583 | throw new TypeError(fun + " is not a function"); 584 | } 585 | 586 | // no value to return if no initial value and an empty array 587 | if (!length && arguments.length === 1) { 588 | throw new TypeError("reduce of empty array with no initial value"); 589 | } 590 | 591 | var i = 0; 592 | var result; 593 | if (arguments.length >= 2) { 594 | result = arguments[1]; 595 | } else { 596 | do { 597 | if (i in self) { 598 | result = self[i++]; 599 | break; 600 | } 601 | 602 | // if array contains no values, no initial value to return 603 | if (++i >= length) { 604 | throw new TypeError("reduce of empty array with no initial value"); 605 | } 606 | } while (true); 607 | } 608 | 609 | for (; i < length; i++) { 610 | if (i in self) { 611 | result = fun.call(void 0, result, self[i], i, object); 612 | } 613 | } 614 | 615 | return result; 616 | } 617 | }, !reduceCoercesToObject); 618 | 619 | // ES5 15.4.4.22 620 | // http://es5.github.com/#x15.4.4.22 621 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight 622 | var reduceRightCoercesToObject = false; 623 | if (ArrayPrototype.reduceRight) { 624 | reduceRightCoercesToObject = typeof ArrayPrototype.reduceRight.call('es5', function (_, __, ___, list) { return list; }) === 'object'; 625 | } 626 | defineProperties(ArrayPrototype, { 627 | reduceRight: function reduceRight(fun /*, initial*/) { 628 | var object = toObject(this), 629 | self = splitString && isString(this) ? this.split('') : object, 630 | length = self.length >>> 0; 631 | 632 | // If no callback function or if callback is not a callable function 633 | if (!isFunction(fun)) { 634 | throw new TypeError(fun + " is not a function"); 635 | } 636 | 637 | // no value to return if no initial value, empty array 638 | if (!length && arguments.length === 1) { 639 | throw new TypeError("reduceRight of empty array with no initial value"); 640 | } 641 | 642 | var result, i = length - 1; 643 | if (arguments.length >= 2) { 644 | result = arguments[1]; 645 | } else { 646 | do { 647 | if (i in self) { 648 | result = self[i--]; 649 | break; 650 | } 651 | 652 | // if array contains no values, no initial value to return 653 | if (--i < 0) { 654 | throw new TypeError("reduceRight of empty array with no initial value"); 655 | } 656 | } while (true); 657 | } 658 | 659 | if (i < 0) { 660 | return result; 661 | } 662 | 663 | do { 664 | if (i in self) { 665 | result = fun.call(void 0, result, self[i], i, object); 666 | } 667 | } while (i--); 668 | 669 | return result; 670 | } 671 | }, !reduceRightCoercesToObject); 672 | 673 | // ES5 15.4.4.14 674 | // http://es5.github.com/#x15.4.4.14 675 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf 676 | var hasFirefox2IndexOfBug = Array.prototype.indexOf && [0, 1].indexOf(1, 2) !== -1; 677 | defineProperties(ArrayPrototype, { 678 | indexOf: function indexOf(sought /*, fromIndex */ ) { 679 | var self = splitString && isString(this) ? this.split('') : toObject(this), 680 | length = self.length >>> 0; 681 | 682 | if (!length) { 683 | return -1; 684 | } 685 | 686 | var i = 0; 687 | if (arguments.length > 1) { 688 | i = toInteger(arguments[1]); 689 | } 690 | 691 | // handle negative indices 692 | i = i >= 0 ? i : Math.max(0, length + i); 693 | for (; i < length; i++) { 694 | if (i in self && self[i] === sought) { 695 | return i; 696 | } 697 | } 698 | return -1; 699 | } 700 | }, hasFirefox2IndexOfBug); 701 | 702 | // ES5 15.4.4.15 703 | // http://es5.github.com/#x15.4.4.15 704 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf 705 | var hasFirefox2LastIndexOfBug = Array.prototype.lastIndexOf && [0, 1].lastIndexOf(0, -3) !== -1; 706 | defineProperties(ArrayPrototype, { 707 | lastIndexOf: function lastIndexOf(sought /*, fromIndex */) { 708 | var self = splitString && isString(this) ? this.split('') : toObject(this), 709 | length = self.length >>> 0; 710 | 711 | if (!length) { 712 | return -1; 713 | } 714 | var i = length - 1; 715 | if (arguments.length > 1) { 716 | i = Math.min(i, toInteger(arguments[1])); 717 | } 718 | // handle negative indices 719 | i = i >= 0 ? i : length - Math.abs(i); 720 | for (; i >= 0; i--) { 721 | if (i in self && sought === self[i]) { 722 | return i; 723 | } 724 | } 725 | return -1; 726 | } 727 | }, hasFirefox2LastIndexOfBug); 728 | 729 | // 730 | // Object 731 | // ====== 732 | // 733 | 734 | // ES5 15.2.3.14 735 | // http://es5.github.com/#x15.2.3.14 736 | 737 | // http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation 738 | var hasDontEnumBug = !({'toString': null}).propertyIsEnumerable('toString'), 739 | hasProtoEnumBug = (function () {}).propertyIsEnumerable('prototype'), 740 | dontEnums = [ 741 | "toString", 742 | "toLocaleString", 743 | "valueOf", 744 | "hasOwnProperty", 745 | "isPrototypeOf", 746 | "propertyIsEnumerable", 747 | "constructor" 748 | ], 749 | dontEnumsLength = dontEnums.length; 750 | 751 | defineProperties(Object, { 752 | keys: function keys(object) { 753 | var isFn = isFunction(object), 754 | isArgs = isArguments(object), 755 | isObject = object !== null && typeof object === 'object', 756 | isStr = isObject && isString(object); 757 | 758 | if (!isObject && !isFn && !isArgs) { 759 | throw new TypeError("Object.keys called on a non-object"); 760 | } 761 | 762 | var theKeys = []; 763 | var skipProto = hasProtoEnumBug && isFn; 764 | if (isStr || isArgs) { 765 | for (var i = 0; i < object.length; ++i) { 766 | theKeys.push(String(i)); 767 | } 768 | } else { 769 | for (var name in object) { 770 | if (!(skipProto && name === 'prototype') && owns(object, name)) { 771 | theKeys.push(String(name)); 772 | } 773 | } 774 | } 775 | 776 | if (hasDontEnumBug) { 777 | var ctor = object.constructor, 778 | skipConstructor = ctor && ctor.prototype === object; 779 | for (var j = 0; j < dontEnumsLength; j++) { 780 | var dontEnum = dontEnums[j]; 781 | if (!(skipConstructor && dontEnum === 'constructor') && owns(object, dontEnum)) { 782 | theKeys.push(dontEnum); 783 | } 784 | } 785 | } 786 | return theKeys; 787 | } 788 | }); 789 | 790 | var keysWorksWithArguments = Object.keys && (function () { 791 | // Safari 5.0 bug 792 | return Object.keys(arguments).length === 2; 793 | }(1, 2)); 794 | var originalKeys = Object.keys; 795 | defineProperties(Object, { 796 | keys: function keys(object) { 797 | if (isArguments(object)) { 798 | return originalKeys(ArrayPrototype.slice.call(object)); 799 | } else { 800 | return originalKeys(object); 801 | } 802 | } 803 | }, !keysWorksWithArguments); 804 | 805 | // 806 | // Date 807 | // ==== 808 | // 809 | 810 | // ES5 15.9.5.43 811 | // http://es5.github.com/#x15.9.5.43 812 | // This function returns a String value represent the instance in time 813 | // represented by this Date object. The format of the String is the Date Time 814 | // string format defined in 15.9.1.15. All fields are present in the String. 815 | // The time zone is always UTC, denoted by the suffix Z. If the time value of 816 | // this object is not a finite Number a RangeError exception is thrown. 817 | var negativeDate = -62198755200000; 818 | var negativeYearString = "-000001"; 819 | var hasNegativeDateBug = Date.prototype.toISOString && new Date(negativeDate).toISOString().indexOf(negativeYearString) === -1; 820 | 821 | defineProperties(Date.prototype, { 822 | toISOString: function toISOString() { 823 | var result, length, value, year, month; 824 | if (!isFinite(this)) { 825 | throw new RangeError("Date.prototype.toISOString called on non-finite value."); 826 | } 827 | 828 | year = this.getUTCFullYear(); 829 | 830 | month = this.getUTCMonth(); 831 | // see https://github.com/es-shims/es5-shim/issues/111 832 | year += Math.floor(month / 12); 833 | month = (month % 12 + 12) % 12; 834 | 835 | // the date time string format is specified in 15.9.1.15. 836 | result = [month + 1, this.getUTCDate(), this.getUTCHours(), this.getUTCMinutes(), this.getUTCSeconds()]; 837 | year = ( 838 | (year < 0 ? "-" : (year > 9999 ? "+" : "")) + 839 | ("00000" + Math.abs(year)).slice(0 <= year && year <= 9999 ? -4 : -6) 840 | ); 841 | 842 | length = result.length; 843 | while (length--) { 844 | value = result[length]; 845 | // pad months, days, hours, minutes, and seconds to have two 846 | // digits. 847 | if (value < 10) { 848 | result[length] = "0" + value; 849 | } 850 | } 851 | // pad milliseconds to have three digits. 852 | return ( 853 | year + "-" + result.slice(0, 2).join("-") + 854 | "T" + result.slice(2).join(":") + "." + 855 | ("000" + this.getUTCMilliseconds()).slice(-3) + "Z" 856 | ); 857 | } 858 | }, hasNegativeDateBug); 859 | 860 | 861 | // ES5 15.9.5.44 862 | // http://es5.github.com/#x15.9.5.44 863 | // This function provides a String representation of a Date object for use by 864 | // JSON.stringify (15.12.3). 865 | var dateToJSONIsSupported = false; 866 | try { 867 | dateToJSONIsSupported = ( 868 | Date.prototype.toJSON && 869 | new Date(NaN).toJSON() === null && 870 | new Date(negativeDate).toJSON().indexOf(negativeYearString) !== -1 && 871 | Date.prototype.toJSON.call({ // generic 872 | toISOString: function () { 873 | return true; 874 | } 875 | }) 876 | ); 877 | } catch (e) { 878 | } 879 | if (!dateToJSONIsSupported) { 880 | Date.prototype.toJSON = function toJSON(key) { 881 | // When the toJSON method is called with argument key, the following 882 | // steps are taken: 883 | 884 | // 1. Let O be the result of calling ToObject, giving it the this 885 | // value as its argument. 886 | // 2. Let tv be toPrimitive(O, hint Number). 887 | var o = Object(this), 888 | tv = toPrimitive(o), 889 | toISO; 890 | // 3. If tv is a Number and is not finite, return null. 891 | if (typeof tv === "number" && !isFinite(tv)) { 892 | return null; 893 | } 894 | // 4. Let toISO be the result of calling the [[Get]] internal method of 895 | // O with argument "toISOString". 896 | toISO = o.toISOString; 897 | // 5. If IsCallable(toISO) is false, throw a TypeError exception. 898 | if (typeof toISO !== "function") { 899 | throw new TypeError("toISOString property is not callable"); 900 | } 901 | // 6. Return the result of calling the [[Call]] internal method of 902 | // toISO with O as the this value and an empty argument list. 903 | return toISO.call(o); 904 | 905 | // NOTE 1 The argument is ignored. 906 | 907 | // NOTE 2 The toJSON function is intentionally generic; it does not 908 | // require that its this value be a Date object. Therefore, it can be 909 | // transferred to other kinds of objects for use as a method. However, 910 | // it does require that any such object have a toISOString method. An 911 | // object is free to use the argument key to filter its 912 | // stringification. 913 | }; 914 | } 915 | 916 | // ES5 15.9.4.2 917 | // http://es5.github.com/#x15.9.4.2 918 | // based on work shared by Daniel Friesen (dantman) 919 | // http://gist.github.com/303249 920 | var supportsExtendedYears = Date.parse('+033658-09-27T01:46:40.000Z') === 1e15; 921 | var acceptsInvalidDates = !isNaN(Date.parse('2012-04-04T24:00:00.500Z')) || !isNaN(Date.parse('2012-11-31T23:59:59.000Z')); 922 | var doesNotParseY2KNewYear = isNaN(Date.parse("2000-01-01T00:00:00.000Z")); 923 | if (!Date.parse || doesNotParseY2KNewYear || acceptsInvalidDates || !supportsExtendedYears) { 924 | // XXX global assignment won't work in embeddings that use 925 | // an alternate object for the context. 926 | Date = (function (NativeDate) { 927 | 928 | // Date.length === 7 929 | function Date(Y, M, D, h, m, s, ms) { 930 | var length = arguments.length; 931 | if (this instanceof NativeDate) { 932 | var date = length === 1 && String(Y) === Y ? // isString(Y) 933 | // We explicitly pass it through parse: 934 | new NativeDate(Date.parse(Y)) : 935 | // We have to manually make calls depending on argument 936 | // length here 937 | length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) : 938 | length >= 6 ? new NativeDate(Y, M, D, h, m, s) : 939 | length >= 5 ? new NativeDate(Y, M, D, h, m) : 940 | length >= 4 ? new NativeDate(Y, M, D, h) : 941 | length >= 3 ? new NativeDate(Y, M, D) : 942 | length >= 2 ? new NativeDate(Y, M) : 943 | length >= 1 ? new NativeDate(Y) : 944 | new NativeDate(); 945 | // Prevent mixups with unfixed Date object 946 | date.constructor = Date; 947 | return date; 948 | } 949 | return NativeDate.apply(this, arguments); 950 | } 951 | 952 | // 15.9.1.15 Date Time String Format. 953 | var isoDateExpression = new RegExp("^" + 954 | "(\\d{4}|[\+\-]\\d{6})" + // four-digit year capture or sign + 955 | // 6-digit extended year 956 | "(?:-(\\d{2})" + // optional month capture 957 | "(?:-(\\d{2})" + // optional day capture 958 | "(?:" + // capture hours:minutes:seconds.milliseconds 959 | "T(\\d{2})" + // hours capture 960 | ":(\\d{2})" + // minutes capture 961 | "(?:" + // optional :seconds.milliseconds 962 | ":(\\d{2})" + // seconds capture 963 | "(?:(\\.\\d{1,}))?" + // milliseconds capture 964 | ")?" + 965 | "(" + // capture UTC offset component 966 | "Z|" + // UTC capture 967 | "(?:" + // offset specifier +/-hours:minutes 968 | "([-+])" + // sign capture 969 | "(\\d{2})" + // hours offset capture 970 | ":(\\d{2})" + // minutes offset capture 971 | ")" + 972 | ")?)?)?)?" + 973 | "$"); 974 | 975 | var months = [ 976 | 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 977 | ]; 978 | 979 | function dayFromMonth(year, month) { 980 | var t = month > 1 ? 1 : 0; 981 | return ( 982 | months[month] + 983 | Math.floor((year - 1969 + t) / 4) - 984 | Math.floor((year - 1901 + t) / 100) + 985 | Math.floor((year - 1601 + t) / 400) + 986 | 365 * (year - 1970) 987 | ); 988 | } 989 | 990 | function toUTC(t) { 991 | return Number(new NativeDate(1970, 0, 1, 0, 0, 0, t)); 992 | } 993 | 994 | // Copy any custom methods a 3rd party library may have added 995 | for (var key in NativeDate) { 996 | Date[key] = NativeDate[key]; 997 | } 998 | 999 | // Copy "native" methods explicitly; they may be non-enumerable 1000 | Date.now = NativeDate.now; 1001 | Date.UTC = NativeDate.UTC; 1002 | Date.prototype = NativeDate.prototype; 1003 | Date.prototype.constructor = Date; 1004 | 1005 | // Upgrade Date.parse to handle simplified ISO 8601 strings 1006 | Date.parse = function parse(string) { 1007 | var match = isoDateExpression.exec(string); 1008 | if (match) { 1009 | // parse months, days, hours, minutes, seconds, and milliseconds 1010 | // provide default values if necessary 1011 | // parse the UTC offset component 1012 | var year = Number(match[1]), 1013 | month = Number(match[2] || 1) - 1, 1014 | day = Number(match[3] || 1) - 1, 1015 | hour = Number(match[4] || 0), 1016 | minute = Number(match[5] || 0), 1017 | second = Number(match[6] || 0), 1018 | millisecond = Math.floor(Number(match[7] || 0) * 1000), 1019 | // When time zone is missed, local offset should be used 1020 | // (ES 5.1 bug) 1021 | // see https://bugs.ecmascript.org/show_bug.cgi?id=112 1022 | isLocalTime = Boolean(match[4] && !match[8]), 1023 | signOffset = match[9] === "-" ? 1 : -1, 1024 | hourOffset = Number(match[10] || 0), 1025 | minuteOffset = Number(match[11] || 0), 1026 | result; 1027 | if ( 1028 | hour < ( 1029 | minute > 0 || second > 0 || millisecond > 0 ? 1030 | 24 : 25 1031 | ) && 1032 | minute < 60 && second < 60 && millisecond < 1000 && 1033 | month > -1 && month < 12 && hourOffset < 24 && 1034 | minuteOffset < 60 && // detect invalid offsets 1035 | day > -1 && 1036 | day < ( 1037 | dayFromMonth(year, month + 1) - 1038 | dayFromMonth(year, month) 1039 | ) 1040 | ) { 1041 | result = ( 1042 | (dayFromMonth(year, month) + day) * 24 + 1043 | hour + 1044 | hourOffset * signOffset 1045 | ) * 60; 1046 | result = ( 1047 | (result + minute + minuteOffset * signOffset) * 60 + 1048 | second 1049 | ) * 1000 + millisecond; 1050 | if (isLocalTime) { 1051 | result = toUTC(result); 1052 | } 1053 | if (-8.64e15 <= result && result <= 8.64e15) { 1054 | return result; 1055 | } 1056 | } 1057 | return NaN; 1058 | } 1059 | return NativeDate.parse.apply(this, arguments); 1060 | }; 1061 | 1062 | return Date; 1063 | })(Date); 1064 | } 1065 | 1066 | // ES5 15.9.4.4 1067 | // http://es5.github.com/#x15.9.4.4 1068 | if (!Date.now) { 1069 | Date.now = function now() { 1070 | return new Date().getTime(); 1071 | }; 1072 | } 1073 | 1074 | 1075 | // 1076 | // Number 1077 | // ====== 1078 | // 1079 | 1080 | // ES5.1 15.7.4.5 1081 | // http://es5.github.com/#x15.7.4.5 1082 | var hasToFixedBugs = NumberPrototype.toFixed && ( 1083 | (0.00008).toFixed(3) !== '0.000' 1084 | || (0.9).toFixed(0) !== '1' 1085 | || (1.255).toFixed(2) !== '1.25' 1086 | || (1000000000000000128).toFixed(0) !== "1000000000000000128" 1087 | ); 1088 | 1089 | var toFixedHelpers = { 1090 | base: 1e7, 1091 | size: 6, 1092 | data: [0, 0, 0, 0, 0, 0], 1093 | multiply: function multiply(n, c) { 1094 | var i = -1; 1095 | while (++i < toFixedHelpers.size) { 1096 | c += n * toFixedHelpers.data[i]; 1097 | toFixedHelpers.data[i] = c % toFixedHelpers.base; 1098 | c = Math.floor(c / toFixedHelpers.base); 1099 | } 1100 | }, 1101 | divide: function divide(n) { 1102 | var i = toFixedHelpers.size, c = 0; 1103 | while (--i >= 0) { 1104 | c += toFixedHelpers.data[i]; 1105 | toFixedHelpers.data[i] = Math.floor(c / n); 1106 | c = (c % n) * toFixedHelpers.base; 1107 | } 1108 | }, 1109 | numToString: function numToString() { 1110 | var i = toFixedHelpers.size; 1111 | var s = ''; 1112 | while (--i >= 0) { 1113 | if (s !== '' || i === 0 || toFixedHelpers.data[i] !== 0) { 1114 | var t = String(toFixedHelpers.data[i]); 1115 | if (s === '') { 1116 | s = t; 1117 | } else { 1118 | s += '0000000'.slice(0, 7 - t.length) + t; 1119 | } 1120 | } 1121 | } 1122 | return s; 1123 | }, 1124 | pow: function pow(x, n, acc) { 1125 | return (n === 0 ? acc : (n % 2 === 1 ? pow(x, n - 1, acc * x) : pow(x * x, n / 2, acc))); 1126 | }, 1127 | log: function log(x) { 1128 | var n = 0; 1129 | while (x >= 4096) { 1130 | n += 12; 1131 | x /= 4096; 1132 | } 1133 | while (x >= 2) { 1134 | n += 1; 1135 | x /= 2; 1136 | } 1137 | return n; 1138 | } 1139 | }; 1140 | 1141 | defineProperties(NumberPrototype, { 1142 | toFixed: function toFixed(fractionDigits) { 1143 | var f, x, s, m, e, z, j, k; 1144 | 1145 | // Test for NaN and round fractionDigits down 1146 | f = Number(fractionDigits); 1147 | f = f !== f ? 0 : Math.floor(f); 1148 | 1149 | if (f < 0 || f > 20) { 1150 | throw new RangeError("Number.toFixed called with invalid number of decimals"); 1151 | } 1152 | 1153 | x = Number(this); 1154 | 1155 | // Test for NaN 1156 | if (x !== x) { 1157 | return "NaN"; 1158 | } 1159 | 1160 | // If it is too big or small, return the string value of the number 1161 | if (x <= -1e21 || x >= 1e21) { 1162 | return String(x); 1163 | } 1164 | 1165 | s = ""; 1166 | 1167 | if (x < 0) { 1168 | s = "-"; 1169 | x = -x; 1170 | } 1171 | 1172 | m = "0"; 1173 | 1174 | if (x > 1e-21) { 1175 | // 1e-21 < x < 1e21 1176 | // -70 < log2(x) < 70 1177 | e = toFixedHelpers.log(x * toFixedHelpers.pow(2, 69, 1)) - 69; 1178 | z = (e < 0 ? x * toFixedHelpers.pow(2, -e, 1) : x / toFixedHelpers.pow(2, e, 1)); 1179 | z *= 0x10000000000000; // Math.pow(2, 52); 1180 | e = 52 - e; 1181 | 1182 | // -18 < e < 122 1183 | // x = z / 2 ^ e 1184 | if (e > 0) { 1185 | toFixedHelpers.multiply(0, z); 1186 | j = f; 1187 | 1188 | while (j >= 7) { 1189 | toFixedHelpers.multiply(1e7, 0); 1190 | j -= 7; 1191 | } 1192 | 1193 | toFixedHelpers.multiply(toFixedHelpers.pow(10, j, 1), 0); 1194 | j = e - 1; 1195 | 1196 | while (j >= 23) { 1197 | toFixedHelpers.divide(1 << 23); 1198 | j -= 23; 1199 | } 1200 | 1201 | toFixedHelpers.divide(1 << j); 1202 | toFixedHelpers.multiply(1, 1); 1203 | toFixedHelpers.divide(2); 1204 | m = toFixedHelpers.numToString(); 1205 | } else { 1206 | toFixedHelpers.multiply(0, z); 1207 | toFixedHelpers.multiply(1 << (-e), 0); 1208 | m = toFixedHelpers.numToString() + '0.00000000000000000000'.slice(2, 2 + f); 1209 | } 1210 | } 1211 | 1212 | if (f > 0) { 1213 | k = m.length; 1214 | 1215 | if (k <= f) { 1216 | m = s + '0.0000000000000000000'.slice(0, f - k + 2) + m; 1217 | } else { 1218 | m = s + m.slice(0, k - f) + '.' + m.slice(k - f); 1219 | } 1220 | } else { 1221 | m = s + m; 1222 | } 1223 | 1224 | return m; 1225 | } 1226 | }, hasToFixedBugs); 1227 | 1228 | 1229 | // 1230 | // String 1231 | // ====== 1232 | // 1233 | 1234 | // ES5 15.5.4.14 1235 | // http://es5.github.com/#x15.5.4.14 1236 | 1237 | // [bugfix, IE lt 9, firefox 4, Konqueror, Opera, obscure browsers] 1238 | // Many browsers do not split properly with regular expressions or they 1239 | // do not perform the split correctly under obscure conditions. 1240 | // See http://blog.stevenlevithan.com/archives/cross-browser-split 1241 | // I've tested in many browsers and this seems to cover the deviant ones: 1242 | // 'ab'.split(/(?:ab)*/) should be ["", ""], not [""] 1243 | // '.'.split(/(.?)(.?)/) should be ["", ".", "", ""], not ["", ""] 1244 | // 'tesst'.split(/(s)*/) should be ["t", undefined, "e", "s", "t"], not 1245 | // [undefined, "t", undefined, "e", ...] 1246 | // ''.split(/.?/) should be [], not [""] 1247 | // '.'.split(/()()/) should be ["."], not ["", "", "."] 1248 | 1249 | var string_split = StringPrototype.split; 1250 | if ( 1251 | 'ab'.split(/(?:ab)*/).length !== 2 || 1252 | '.'.split(/(.?)(.?)/).length !== 4 || 1253 | 'tesst'.split(/(s)*/)[1] === "t" || 1254 | 'test'.split(/(?:)/, -1).length !== 4 || 1255 | ''.split(/.?/).length || 1256 | '.'.split(/()()/).length > 1 1257 | ) { 1258 | (function () { 1259 | var compliantExecNpcg = /()??/.exec("")[1] === void 0; // NPCG: nonparticipating capturing group 1260 | 1261 | StringPrototype.split = function (separator, limit) { 1262 | var string = this; 1263 | if (separator === void 0 && limit === 0) { 1264 | return []; 1265 | } 1266 | 1267 | // If `separator` is not a regex, use native split 1268 | if (_toString.call(separator) !== "[object RegExp]") { 1269 | return string_split.call(this, separator, limit); 1270 | } 1271 | 1272 | var output = [], 1273 | flags = (separator.ignoreCase ? "i" : "") + 1274 | (separator.multiline ? "m" : "") + 1275 | (separator.extended ? "x" : "") + // Proposed for ES6 1276 | (separator.sticky ? "y" : ""), // Firefox 3+ 1277 | lastLastIndex = 0, 1278 | // Make `global` and avoid `lastIndex` issues by working with a copy 1279 | separator2, match, lastIndex, lastLength; 1280 | separator = new RegExp(separator.source, flags + "g"); 1281 | string += ""; // Type-convert 1282 | if (!compliantExecNpcg) { 1283 | // Doesn't need flags gy, but they don't hurt 1284 | separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags); 1285 | } 1286 | /* Values for `limit`, per the spec: 1287 | * If undefined: 4294967295 // Math.pow(2, 32) - 1 1288 | * If 0, Infinity, or NaN: 0 1289 | * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; 1290 | * If negative number: 4294967296 - Math.floor(Math.abs(limit)) 1291 | * If other: Type-convert, then use the above rules 1292 | */ 1293 | limit = limit === void 0 ? 1294 | -1 >>> 0 : // Math.pow(2, 32) - 1 1295 | ToUint32(limit); 1296 | while (match = separator.exec(string)) { 1297 | // `separator.lastIndex` is not reliable cross-browser 1298 | lastIndex = match.index + match[0].length; 1299 | if (lastIndex > lastLastIndex) { 1300 | output.push(string.slice(lastLastIndex, match.index)); 1301 | // Fix browsers whose `exec` methods don't consistently return `undefined` for 1302 | // nonparticipating capturing groups 1303 | if (!compliantExecNpcg && match.length > 1) { 1304 | match[0].replace(separator2, function () { 1305 | for (var i = 1; i < arguments.length - 2; i++) { 1306 | if (arguments[i] === void 0) { 1307 | match[i] = void 0; 1308 | } 1309 | } 1310 | }); 1311 | } 1312 | if (match.length > 1 && match.index < string.length) { 1313 | ArrayPrototype.push.apply(output, match.slice(1)); 1314 | } 1315 | lastLength = match[0].length; 1316 | lastLastIndex = lastIndex; 1317 | if (output.length >= limit) { 1318 | break; 1319 | } 1320 | } 1321 | if (separator.lastIndex === match.index) { 1322 | separator.lastIndex++; // Avoid an infinite loop 1323 | } 1324 | } 1325 | if (lastLastIndex === string.length) { 1326 | if (lastLength || !separator.test("")) { 1327 | output.push(""); 1328 | } 1329 | } else { 1330 | output.push(string.slice(lastLastIndex)); 1331 | } 1332 | return output.length > limit ? output.slice(0, limit) : output; 1333 | }; 1334 | }()); 1335 | 1336 | // [bugfix, chrome] 1337 | // If separator is undefined, then the result array contains just one String, 1338 | // which is the this value (converted to a String). If limit is not undefined, 1339 | // then the output array is truncated so that it contains no more than limit 1340 | // elements. 1341 | // "0".split(undefined, 0) -> [] 1342 | } else if ("0".split(void 0, 0).length) { 1343 | StringPrototype.split = function split(separator, limit) { 1344 | if (separator === void 0 && limit === 0) { return []; } 1345 | return string_split.call(this, separator, limit); 1346 | }; 1347 | } 1348 | 1349 | var str_replace = StringPrototype.replace; 1350 | var replaceReportsGroupsCorrectly = (function () { 1351 | var groups = []; 1352 | 'x'.replace(/x(.)?/g, function (match, group) { 1353 | groups.push(group); 1354 | }); 1355 | return groups.length === 1 && typeof groups[0] === 'undefined'; 1356 | }()); 1357 | 1358 | if (!replaceReportsGroupsCorrectly) { 1359 | StringPrototype.replace = function replace(searchValue, replaceValue) { 1360 | var isFn = isFunction(replaceValue); 1361 | var hasCapturingGroups = isRegex(searchValue) && (/\)[*?]/).test(searchValue.source); 1362 | if (!isFn || !hasCapturingGroups) { 1363 | return str_replace.call(this, searchValue, replaceValue); 1364 | } else { 1365 | var wrappedReplaceValue = function (match) { 1366 | var length = arguments.length; 1367 | var originalLastIndex = searchValue.lastIndex; 1368 | searchValue.lastIndex = 0; 1369 | var args = searchValue.exec(match); 1370 | searchValue.lastIndex = originalLastIndex; 1371 | args.push(arguments[length - 2], arguments[length - 1]); 1372 | return replaceValue.apply(this, args); 1373 | }; 1374 | return str_replace.call(this, searchValue, wrappedReplaceValue); 1375 | } 1376 | }; 1377 | } 1378 | 1379 | // ECMA-262, 3rd B.2.3 1380 | // Not an ECMAScript standard, although ECMAScript 3rd Edition has a 1381 | // non-normative section suggesting uniform semantics and it should be 1382 | // normalized across all browsers 1383 | // [bugfix, IE lt 9] IE < 9 substr() with negative value not working in IE 1384 | var string_substr = StringPrototype.substr; 1385 | var hasNegativeSubstrBug = "".substr && "0b".substr(-1) !== "b"; 1386 | defineProperties(StringPrototype, { 1387 | substr: function substr(start, length) { 1388 | return string_substr.call( 1389 | this, 1390 | start < 0 ? ((start = this.length + start) < 0 ? 0 : start) : start, 1391 | length 1392 | ); 1393 | } 1394 | }, hasNegativeSubstrBug); 1395 | 1396 | // ES5 15.5.4.20 1397 | // whitespace from: http://es5.github.io/#x15.5.4.20 1398 | var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + 1399 | "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + 1400 | "\u2029\uFEFF"; 1401 | var zeroWidth = '\u200b'; 1402 | var wsRegexChars = "[" + ws + "]"; 1403 | var trimBeginRegexp = new RegExp("^" + wsRegexChars + wsRegexChars + "*"); 1404 | var trimEndRegexp = new RegExp(wsRegexChars + wsRegexChars + "*$"); 1405 | var hasTrimWhitespaceBug = StringPrototype.trim && (ws.trim() || !zeroWidth.trim()); 1406 | defineProperties(StringPrototype, { 1407 | // http://blog.stevenlevithan.com/archives/faster-trim-javascript 1408 | // http://perfectionkills.com/whitespace-deviations/ 1409 | trim: function trim() { 1410 | if (this === void 0 || this === null) { 1411 | throw new TypeError("can't convert " + this + " to object"); 1412 | } 1413 | return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); 1414 | } 1415 | }, hasTrimWhitespaceBug); 1416 | 1417 | // ES-5 15.1.2.2 1418 | if (parseInt(ws + '08') !== 8 || parseInt(ws + '0x16') !== 22) { 1419 | parseInt = (function (origParseInt) { 1420 | var hexRegex = /^0[xX]/; 1421 | return function parseIntES5(str, radix) { 1422 | str = String(str).trim(); 1423 | if (!Number(radix)) { 1424 | radix = hexRegex.test(str) ? 16 : 10; 1425 | } 1426 | return origParseInt(str, radix); 1427 | }; 1428 | }(parseInt)); 1429 | } 1430 | 1431 | })); 1432 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'), 2 | util = require('util'), 3 | pkg = require('./package.json'); 4 | 5 | var bannerString = util.format('ReactMiniRouter %s - https://github.com/larrymyers/react-mini-router', pkg.version); 6 | 7 | module.exports = { 8 | entry: { 9 | main: './index.js' 10 | }, 11 | output: { 12 | path: './dist', 13 | filename: 'react-mini-router.js', 14 | library: 'ReactMiniRouter', 15 | libraryTarget: 'var' 16 | }, 17 | externals: { 18 | react: 'React', 19 | 'react-dom': 'ReactDOM' 20 | }, 21 | plugins: [ 22 | new webpack.BannerPlugin(bannerString, { entryOnly: true }) 23 | ] 24 | }; 25 | --------------------------------------------------------------------------------