diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 68ffd3bce..000000000 --- a/.babelrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "presets": [ - "es2015", - "stage-1", - "react" - ], - "plugins": [ - "transform-object-assign" - ] -} diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4a7ea3036..000000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 63585f08e..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -subjects/migrating-to-react/backbone-todomvc diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 45ec85f00..000000000 --- a/.eslintrc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "parser": "babel-eslint", - "env": { - "browser": true, - "node": true - }, - "plugins": [ - "import", - "react" - ], - "extends": [ - "eslint:recommended", - "plugin:import/errors", - "plugin:react/recommended" - ], - "settings": { - "import/resolver": "webpack" - }, - "rules": { - "array-bracket-spacing": [ 1, "always" ], - "comma-dangle": [ 1, "never" ], - "eqeqeq": [ 2, "smart" ], - "jsx-quotes": [ 1, "prefer-double" ], - "no-unused-vars": 0, - "object-curly-spacing": [ 1, "always" ], - "quotes": [ 1, "single", "avoid-escape" ], - "react/jsx-space-before-closing": [ 1, "never" ], - "react/no-did-mount-set-state": 0, - "react/prop-types": 1, - "semi": [ 1, "never" ], - "space-before-blocks": [ 1, "always" ] - } -} diff --git a/.gitignore b/.gitignore index 00026d63b..ded057a5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -subjects/index.html -subjects/**/lecture.html -subjects/**/exercise.html -subjects/**/solution.html -!subjects/**/backbone-todomvc/index.html node_modules +yarn-error.log +public/**/*.html diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d2091e4df..000000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: -- stable -branches: - only: - - master - - /^greenkeeper-/ diff --git a/JavaScriptPrimer.md b/JavaScriptPrimer.md index 221fb98e9..3372bbcfd 100644 --- a/JavaScriptPrimer.md +++ b/JavaScriptPrimer.md @@ -7,44 +7,44 @@ Note: All of the syntax discussed here is actually part of the JavaScript spec, JavaScript has always had `var`: ```js -var name = 'Ryan' +var name = "Michael"; ``` `var` can be hard to manage especially because of it's "function scoping", so now we've got two other ways to define values that have "block scope": ```js // var does not have block scope -var name = 'Ryan' +var name = "Michael"; if (true) { - var name = 'Michael' - name // 'Michael' + var name = "Bruce"; + name; // 'Bruce' } -name // 'Michael' +name; // 'Bruce' // let has block scope -let name = 'Ryan' +let name = "Michael"; if (true) { - let name = 'Michael' - name // 'Michael' + let name = "Bruce"; + name; // 'Bruce' } -name // 'Ryan' +name; // 'Michael' // const has block scope too -const name = 'Ryan' +const name = "Michael"; if (true) { - const name = 'Michael' - name // 'Michael' + const name = "Bruce"; + name; // 'Bruce' } -name // 'Ryan' +name; // 'Michael' // let can be reassigned -let isOpen = true -isOpen = false -isOpen // false +let isOpen = true; +isOpen = false; +isOpen; // false // const cannot be reassigned -const isOpen = true -isOpen = false // throws error +const isOpen = true; +isOpen = false; // throws error ``` We find block scope to make more sense to people and is generally more useful, therefore we don't use `var`. @@ -56,17 +56,17 @@ In practice, nearly everything is `const`. ## String templates ```js -const something = 'ugly stuff' -const str = 'instead of ' + something + ' like this' +const something = "ugly stuff"; +const str = "instead of " + something + " like this"; -const something = 'lovely stuff' -const str = `you can do ${something} like this` +const something = "lovely stuff"; +const str = `you can do ${something} like this`; const str = ` also multiline is totally cool -` +`; ``` ## Concise object methods @@ -82,7 +82,7 @@ const obj = { youCanDoThis() { // do stuff } -} +}; ``` ## Arrow functions @@ -91,33 +91,32 @@ Arrow functions remove the context from a function, meaning the function has no ```js const obj = { - url: '/api/stuff', + url: "/api/stuff", fetch(users) { - users.forEach((user) => { + users.forEach(user => { // `this` is the `this` from outside this function because // there is no context inside an arrow function - getUser(`${this.url}/${user.id}`) - }) + getUser(`${this.url}/${user.id}`); + }); } -} +}; ``` Also, if the other side of an arrow function is an expression, it acts like an implicit return: ```js -const add = function(x, y) { return x + y } +const add = function(x, y) { + return x + y; +}; // becomes -const add = (x, y) => { return x + y } +const add = (x, y) => { + return x + y; +}; // which can be shorter with explicit expression return -const add = (x, y) => x + y - -// if we want multiline, we can create an expression with () -const add = (x, y) => ( - x + y -) +const add = (x, y) => x + y; ``` ## Arrays @@ -125,55 +124,57 @@ const add = (x, y) => ( We do a lot with arrays, here are a few methods we use often: ```js -const numbers = [ 1, 2, 3, 4, 5 ] +const numbers = [1, 2, 3, 4, 5]; // map converts an array to a new, transformed array -const doubled = numbers.map((number) => { - return number * 2 -}) -doubled // [ 2, 4, 6, 8, 10 ] +const doubled = numbers.map(number => { + return number * 2; +}); +doubled; // [ 2, 4, 6, 8, 10 ] // filter, return false to remove from an array -const lessThan3 = numbers.filter((n) => { - return n < 3 -}) -lessThan3 // [ 1, 2 ] +const lessThan3 = numbers.filter(n => { + return n < 3; +}); +lessThan3; // [ 1, 2 ] // remember, that can be super short -const lessThan3 = numbers.filter(n => n < 3) +const lessThan3 = numbers.filter(n => n < 3); ``` ## Destructuring ```js -const obj = { x: 1, y: 2 } +const obj = { x: 1, y: 2 }; // instead of: -const x = obj.x -const y = obj.y +const x = obj.x; +const y = obj.y; // we can "destructure" the values off -const { x, y } = obj -x // 1 -y // 2 +const { x, y } = obj; +x; // 1 +y; // 2 // you can use this all over the place, like function parameters -function add({x, y}) { return x + y} -add({x: 3, y: 4}) // 7 +function add({ x, y }) { + return x + y; +} +add({ x: 3, y: 4 }); // 7 ``` ## Modules ```js // instead of cjs -var React = require('react') +var React = require("react"); // we use ES modules -import React from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; // and with destructuring to boot! -import { render } from 'react-dom' +import { render } from "react-dom"; ``` Our training material then uses [Webpack](https://webpack.github.io/), a module bundler, to graph the dependencies and create a build so that this works in browsers that don't yet support some of these features natively. diff --git a/README.md b/README.md index 44678f4b5..7ad0968c8 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ This repo contains the course material for [React Training](https://reacttrainin First, install [git](http://git-scm.com/downloads) and the latest stable version of [node](https://nodejs.org/). Then: ```sh -$ git clone https://github.com/ReactTraining/react-subjects.git -$ cd react-subjects +$ git clone https://github.com/ReactTraining/react-workshop.git +$ cd react-workshop $ npm install $ npm start ``` -Then open your browser to [http://localhost:8080](http://localhost:8080). You should see a list of subjects. +Your web browser should open to [http://localhost:8080](http://localhost:8080)where you'll see a list of subjects. **IMPORTANT:** Please read the [JavaScript Primer](https://github.com/ReactTraining/react-subjects/blob/master/JavaScriptPrimer.md) before attending the workshop. It's a refresher on some of the newer bits of JavaScript you'll want to be familiar with in order to get the most out of the experience. @@ -19,7 +19,7 @@ Then open your browser to [http://localhost:8080](http://localhost:8080). You sh A few common problems: -- **You're having problems cloning the repository.** Some corporate networks block port 22, which git uses to communicate with GitHub over SSH. Instead of using SSH, clone the repo over HTTPS. Use the following command to tell git to always use `https` instead of `git`: +* **You're having problems cloning the repository.** Some corporate networks block port 22, which git uses to communicate with GitHub over SSH. Instead of using SSH, clone the repo over HTTPS. Use the following command to tell git to always use `https` instead of `git`: ```sh $ git config --global url."https://".insteadOf git:// @@ -29,19 +29,19 @@ $ git config --global url."https://".insteadOf git:// insteadOf = git:// ``` -- **You're having trouble installing node.** We recommend using [nvm](https://github.com/creationix/nvm). nvm makes it really easy to use multiple versions of node on the same machine painlessly. After you install nvm, install the latest stable version of node with the following command: +* **You're having trouble installing node.** We recommend using [nvm](https://github.com/creationix/nvm). nvm makes it really easy to use multiple versions of node on the same machine painlessly. After you install nvm, install the latest stable version of node with the following command: ```sh $ nvm use default stable ``` -- **You don't have permissions to install stuff.** You might see an error like `EACCES` during the `npm install` step. If that's the case, it probably means that at some point you did an `sudo npm install` and installed some stuff with root permissions. To fix this, you need to forcefully remove all files that npm caches on your machine and re-install without sudo. +* **You don't have permissions to install stuff.** You might see an error like `EACCES` during the `npm install` step. If that's the case, it probably means that at some point you did an `sudo npm install` and installed some stuff with root permissions. To fix this, you need to forcefully remove all files that npm caches on your machine and re-install without sudo. ```sh $ sudo rm -rf node_modules # If you installed node with nvm (suggested): -$ sudo rm -rf ~/.npm +$ sudo rm -rf ~/.npm # If you installed node with Homebrew: $ sudo rm -rf /usr/local/lib/node_modules diff --git a/Syllabus.md b/Syllabus.md deleted file mode 100644 index 6795dba81..000000000 --- a/Syllabus.md +++ /dev/null @@ -1,209 +0,0 @@ -# Syllabus - -This document contains a list of subjects we cover in our React training workshops. The stuff near the top is generally part of our "React Fundamentals" workshop and the stuff further down we generally cover in "Advanced React" workshops. - -For any workshop, please be sure you are familiar with the material in our [JavaScript primer](https://github.com/ReactJSTraining/react-subjects/blob/master/JavaScriptPrimer.md) before attending. - -## Eliminating Time - -Solutions are nonsense without understanding the problems they solve. We'll take a trip down memory lane to shine a light on the stuff that led to all the bugs we've written in the past to prime us to see clearly how React is going to help. - -Objectives: -- Understand the problems React solves -- Get people thinking about [the conceptual gap between the static program and the dynamic process][Dijkstra], and what it means to "eliminate time" in their apps - -[Dijkstra]: https://en.wikiquote.org/wiki/Edsger_W._Dijkstra - - -## Rendering with React - -We'll explore the lowest level of React: rendering UI. We'll take a look at what the "Virtual DOM" and JSX are, brush up on some JavaScript array methods, and do our first coding exercise. - -Objectives: -- Render UI with React -- Transform and massage data into UI -- Become familiar with JSX -- Brush up on JavaScript expressions and scope - - -## Components - -Components are the building blocks of UI in React. We'll discuss a few parts of a component's lifecycle, it's interface with the rest of the application, component state, encapsulation, and how to make components reusable. - -Objectives: -- Define components -- Determine good values for component state -- Interface with the rest of the world (props) -- Use propTypes as "runnable documentation" -- Handle user interaction - - -## Props vs. State - -One of the first questions that comes up when you start using React is "when do I use state, and when do I use props?" We'll explore an app with changing requirements that lets us experience state moving to props and why. We'll discuss how data flows, or rather, how components communicate with each other. Finally, we'll explore how component composition helps answer the "props vs. state" question. - -Objectives: -- Understand the difference between props and state -- Know when to use props vs. state -- Pass data up and down the view hierarchy -- Compose generic components into specialized components - - -## Imperative to Declarative - -React is "declarative". What does that mean? What is imperative code? We'll answer these questions, and show the power that comes from shifting your imperative code into declarative components using more of the React component lifecycle. - -Objectives: -- Turn imperative code into declarative code -- Experience the benefits that come from declarative programming -- Learn to use existing JS libraries declaratively -- Use more of the React component lifecycle -- Understand how and when to use `ref`s - - -## Forms - -Solidify your [declarative](#imperative-to-declarative) understanding by working with forms in React. We'll discuss the various use-cases for forms and how to handle them in React. - -Objectives: -- Understand [controlled vs. uncontrolled](https://facebook.github.io/react/docs/forms.html) components -- Work with forms - - -## Testing - -Testing UI has never been this straightforward. You'll learn how to test any UI interaction your app has. - -Objectives: -- Test stateless components -- Test stateful components -- Simulate events - - -## Compound Components - -Some components get really, really big. Not only do their render methods get large, but as more people try to use the component, the props it takes grow as well. Eventually you end up with way too many properties and a really difficult component to work with that has to change with every new use-case you throw at it. There's a better way with compound components. - -Objectives: -- Create reusable components by compounding related components together -- Dynamically flow data between components - - -## Controlled Compound Components - -Some reusable components are a lot like form elements and need to be controlled or uncontrolled. We'll examine techniques to implement components with this kind of behavior. - -Objectives: -- Create a component that can be controlled or uncontrolled - -Prerequisites: -- [Compound Components](#compound-components) -- [Forms](#forms) - - -## Context - -Context is an advanced, slightly-unstable, and powerful tool that solves a handful of use-cases really elegantly. We'll discuss when and why it's a good solution, and when it's not. - -Objectives: -- Learn to use context -- Learn when it's useful and when to use a different pattern - -Prerequisites: -- [Compound Components](#compound-components) - - -## Higher Order Components - -Higher order components allow you to compose behavior into components without inheritence or modifying the component itself with a wide range of use-cases. - -Objectives: -- Create a higher order component -- Understand when to use a higher order component - - -## Render Props - -As we begin to make more things declarative we run into code that seems unlikely to be made declarative. We'll explore this pattern and see how it allows us to make anything in our app declarative. - -Objectives: -- Learn to use render props -- Learn when they are most useful - - -## Performance and Render Optimizations - -As your app grows you'll eventually hit some slow interactions. We'll explore how to identify the bottlenecks and how to fix them. We'll also see how React can help us avoid the bottlenecks in the first place. - -Objectives: -- Identify performance bottlenecks -- Learn how to do "windowing" with React - - -## Animation - -We'll showcase several ways to do animation including imperatively with 3rd-party animation libs like jQuery and declaratively with [react-motion](https://github.com/chenglou/react-motion). - -Objectives: -- Animate elements - -Prerequisites: -- [Imperative to Declarative](#imperative-to-declarative) -- [Render Props](#render-props) - - -## Routing - -Keep your application UI and the URL in sync, no more broken back buttons. We'll introduce some basic uses of [React Router](https://github.com/ReactTraining/react-router), and how it helps you weave together your components into an application with multiple screens. - -Objectives: -- Understand principles of client-side "routing" -- Understand use-cases for routing -- Create UI at specific URLs -- Transition between screens - -Prerequisites: -- [Render Props](#render-props) - - -## Server Rendering - -React lets you render your components server-side for improved SEO and performance. In this section we'll discuss the techniques and trade-offs of rendering your React components on the server. We'll also transition a fully client-side app to server-side rendering, and discuss the use cases we solve each step of the way. - -Objectives: -- Understand principles of server-side rendering and when it's useful -- Render components server-side -- Fetch data before rendering server-side -- Seamlessly transition to the client page without replacing HTML already in the page - - -## Redux - -Redux is a library for managing application state that can be useful when using React. We'll work on a real Redux app that makes requests to a real server and explore some techniques for managing shared state client-side. - -Objectives: -- Understand one-way data flow -- Understand Redux concepts like stores and actions -- Implement a feature using Redux -- Handle latency and errors using Redux - - -## Implementing Redux - -The react-redux bindings make use of several advanced features of React including [context](#context) and [higher-order components](#higher-order-components). We'll use both of these concepts to build our own "mini Redux" and discover how Redux really works behind the scenes. - -Objectives: -- Use higher-order components to put stuff on context -- Use context to pass state down the hierarchy - -Prerequisites: -- [Context](#context) -- [Higher Order Components](#higher-order-components) - - -## Migrating to React - -This section is designed for teams that want to migrate, instead of rewrite, their existing app to React in a way that won't block everyone else on the team. You'll be writing and shipping your new code incrementally. We'll also discuss how to integrate with existing JS libs as you encounter them in your app. - -Objectives: -- Learn how to integrate React into an existing code-base diff --git a/package.json b/package.json index f3def1cc2..c55dc9b99 100644 --- a/package.json +++ b/package.json @@ -1,71 +1,62 @@ { - "description": "Course material for http://reacttraining.com", - "repository": "ReactTraining/react-subjects", + "private": true, + "name": "react-workshop", + "description": "Lectures and exercises for React Training workshops", + "repository": "ReactTraining/react-workshop", + "homepage": "https://reacttraining.com", + "author": "React Training LLC", "license": "GPL-3.0", - "authors": [ - "Ryan Florence", - "Michael Jackson" - ], "scripts": { - "start": "node ./scripts/build.js && webpack-dev-server --inline --content-base subjects", - "server-exercise": "supervisor -- -r babel-register subjects/ServerRendering/exercise/server.js", - "server-solution": "supervisor -- -r babel-register subjects/ServerRendering/solution/server.js", - "start-api": "node api.js", - "lint": "eslint subjects/**/*.js", - "test": "npm run lint" + "start": "node ./scripts/build.js && webpack-dev-server --inline --content-base public", + "ssr-exercise": "supervisor -- -r babel-register 'subjects/14-Server-Rendering/exercise/server.js'", + "ssr-solution": "supervisor -- -r babel-register 'subjects/14-Server-Rendering/solution/server.js'" }, "dependencies": { "angular": "1.5.8", - "assert": "1.4.1", + "babel-core": "^6.23.1", "babel-loader": "^6.2.4", - "babel-plugin-transform-object-assign": "^6.5.0", - "babel-preset-es2015": "^6.6.0", + "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.5.0", - "babel-preset-stage-1": "^6.5.0", + "babel-preset-stage-2": "^6.24.1", "babel-register": "^6.9.0", "backbone": "^1.2.3", "body-parser": "^1.15.2", "bootstrap": "^3.3.4", "bootstrap-webpack": "0.0.5", + "create-react-class": "^15.6.3", "css-loader": "^0.23.1", "events": "^1.0.2", - "expect": "^1.13.4", + "expect": "^23.0.0", "exports-loader": "^0.6.3", - "expose-loader": "^0.7.1", + "expose-loader": "0.7.1", "express": "^4.14.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.9.0", "firebase": "^2.4.2", - "flux": "^2.0.1", "form-serialize": "0.7.1", - "immstruct": "^2.0.0", - "immutable": "^3.7.3", "imports-loader": "0.6.5", "invariant": "^2.1.0", "isomorphic-fetch": "^2.2.1", "jquery": "^3.1.0", "jsonp": "0.2.0", - "key-mirror": "^1.0.1", "less": "^2.7.1", "less-loader": "^2.2.3", - "match-sorter": "^1.5.0", "md5": "2.1.0", - "mkdirp": "^0.5.0", + "mkdirp": "^0.5.1", "mocha": "3.0.2", "mustache": "^2.2.1", + "open-browser-webpack-plugin": "^0.0.5", + "prop-types": "^15.5.10", "purecss": "0.6.0", - "react": "^15.1.0", + "react": "^16.3.2", "react-addons-css-transition-group": "^15.1.0", - "react-addons-perf": "^15.1.0", - "react-addons-test-utils": "^15.1.0", "react-addons-transition-group": "^15.1.0", - "react-dom": "^15.1.0", + "react-dom": "^16.3.2", "react-motion": "^0.4.2", - "react-point": "^2.1.0", - "react-redux": "^4.4.5", - "react-router": "^4.0.0-alpha.4", + "react-redux": "^5.0.2", + "react-router-dom": "^4.0.0", "react-tween-state": "0.1.5", - "redux": "3.0.4", + "redux": "^3.6.0", "redux-logger": "2.2.1", "redux-thunk": "1.0.0", "sort-by": "^1.1.0", @@ -76,11 +67,7 @@ "webpack": "^1.7.3", "webpack-dev-server": "^1.8.0" }, - "devDependencies": { - "babel-eslint": "^6.1.2", - "eslint": "^3.2.2", - "eslint-import-resolver-webpack": "^0.5.1", - "eslint-plugin-import": "^1.13.0", - "eslint-plugin-react": "^6.0.0" + "prettier": { + "printWidth": 72 } } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 000000000..9298d05ce Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/shared.css b/public/shared.css new file mode 100644 index 000000000..6aaf9ab10 --- /dev/null +++ b/public/shared.css @@ -0,0 +1,27 @@ +html { + box-sizing: border-box; +} +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + background: white; +} + +a:link, +a:visited { + color: rebeccapurple; + text-decoration: none; +} +a:link:hover { + text-decoration: underline; +} + +.hot { + color: red; +} diff --git a/scripts/build.js b/scripts/build.js index 21f2a12ff..7d1fd6a07 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,87 +1,90 @@ -const fs = require('fs') -const path = require('path') -const React = require('react') -const ReactDOMServer = require('react-dom/server') +const fs = require("fs"); +const path = require("path"); +const mkdirp = require("mkdirp"); +const React = require("react"); +const ReactDOMServer = require("react-dom/server"); -function createMarkup(mainBundle) { - return ReactDOMServer.renderToStaticMarkup( - React.DOM.html({}, - React.DOM.head({}, - React.DOM.link({ rel: 'stylesheet', href: '/shared.css' }) - ), - React.DOM.body({}, - React.DOM.div({ id: 'app' }), - React.DOM.script({ src: '/__build__/shared.js' }), - React.DOM.script({ src: '/__build__/' + mainBundle + '.js' }) - ) - ) - ) +function writeFile(file, contents) { + mkdirp.sync(path.dirname(file)); + fs.writeFileSync(file, contents); } -const RootDir = path.resolve(__dirname, '..') -const SubjectsDir = path.join(RootDir, 'subjects') - -const Subjects = { - HelloWorld: 'Hello World', - Rendering: 'Rendering', - Components: 'Components', - PropsVsState: 'Props vs. State', - ImperativeToDeclarative: 'Imperative to Declarative', - Forms: 'Forms', - Testing: 'Testing', - CompoundComponents: 'Compound Components', - Context: 'Context', - HigherOrderComponents: 'Higher Order Components', - RenderProps: 'Render Props', - RenderOptimizations: 'Performance and Render Optimizations', - TweenState: 'TweenState', - Animation: 'Animation', - Routing: 'Routing', - ServerRendering: 'Server Rendering', - JSONTable: 'JSON Table', - Select: 'Select', - Slider: 'Slider', - Calculator: 'Calculator', - ChatApp: 'Chat App', - Cursors: 'Cursors', - MigratingToReact: 'Migrating to React', - Redux: 'Redux', - MiniRedux: 'Implementing Redux' +function renderPage(element) { + return ( + "" + ReactDOMServer.renderToStaticMarkup(element) + ); } -const SubjectDirNames = Object.keys(Subjects) +const e = React.createElement; -const markup = ReactDOMServer.renderToStaticMarkup( - React.DOM.html({}, - React.DOM.head({}, - React.DOM.link({ rel: 'stylesheet', href: '/shared.css' }) +function HostPage({ chunk, data, title = "React Training" }) { + return e( + "html", + null, + e( + "head", + null, + e("meta", { charSet: "utf-8" }), + e("meta", { + name: "viewport", + content: "width=device-width, initial-scale=1" + }), + e("title", null, title), + e("link", { rel: "icon", href: "/favicon.ico?react-workshop" }), + e("link", { rel: "stylesheet", href: "/shared.css" }), + data && + e("script", { + dangerouslySetInnerHTML: { + __html: `window.__DATA__ = ${JSON.stringify(data)}` + } + }) ), - React.DOM.body({ id: 'index' }, - React.DOM.table({ cellSpacing: 0, cellPadding: 0 }, - React.DOM.tbody({}, - SubjectDirNames.map(function (dir, index) { - return React.DOM.tr({ key: dir, className: (index % 2) ? 'odd' : 'even' }, - React.DOM.td({ className: 'lecture-link' }, - React.DOM.a({ href: '/' + dir + '/lecture.html' }, Subjects[dir]) - ), - React.DOM.td({ className: 'exercise-link' }, - React.DOM.a({ href: '/' + dir + '/exercise.html' }, 'exercise') - ), - React.DOM.td({ className: 'solution-link' }, - React.DOM.a({ href: '/' + dir + '/solution.html' }, 'solution') - ) - ) - }) - ) - ) + e( + "body", + null, + e("div", { id: "app" }), + e("script", { src: "/shared.js" }), + e("script", { src: `/${chunk}.js` }) ) - ) -) + ); +} + +const publicDir = path.resolve(__dirname, "..", "public"); +const subjectsDir = path.resolve(__dirname, "..", "subjects"); +const subjectDirs = fs + .readdirSync(subjectsDir) + .map(file => path.join(subjectsDir, file)) + .filter(file => fs.statSync(file).isDirectory()); + +const subjects = []; + +subjectDirs.forEach(dir => { + const base = path.basename(dir); + const match = base.match(/^(\d+)-(.+)$/); + const subject = { + number: match[1], + name: match[2].replace(/-/g, " ") + }; + + ["lecture", "exercise", "solution"].forEach(name => { + if (fs.existsSync(path.join(dir, `${name}.js`))) { + console.log(`Building /${base}/${name}.html...`); + + writeFile( + path.join(publicDir, base, `${name}.html`), + renderPage(e(HostPage, { chunk: `${base}-${name}` })) + ); + + subject[name] = `/${base}/${name}.html`; + } + }); + + subjects.push(subject); +}); -fs.writeFileSync(path.join(SubjectsDir, 'index.html'), markup) +console.log(`Building /index.html...`); -SubjectDirNames.forEach(function (dir) { - fs.writeFileSync(path.join(SubjectsDir, dir, 'lecture.html'), createMarkup(dir + '-lecture')) - fs.writeFileSync(path.join(SubjectsDir, dir, 'exercise.html'), createMarkup(dir + '-exercise')) - fs.writeFileSync(path.join(SubjectsDir, dir, 'solution.html'), createMarkup(dir + '-solution')) -}) +writeFile( + path.join(publicDir, "index.html"), + renderPage(e(HostPage, { chunk: "index", data: { subjects } })) +); diff --git a/subjects/Flux/Flux.png b/slides/Flux.png similarity index 100% rename from subjects/Flux/Flux.png rename to slides/Flux.png diff --git a/slides/Redux.gif b/slides/Redux.gif new file mode 100644 index 000000000..6da63cddc Binary files /dev/null and b/slides/Redux.gif differ diff --git a/subjects/.babelrc b/subjects/.babelrc new file mode 100644 index 000000000..4e73ad6ca --- /dev/null +++ b/subjects/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": [ + "env", + "stage-2", + "react" + ] +} diff --git a/subjects/HelloWorld/exercise.js b/subjects/00-Hello-World/exercise.js similarity index 68% rename from subjects/HelloWorld/exercise.js rename to subjects/00-Hello-World/exercise.js index 7c11b17ae..68f67358a 100644 --- a/subjects/HelloWorld/exercise.js +++ b/subjects/00-Hello-World/exercise.js @@ -4,11 +4,11 @@ // - change the contents of the render function and save the file // - see the updates automatically in your browser without refreshing! //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; function App() { - return
Hello world!
+ return
Hello world!
; } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/00-Hello-World/lecture.js b/subjects/00-Hello-World/lecture.js new file mode 100644 index 000000000..e73e51b73 --- /dev/null +++ b/subjects/00-Hello-World/lecture.js @@ -0,0 +1,382 @@ +import $ from "jquery"; +import { search } from "./utils/searchWikipedia"; + +const html = ` +
+

Wikipedia

+
+ + +
+
Loading...
+
+

Results for:

+

+ +

+
+
    +
    +`; + +$("#app").html(html); // <-- component + +$("#form") + .on("submit", event => { + // <-- state change + event.preventDefault(); + const term = $("#input").val(); // <-- state + $("#loading").show(); // <-- time + $("#meta").hide(); // <-- time + $("#results").empty(); // <-- time + search(term, (err, results) => { + $("#loading").hide(); // <-- time + $("#meta").show(); // <-- time + $("#title").html(term); // <-- time + results.forEach(result => { + const li = $("
  • "); + const html = ` +
    + ${result.title} + +
    + +`; + li.html(html); // <-- time + if ($("#descending").is(":checked")) { + // <-- state + li.prependTo($("#results")); // <-- time + } else { + li.appendTo($("#results")); // <-- time + } + li.find("button").on("click", () => { + // <-- component + li.find(".toggler").toggle(); // <-- time + const isHidden = li.find(".toggler").is(":hidden"); // <-- state + li.find("button").html(isHidden ? "show more" : "hide"); // <-- time + }); + }); + }); + }) + .trigger("submit"); // <-- state change + +$("#descending").on("click", event => { + // <-- state change + $("#results li").each((i, li) => { + $("#results").prepend(li); // <-- time + }); +}); + +// What's awesome: +// +// I can still bang out this code even after not using jQuery for +// 4 years. +// +// What's not awesome: +// +// When our code... +// +// - is written as flows +// - doesn't call out state +// - has no entry point to change state +// +// ...it gets really hard to deal with. After you identify state, +// and how to change it, you must write code to connect every state +// to nearly every other state. Every feature requires changes to code +// in multiple places. Also, it's just too hard to think about for most +// of us, leading to lots of bugs. + +//////////////////////////////////////////////////////////////////////////////// + +// import Backbone from "backbone"; +// import $ from "jquery"; +// import _ from "underscore"; +// import { search } from "./utils/searchWikipedia"; + +// const appTemplate = _.template(` +//
    +//

    <%= title %>

    +//
    +// +// +//
    +// <% if (loading) { %> +//
    Loading...
    +// <% } else { %> +//
    +//

    Results for: <%= term %>

    +//

    +// +//

    +//
    +// <% } %> +//
      +// <% results.forEach(function(result) { %> +//
    • +// <% }) %> +//
    +//
    +// `); + +// const AppView = Backbone.View.extend({ +// template: appTemplate, + +// events: { +// // <-- delegated state changes +// "submit #form": "handleSubmit", +// "click #descending": "handleDescending" +// }, + +// initialize() { +// this.listenTo(this.model, "all", this.render); +// this.listenTo(this.model, "change:term", this.search); +// this.render(); +// this.search(); +// }, + +// handleSubmit(event) { +// event.preventDefault(); +// this.model.set("term", this.$el.find("#input").val()); // KVO Web +// }, + +// search() { +// this.model.set({ +// // KVO web +// loading: true, +// results: [], +// descending: false // cascading update! +// }); +// search(this.model.get("term"), (err, results) => { +// this.model.set({ +// // KVO web +// loading: false, +// results: results +// }); +// }); +// }, + +// handleDescending() { +// this.model.set( +// // <-- KVO web +// "descending", +// !this.model.get("descending") +// ); +// }, + +// render() { +// const state = this.model.toJSON(); +// if (state.descending) +// state.results = state.results.slice(0).reverse(); +// this.$el.html(this.template(state)); // DOM Bomb! +// this.$el.find("#results li").each((index, el) => { +// new ToggleView({ +// // <-- imperative (re)composition! +// el: el, +// model: new Backbone.Model(state.results[index]) +// }).render(); +// }); +// } +// }); + +// const ToggleView = Backbone.View.extend({ +// template: _.template(` +//
    +// <%= title %> +// +//
    +// <% if (isOpen) { %> +//
    +//

    <%= description %>

    +//
    +// <% } %> +// `), + +// events: { +// "click button": "toggle" +// }, + +// initialize() { +// this.model.set("isOpen", false, { silent: true }); // <-- model ownership? +// this.listenTo(this.model, "change:isOpen", this.render); +// }, + +// toggle() { +// this.model.set("isOpen", !this.model.get("isOpen")); // <-- KVO web +// }, + +// render() { +// this.$el.html(this.template(this.model.toJSON())); +// } +// }); + +// new AppView({ +// el: "#app", +// model: new Backbone.Model({ +// title: "Wikipedia", +// loading: false, +// term: "tacos", +// descending: false, +// results: [] +// }) +// }); + +// What's awesome +// +// - Moved state to models so we can identify what state changes +// the app. +// - Moved creating UI into templates, one step closer to being +// declarative. +// +// What's not so awesome +// +// - DOM Bombs +// - kill focus for assistive devices +// - non-performant +// +// - KVO Web +// - can't predict what will happen if you change state +// > Events complect communication and flow of control. +// > ... their fundamental nature, ... is that upon an event +// > an arbitrary amount of other code is run +// > http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html +// +// - leads to cascading updates +// - non-performant +// - to fix leads to knowing how your app changes over time intimately +// +// - imperative composition +// - non-performant +// - to fix +// - have to know how your app changes over time intimately +// - lots of code to manage instances +// - lots of mistakes + +//////////////////////////////////////////////////////////////////////////////// + +// import angular from "angular"; +// import { search } from "./utils/searchWikipedia"; + +// document.documentElement.setAttribute("ng-app", "wikipedia"); + +// document.getElementById("app").innerHTML = ` +//
    +//

    Wikipedia

    +//
    +// +// +//
    +//
    Loading...
    +//
    +//

    {{main.sortedResults().length}} results for: {{main.term}}

    +//

    +// +//

    +//
    +//
      +//
    • +// +//

      {{result.description}}

      +//
      +//
    • +//
    +//
    +// `; + +// const app = angular.module("wikipedia", []); + +// app.controller("MainController", function($rootScope) { +// const main = this; +// main.term = "taco"; // <-- shared state! +// main.results = []; +// main.loading = false; +// main.descending = false; + +// main.getFriends = () => { +// return [{ name: "Sarah" }, { name: "Max" }]; +// }; + +// main.handleSubmit = () => { +// main.loading = true; +// search(main.term, (err, results) => { +// main.results = results; +// main.loading = false; +// $rootScope.$digest(); // <-- time! +// }); +// }; + +// main.sortedResults = () => { +// return main.descending +// ? main.results.slice(0).reverse() +// : main.results; +// }; + +// main.handleSubmit(); +// }); + +// app.directive("toggler", () => { +// // <-- Global! +// return { +// restrict: "E", // WTH? +// scope: { +// title: "@" // WTH? +// }, +// controller($scope) { +// $scope.isOpen = false; +// $scope.toggle = () => { +// $scope.isOpen = !$scope.isOpen; +// }; +// }, +// replace: true, +// transclude: true, // WTH? +// template: ` +//
    +//
    +// {{title}} +// +//
    +//
    +//
    +// ` +// }; +// }); + +// What's awesome +// +// - fully declarative templates +// - declarative component composition +// +// What's not so awesome +// +// - directives and filters are globals +// - have to think about time with $apply/$watch, etc. +// - rendering assumptions require you to keep object identity +// and therefore think about time +// - and the real kicker: shared mutable state +// +// > July 7, 2014 +// > +// > Vojta brought up some points that we don’t yet have plans to solve +// > some problems we see in larger apps. In particular, how developers +// > can reason about data flow within an app. +// > +// > Key points: scope hierarchy is a huge pile of shared state that many +// > components from the application because of two way data-binding it's +// > not clear what how the data flows because it can flow in all +// > directions (including from child components to parents) - this makes +// > it hard to understand the app and understand of impact of model +// > changes in one part of the app on another (seemingly unrelated) part +// > of it. +// https://twitter.com/teozaurus/status/518071391959388160 diff --git a/subjects/HelloWorld/notes.md b/subjects/00-Hello-World/notes.md similarity index 100% rename from subjects/HelloWorld/notes.md rename to subjects/00-Hello-World/notes.md diff --git a/subjects/00-Hello-World/utils/searchWikipedia.js b/subjects/00-Hello-World/utils/searchWikipedia.js new file mode 100644 index 000000000..67bc87171 --- /dev/null +++ b/subjects/00-Hello-World/utils/searchWikipedia.js @@ -0,0 +1,24 @@ +import jsonp from "jsonp"; + +const API = + "https://en.wikipedia.org/w/api.php?action=opensearch&format=json"; + +export function search(term, cb) { + jsonp(`${API}&search=${term}`, (err, data) => { + if (err) { + cb(err); + } else { + const [searchTerm, titles, descriptions, urls] = data; + cb( + null, + titles.sort().map((title, index) => { + return { + title, + description: descriptions[index], + url: urls[index] + }; + }) + ); + } + }); +} diff --git a/subjects/01-Rendering/exercise.js b/subjects/01-Rendering/exercise.js new file mode 100644 index 000000000..3cd78f0bb --- /dev/null +++ b/subjects/01-Rendering/exercise.js @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Render `DATA.title` in an

    +// - Render a
      with each of `DATA.items` as an
    • +// - Now only render an
    • for mexican food (hint: use DATA.items.filter(...)) +// - Sort the items in alphabetical order by name (hint: use sort-by https://github.com/staygrimm/sort-by#example) +// +// Got extra time? +// +// - Add a +// +// +// ... +// + +// in angular we'd have something like this: +// + +// Things you have to learn to make this work: +// - ng-repeat +// - `month in months` DSL +// - auto-injected `$index` +// - that `|` is called a filter so you can google to learn... +// - ... how to create a filter + +// const months = [ +// "January", +// "February", +// "March", +// "April", +// "May", +// "June", +// "July", +// "August", +// "September", +// "October", +// "November", +// "December" +// ]; + +// function padMonth(index) { +// const realIndex = index + 1; +// return realIndex > 9 ? "" + realIndex : "0" + realIndex; +// } + +// ReactDOM.render( +// , +// document.getElementById("app") +// ); + +// Things you have to know +// - JavaScript +// - JSX ... or not + +// const { select, option } = React.DOM; +// ReactDOM.render( +// select( +// {}, +// months.map((month, index) => +// option({}, `(${padMonth(index)}) ${month}`) +// ) +// ), +// document.getElementById("app") +// ); + +//////////////////////////////////////////////////////////////////////////////// +// Because React is generally just a bunch of functions, you don't have to ask +// React how to solve a problem in your app, you can use everything you know +// about programming already. +// function monthOption(month, index) { +// return ( +// +// ); +// } + +// function MonthSelect() { +// return ; +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// - Always re-render +// - Virtual DOM makes it efficient +// - its like PHP but EVEN BETTER diff --git a/subjects/01-Rendering/solution.js b/subjects/01-Rendering/solution.js new file mode 100644 index 000000000..581167057 --- /dev/null +++ b/subjects/01-Rendering/solution.js @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Render `DATA.title` in an

      +// - Render a
        with each of `DATA.items` as an
      • +// - Now only render an
      • for mexican food (hint: use DATA.items.filter(...)) +// - Sort the items in alphabetical order by name (hint: use sort-by https://github.com/staygrimm/sort-by#example) +// +// Got extra time? +// +// - Add a +

        - +

        -
        +
        - + Shipping Address

        - +

        - +

        + +

        + +

    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/04-Controlled-vs-Uncontrolled/lecture.js b/subjects/04-Controlled-vs-Uncontrolled/lecture.js new file mode 100644 index 000000000..843ce0342 --- /dev/null +++ b/subjects/04-Controlled-vs-Uncontrolled/lecture.js @@ -0,0 +1,272 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import serializeForm from "form-serialize"; + +class App extends React.Component { + render() { + return ( +
    +

    Forms

    +
    + +
    +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Give the a default value. + +// class App extends React.Component { +// render() { +// return ( +//
    +//

    Forms

    +//
    +// +//
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// Access the value using event.target. + +// class App extends React.Component { +// handleChange = event => { +// console.log(event.target.value); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +//
    +// +//
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// Or use a ref. + +// class App extends React.Component { +// handleChange = () => { +// console.log(this.input.value); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +//
    +// (this.input = node)} +// /> +//
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// Or you can "control" the and have its value in state. +// What happens if we don't have an `onChange` but provide a value? + +// class App extends React.Component { +// state = { +// inputValue: "cupcakes" +// }; + +// handleChange = () => { +// this.setState({ +// inputValue: this.input.value +// }); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +//
    +// (this.input = node)} +// value={this.state.inputValue} +// onChange={this.handleChange} +// /> +//
    +//
    {JSON.stringify(this.state, null, 2)}
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// When it's controlled, we can setState elsewhere and it stays in sync. + +// class App extends React.Component { +// state = { +// inputValue: "cupcakes" +// }; + +// handleChange = () => { +// this.setState({ +// inputValue: this.input.value +// }); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +//
    +// +// (this.input = node)} +// value={this.state.inputValue} +// type="text" +// onChange={this.handleChange} +// /> +//
    +//
    {JSON.stringify(this.state, null, 2)}
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// Some forms are transactional, so modeling in state isn't necessary, just +// use DOM APIs to access the data, like when you need to save off some data +// somewhere and reset the form, but the values in the form are never +// important to `render`. + +// class App extends React.Component { +// handleSubmit = event => { +// event.preventDefault(); +// const values = serializeForm(event.target, { hash: true }); +// console.log(values); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +//
    +//

    +// +//

    + +//

    +// +//

    + +//

    +// +//

    +//
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// If we did want it all in state, we don't have to link up every single +// element to state, can use
    . However, updates won't be +// synchronized when other parts of the app manipulate the state like the +// button we had earlier. + +// class App extends React.Component { +// state = { +// firstName: "Michael", +// lastName: "Jackson" +// }; + +// handleFormChange = event => { +// event.preventDefault(); +// const values = serializeForm(this.form, { hash: true }); +// this.setState(values); +// }; + +// render() { +// return ( +//
    +//

    Forms

    +// (this.form = node)} +// > +//

    +// +//

    + +//

    +// +//

    + +//

    +// +//

    +// +//
    {JSON.stringify(this.state, null, 2)}
    +//
    +// ); +// } +// } + +//////////////////////////////////////////////////////////////////////////////// +// Use-cases: +// +// 1. Transactional forms, don't need anything in state, just use +// `defaultValue` and `onSubmit` +// 2. Some other part of the app needs the forms state to render, +// but nothing else needs to manipulate that state (one-way), +// use
    and DOM APIs to slurp form values into +// state +// 3. Multiple parts of the app manipulate the state, changes need +// to be reflected in the input (two-way), use `value` and +// `onChange` diff --git a/subjects/04-Controlled-vs-Uncontrolled/solution.js b/subjects/04-Controlled-vs-Uncontrolled/solution.js new file mode 100644 index 000000000..a5474c8c4 --- /dev/null +++ b/subjects/04-Controlled-vs-Uncontrolled/solution.js @@ -0,0 +1,151 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise +// +// - When the checkbox is checked: +// - Fill in the shipping fields with the values from billing +// - Disable the shipping fields so they are not directly editable +// - Keep the shipping fields up to date as billing fields change +// - Hint: you can get the checkbox value from `event.target.checked` +// - When the form submits, console.log the values +// +// Got extra time? +// +// - If there are more than two characters in the "state" field, let the user +// know they should use the two-character abbreviation +// - Save the state of the form and restore it when the page first loads, in +// case the user accidentally closes the tab before the form is submitted +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import serializeForm from "form-serialize"; + +class CheckoutForm extends React.Component { + state = { + billingName: "Michael Jackson", + billingState: "CA", + shippingName: "", + shippingState: "", + shippingSameAsBilling: false + }; + + handleSubmit = event => { + event.preventDefault(); + + const values = serializeForm(event.target, { hash: true }); + + console.log(values); + }; + + render() { + const { + billingName, + billingState, + shippingName, + shippingState, + shippingSameAsBilling + } = this.state; + + return ( +
    +

    Checkout

    + +
    + Billing Address +

    + +

    +

    + +

    +
    + +
    + +
    + + Shipping Address +

    + +

    +

    + +

    +
    + +

    + +

    + +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/ImperativeToDeclarative/exercise-no-bootstrap.js b/subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js similarity index 53% rename from subjects/ImperativeToDeclarative/exercise-no-bootstrap.js rename to subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js index c1b7a418d..492a4b235 100644 --- a/subjects/ImperativeToDeclarative/exercise-no-bootstrap.js +++ b/subjects/05-Imperative-to-Declarative/exercise-no-bootstrap.js @@ -5,78 +5,78 @@ // - even though this Modal is a React component, the author created an // imperative API D: Make it declarative! //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; class Modal extends React.Component { - state = { isOpen: false } + state = { isOpen: false }; containerStyle = { - position: 'fixed', + position: "fixed", left: 0, right: 0, top: 0, bottom: 0, - background: 'rgba(255, 255, 255, 0.75)' - } + background: "rgba(255, 255, 255, 0.75)" + }; contentStyle = { - position: 'fixed', + position: "fixed", padding: 10, left: 50, right: 50, top: 50, bottom: 50, - background: '#fff', - border: '1px solid', - textAlign: 'center' - } + background: "#fff", + border: "1px solid", + textAlign: "center" + }; open() { - this.setState({ isOpen: true }) + this.setState({ isOpen: true }); } close() { - this.setState({ isOpen: false }) + this.setState({ isOpen: false }); } render() { - if (this.state.isOpen === false) - return null + if (this.state.isOpen === false) return null; return (
    -
    - {this.props.children} -
    +
    {this.props.children}
    - ) + ); } } class App extends React.Component { openModal = () => { - this.refs.modal.open() - } + this.refs.modal.open(); + }; closeModal = () => { - this.refs.modal.close() - } + this.refs.modal.close(); + }; render() { return (

    Unbreakable

    -

    + +
    +
    - +

    - Your bones don’t break, mine do. That’s clear. Your cells react to - bacteria and viruses differently than mine. You don’t get sick, I do. - That’s also clear. But for some reason, you and I react the exact - same way to water. We swallow it too fast, we choke. We get some in - our lungs, we drown. However unreal it may seem, we are connected, - you and I. We’re on the same curve, just on opposite ends. + Your bones don’t break, mine do. That’s clear. Your cells + react to bacteria and viruses differently than mine. You don’t + get sick, I do. That’s also clear. But for some reason, you + and I react the exact same way to water. We swallow it too + fast, we choke. We get some in our lungs, we drown. However + unreal it may seem, we are connected, you and I. We’re on the + same curve, just on opposite ends.

    @@ -84,8 +84,8 @@ class App extends React.Component {

    Are you sure?

    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/ImperativeToDeclarative/exercise.js b/subjects/05-Imperative-to-Declarative/exercise.js similarity index 53% rename from subjects/ImperativeToDeclarative/exercise.js rename to subjects/05-Imperative-to-Declarative/exercise.js index c1ec4f0d9..cc5678c14 100644 --- a/subjects/ImperativeToDeclarative/exercise.js +++ b/subjects/05-Imperative-to-Declarative/exercise.js @@ -4,68 +4,84 @@ // This Modal, even though its a React component, has an imperative API to // open and close it. Can you convert it to a declarative API? //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import ReactDOM, { findDOMNode } from 'react-dom' -import $ from 'jquery' -import 'bootstrap-webpack' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import $ from "jquery"; +import "bootstrap-webpack"; class Modal extends React.Component { static propTypes = { title: PropTypes.string.isRequired, children: PropTypes.node + }; + + open() { + $(this.node).modal("show"); + } + + close() { + $(this.node).modal("hide"); } render() { return ( -
    +
    (this.node = node)}>

    {this.props.title}

    -
    - {this.props.children} -
    +
    {this.props.children}
    - ) + ); } } class App extends React.Component { openModal = () => { - $(findDOMNode(this.refs.modal)).modal('show') - } + this.modal.open(); + }; closeModal = () => { - $(findDOMNode(this.refs.modal)).modal('hide') - } + this.modal.close(); + }; render() { return (

    Let’s make bootstrap modal declarative

    - + - + (this.modal = modal)} + >

    Calling methods on instances is a FLOW not a STOCK!

    -

    It’s the dynamic process, not the static program in text space.

    -

    You have to experience it over time, rather than in snapshots of state.

    +

    + It’s the dynamic process, not the static program in text + space. +

    +

    + You have to experience it over time, rather than in + snapshots of state. +

    + > + Close +
    -
    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/ImperativeToDeclarative/lecture.js b/subjects/05-Imperative-to-Declarative/lecture.js similarity index 89% rename from subjects/ImperativeToDeclarative/lecture.js rename to subjects/05-Imperative-to-Declarative/lecture.js index cc6fa5e44..e1c2556c2 100644 --- a/subjects/ImperativeToDeclarative/lecture.js +++ b/subjects/05-Imperative-to-Declarative/lecture.js @@ -1,41 +1,47 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' -import createOscillator from './utils/createOscillator' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import createOscillator from "./utils/createOscillator"; -const styles = {} +const styles = {}; styles.theremin = { height: 200, width: 200, fontSize: 10, - border: '1px solid', - cursor: 'crosshair', + border: "1px solid", + cursor: "crosshair", margin: 10, - display: 'inline-block' -} + display: "inline-block" +}; class App extends React.Component { componentDidMount() { - this.oscillator = createOscillator() + this.oscillator = createOscillator(); } play = () => { - this.oscillator.play() - } + this.oscillator.play(); + }; stop = () => { - this.oscillator.stop() - } + this.oscillator.stop(); + }; - changeTone = (event) => { - const { clientX, clientY } = event - const { top, right, bottom, left } = event.target.getBoundingClientRect() - const pitch = (clientX - left) / (right - left) - const volume = 1 - (clientY - top) / (bottom - top) + changeTone = event => { + const { clientX, clientY } = event; + const { + top, + right, + bottom, + left + } = event.target.getBoundingClientRect(); + const pitch = (clientX - left) / (right - left); + const volume = 1 - (clientY - top) / (bottom - top); - this.oscillator.setPitchBend(pitch) - this.oscillator.setVolume(volume) - } + this.oscillator.setPitchBend(pitch); + this.oscillator.setVolume(volume); + }; render() { return ( @@ -48,11 +54,11 @@ class App extends React.Component { onMouseMove={this.changeTone} />
    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); //////////////////////////////////////////////////////////////////////////////// // Can't predict what the sound is going to be by looking at state or the render diff --git a/subjects/ImperativeToDeclarative/solution.js b/subjects/05-Imperative-to-Declarative/solution.js similarity index 51% rename from subjects/ImperativeToDeclarative/solution.js rename to subjects/05-Imperative-to-Declarative/solution.js index 6fd139ff8..204fa715c 100644 --- a/subjects/ImperativeToDeclarative/solution.js +++ b/subjects/05-Imperative-to-Declarative/solution.js @@ -4,103 +4,96 @@ // This Modal, even though its a React component, has an imperative API to // open and close it. Can you convert it to a declarative API? //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import ReactDOM, { findDOMNode } from 'react-dom' -import $ from 'jquery' -import 'bootstrap-webpack' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import $ from "jquery"; +import "bootstrap-webpack"; class Modal extends React.Component { static propTypes = { - isOpen: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, - onClose: PropTypes.func, - children: PropTypes.node - } + children: PropTypes.node, + isOpen: PropTypes.bool.isRequired + }; componentDidMount() { - this.doImperativeWork() - - // This is only necessary to keep state in sync - // with the DOM. Since we're keeping state now, - // we should make sure it's accurate. - $(findDOMNode(this)).on('hidden.bs.modal', () => { - if (this.props.onClose) - this.props.onClose() - }) + this.doImperativeWork(); } componentDidUpdate(prevProps) { - if (prevProps.isOpen !== this.props.isOpen) - this.doImperativeWork() + if (prevProps.isOpen !== this.props.isOpen) { + this.doImperativeWork(); + } } doImperativeWork() { - if (this.props.isOpen === true) { - $(findDOMNode(this)).modal('show') - } else { - $(findDOMNode(this)).modal('hide') - } + $(this.node).modal(this.props.isOpen ? "show" : "hide"); } render() { return ( -
    +
    (this.node = node)}>

    {this.props.title}

    -
    - {this.props.children} -
    +
    {this.props.children}
    - ) + ); } } class App extends React.Component { state = { isModalOpen: false - } + }; openModal = () => { - this.setState({ isModalOpen: true }) - } + this.setState({ isModalOpen: true }); + }; closeModal = () => { - this.setState({ isModalOpen: false }) - } + this.setState({ isModalOpen: false }); + }; render() { return (

    Let’s make bootstrap modal declarative

    - +

    Calling methods on instances is a FLOW not a STOCK!

    -

    It’s the dynamic process, not the static program in text space.

    -

    You have to experience it over time, rather than in snapshots of state.

    +

    + It’s the dynamic process, not the static program in text + space. +

    +

    + You have to experience it over time, rather than in + snapshots of state. +

    + > + Close +
    -
    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Animation/utils/AudioContextMonkeyPatch.js b/subjects/05-Imperative-to-Declarative/utils/AudioContextMonkeyPatch.js similarity index 59% rename from subjects/Animation/utils/AudioContextMonkeyPatch.js rename to subjects/05-Imperative-to-Declarative/utils/AudioContextMonkeyPatch.js index 524b20bc0..d03436608 100644 --- a/subjects/Animation/utils/AudioContextMonkeyPatch.js +++ b/subjects/05-Imperative-to-Declarative/utils/AudioContextMonkeyPatch.js @@ -13,16 +13,16 @@ limitations under the License. */ -/* +/* This monkeypatch library is intended to be included in projects that are -written to the proper AudioContext spec (instead of webkitAudioContext), -and that use the new naming and proper bits of the Web Audio API (e.g. +written to the proper AudioContext spec (instead of webkitAudioContext), +and that use the new naming and proper bits of the Web Audio API (e.g. using BufferSourceNode.start() instead of BufferSourceNode.noteOn()), but may have to run on systems that only support the deprecated bits. -This library should be harmless to include if the browser supports -unprefixed "AudioContext", and/or if it supports the new names. +This library should be harmless to include if the browser supports +unprefixed "AudioContext", and/or if it supports the new names. The patches this library handles: if window.AudioContext is unsupported, it will be aliased to webkitAudioContext(). @@ -39,66 +39,74 @@ OscillatorNode.start() is aliased to noteOn() OscillatorNode.stop() is aliased to noteOff() AudioParam.setTargetAtTime() is aliased to setTargetValueAtTime() -This library does NOT patch the enumerated type changes, as it is +This library does NOT patch the enumerated type changes, as it is recommended in the specification that implementations support both integer -and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel +and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel BiquadFilterNode.type and OscillatorNode.type. */ -(function (global, exports, perf) { - 'use strict'; +(function(global, exports, perf) { + "use strict"; function fixSetTarget(param) { - if (!param) // if NYI, just return + if (!param) + // if NYI, just return return; if (!param.setTargetAtTime) - param.setTargetAtTime = param.setTargetValueAtTime; + param.setTargetAtTime = param.setTargetValueAtTime; } - if (window.hasOwnProperty('webkitAudioContext') && - !window.hasOwnProperty('AudioContext')) { + if ( + window.hasOwnProperty("webkitAudioContext") && + !window.hasOwnProperty("AudioContext") + ) { window.AudioContext = webkitAudioContext; - if (!AudioContext.prototype.hasOwnProperty('createGain')) - AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; - if (!AudioContext.prototype.hasOwnProperty('createDelay')) - AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; - if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor')) - AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; - - AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; - AudioContext.prototype.createGain = function() { + if (!AudioContext.prototype.hasOwnProperty("createGain")) + AudioContext.prototype.createGain = + AudioContext.prototype.createGainNode; + if (!AudioContext.prototype.hasOwnProperty("createDelay")) + AudioContext.prototype.createDelay = + AudioContext.prototype.createDelayNode; + if (!AudioContext.prototype.hasOwnProperty("createScriptProcessor")) + AudioContext.prototype.createScriptProcessor = + AudioContext.prototype.createJavaScriptNode; + + AudioContext.prototype.internal_createGain = + AudioContext.prototype.createGain; + AudioContext.prototype.createGain = function() { const node = this.internal_createGain(); fixSetTarget(node.gain); return node; }; - AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; - AudioContext.prototype.createDelay = function() { + AudioContext.prototype.internal_createDelay = + AudioContext.prototype.createDelay; + AudioContext.prototype.createDelay = function() { const node = this.internal_createDelay(); fixSetTarget(node.delayTime); return node; }; - AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; - AudioContext.prototype.createBufferSource = function() { + AudioContext.prototype.internal_createBufferSource = + AudioContext.prototype.createBufferSource; + AudioContext.prototype.createBufferSource = function() { const node = this.internal_createBufferSource(); if (!node.start) { - node.start = function ( when, offset, duration ) { - if ( offset || duration ) - this.noteGrainOn( when, offset, duration ); - else - this.noteOn( when ); - } + node.start = function(when, offset, duration) { + if (offset || duration) + this.noteGrainOn(when, offset, duration); + else this.noteOn(when); + }; } - if (!node.stop) - node.stop = node.noteoff; + if (!node.stop) node.stop = node.noteoff; fixSetTarget(node.playbackRate); return node; }; - AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; - AudioContext.prototype.createDynamicsCompressor = function() { + AudioContext.prototype.internal_createDynamicsCompressor = + AudioContext.prototype.createDynamicsCompressor; + AudioContext.prototype.createDynamicsCompressor = function() { const node = this.internal_createDynamicsCompressor(); fixSetTarget(node.threshold); fixSetTarget(node.knee); @@ -109,8 +117,9 @@ BiquadFilterNode.type and OscillatorNode.type. return node; }; - AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; - AudioContext.prototype.createBiquadFilter = function() { + AudioContext.prototype.internal_createBiquadFilter = + AudioContext.prototype.createBiquadFilter; + AudioContext.prototype.createBiquadFilter = function() { const node = this.internal_createBiquadFilter(); fixSetTarget(node.frequency); fixSetTarget(node.detune); @@ -119,18 +128,17 @@ BiquadFilterNode.type and OscillatorNode.type. return node; }; - if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) { - AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; - AudioContext.prototype.createOscillator = function() { + if (AudioContext.prototype.hasOwnProperty("createOscillator")) { + AudioContext.prototype.internal_createOscillator = + AudioContext.prototype.createOscillator; + AudioContext.prototype.createOscillator = function() { const node = this.internal_createOscillator(); - if (!node.start) - node.start = node.noteOn; - if (!node.stop) - node.stop = node.noteOff; + if (!node.start) node.start = node.noteOn; + if (!node.stop) node.stop = node.noteOff; fixSetTarget(node.frequency); fixSetTarget(node.detune); return node; }; } } -}(window)); +})(window); diff --git a/subjects/05-Imperative-to-Declarative/utils/createOscillator.js b/subjects/05-Imperative-to-Declarative/utils/createOscillator.js new file mode 100644 index 000000000..8d0c41424 --- /dev/null +++ b/subjects/05-Imperative-to-Declarative/utils/createOscillator.js @@ -0,0 +1,63 @@ +import "./AudioContextMonkeyPatch"; + +function Oscillator(audioContext) { + // TODO make more things not use this. + const oscillatorNode = audioContext.createOscillator(); + oscillatorNode.start(0); + + const gainNode = audioContext.createGain(); + this.pitchBase = 50; + this.pitchBend = 0; + this.pitchRange = 2000; + this.volume = 0.5; + this.maxVolume = 1; + this.frequency = this.pitchBase; + + let hasConnected = false; + let frequency = this.pitchBase; + + this.play = function() { + oscillatorNode.connect(gainNode); + hasConnected = true; + }; + + this.stop = function() { + if (hasConnected) { + oscillatorNode.disconnect(gainNode); + hasConnected = false; + } + }; + + this.setType = function(type) { + oscillatorNode.type = type; + }; + + this.setPitchBend = function(v) { + this.pitchBend = v; + frequency = this.pitchBase + this.pitchBend * this.pitchRange; + oscillatorNode.frequency.value = frequency; + this.frequency = frequency; + }; + + this.setVolume = function(v) { + this.volume = this.maxVolume * v; + gainNode.gain.value = this.volume; + }; + + this.connect = function(output) { + gainNode.connect(output); + }; + + return this; +} + +function createOscillator() { + const audioContext = new AudioContext(); + const theremin = new Oscillator(audioContext); + + theremin.connect(audioContext.destination); + + return theremin; +} + +export default createOscillator; diff --git a/subjects/06-Higher-Order-Components/exercise.js b/subjects/06-Higher-Order-Components/exercise.js new file mode 100644 index 000000000..c942aedd9 --- /dev/null +++ b/subjects/06-Higher-Order-Components/exercise.js @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Make `withMouse` a "higher-order component" that sends the mouse position +// to the component as props (hint: use `event.clientX` and `event.clientY`). +// +// Got extra time? +// +// - Make a `withCat` HOC that shows a cat chasing the mouse around the screen! +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +function withMouse(Component) { + return Component; +} + +class App extends React.Component { + static propTypes = { + mouse: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) + }; + + render() { + const { mouse } = this.props; + + return ( +
    + {mouse ? ( +

    + The mouse position is ({mouse.x}, {mouse.y}) +

    + ) : ( +

    We don't know the mouse position yet :(

    + )} +
    + ); + } +} + +const AppWithMouse = withMouse(App); + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/06-Higher-Order-Components/images/cat.jpg b/subjects/06-Higher-Order-Components/images/cat.jpg new file mode 100644 index 000000000..1276e88ef Binary files /dev/null and b/subjects/06-Higher-Order-Components/images/cat.jpg differ diff --git a/subjects/06-Higher-Order-Components/images/mouse.png b/subjects/06-Higher-Order-Components/images/mouse.png new file mode 100644 index 000000000..3c88c464b Binary files /dev/null and b/subjects/06-Higher-Order-Components/images/mouse.png differ diff --git a/subjects/06-Higher-Order-Components/lecture.js b/subjects/06-Higher-Order-Components/lecture.js new file mode 100644 index 000000000..2ae56bb3f --- /dev/null +++ b/subjects/06-Higher-Order-Components/lecture.js @@ -0,0 +1,93 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import createMediaListener from "./utils/createMediaListener"; + +const media = createMediaListener({ + big: "(min-width : 1000px)", + tiny: "(max-width: 400px)" +}); + +class App extends React.Component { + state = { + media: media.getState() + }; + + componentDidMount() { + media.listen(media => this.setState({ media })); + } + + componentWillUnmount() { + media.dispose(); + } + + render() { + const { media } = this.state; + + return media.big ? ( +

    Hey, this is a big screen

    + ) : media.tiny ? ( +
    tiny tiny tiny
    + ) : ( +

    Somewhere in between

    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// We can move all of that code into a higher-order component. A higher-order +// component (HOC) is a function that takes a `Component` as an argument, and +// returns a new component renders that `Component` with some extra props. + +// const mediaComponent = (Component, queries) => { +// const media = createMediaListener(queries); + +// return class extends React.Component { +// state = { +// media: media.getState() +// }; + +// componentDidMount() { +// media.listen(media => this.setState({ media })); +// } + +// componentWillUnmount() { +// media.dispose(); +// } + +// render() { +// return ; +// } +// }; +// }; + +// class App extends React.Component { +// static propTypes = { +// media: PropTypes.shape({ +// big: PropTypes.bool, +// tiny: PropTypes.bool +// }) +// }; + +// render() { +// const { media } = this.props; + +// return media.big ? ( +//

    Hey, this is a big screen

    +// ) : media.tiny ? ( +//
    tiny tiny tiny
    +// ) : ( +//

    Somewhere in between

    +// ); +// } +// } + +// const AppWithMedia = mediaComponent(App, { +// big: "(min-width : 1000px)", +// tiny: "(max-width: 400px)" +// }); + +// ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/06-Higher-Order-Components/solution-extra.js b/subjects/06-Higher-Order-Components/solution-extra.js new file mode 100644 index 000000000..b4832ec2d --- /dev/null +++ b/subjects/06-Higher-Order-Components/solution-extra.js @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Make `withMouse` a "higher-order component" that sends the mouse position +// to the component as props (hint: use `event.clientX` and `event.clientY`). +// +// Got extra time? +// +// Make a `withCat` HOC that shows a cat chasing the mouse around the screen! +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +function withMouse(Component) { + return class extends React.Component { + state = { x: 0, y: 0 }; + + handleMouseMove = event => { + this.setState({ + x: event.clientX, + y: event.clientY + }); + }; + + render() { + return ( +
    + +
    + ); + } + }; +} + +function withCat(Component) { + return class extends React.Component { + state = { top: 0, left: 0 }; + + componentDidUpdate(prevProps) { + const { mouse } = this.props; + + if ( + mouse.x !== prevProps.mouse.x || + mouse.y !== prevProps.mouse.y + ) { + this.setState({ + top: mouse.y - Math.round(this.node.offsetHeight / 2), + left: mouse.x - Math.round(this.node.offsetWidth / 2) + }); + } + } + + render() { + return ( +
    +
    (this.node = node)} + className="cat" + style={this.state} + /> + +
    + ); + } + }; +} + +class App extends React.Component { + static propTypes = { + mouse: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) + }; + + render() { + const { mouse } = this.props; + + return ( +
    + {mouse ? ( +

    + The mouse position is ({mouse.x}, {mouse.y}) +

    + ) : ( +

    We don't know the mouse position yet :(

    + )} +
    + ); + } +} + +const AppWithMouse = withMouse(withCat(App)); + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/06-Higher-Order-Components/solution.js b/subjects/06-Higher-Order-Components/solution.js new file mode 100644 index 000000000..8a2cc63ac --- /dev/null +++ b/subjects/06-Higher-Order-Components/solution.js @@ -0,0 +1,65 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Make `withMouse` a "higher-order component" that sends the mouse position +// to the component as props (hint: use `event.clientX` and `event.clientY`). +// +// Got extra time? +// +// - Make a `withCat` HOC that shows a cat chasing the mouse around the screen! +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +function withMouse(Component) { + return class extends React.Component { + state = { x: 0, y: 0 }; + + handleMouseMove = event => { + this.setState({ + x: event.clientX, + y: event.clientY + }); + }; + + render() { + return ( +
    + +
    + ); + } + }; +} + +class App extends React.Component { + static propTypes = { + mouse: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) + }; + + render() { + const { mouse } = this.props; + + return ( +
    + {mouse ? ( +

    + The mouse position is ({mouse.x}, {mouse.y}) +

    + ) : ( +

    We don't know the mouse position yet :(

    + )} +
    + ); + } +} + +const AppWithMouse = withMouse(App); + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/06-Higher-Order-Components/styles.css b/subjects/06-Higher-Order-Components/styles.css new file mode 100644 index 000000000..466ac78be --- /dev/null +++ b/subjects/06-Higher-Order-Components/styles.css @@ -0,0 +1,21 @@ +.container { + position: absolute; + width: 100vw; + height: 100vh; + top: 0px; + left: 0px; + padding: 0 20px; + cursor: url(./images/mouse.png), auto; +} + +.cat { + position: absolute; + transition: top 200ms ease-out, left 200ms ease-out; + width: 100px; + height: 100px; + background-size: contain; + background-repeat: no-repeat; + background-position: 50% 0; + background-image: url(./images/cat.jpg); + cursor: url(./images/mouse.png), auto; +} diff --git a/subjects/06-Higher-Order-Components/utils/createMediaListener.js b/subjects/06-Higher-Order-Components/utils/createMediaListener.js new file mode 100644 index 000000000..61697bdd1 --- /dev/null +++ b/subjects/06-Higher-Order-Components/utils/createMediaListener.js @@ -0,0 +1,57 @@ +/* +const listener = createMediaListener({ + mobile: "(max-width: 767px)", + small: "(max-width: 568px), (max-height: 400px)" +}) + +listener.listen((state) => {}) +listener.getState() +listenter.dispose() +*/ + +export default media => { + let transientListener = null; + + const mediaKeys = Object.keys(media); + + const queryLists = mediaKeys.reduce((queryLists, key) => { + queryLists[key] = window.matchMedia(media[key]); + return queryLists; + }, {}); + + const mediaState = mediaKeys.reduce((state, key) => { + state[key] = queryLists[key].matches; + return state; + }, {}); + + const notify = () => { + if (transientListener != null) transientListener(mediaState); + }; + + const listeners = mediaKeys.reduce((listeners, key) => { + listeners[key] = event => { + mediaState[key] = event.matches; + notify(); + }; + + return listeners; + }, {}); + + const listen = listener => { + transientListener = listener; + mediaKeys.forEach(key => { + queryLists[key].addListener(listeners[key]); + }); + }; + + const dispose = () => { + transientListener = null; + mediaKeys.forEach(key => { + queryLists[key].removeListener(listeners[key]); + }); + }; + + const getState = () => mediaState; + + return { listen, dispose, getState }; +}; diff --git a/subjects/07-Render-Props/LoadingDots.js b/subjects/07-Render-Props/LoadingDots.js new file mode 100644 index 000000000..f00d092d9 --- /dev/null +++ b/subjects/07-Render-Props/LoadingDots.js @@ -0,0 +1,40 @@ +import React from "react"; +import PropTypes from "prop-types"; + +class LoadingDots extends React.Component { + static propTypes = { + interval: PropTypes.number, + dots: PropTypes.number + }; + + static defaultProps = { + interval: 300, + dots: 3 + }; + + state = { frame: 1 }; + + componentDidMount() { + this.interval = setInterval(() => { + this.setState({ + frame: this.state.frame + 1 + }); + }, this.props.interval); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + let dots = this.state.frame % (this.props.dots + 1); + let text = ""; + while (dots > 0) { + text += "."; + dots--; + } + return {text} ; + } +} + +export default LoadingDots; diff --git a/subjects/07-Render-Props/exercise.js b/subjects/07-Render-Props/exercise.js new file mode 100644 index 000000000..cb4fa11fe --- /dev/null +++ b/subjects/07-Render-Props/exercise.js @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Create a component that encapsulates the geo state and +// watching logic and uses a render prop to pass the coordinates back to +// the +// +// Got extra time? +// +// - Create a component that translates the geo coordinates to a +// physical address and prints it to the screen (hint: use +// `getAddressFromCoords`) +// - You should be able to compose and beneath it to +// naturally compose both the UI and the state needed to render it +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import getAddressFromCoords from "./utils/getAddressFromCoords"; +import LoadingDots from "./LoadingDots"; + +class App extends React.Component { + state = { + coords: { + latitude: null, + longitude: null + }, + error: null + }; + + componentDidMount() { + this.geoId = navigator.geolocation.watchPosition( + position => { + this.setState({ + coords: { + latitude: position.coords.latitude, + longitude: position.coords.longitude + } + }); + }, + error => { + this.setState({ error }); + } + ); + } + + componentWillUnmount() { + navigator.geolocation.clearWatch(this.geoId); + } + + render() { + return ( +
    +

    Geolocation

    + {this.state.error ? ( +
    Error: {this.state.error.message}
    + ) : ( +
    +
    Latitude
    +
    {this.state.coords.latitude || }
    +
    Longitude
    +
    {this.state.coords.longitude || }
    +
    + )} +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/RenderProps/lecture.js b/subjects/07-Render-Props/lecture.js similarity index 74% rename from subjects/RenderProps/lecture.js rename to subjects/07-Render-Props/lecture.js index 4084c92dc..aeeb1d062 100644 --- a/subjects/RenderProps/lecture.js +++ b/subjects/07-Render-Props/lecture.js @@ -1,5 +1,6 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; document.body.style.background = ` linear-gradient(135deg, @@ -8,52 +9,55 @@ document.body.style.background = ` #207cca 51%, #7db9e8 100% ) -` +`; -const getHeaderStyle = (y) => { - const pin = y >= 100 - const top = -y / 2 +const getHeaderStyle = y => { + const pin = y >= 100; + const top = -y / 2; return { - textTransform: 'uppercase', - textAlign: 'center', - width: '100%', + textTransform: "uppercase", + textAlign: "center", + width: "100%", margin: 0, - position: 'fixed', - top: pin ? '0px' : `${top + 50}px`, - textShadow: pin ? `0px ${(y-100)/5}px ${Math.min((y-100)/10, 20)}px rgba(0, 0, 0, 0.5)` : 'none' - } -} + position: "fixed", + top: pin ? "0px" : `${top + 50}px`, + textShadow: pin + ? `0px ${(y - 100) / 5}px ${Math.min( + (y - 100) / 10, + 20 + )}px rgba(0, 0, 0, 0.5)` + : "none" + }; +}; class App extends React.Component { - state = { y: 0 } + state = { y: 0 }; handleWindowScroll = () => { - this.setState({ y: window.scrollY }) - } + this.setState({ y: window.scrollY }); + }; componentDidMount() { - this.handleWindowScroll() - window.addEventListener('scroll', this.handleWindowScroll) + this.handleWindowScroll(); + window.addEventListener("scroll", this.handleWindowScroll); } componentWillUnmount() { - window.removeEventListener('scroll', this.handleWindowScroll) + window.removeEventListener("scroll", this.handleWindowScroll); } render() { - const { y } = this.state + const { y } = this.state; return ( -
    -

    - Scroll down! -

    +
    +

    Scroll down!

    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); /////////////////////////////////////////////////////////////////////////////// // We can wrap up the scroll listening into a component with diff --git a/subjects/07-Render-Props/solution.js b/subjects/07-Render-Props/solution.js new file mode 100644 index 000000000..e20bfac51 --- /dev/null +++ b/subjects/07-Render-Props/solution.js @@ -0,0 +1,133 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Create a component that encapsulates the geo state and +// watching logic and uses a render prop to pass the coordinates back to +// the +// +// Got extra time? +// +// - Create a component that translates the geo coordinates to a +// physical address and prints it to the screen (hint: use +// `getAddressFromCoords`) +// - You should be able to compose and beneath it to +// naturally compose both the UI and the state needed to render it +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import getAddressFromCoords from "./utils/getAddressFromCoords"; +import LoadingDots from "./LoadingDots"; + +class GeoPosition extends React.Component { + static propTypes = { + children: PropTypes.func.isRequired + }; + + state = { + coords: { + latitude: null, + longitude: null + }, + error: null + }; + + componentDidMount() { + this.geoId = navigator.geolocation.watchPosition( + position => { + this.setState({ + coords: { + latitude: position.coords.latitude, + longitude: position.coords.longitude + } + }); + }, + error => { + this.setState({ error }); + } + ); + } + + componentWillUnmount() { + navigator.geolocation.clearWatch(this.geoId); + } + + render() { + return this.props.children(this.state); + } +} + +class GeoAddress extends React.Component { + static propTypes = { + latitude: PropTypes.number, + longitude: PropTypes.number, + children: PropTypes.func.isRequired + }; + + state = { address: null }; + + componentDidMount() { + if (this.props.latitude && this.props.longitude) this.fetch(); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.longitude !== this.props.longitude || + prevProps.latitude !== this.props.latitude + ) + this.fetch(); + } + + fetch() { + const { latitude, longitude } = this.props; + + getAddressFromCoords(latitude, longitude).then(address => { + this.setState({ address }); + }); + } + + render() { + return this.props.children(this.state); + } +} + +class App extends React.Component { + render() { + return ( +
    +

    Geolocation

    + +

    GeoPosition

    + + {state => + state.error ? ( +
    Error: {state.error.message}
    + ) : ( +
    +
    Latitude
    +
    {state.coords.latitude || }
    +
    Longitude
    +
    {state.coords.longitude || }
    +
    + ) + } +
    + +

    GeoAddress Composition

    + + {({ coords }) => ( + + {({ address }) =>

    {address || }

    } +
    + )} +
    +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/07-Render-Props/utils/getAddressFromCoords.js b/subjects/07-Render-Props/utils/getAddressFromCoords.js new file mode 100644 index 000000000..5087f3df8 --- /dev/null +++ b/subjects/07-Render-Props/utils/getAddressFromCoords.js @@ -0,0 +1,60 @@ +const GoogleMapsAPI = "https://maps.googleapis.com/maps/api"; + +function wait(timeout, work) { + return new Promise(resolve => { + setTimeout(() => { + try { + resolve(work()); + } catch (error) { + reject(error); + } + }, timeout); + }); +} + +const retryTimeout = 5000; + +function getAddressFromCoords(latitude, longitude) { + const url = `${GoogleMapsAPI}/geocode/json?latlng=${latitude},${longitude}`; + + return fetch(url) + .then(res => res.json()) + .then( + json => + json.status === "OVER_QUERY_LIMIT" + ? // Wait for the query limit to reset. + wait(retryTimeout, () => + getAddressFromCoords(latitude, longitude) + ) + : json.results[0].formatted_address + ); +} + +let lastCallTime = 0; +let alreadyWarned = false; +let promise = null; + +function throttledGetAddressFromCoords(latitude, longitude) { + if (latitude == null || longitude == null) { + return Promise.resolve(null); + } + + const currentTime = Date.now(); + + if (lastCallTime + retryTimeout < currentTime) { + lastCallTime = currentTime; + promise = getAddressFromCoords(latitude, longitude); + } else if (!alreadyWarned) { + alreadyWarned = true; + + window.alert( + "It looks like you're calling getAddressFromCoords many times " + + "quickly in a loop. Take a closer look at the componentDidUpdate " + + "function in ..." + ); + } + + return promise; +} + +export default throttledGetAddressFromCoords; diff --git a/subjects/CompoundComponents/exercise.js b/subjects/08-Compound-Components/exercise.js similarity index 51% rename from subjects/CompoundComponents/exercise.js rename to subjects/08-Compound-Components/exercise.js index d79cba950..2868a34c3 100644 --- a/subjects/CompoundComponents/exercise.js +++ b/subjects/08-Compound-Components/exercise.js @@ -7,75 +7,64 @@ // - The selected should pass the correct value to its // - The `defaultValue` should be set on first render // -// Hints to get started: -// -// - will need some state -// - It then needs to pass that state to the s so they know -// whether or not they are active -// // Got extra time? // -// Implement a `value` prop and allow this to work like a "controlled input" -// (https://facebook.github.io/react/docs/forms.html#controlled-components) -// -// - Add a button to that sets `this.state.radioValue` to a pre-determined -// value, like "tape" -// - Make the update accordingly -// -// Implement keyboard controls on the (you'll need tabIndex="0" on -// the s so the keyboard will work) -// -// - Enter and space bar should select the option -// - Arrow right, arrow down should select the next option -// - Arrow left, arrow up should select the previous option +// - Implement an `onChange` prop that communicates the 's state +// back to the so it can use it to render something +// - Implement keyboard controls on the +// - Hint: Use tabIndex="0" on the s so the keyboard will work +// - Enter and space bar should select the option +// - Arrow right, arrow down should select the next option +// - Arrow left, arrow up should select the previous option //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; class RadioGroup extends React.Component { static propTypes = { defaultValue: PropTypes.string - } + }; render() { - return
    {this.props.children}
    + return
    {this.props.children}
    ; } } class RadioOption extends React.Component { static propTypes = { value: PropTypes.string - } + }; render() { return (
    - {this.props.children} + {this.props.children}
    - ) + ); } } class RadioIcon extends React.Component { static propTypes = { isSelected: PropTypes.bool.isRequired - } + }; render() { return (
    - ) + ); } } @@ -92,8 +81,8 @@ class App extends React.Component { Aux
    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/08-Compound-Components/lecture.js b/subjects/08-Compound-Components/lecture.js new file mode 100644 index 000000000..6c8d36089 --- /dev/null +++ b/subjects/08-Compound-Components/lecture.js @@ -0,0 +1,419 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import * as styles from "./styles"; + +class Tabs extends React.Component { + state = { + activeIndex: 0 + }; + + selectTabIndex(activeIndex) { + this.setState({ activeIndex }); + } + + renderTabs() { + return this.props.data.map((tab, index) => { + const isActive = this.state.activeIndex === index; + return ( +
    this.selectTabIndex(index)} + > + {tab.label} +
    + ); + }); + } + + renderPanel() { + const tab = this.props.data[this.state.activeIndex]; + return
    {tab.description}
    ; + } + + render() { + return ( +
    +
    {this.renderTabs()}
    +
    {this.renderPanel()}
    +
    + ); + } +} + +class App extends React.Component { + render() { + const tabData = [ + { + label: "Tacos", + description:

    Tacos are delicious

    + }, + { + label: "Burritos", + description:

    Sometimes a burrito is what you really need

    + }, + { + label: "Coconut Korma", + description:

    Might be your best option

    + } + ]; + + return ( +
    + +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// What if I wanted tabs on the bottom? + +// class Tabs extends React.Component { +// static defaultProps = { +// tabsPlacement: "top" +// }; + +// state = { +// activeIndex: 0 +// }; + +// selectTabIndex(activeIndex) { +// this.setState({ activeIndex }); +// } + +// renderTabs() { +// return this.props.data.map((tab, index) => { +// const isActive = this.state.activeIndex === index; +// return ( +//
    this.selectTabIndex(index)} +// > +// {tab.label} +//
    +// ); +// }); +// } + +// renderPanel() { +// const tab = this.props.data[this.state.activeIndex]; +// return
    {tab.description}
    ; +// } + +// render() { +// const tabs = ( +//
    +// {this.renderTabs()} +//
    +// ); +// const panels = ( +//
    +// {this.renderPanel()} +//
    +// ); +// return ( +//
    +// {this.props.tabsPlacement === "top" +// ? [tabs, panels] +// : [panels, tabs]} +//
    +// ); +// } +// } + +// class App extends React.Component { +// render() { +// const tabData = [ +// { +// label: "Tacos", +// description:

    Tacos are delicious

    +// }, +// { +// label: "Burritos", +// description:

    Sometimes a burrito is what you really need

    +// }, +// { +// label: "Coconut Korma", +// description:

    Might be your best option

    +// } +// ]; + +// return ( +//
    +// +//
    +// ); +// } +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// That wasn't too bad, but it added a lot of complexity for something that +// didn't seem to warrant that much of a change +// +// - render is less obvious +// - have to use keys, or wrap stuff in extra divs +// - adding another option that has to do with rendering will add even more +// complexity + +//////////////////////////////////////////////////////////////////////////////// +// Lets add "disabled" to a tab, what does jQuery UI do? +// https://api.jqueryui.com/tabs/#option-disabled + +// class Tabs extends React.Component { +// static defaultProps = { +// tabsPlacement: "top", +// disabled: [] +// }; + +// state = { +// activeIndex: 0 +// }; + +// selectTabIndex(activeIndex) { +// this.setState({ activeIndex }); +// } + +// renderTabs() { +// return this.props.data.map((tab, index) => { +// const isActive = this.state.activeIndex === index; +// const isDisabled = this.props.disabled.indexOf(index) !== -1; +// const props = { +// key: tab.label, +// style: isDisabled +// ? styles.disabledTab +// : isActive ? styles.activeTab : styles.tab, +// onClick: isDisabled ? null : () => this.selectTabIndex(index) +// }; +// return
    {tab.label}
    ; +// }); +// } + +// renderPanel() { +// const tab = this.props.data[this.state.activeIndex]; +// return
    {tab.description}
    ; +// } + +// render() { +// const tabs = ( +//
    +// {this.renderTabs()} +//
    +// ); +// const panels = ( +//
    +// {this.renderPanel()} +//
    +// ); +// return ( +//
    +// {this.props.tabsPlacement === "top" +// ? [tabs, panels] +// : [panels, tabs]} +//
    +// ); +// } +// } + +// class App extends React.Component { +// render() { +// const tabData = [ +// { +// label: "Tacos", +// description:

    Tacos are delicious

    +// }, +// { +// label: "Burritos", +// description:

    Sometimes a burrito is what you really need

    +// }, +// { +// label: "Coconut Korma", +// description:

    Might be your best option

    +// } +// ]; + +// return ( +//
    +// +//
    +// ); +// } +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Feels weird ... whenever your options affect rendering, its a great +// opportunity to create child components instead! + +// function TabList({ children, _activeIndex, _onTabSelect }) { +// return ( +//
    +// {React.Children.map(children, (child, index) => { +// return React.cloneElement(child, { +// _isActive: index === _activeIndex, +// _onSelect: () => _onTabSelect(index) +// }); +// })} +//
    +// ); +// } + +// function Tab({ children, disabled, _isActive, _onSelect }) { +// return ( +//
    +// {children} +//
    +// ); +// } + +// function TabPanels({ children, _activeIndex }) { +// return ( +//
    +// {React.Children.toArray(children)[_activeIndex]} +//
    +// ); +// } + +// function TabPanel({ children }) { +// return
    {children}
    ; +// } + +// class Tabs extends React.Component { +// state = { +// activeIndex: 0 +// }; + +// render() { +// const children = React.Children.map( +// this.props.children, +// (child, index) => { +// if (child.type === TabPanels) { +// return React.cloneElement(child, { +// _activeIndex: this.state.activeIndex +// }); +// } else if (child.type === TabList) { +// return React.cloneElement(child, { +// _activeIndex: this.state.activeIndex, +// _onTabSelect: index => this.setState({ activeIndex: index }) +// }); +// } else { +// return child; +// } +// } +// ); + +// return
    {children}
    ; +// } +// } + +// function App() { +// return ( +//
    +// +// +// Tacos +// Burritos +// Coconut Korma +// +// +// +//

    Tacos are delicious

    +//
    +// +//

    Sometimes a burrito is what you really need

    +//
    +// +//

    Might be your best option

    +//
    +//
    +//
    +//
    +// ); +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Now this is really flexible +// +// - can change order of panels v. tabs +// - can pass in our own styles to tabs +// - can even have unrelated elements inside +// - in other words, we now have control over rendering while +// Tabs handles the interaction +// +// Oh but you really loved the old tabs yeah? + +// class DataTabs extends React.Component { +// static defaultProps = { +// disabled: [] +// }; + +// render() { +// return ( +// +// +// {this.props.data.map((item, index) => ( +// +// {item.label} +// +// ))} +// +// +// {this.props.data.map(item => ( +// {item.description} +// ))} +// +// +// ); +// } +// } + +// function App() { +// const tabData = [ +// { +// label: "Tacos", +// description:

    Tacos are delicious

    +// }, +// { +// label: "Burritos", +// description:

    Sometimes a burrito is what you really need

    +// }, +// { +// label: "Coconut Korma", +// description:

    Might be your best option

    +// } +// ]; + +// return ( +//
    +// +//
    +// ); +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Instead of creating a handful of options, compose several components together +// and then compose them together into their own components. +// +// A really awesome library that does this is react-soundplayer diff --git a/subjects/08-Compound-Components/solution-extra.js b/subjects/08-Compound-Components/solution-extra.js new file mode 100644 index 000000000..13476a7d3 --- /dev/null +++ b/subjects/08-Compound-Components/solution-extra.js @@ -0,0 +1,173 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Implement a radio group form control with the API found in . +// +// - Clicking a should update the value of +// - The selected should pass the correct value to its +// - The `defaultValue` should be set on first render +// +// Got extra time? +// +// - Implement an `onChange` prop that communicates the 's state +// back to the parent so it can use it to render +// - Implement keyboard controls on the +// - Hint: Use tabIndex="0" on the s so the keyboard will work +// - Enter and space bar should select the option +// - Arrow right, arrow down should select the next option +// - Arrow left, arrow up should select the previous option +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +class RadioGroup extends React.Component { + static propTypes = { + defaultValue: PropTypes.string, + onChange: PropTypes.func + }; + + state = { value: this.props.defaultValue }; + + updateFocus(index) { + // Temporarily set this instance variable so we know to update + // the focus on the next render. It's safe to do this here since + // we know that we already have the focus since we just received + // a keyboard event. + this._focusIndex = index; + + this.forceUpdate(() => { + // Immediately unset the variable after the next render so we + // don't inadvertently steal the focus from other elements when + // we update but we don't have the focus. + delete this._focusIndex; + }); + } + + render() { + const length = React.Children.toArray(this.props.children).length; + + return ( +
    + {React.Children.map(this.props.children, (child, index) => + React.cloneElement(child, { + _isSelected: this.state.value === child.props.value, + _makeFocused: this._focusIndex === index, + _onSelect: () => { + this.setState({ value: child.props.value }, () => { + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + }); + }, + _onFocusPrev: () => { + this.updateFocus((index === 0 ? length : index) - 1); + }, + _onFocusNext: () => { + this.updateFocus(index === length - 1 ? 0 : index + 1); + } + }) + )} +
    + ); + } +} + +class RadioOption extends React.Component { + static propTypes = { + value: PropTypes.string, + _isSelected: PropTypes.bool, + _onSelect: PropTypes.func, + _onFocusPrev: PropTypes.func, + _onFocusNext: PropTypes.func + }; + + handleKeyDown = event => { + if (event.key === "Enter" || event.key === " ") { + this.props._onSelect(); + } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") { + this.props._onFocusPrev(); + } else if ( + event.key === "ArrowDown" || + event.key === "ArrowRight" + ) { + this.props._onFocusNext(); + } + }; + + componentDidUpdate() { + if (this.props._makeFocused) this.node.focus(); + } + + render() { + return ( +
    (this.node = node)} + > + {" "} + {this.props.children} +
    + ); + } +} + +class RadioIcon extends React.Component { + static propTypes = { + isSelected: PropTypes.bool.isRequired + }; + + render() { + return ( +
    + ); + } +} + +class App extends React.Component { + state = { radioValue: "fm", inputValue: "" }; + + render() { + return ( +
    +

    ♬ It's about time that we all turned off the radio ♫

    + +

    The {this.state.radioValue} is playing.

    + + this.setState({ radioValue: value })} + > + AM + FM + Tape + Aux + + +

    The input value is: {this.state.inputValue}

    + + + this.setState({ inputValue: event.target.value }) + } + /> +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/CompoundComponents/solution.js b/subjects/08-Compound-Components/solution.js similarity index 52% rename from subjects/CompoundComponents/solution.js rename to subjects/08-Compound-Components/solution.js index 3f59c311e..28bb264bb 100644 --- a/subjects/CompoundComponents/solution.js +++ b/subjects/08-Compound-Components/solution.js @@ -7,99 +7,87 @@ // - The selected should pass the correct value to its // - The `defaultValue` should be set on first render // -// Hints to get started: -// -// - will need some state -// - It then needs to pass that state to the s so they know -// whether or not they are active -// // Got extra time? // -// Implement a `value` prop and allow this to work like a "controlled input" -// (https://facebook.github.io/react/docs/forms.html#controlled-components) -// -// - Add a button to that sets `this.state.radioValue` to a pre-determined -// value, like "tape" -// - Make the update accordingly -// -// Implement keyboard controls on the (you'll need tabIndex="0" on -// the s so the keyboard will work) -// -// - Enter and space bar should select the option -// - Arrow right, arrow down should select the next option -// - Arrow left, arrow up should select the previous option +// - Implement an `onChange` prop that communicates the 's state +// back to the so it can use it to render something +// - Implement keyboard controls on the +// - Hint: Use tabIndex="0" on the s so the keyboard will work +// - Enter and space bar should select the option +// - Arrow right, arrow down should select the next option +// - Arrow left, arrow up should select the previous option //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' - -class RadioIcon extends React.Component { - static propTypes = { - isSelected: PropTypes.bool.isRequired - } - - render() { - return ( -
    - ) - } -} +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; class RadioGroup extends React.Component { static propTypes = { defaultValue: PropTypes.string - } + }; - state = { - value: this.props.defaultValue - } + state = { value: this.props.defaultValue }; select(value) { this.setState({ value }, () => { - this.props.onChange(this.state.value) - }) + this.props.onChange(this.state.value); + }); } render() { - const children = React.Children.map(this.props.children, (child) => ( + const children = React.Children.map(this.props.children, child => React.cloneElement(child, { isSelected: child.props.value === this.state.value, onClick: () => this.select(child.props.value) }) - )) + ); - return
    {children}
    + return
    {children}
    ; } } class RadioOption extends React.Component { static propTypes = { value: PropTypes.string - } + }; render() { return (
    - {this.props.children} + {" "} + {this.props.children}
    - ) + ); + } +} + +class RadioIcon extends React.Component { + static propTypes = { + isSelected: PropTypes.bool.isRequired + }; + + render() { + return ( +
    + ); } } class App extends React.Component { state = { - radioValue: 'fm' - } + radioValue: "fm" + }; render() { return ( @@ -110,7 +98,7 @@ class App extends React.Component { this.setState({ radioValue })} + onChange={radioValue => this.setState({ radioValue })} > AM FM @@ -118,8 +106,8 @@ class App extends React.Component { Aux
    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/08-Compound-Components/styles.js b/subjects/08-Compound-Components/styles.js new file mode 100644 index 000000000..df662c5d1 --- /dev/null +++ b/subjects/08-Compound-Components/styles.js @@ -0,0 +1,25 @@ +export const tabList = {}; + +export const tab = { + display: "inline-block", + padding: 10, + margin: 10, + borderBottom: "4px solid", + borderBottomColor: "#ccc", + cursor: "pointer" +}; + +export const activeTab = { + ...tab, + borderBottomColor: "#000" +}; + +export const disabledTab = { + ...tab, + opacity: 0.25, + cursor: "default" +}; + +export const tabPanels = { + padding: 10 +}; diff --git a/subjects/Context/exercise.js b/subjects/09-Context/exercise.js similarity index 67% rename from subjects/Context/exercise.js rename to subjects/09-Context/exercise.js index 3f8f5e674..12ef1883b 100644 --- a/subjects/Context/exercise.js +++ b/subjects/09-Context/exercise.js @@ -1,34 +1,33 @@ -/*eslint-disable no-alert */ //////////////////////////////////////////////////////////////////////////////// // Exercise: // // Using context, implement the
    , , and // components such that: // -// - Clicking the "submits" the form +// - Clicking the calls the form's `onSubmit` handler // - Hitting "Enter" while in a submits the form // - Don't use a element, we're intentionally recreating the // browser's built-in behavior // // Got extra time? // -// - Send the values of all the s to the handler +// - Send the values of all the s to the form's `onSubmit` handler // without using DOM traversal APIs // - Implement a that resets the s in the form -// //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import ReactDOM from 'react-dom' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; class Form extends React.Component { render() { - return
    {this.props.children}
    + return
    {this.props.children}
    ; } } class SubmitButton extends React.Component { render() { - return + return ; } } @@ -40,32 +39,34 @@ class TextInput extends React.Component { name={this.props.name} placeholder={this.props.placeholder} /> - ) + ); } } class App extends React.Component { handleSubmit = () => { - alert('YOU WIN!') - } + alert("YOU WIN!"); + }; render() { return (
    -

    This isn't even my final <Form/>!

    +

    + This isn't even my final <Form/>! +

    - {' '} - + {" "} +

    Submit

    - ) + ); } } -ReactDOM.render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/09-Context/lecture.js b/subjects/09-Context/lecture.js new file mode 100644 index 000000000..4ec0f437d --- /dev/null +++ b/subjects/09-Context/lecture.js @@ -0,0 +1,328 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import * as styles from "./styles"; + +function TabList({ children, _activeIndex, _onTabSelect }) { + return ( +
    + {React.Children.map(children, (child, index) => { + return React.cloneElement(child, { + _isActive: index === _activeIndex, + _onSelect: () => _onTabSelect(index) + }); + })} +
    + ); +} + +function Tab({ children, disabled, _isActive, _onSelect }) { + return ( +
    + {children} +
    + ); +} + +function TabPanels({ children, _activeIndex }) { + return ( +
    + {React.Children.toArray(children)[_activeIndex]} +
    + ); +} + +function TabPanel({ children }) { + return
    {children}
    ; +} + +class Tabs extends React.Component { + state = { + activeIndex: 0 + }; + + render() { + const children = React.Children.map( + this.props.children, + (child, index) => { + if (child.type === TabPanels) { + return React.cloneElement(child, { + _activeIndex: this.state.activeIndex + }); + } else if (child.type === TabList) { + return React.cloneElement(child, { + _activeIndex: this.state.activeIndex, + _onTabSelect: index => this.setState({ activeIndex: index }) + }); + } else { + return child; + } + } + ); + + return
    {children}
    ; + } +} + +function App() { + return ( +
    + + + Tacos + Burritos + Coconut Korma + + + +

    Tacos are delicious

    +
    + +

    Sometimes a burrito is what you really need

    +
    + +

    Might be your best option

    +
    +
    +
    +
    + ); +} + +ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Sometimes you don't want to specify how deep in the view tree the child +// components need to be, our current implementation expects TabList/TabPanels +// to be immediate children of Tabs, also Tab and TabPanel are required to be +// immediate children of their parent components. We really only care about the +// interactivity between the components, not their hierarchy. +// +// We could recursively check children with each render, which seems like a bad +// plan, so instead we can use a feature called "context". +// +// Wrapping in a div breaks everything! Instead of using +// cloneElement, let's use context to pass the activeIndex down. + +// const TabsContext = React.createContext(); + +// function TabList({ children, _activeIndex, _onTabSelect }) { +// return ( +//
    +// {React.Children.map(children, (child, index) => { +// return React.cloneElement(child, { +// _isActive: index === _activeIndex, +// _onSelect: () => _onTabSelect(index) +// }); +// })} +//
    +// ); +// } + +// function Tab({ children, disabled, _isActive, _onSelect }) { +// return ( +//
    +// {children} +//
    +// ); +// } + +// function TabPanels({ children }) { +// return ( +// +// {({ activeIndex }) => ( +//
    +// {React.Children.toArray(children)[activeIndex]} +//
    +// )} +//
    +// ); +// } + +// function TabPanel({ children }) { +// return
    {children}
    ; +// } + +// class Tabs extends React.Component { +// state = { +// activeIndex: 0 +// }; + +// render() { +// const children = React.Children.map( +// this.props.children, +// (child, index) => { +// if (child.type === TabPanels) { +// return React.cloneElement(child, { +// _activeIndex: this.state.activeIndex +// }); +// } else if (child.type === TabList) { +// return React.cloneElement(child, { +// _activeIndex: this.state.activeIndex, +// _onTabSelect: index => this.setState({ activeIndex: index }) +// }); +// } else { +// return child; +// } +// } +// ); + +// return ( +// +//
    {children}
    +//
    +// ); +// } +// } + +// function App() { +// return ( +//
    +// +// +// Tacos +// Burritos +// Coconut Korma +// +//
    +// +// +//

    Tacos are delicious

    +//
    +// +//

    Sometimes a burrito is what you really need

    +//
    +// +//

    Might be your best option

    +//
    +//
    +//
    +//
    +//
    +// ); +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Wrapping also breaks (no more active styles), lets check context +// for the activeIndex and the click handler instead of props. + +// const TabsContext = React.createContext(); + +// function TabList({ children }) { +// return ( +// +// {({ activeIndex, onTabSelect }) => ( +//
    +// {React.Children.map(children, (child, index) => { +// return React.cloneElement(child, { +// _isActive: index === activeIndex, +// _onSelect: () => onTabSelect(index) +// }); +// })} +//
    +// )} +//
    +// ); +// } + +// function Tab({ children, disabled, _isActive, _onSelect }) { +// return ( +//
    +// {children} +//
    +// ); +// } + +// function TabPanels({ children }) { +// return ( +// +// {({ activeIndex }) => ( +//
    +// {React.Children.toArray(children)[activeIndex]} +//
    +// )} +//
    +// ); +// } + +// function TabPanel({ children }) { +// return
    {children}
    ; +// } + +// class Tabs extends React.Component { +// state = { +// activeIndex: 0 +// }; + +// render() { +// return ( +// this.setState({ activeIndex: index }) +// }} +// > +//
    {this.props.children}
    +//
    +// ); +// } +// } + +// function App() { +// return ( +//
    +// +//
    +// +// Tacos +// Burritos +// Coconut Korma +// +//
    +//
    +// +// +//

    Tacos are delicious

    +//
    +// +//

    Sometimes a burrito is what you really need

    +//
    +// +//

    Might be your best option

    +//
    +//
    +//
    +//
    +//
    +// ); +// } + +// ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/09-Context/solution-extra.js b/subjects/09-Context/solution-extra.js new file mode 100644 index 000000000..19b38e38d --- /dev/null +++ b/subjects/09-Context/solution-extra.js @@ -0,0 +1,136 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Using context, implement the
    , , and +// components such that: +// +// - Clicking the calls the form's `onSubmit` handler +// - Hitting "Enter" while in a submits the form +// - Don't use a element, we're intentionally recreating the +// browser's built-in behavior +// +// Got extra time? +// +// - Send the values of all the s to the form's `onSubmit` handler +// without using DOM traversal APIs +// - Implement a that resets the s in the form +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +const FormContext = React.createContext(); + +class Form extends React.Component { + state = { values: {} }; + + handleChange = (name, value) => { + this.setState(state => ({ + values: { + ...state.values, + [name]: value + } + })); + }; + + handleReset = () => { + this.setState({ values: {} }); + }; + + handleSubmit = () => { + if (this.props.onSubmit) this.props.onSubmit(this.state.values); + }; + + render() { + return ( + +
    {this.props.children}
    +
    + ); + } +} + +class ResetButton extends React.Component { + render() { + return ( + + {context => ( + + )} + + ); + } +} + +class SubmitButton extends React.Component { + render() { + return ( + + {context => ( + + )} + + ); + } +} + +class TextInput extends React.Component { + render() { + return ( + + {context => ( + { + context.change(this.props.name, event.target.value); + }} + onKeyDown={event => { + if (event.key === "Enter") context.submit(); + }} + /> + )} + + ); + } +} + +class App extends React.Component { + handleSubmit = values => { + console.log(values); + }; + + render() { + return ( +
    +

    + This isn't even my final <Form/>! +

    + + +

    + {" "} + +

    +

    + Reset{" "} + Submit +

    + +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/09-Context/solution.js b/subjects/09-Context/solution.js new file mode 100644 index 000000000..d97c018b6 --- /dev/null +++ b/subjects/09-Context/solution.js @@ -0,0 +1,101 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Using context, implement the
    , , and +// components such that: +// +// - Clicking the calls the form's `onSubmit` handler +// - Hitting "Enter" while in a submits the form +// - Don't use a element, we're intentionally recreating the +// browser's built-in behavior +// +// Got extra time? +// +// - Send the values of all the s to the form's `onSubmit` handler +// without using DOM traversal APIs +// - Implement a that resets the s in the form +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +const FormContext = React.createContext(); + +class Form extends React.Component { + handleSubmit = () => { + if (this.props.onSubmit) this.props.onSubmit(); + }; + + render() { + return ( + +
    {this.props.children}
    +
    + ); + } +} + +class SubmitButton extends React.Component { + render() { + return ( + + {context => ( + + )} + + ); + } +} + +class TextInput extends React.Component { + render() { + return ( + + {context => ( + { + if (event.key === "Enter") context.submit(); + }} + /> + )} + + ); + } +} + +class App extends React.Component { + handleSubmit = () => { + alert("YOU WIN!"); + }; + + render() { + return ( +
    +

    + This isn’t even my final <Form/>! +

    + + +

    + {" "} + +

    +

    + Submit +

    + +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/09-Context/styles.js b/subjects/09-Context/styles.js new file mode 100644 index 000000000..df662c5d1 --- /dev/null +++ b/subjects/09-Context/styles.js @@ -0,0 +1,25 @@ +export const tabList = {}; + +export const tab = { + display: "inline-block", + padding: 10, + margin: 10, + borderBottom: "4px solid", + borderBottomColor: "#ccc", + cursor: "pointer" +}; + +export const activeTab = { + ...tab, + borderBottomColor: "#000" +}; + +export const disabledTab = { + ...tab, + opacity: 0.25, + cursor: "default" +}; + +export const tabPanels = { + padding: 10 +}; diff --git a/subjects/10-Routing/Gravatar.js b/subjects/10-Routing/Gravatar.js new file mode 100644 index 000000000..723ac1790 --- /dev/null +++ b/subjects/10-Routing/Gravatar.js @@ -0,0 +1,32 @@ +import React from "react"; +import PropTypes from "prop-types"; +import md5 from "md5"; + +const GravatarURL = "http://gravatar.com/avatar"; + +class Gravatar extends React.Component { + static propTypes = { + email: PropTypes.string.isRequired, + size: PropTypes.number.isRequired + }; + + static defaultProps = { + size: 80 + }; + + render() { + return ( + and in a in 's render method +// +// Got extra time? +// +// - Render a when the URL is something other than / or a user profile +// URL. Manually type in a bad URL to get the component to show up. +// - Add a to the profile page that links back to Home so users don't +// have to use the Back button to get back to the home page +// - Add a from "/users/:userID" to "/profile/:userID", then type in +// the url "users/1" into the url and hit enter +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import { + HashRouter as Router, + Switch, + Route, + Link, + Redirect +} from "react-router-dom"; +import Gravatar from "./Gravatar"; + +const USERS = [ + { + id: 1, + name: "Michael Jackson", + email: "michael@reacttraining.com" + }, + { id: 2, name: "React Training", email: "hello@reacttraining.com" } +]; + +function getUserByID(id) { + for (let i = 0; i < USERS.length; ++i) { + if (USERS[i].id === parseInt(id, 10)) return USERS[i]; + } + + return null; +} + +function Home() { + const contactItems = USERS.map(user => ( +
  • {user.name}
  • + )); + + return ( +
    +

    Home

    +
      {contactItems}
    +
    + ); +} + +function Profile() { + const userId = 1; // TODO: Get this from the URL! + const user = getUserByID(userId); + + if (user == null) return

    Cannot find user with id {userId}

    ; + + return ( +
    + {user.name} +
    + ); +} + +function NoMatch() { + return ( +
    +

    No routes matched...

    +

    + Go home +

    +
    + ); +} + +function App() { + return ( +
    +

    People Viewer

    + +
    + ); +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/10-Routing/lecture.js b/subjects/10-Routing/lecture.js new file mode 100644 index 000000000..2ea9fbf81 --- /dev/null +++ b/subjects/10-Routing/lecture.js @@ -0,0 +1,135 @@ +import React from "react"; +import ReactDOM from "react-dom"; + +function About() { + return

    About

    ; +} + +function Inbox() { + return

    Inbox

    ; +} + +function Home() { + return

    Home

    ; +} + +class App extends React.Component { + render() { + return ( +
    +

    Welcome to the app!

    +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Setup a hashchange listener so we know when the URL changes. When it does, +// update state and pick which child component we're going to render. + +// function About() { +// return

    About

    ; +// } + +// function Inbox() { +// return

    Inbox

    ; +// } + +// function Home() { +// return

    Home

    ; +// } + +// class App extends React.Component { +// state = { +// path: window.location.hash.substring(1) +// }; + +// componentDidMount() { +// window.addEventListener("hashchange", () => { +// this.setState({ path: window.location.hash.substring(1) }); +// }); +// } + +// render() { +// const { path } = this.state; + +// let Child; +// switch (path) { +// case "/about": +// Child = About; +// break; +// case "/inbox": +// Child = Inbox; +// break; +// default: +// Child = Home; +// } + +// return ( +//
    +//

    Welcome to the app!

    +// +// +//
    +// ); +// } +// } + +// ReactDOM.render(, document.getElementById("app")); + +//////////////////////////////////////////////////////////////////////////////// +// Now, with React Router + +// import { HashRouter as Router, Route, Link } from "react-router-dom"; + +// function About() { +// return

    About

    ; +// } + +// function Inbox() { +// return

    Inbox

    ; +// } + +// function Home() { +// return

    Home

    ; +// } + +// class App extends React.Component { +// render() { +// return ( +// +//
    +//

    Welcome to the app!

    +// +// +// +// +// +// +//
    +//
    +// ); +// } +// } + +// ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/10-Routing/solution.js b/subjects/10-Routing/solution.js new file mode 100644 index 000000000..585271970 --- /dev/null +++ b/subjects/10-Routing/solution.js @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Wrap the component in 's render method in a . This +// provides the right context for the s and s we will create +// - Create a to /user/:userId for each person in +// - When a person's is clicked, render instead of . +// Hints: You can get the person's userId from 's "match" prop +// which it gets from the router. Also, you'll probably want to wrap +// and in a in 's render method +// +// Got extra time? +// +// - Render a when the URL is something other than / or a user profile +// URL. Manually type in a bad URL to get the component to show up. +// - Add a to the profile page that links back to Home so users don't +// have to use the Back button to get back to the home page +// - Add a from "/users/:userID" to "/profile/:userID", then type in +// the url "users/1" into the url and hit enter +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; +import { + HashRouter as Router, + Switch, + Route, + Link, + Redirect +} from "react-router-dom"; +import Gravatar from "./Gravatar"; + +const USERS = [ + { + id: 1, + name: "Michael Jackson", + email: "michael@reacttraining.com" + }, + { id: 2, name: "React Training", email: "hello@reacttraining.com" } +]; + +function getUserByID(id) { + for (let i = 0; i < USERS.length; ++i) { + if (USERS[i].id === parseInt(id, 10)) return USERS[i]; + } + + return null; +} + +function Home() { + const contactItems = USERS.map(user => ( +
  • + {user.name} +
  • + )); + + return ( +
    +

    Home

    +
      {contactItems}
    +
    + ); +} + +function Profile({ match }) { + const { userId } = match.params; + const user = getUserByID(userId); + + if (user == null) return

    Cannot find user with id {userId}

    ; + + return ( +
    + {user.name} +
    + ); +} + +function NoMatch() { + return ( +
    +

    No routes matched...

    +

    + Go home +

    +
    + ); +} + +function App() { + return ( +
    +

    People Viewer

    + + + + + + ( + + )} + /> + + + +
    + ); +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/10-Routing/tests.js b/subjects/10-Routing/tests.js new file mode 100644 index 000000000..4604677e8 --- /dev/null +++ b/subjects/10-Routing/tests.js @@ -0,0 +1,36 @@ +import ReactDOM from "react-dom"; +import { Simulate } from "react-dom/test-utils"; +import assert from "../assert"; + +function toArray(nodeList) { + return Array.prototype.slice.call(nodeList, 0); +} + +export function run(component) { + window.location.hash = ""; + + const node = ReactDOM.findDOMNode(component); + const peopleList = node.querySelector(".people-list"); + const profileLinks = toArray(node.querySelectorAll(".people-list a")); + const profileHrefFormat = /\/profile\/\d+$/; + + console.log("on first render"); + + assert(peopleList, "render the list of people"); + assert( + profileLinks.length && + profileLinks.every( + link => + link && profileHrefFormat.test(link.getAttribute("href")) + ), + "render links to the profile page" + ); + + console.log("after clicking on a profile link..."); + + if (profileLinks.length) { + Simulate.click(profileLinks[1], { button: 0 }); + } + + assert(node.querySelector(".profile"), "show the profile page"); +} diff --git a/subjects/11-Motion/components/Draggable.js b/subjects/11-Motion/components/Draggable.js new file mode 100644 index 000000000..1acba0724 --- /dev/null +++ b/subjects/11-Motion/components/Draggable.js @@ -0,0 +1,61 @@ +import React from "react"; +import PropTypes from "prop-types"; + +class Draggable extends React.Component { + static propTypes = { + component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) + .isRequired, + onDragStart: PropTypes.func, + onDrag: PropTypes.func, + onDrop: PropTypes.func + }; + + static defaultProps = { + component: "div" + }; + + componentDidMount() { + this.isDragging = false; + document.addEventListener("mouseup", this.handleMouseUp); + document.addEventListener("mousemove", this.handleMouseMove); + } + + componentWillUnmount() { + document.removeEventListener("mousemove", this.handleMouseMove); + document.removeEventListener("mouseup", this.handleMouseUp); + } + + handleMouseDown = event => { + if (!this.isDragging) { + this.isDragging = true; + + // Prevent Chrome from displaying a text cursor + event.preventDefault(); + + if (this.props.onDragStart) this.props.onDragStart(event); + } + }; + + handleMouseMove = event => { + if (this.isDragging && this.props.onDrag) this.props.onDrag(event); + }; + + handleMouseUp = event => { + if (this.isDragging) { + this.isDragging = false; + + if (this.props.onDrop) this.props.onDrop(event); + } + }; + + render() { + const { component, ...otherProps } = this.props; + + return React.createElement(component, { + ...otherProps, + onMouseDown: this.handleMouseDown + }); + } +} + +export default Draggable; diff --git a/subjects/11-Motion/components/Tone.js b/subjects/11-Motion/components/Tone.js new file mode 100644 index 000000000..e7c34f104 --- /dev/null +++ b/subjects/11-Motion/components/Tone.js @@ -0,0 +1,37 @@ +import React from "react"; +import PropTypes from "prop-types"; +import createOscillator from "../utils/createOscillator"; + +class Tone extends React.Component { + static propTypes = { + isPlaying: PropTypes.bool.isRequired, + pitch: PropTypes.number.isRequired, + volume: PropTypes.number.isRequired + }; + + componentDidMount() { + this.oscillator = createOscillator(); + this.doImperativeWork(); + } + + componentDidUpdate() { + this.doImperativeWork(); + } + + doImperativeWork() { + if (this.props.isPlaying) { + this.oscillator.play(); + } else { + this.oscillator.stop(); + } + + this.oscillator.setPitchBend(this.props.pitch); + this.oscillator.setVolume(this.props.volume); + } + + render() { + return null; + } +} + +export default Tone; diff --git a/subjects/11-Motion/exercise.js b/subjects/11-Motion/exercise.js new file mode 100644 index 000000000..27e5c4768 --- /dev/null +++ b/subjects/11-Motion/exercise.js @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Use a to animate the transition of the red "marker" to its +// destination when it is dropped +// +// Got extra time? +// +// - If you didn't already, use a custom spring to give the animation +// an elastic, bouncy feel +// - Add a "drop hint" element that indicates which element will receive +// the marker when it is dropped to improve usability +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import { Motion, spring } from "react-motion"; +import Draggable from "./components/Draggable"; + +class DropGrid extends React.Component { + state = { + isDraggingMarker: false, + startX: 0, + startY: 0, + mouseX: 0, + mouseY: 0 + }; + + getRelativeXY({ clientX, clientY }) { + const { offsetLeft, offsetTop } = this.node; + + return { + x: clientX - offsetLeft, + y: clientY - offsetTop + }; + } + + handleDragStart = event => { + const { x, y } = this.getRelativeXY(event); + const { offsetLeft, offsetTop } = event.target; + + // Prevent Chrome from displaying a text cursor + event.preventDefault(); + + this.setState({ + isDraggingMarker: true, + startX: x - offsetLeft, + startY: y - offsetTop, + mouseX: x, + mouseY: y + }); + }; + + handleDrag = event => { + const { x, y } = this.getRelativeXY(event); + + this.setState({ + mouseX: x, + mouseY: y + }); + }; + + handleDrop = () => { + this.setState({ isDraggingMarker: false }); + }; + + render() { + const { + isDraggingMarker, + startX, + startY, + mouseX, + mouseY + } = this.state; + + let markerLeft, markerTop; + if (isDraggingMarker) { + markerLeft = mouseX - startX; + markerTop = mouseY - startY; + } else { + markerLeft = + Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150; + markerTop = + Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150; + } + + const markerStyle = { + left: markerLeft, + top: markerTop + }; + + return ( +
    (this.node = node)}> + +
    1
    +
    2
    +
    3
    +
    4
    +
    5
    +
    6
    +
    7
    +
    8
    +
    9
    +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Animation/lecture.js b/subjects/11-Motion/lecture.js similarity index 88% rename from subjects/Animation/lecture.js rename to subjects/11-Motion/lecture.js index 4dc182e55..413765a41 100644 --- a/subjects/Animation/lecture.js +++ b/subjects/11-Motion/lecture.js @@ -1,34 +1,31 @@ -import React from 'react' -import ReactDOM, { findDOMNode } from 'react-dom' -import { Motion, StaggeredMotion, spring, presets } from 'react-motion' -import $ from 'jquery' +import "./styles.css"; -import './styles' +import React from "react"; +import ReactDOM from "react-dom"; +import { Motion, StaggeredMotion, spring, presets } from "react-motion"; +import $ from "jquery"; class ToggleSwitch extends React.Component { state = { isActive: false - } + }; toggle = () => { - this.setState({ isActive: !this.state.isActive }) - } + this.setState({ isActive: !this.state.isActive }); + }; render() { - const x = this.state.isActive ? 400 : 0 + const x = this.state.isActive ? 400 : 0; return (
    -
    +
    - ) + ); } } -ReactDOM.render( - , - document.getElementById('app') -) +ReactDOM.render(, document.getElementById("app")); /////////////////////////////////////////////////////////////////////////////// // We can integrate with other DOM animation libraries by doing imperative work diff --git a/subjects/11-Motion/solution.js b/subjects/11-Motion/solution.js new file mode 100644 index 000000000..997c9d486 --- /dev/null +++ b/subjects/11-Motion/solution.js @@ -0,0 +1,124 @@ +/////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Use a to animate the transition of the red "marker" to its +// destination when it is dropped +// +// Got extra time? +// +// - If you didn't already, use a custom spring to give the animation +// an elastic, bouncy feel +// - Add a "drop hint" element that indicates which element will receive +// the marker when it is dropped to improve usability +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import { Motion, spring } from "react-motion"; +import Draggable from "./components/Draggable"; + +class DropGrid extends React.Component { + state = { + isDraggingMarker: false, + startX: 0, + startY: 0, + mouseX: 0, + mouseY: 0 + }; + + getRelativeXY({ clientX, clientY }) { + const { offsetLeft, offsetTop } = this.node; + + return { + x: clientX - offsetLeft, + y: clientY - offsetTop + }; + } + + handleDragStart = event => { + const { x, y } = this.getRelativeXY(event); + const { offsetLeft, offsetTop } = event.target; + + // Prevent Chrome from displaying a text cursor + event.preventDefault(); + + this.setState({ + isDraggingMarker: true, + startX: x - offsetLeft, + startY: y - offsetTop, + mouseX: x, + mouseY: y + }); + }; + + handleDrag = event => { + const { x, y } = this.getRelativeXY(event); + + this.setState({ + mouseX: x, + mouseY: y + }); + }; + + handleDrop = () => { + this.setState({ isDraggingMarker: false }); + }; + + render() { + const { + isDraggingMarker, + startX, + startY, + mouseX, + mouseY + } = this.state; + + let markerLeft, markerTop; + if (isDraggingMarker) { + markerLeft = mouseX - startX; + markerTop = mouseY - startY; + } else { + markerLeft = + Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150; + markerTop = + Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150; + } + + const bouncySpring = style => + spring(style, { stiffness: 170, damping: 8 }); + + const markerStyle = { + left: isDraggingMarker ? markerLeft : bouncySpring(markerLeft), + top: isDraggingMarker ? markerTop : bouncySpring(markerTop) + }; + + return ( +
    (this.node = node)}> + + {style => ( + + )} + +
    1
    +
    2
    +
    3
    +
    4
    +
    5
    +
    6
    +
    7
    +
    8
    +
    9
    +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Animation/styles.css b/subjects/11-Motion/styles.css similarity index 84% rename from subjects/Animation/styles.css rename to subjects/11-Motion/styles.css index 49b8b4ffc..19fe728b2 100644 --- a/subjects/Animation/styles.css +++ b/subjects/11-Motion/styles.css @@ -1,12 +1,9 @@ -html { - box-sizing: border-box; -} -*, *:before, *:after { - box-sizing: inherit; -} - body { - padding: 10px; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; } .toggle-switch { @@ -20,7 +17,6 @@ body { -ms-user-select: none; -webkit-user-select: none; } - .toggle-switch-knob { position: absolute; width: 50px; @@ -39,10 +35,6 @@ body { width: 350px; } - - - - .grid { position: absolute; margin: auto; @@ -55,7 +47,7 @@ body { width: 451px; height: 451px; } -.grid .cell { +.grid-cell { border: 1px solid #999; border-width: 0 0 1px 1px; width: 150px; @@ -67,11 +59,12 @@ body { -webkit-user-select: none; user-select: none; } -.grid .marker, .grid .drop-hint { + +.grid-marker, .grid .drop-hint { position: absolute; background: red; - width: 150; - height: 150; + width: 150px; + height: 150px; opacity: 0.6; } .grid .drop-hint { diff --git a/subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js b/subjects/11-Motion/utils/AudioContextMonkeyPatch.js similarity index 59% rename from subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js rename to subjects/11-Motion/utils/AudioContextMonkeyPatch.js index 524b20bc0..d03436608 100644 --- a/subjects/ImperativeToDeclarative/utils/AudioContextMonkeyPatch.js +++ b/subjects/11-Motion/utils/AudioContextMonkeyPatch.js @@ -13,16 +13,16 @@ limitations under the License. */ -/* +/* This monkeypatch library is intended to be included in projects that are -written to the proper AudioContext spec (instead of webkitAudioContext), -and that use the new naming and proper bits of the Web Audio API (e.g. +written to the proper AudioContext spec (instead of webkitAudioContext), +and that use the new naming and proper bits of the Web Audio API (e.g. using BufferSourceNode.start() instead of BufferSourceNode.noteOn()), but may have to run on systems that only support the deprecated bits. -This library should be harmless to include if the browser supports -unprefixed "AudioContext", and/or if it supports the new names. +This library should be harmless to include if the browser supports +unprefixed "AudioContext", and/or if it supports the new names. The patches this library handles: if window.AudioContext is unsupported, it will be aliased to webkitAudioContext(). @@ -39,66 +39,74 @@ OscillatorNode.start() is aliased to noteOn() OscillatorNode.stop() is aliased to noteOff() AudioParam.setTargetAtTime() is aliased to setTargetValueAtTime() -This library does NOT patch the enumerated type changes, as it is +This library does NOT patch the enumerated type changes, as it is recommended in the specification that implementations support both integer -and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel +and string types for AudioPannerNode.panningModel, AudioPannerNode.distanceModel BiquadFilterNode.type and OscillatorNode.type. */ -(function (global, exports, perf) { - 'use strict'; +(function(global, exports, perf) { + "use strict"; function fixSetTarget(param) { - if (!param) // if NYI, just return + if (!param) + // if NYI, just return return; if (!param.setTargetAtTime) - param.setTargetAtTime = param.setTargetValueAtTime; + param.setTargetAtTime = param.setTargetValueAtTime; } - if (window.hasOwnProperty('webkitAudioContext') && - !window.hasOwnProperty('AudioContext')) { + if ( + window.hasOwnProperty("webkitAudioContext") && + !window.hasOwnProperty("AudioContext") + ) { window.AudioContext = webkitAudioContext; - if (!AudioContext.prototype.hasOwnProperty('createGain')) - AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; - if (!AudioContext.prototype.hasOwnProperty('createDelay')) - AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; - if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor')) - AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; - - AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; - AudioContext.prototype.createGain = function() { + if (!AudioContext.prototype.hasOwnProperty("createGain")) + AudioContext.prototype.createGain = + AudioContext.prototype.createGainNode; + if (!AudioContext.prototype.hasOwnProperty("createDelay")) + AudioContext.prototype.createDelay = + AudioContext.prototype.createDelayNode; + if (!AudioContext.prototype.hasOwnProperty("createScriptProcessor")) + AudioContext.prototype.createScriptProcessor = + AudioContext.prototype.createJavaScriptNode; + + AudioContext.prototype.internal_createGain = + AudioContext.prototype.createGain; + AudioContext.prototype.createGain = function() { const node = this.internal_createGain(); fixSetTarget(node.gain); return node; }; - AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; - AudioContext.prototype.createDelay = function() { + AudioContext.prototype.internal_createDelay = + AudioContext.prototype.createDelay; + AudioContext.prototype.createDelay = function() { const node = this.internal_createDelay(); fixSetTarget(node.delayTime); return node; }; - AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; - AudioContext.prototype.createBufferSource = function() { + AudioContext.prototype.internal_createBufferSource = + AudioContext.prototype.createBufferSource; + AudioContext.prototype.createBufferSource = function() { const node = this.internal_createBufferSource(); if (!node.start) { - node.start = function ( when, offset, duration ) { - if ( offset || duration ) - this.noteGrainOn( when, offset, duration ); - else - this.noteOn( when ); - } + node.start = function(when, offset, duration) { + if (offset || duration) + this.noteGrainOn(when, offset, duration); + else this.noteOn(when); + }; } - if (!node.stop) - node.stop = node.noteoff; + if (!node.stop) node.stop = node.noteoff; fixSetTarget(node.playbackRate); return node; }; - AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; - AudioContext.prototype.createDynamicsCompressor = function() { + AudioContext.prototype.internal_createDynamicsCompressor = + AudioContext.prototype.createDynamicsCompressor; + AudioContext.prototype.createDynamicsCompressor = function() { const node = this.internal_createDynamicsCompressor(); fixSetTarget(node.threshold); fixSetTarget(node.knee); @@ -109,8 +117,9 @@ BiquadFilterNode.type and OscillatorNode.type. return node; }; - AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; - AudioContext.prototype.createBiquadFilter = function() { + AudioContext.prototype.internal_createBiquadFilter = + AudioContext.prototype.createBiquadFilter; + AudioContext.prototype.createBiquadFilter = function() { const node = this.internal_createBiquadFilter(); fixSetTarget(node.frequency); fixSetTarget(node.detune); @@ -119,18 +128,17 @@ BiquadFilterNode.type and OscillatorNode.type. return node; }; - if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) { - AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; - AudioContext.prototype.createOscillator = function() { + if (AudioContext.prototype.hasOwnProperty("createOscillator")) { + AudioContext.prototype.internal_createOscillator = + AudioContext.prototype.createOscillator; + AudioContext.prototype.createOscillator = function() { const node = this.internal_createOscillator(); - if (!node.start) - node.start = node.noteOn; - if (!node.stop) - node.stop = node.noteOff; + if (!node.start) node.start = node.noteOn; + if (!node.stop) node.stop = node.noteOff; fixSetTarget(node.frequency); fixSetTarget(node.detune); return node; }; } } -}(window)); +})(window); diff --git a/subjects/11-Motion/utils/createOscillator.js b/subjects/11-Motion/utils/createOscillator.js new file mode 100644 index 000000000..8d0c41424 --- /dev/null +++ b/subjects/11-Motion/utils/createOscillator.js @@ -0,0 +1,63 @@ +import "./AudioContextMonkeyPatch"; + +function Oscillator(audioContext) { + // TODO make more things not use this. + const oscillatorNode = audioContext.createOscillator(); + oscillatorNode.start(0); + + const gainNode = audioContext.createGain(); + this.pitchBase = 50; + this.pitchBend = 0; + this.pitchRange = 2000; + this.volume = 0.5; + this.maxVolume = 1; + this.frequency = this.pitchBase; + + let hasConnected = false; + let frequency = this.pitchBase; + + this.play = function() { + oscillatorNode.connect(gainNode); + hasConnected = true; + }; + + this.stop = function() { + if (hasConnected) { + oscillatorNode.disconnect(gainNode); + hasConnected = false; + } + }; + + this.setType = function(type) { + oscillatorNode.type = type; + }; + + this.setPitchBend = function(v) { + this.pitchBend = v; + frequency = this.pitchBase + this.pitchBend * this.pitchRange; + oscillatorNode.frequency.value = frequency; + this.frequency = frequency; + }; + + this.setVolume = function(v) { + this.volume = this.maxVolume * v; + gainNode.gain.value = this.volume; + }; + + this.connect = function(output) { + gainNode.connect(output); + }; + + return this; +} + +function createOscillator() { + const audioContext = new AudioContext(); + const theremin = new Oscillator(audioContext); + + theremin.connect(audioContext.destination); + + return theremin; +} + +export default createOscillator; diff --git a/subjects/TweenState/components/HeightFader.js b/subjects/12-Transitions/components/HeightFader.js similarity index 55% rename from subjects/TweenState/components/HeightFader.js rename to subjects/12-Transitions/components/HeightFader.js index 79b29fea7..411ae940a 100644 --- a/subjects/TweenState/components/HeightFader.js +++ b/subjects/12-Transitions/components/HeightFader.js @@ -1,63 +1,61 @@ -import React from 'react' -import { Mixin as TweenStateMixin } from 'react-tween-state' +import React from "react"; +import { Mixin as TweenStateMixin } from "react-tween-state"; function getHeight(node) { - return node.scrollHeight + return node.scrollHeight; } const HeightFader = React.createClass({ - - mixins: [ TweenStateMixin ], + mixins: [TweenStateMixin], getDefaultProps() { return { - component: 'li' - } + component: "li" + }; }, getInitialState() { return { opacity: 0, height: 0 - } + }; }, componentWillEnter(cb) { - this.tweenState('opacity', { + this.tweenState("opacity", { duration: 250, endValue: 1 - }) + }); - this.tweenState('height', { + this.tweenState("height", { duration: 250, endValue: getHeight(React.findDOMNode(this)), onEnd: cb - }) + }); }, componentWillLeave(cb) { - this.tweenState('opacity', { + this.tweenState("opacity", { duration: 250, endValue: 0 - }) + }); - this.tweenState('height', { + this.tweenState("height", { duration: 250, endValue: 0, onEnd: cb - }) + }); }, render() { return React.createElement(this.props.component, { ...this.props, style: { - opacity: this.getTweeningValue('opacity'), - height: this.getTweeningValue('height') + opacity: this.getTweeningValue("opacity"), + height: this.getTweeningValue("height") } - }) + }); } +}); -}) - -export default HeightFader +export default HeightFader; diff --git a/subjects/TweenState/exercise.js b/subjects/12-Transitions/exercise.js similarity index 71% rename from subjects/TweenState/exercise.js rename to subjects/12-Transitions/exercise.js index e54588671..7f6e36b5b 100644 --- a/subjects/TweenState/exercise.js +++ b/subjects/12-Transitions/exercise.js @@ -5,11 +5,15 @@ // - Experiment with different types of easing (hint: use easingTypes at // https://github.com/chenglou/tween-functions/blob/master/index.js) //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import { render } from 'react-dom' -import { easingTypes, Mixin as TweenStateMixin } from 'react-tween-state' +import "./styles.css"; -require('./styles') +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import { + easingTypes, + Mixin as TweenStateMixin +} from "react-tween-state"; const ToggleSwitch = React.createClass({ propTypes: { @@ -19,37 +23,37 @@ const ToggleSwitch = React.createClass({ getDefaultProps() { return { animationDuration: 350 - } + }; }, getInitialState() { return { knobLeft: 0 - } + }; }, toggle() { this.setState({ knobLeft: this.state.knobLeft === 0 ? 400 : 0 - }) + }); }, handleClick() { - this.toggle() + this.toggle(); }, render() { const knobStyle = { WebkitTransform: `translate3d(${this.state.knobLeft}px,0,0)`, transform: `translate3d(${this.state.knobLeft}px,0,0)` - } + }; return (
    -
    +
    - ) + ); } -}) +}); -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/12-Transitions/lecture.js b/subjects/12-Transitions/lecture.js new file mode 100644 index 000000000..37ed554c2 --- /dev/null +++ b/subjects/12-Transitions/lecture.js @@ -0,0 +1,66 @@ +import React from "react"; +import { render } from "react-dom"; +import TransitionGroup from "react-addons-transition-group"; +import HeightFader from "./components/HeightFader"; + +const List = React.createClass({ + getInitialState() { + return { + items: [] + }; + }, + + addItem(e) { + if (e.key === "Enter") { + if (this.guid == null) this.guid = 1; + + const newItem = { + id: this.guid++, + label: e.target.value + }; + + this.setState({ + items: [newItem].concat(this.state.items) + }); + + e.target.value = ""; + } + }, + + removeItem(item) { + this.setState({ + items: this.state.items.filter(i => i !== item) + }); + }, + + render() { + return ( +
    +

    {this.props.name}

    + +
      + {this.state.items.map(item => ( +
    • + {item.label}{" "} + +
    • + ))} +
    +
    + ); + } +}); + +const App = React.createClass({ + render() { + return ( +
    + +
    + ); + } +}); + +render(, document.getElementById("app")); diff --git a/subjects/TweenState/solution.js b/subjects/12-Transitions/solution.js similarity index 57% rename from subjects/TweenState/solution.js rename to subjects/12-Transitions/solution.js index bbe80e9ce..9c3798b81 100644 --- a/subjects/TweenState/solution.js +++ b/subjects/12-Transitions/solution.js @@ -9,12 +9,13 @@ // // - Use a to animate the transition //////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import { render } from 'react-dom' -import { Mixin as TweenStateMixin } from 'react-tween-state' -import { Motion, spring } from 'react-motion' +import "./styles.css"; -require('./styles') +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import { Mixin as TweenStateMixin } from "react-tween-state"; +import { Motion, spring } from "react-motion"; const TweenToggleSwitch = React.createClass({ propTypes: { @@ -22,41 +23,41 @@ const TweenToggleSwitch = React.createClass({ isActive: PropTypes.bool.isRequired }, - mixins: [ TweenStateMixin ], + mixins: [TweenStateMixin], getDefaultProps() { return { animationDuration: 350 - } + }; }, getInitialState() { return { knobLeft: 0 - } + }; }, componentWillReceiveProps(nextProps) { - this.tweenState('knobLeft', { + this.tweenState("knobLeft", { duration: this.props.animationDuration, - endValue: (nextProps.isActive ? 400 : 0) - }) + endValue: nextProps.isActive ? 400 : 0 + }); }, render() { - const knobLeft = this.getTweeningValue('knobLeft') + const knobLeft = this.getTweeningValue("knobLeft"); const knobStyle = { WebkitTransform: `translate3d(${knobLeft}px,0,0)`, transform: `translate3d(${knobLeft}px,0,0)` - } + }; return (
    -
    +
    - ) + ); } -}) +}); const SpringToggleSwitch = React.createClass({ propTypes: { @@ -64,49 +65,56 @@ const SpringToggleSwitch = React.createClass({ }, render() { - const x = this.props.isActive ? 400 : 0 + const x = this.props.isActive ? 400 : 0; return ( - {s => ( -
    -
    -
    - )} + {s => ( +
    +
    +
    + )} - ) + ); } -}) +}); const App = React.createClass({ getInitialState() { return { isActive: false - } + }; }, toggle() { this.setState({ isActive: !this.state.isActive - }) + }); }, handleClick() { - this.toggle() + this.toggle(); }, render() { return (
    - - + +
    - ) + ); } -}) +}); -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/TweenState/styles.css b/subjects/12-Transitions/styles.css similarity index 100% rename from subjects/TweenState/styles.css rename to subjects/12-Transitions/styles.css diff --git a/subjects/13-Render-Optimizations/RainbowListDelegate.js b/subjects/13-Render-Optimizations/RainbowListDelegate.js new file mode 100644 index 000000000..b53451a9d --- /dev/null +++ b/subjects/13-Render-Optimizations/RainbowListDelegate.js @@ -0,0 +1,22 @@ +import React from "react"; +import convertNumberToEnglish from "./utils/convertNumberToEnglish"; +import computeHSLRainbowColor from "./utils/computeHSLRainbowColor"; + +export const numColors = 100; + +export const rowHeight = 30; + +export function renderRowAtIndex(index) { + return ( +
    + {convertNumberToEnglish(index + 1)} +
    + ); +} diff --git a/subjects/13-Render-Optimizations/exercise.js b/subjects/13-Render-Optimizations/exercise.js new file mode 100644 index 000000000..6b107e1eb --- /dev/null +++ b/subjects/13-Render-Optimizations/exercise.js @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Modify so that it only renders the list items that are visible! +// +// Got extra time? +// +// - Render fewer rows as the size of the window changes (hint: Listen +// for the window's "resize" event) +// - Remember the scroll position when you refresh the page +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import * as RainbowListDelegate from "./RainbowListDelegate"; + +class ListView extends React.Component { + static propTypes = { + numRows: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + renderRowAtIndex: PropTypes.func.isRequired + }; + + render() { + const { numRows, rowHeight, renderRowAtIndex } = this.props; + const totalHeight = numRows * rowHeight; + + const items = []; + + let index = 0; + while (index < numRows) { + items.push(
  • {renderRowAtIndex(index)}
  • ); + index++; + } + + return ( +
    +
    +
      {items}
    +
    +
    + ); + } +} + +ReactDOM.render( + , + document.getElementById("app") +); diff --git a/subjects/13-Render-Optimizations/lecture.js b/subjects/13-Render-Optimizations/lecture.js new file mode 100644 index 000000000..a70c3a47e --- /dev/null +++ b/subjects/13-Render-Optimizations/lecture.js @@ -0,0 +1,101 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +class TodoItem extends React.Component { + state = { + done: false + }; + + render() { + return ( +
  • + +
  • + ); + } +} + +class TodoList extends React.Component { + static propTypes = { + initialLength: PropTypes.number.isRequired + }; + + state = { + items: Array.from(new Array(this.props.initialLength)).map( + (_, index) => ({ + id: index, + body: `item ${index + 1}` + }) + ) + }; + + handleSubmit = event => { + event.preventDefault(); + + const item = { + id: this.state.items.length, + body: event.target.elements[0].value + }; + + event.target.reset(); + + this.setState({ + items: [item].concat(this.state.items) + }); + }; + + render() { + return ( +
    +
    + +
    +
      + {this.state.items.map(item => ( + + ))} +
    +
    + ); + } +} + +ReactDOM.render( + , + document.getElementById("app") +); + +/////////////////////////////////////////////////////////////////////////////// +// Rendering large lists can be super slow. This is an old UI problem. + +/////////////////////////////////////////////////////////////////////////////// +// One possible solution is to only render the stuff that's actually in the +// view. Native mobile frameworks have been doing this for years: +// +// https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/index.html + +/////////////////////////////////////////////////////////////////////////////// +// I'd really like to do this in my web app! What does it look like when we +// try to do this with imperative JavaScript? +// +// https://github.com/airbnb/infinity +// https://github.com/emberjs/list-view diff --git a/subjects/13-Render-Optimizations/solution-extra.js b/subjects/13-Render-Optimizations/solution-extra.js new file mode 100644 index 000000000..67758d1e3 --- /dev/null +++ b/subjects/13-Render-Optimizations/solution-extra.js @@ -0,0 +1,112 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Modify so that it only renders the list items that are visible! +// +// Got extra time? +// +// - Render fewer rows as the size of the window changes (hint: Listen +// for the window's "resize" event) +// - Remember the scroll position when you refresh the page +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import * as RainbowListDelegate from "./RainbowListDelegate"; + +class ListView extends React.Component { + static propTypes = { + numRows: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + renderRowAtIndex: PropTypes.func.isRequired + }; + + state = { + availableHeight: 0, + scrollTop: 0 + }; + + componentWillMount() { + if (localStorage.scrollTop) { + this.setState({ + scrollTop: JSON.parse(localStorage.scrollTop) + }); + } + } + + componentDidMount() { + this.handleWindowResize(); + window.addEventListener("resize", this.handleWindowResize); + + window.addEventListener("beforeunload", () => { + localStorage.scrollTop = JSON.stringify(this.state.scrollTop); + }); + + if (this.node.scrollTop !== this.state.scrollTop) { + this.node.scrollTop = this.state.scrollTop; + } + } + + componentWillUnmount() { + window.removeEventListener("resize", this.handleWindowResize); + } + + handleWindowResize = () => { + this.setState({ + availableHeight: this.node.clientHeight + }); + }; + + handleScroll = event => { + this.setState({ + scrollTop: event.target.scrollTop + }); + }; + + render() { + const { availableHeight, scrollTop } = this.state; + const { numRows, rowHeight, renderRowAtIndex } = this.props; + const totalHeight = rowHeight * numRows; + + const startIndex = Math.floor(scrollTop / rowHeight); + const endIndex = + startIndex + Math.ceil(availableHeight / rowHeight) + 1; + + const items = []; + + let index = startIndex; + while (index < endIndex) { + items.push(
  • {renderRowAtIndex(index)}
  • ); + index++; + } + + return ( +
    (this.node = node)} + > +
    +
      {items}
    +
    +
    + ); + } +} + +ReactDOM.render( + , + document.getElementById("app") +); diff --git a/subjects/13-Render-Optimizations/solution.js b/subjects/13-Render-Optimizations/solution.js new file mode 100644 index 000000000..8f985a286 --- /dev/null +++ b/subjects/13-Render-Optimizations/solution.js @@ -0,0 +1,87 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Modify so that it only renders the list items that are visible! +// +// Got extra time? +// +// - Render fewer rows as the size of the window changes (hint: Listen +// for the window's "resize" event) +// - Remember the scroll position when you refresh the page +//////////////////////////////////////////////////////////////////////////////// +import "./styles.css"; + +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; + +import * as RainbowListDelegate from "./RainbowListDelegate"; + +class ListView extends React.Component { + static propTypes = { + numRows: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + renderRowAtIndex: PropTypes.func.isRequired + }; + + state = { + availableHeight: 0, + scrollTop: 0 + }; + + componentDidMount() { + this.setState({ + availableHeight: this.node.clientHeight + }); + } + + handleScroll = event => { + this.setState({ + scrollTop: event.target.scrollTop + }); + }; + + render() { + const { availableHeight, scrollTop } = this.state; + const { numRows, rowHeight, renderRowAtIndex } = this.props; + const totalHeight = rowHeight * numRows; + + const startIndex = Math.floor(scrollTop / rowHeight); + const endIndex = + startIndex + Math.ceil(availableHeight / rowHeight) + 1; + + const items = []; + + let index = startIndex; + while (index < endIndex) { + items.push(
  • {renderRowAtIndex(index)}
  • ); + index++; + } + + return ( +
    (this.node = node)} + > +
    +
      {items}
    +
    +
    + ); + } +} + +ReactDOM.render( + , + document.getElementById("app") +); diff --git a/subjects/RenderOptimizations/styles.css b/subjects/13-Render-Optimizations/styles.css similarity index 59% rename from subjects/RenderOptimizations/styles.css rename to subjects/13-Render-Optimizations/styles.css index 7dc3e4a2b..53c2bd935 100644 --- a/subjects/RenderOptimizations/styles.css +++ b/subjects/13-Render-Optimizations/styles.css @@ -1,10 +1,3 @@ -html { - box-sizing: border-box; -} -*, *:before, *:after { - box-sizing: inherit; -} - body { margin: 0; padding: 0; @@ -16,3 +9,7 @@ ol { margin: 0; padding: 0; } + +li { + white-space: nowrap; +} diff --git a/subjects/13-Render-Optimizations/utils/computeHSLRainbowColor.js b/subjects/13-Render-Optimizations/utils/computeHSLRainbowColor.js new file mode 100644 index 000000000..1861b7fa1 --- /dev/null +++ b/subjects/13-Render-Optimizations/utils/computeHSLRainbowColor.js @@ -0,0 +1,5 @@ +function computeHSLRainbowColor(n, period) { + return `hsl(${Math.round(n / period * 360)},100%,50%)`; +} + +export default computeHSLRainbowColor; diff --git a/subjects/13-Render-Optimizations/utils/convertNumberToEnglish.js b/subjects/13-Render-Optimizations/utils/convertNumberToEnglish.js new file mode 100644 index 000000000..d9f837b10 --- /dev/null +++ b/subjects/13-Render-Optimizations/utils/convertNumberToEnglish.js @@ -0,0 +1,74 @@ +const ones = [ + "", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine" +]; + +const tens = [ + "", + "", + "twenty", + "thirty", + "forty", + "fifty", + "sixty", + "seventy", + "eighty", + "ninety" +]; + +const teens = [ + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen" +]; + +function convertTens(n) { + return n < 10 + ? ones[n] + : n >= 10 && n < 20 + ? teens[n - 10] + : tens[Math.floor(n / 10)] + " " + ones[n % 10]; +} + +function convertHundreds(n) { + return n > 99 + ? ones[Math.floor(n / 100)] + " hundred " + convertTens(n % 100) + : convertTens(n); +} + +function convertThousands(n) { + return n >= 1000 + ? convertHundreds(Math.floor(n / 1000)) + + " thousand " + + convertHundreds(n % 1000) + : convertHundreds(n); +} + +function convertMillions(n) { + return n >= 1000000 + ? convertMillions(Math.floor(n / 1000000)) + + " million " + + convertThousands(n % 1000000) + : convertThousands(n); +} + +function convertNumberToEnglish(n) { + return n === 0 ? "zero" : convertMillions(n); +} + +export default convertNumberToEnglish; diff --git a/subjects/ServerRendering/exercise.js b/subjects/14-Server-Rendering/exercise.js similarity index 85% rename from subjects/ServerRendering/exercise.js rename to subjects/14-Server-Rendering/exercise.js index 58d646c01..9f7c834bc 100644 --- a/subjects/ServerRendering/exercise.js +++ b/subjects/14-Server-Rendering/exercise.js @@ -3,8 +3,8 @@ // // First, fire up the server: // -// 1. Run `npm run server-exercise` from the root of this repository -// 2. Open http://localhost:8081 (not 8080) +// 1. Run `npm run ssr-exercise` from the root of this repository +// 2. Open http://localhost:8090 (not 8080) // // Now let's write some code: // @@ -28,9 +28,9 @@ // Note: As you go through the steps, try using the "view source" feature of // your web browser to see the actual HTML you're rendering on the server. //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import { render } from 'react-dom' -import App from './exercise/App' +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./exercise/App"; // TODO: Pass contacts data into the via a prop. -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/14-Server-Rendering/exercise/App.js b/subjects/14-Server-Rendering/exercise/App.js new file mode 100644 index 000000000..07ba89d41 --- /dev/null +++ b/subjects/14-Server-Rendering/exercise/App.js @@ -0,0 +1,40 @@ +import React from "react"; +import fetchContacts from "./fetchContacts"; + +class App extends React.Component { + // TODO: Move this state to a prop. That will make it + // possible to render on the server. + state = { + contacts: [] + }; + + componentDidMount() { + // TODO: Move this call into the request handler on the server. + fetchContacts((error, contacts) => { + this.setState({ contacts }); + }); + } + + render() { + const { contacts } = this.state; + + return ( +
    +

    ¡Universal App!

    + {contacts ? ( +
      + {contacts.map(contact => ( +
    • + {contact.first} {contact.last} +
    • + ))} +
    + ) : ( +

    Loading...

    + )} +
    + ); + } +} + +export default App; diff --git a/subjects/14-Server-Rendering/exercise/fetchContacts.js b/subjects/14-Server-Rendering/exercise/fetchContacts.js new file mode 100644 index 000000000..e89966433 --- /dev/null +++ b/subjects/14-Server-Rendering/exercise/fetchContacts.js @@ -0,0 +1,10 @@ +import "isomorphic-fetch"; + +const fetchContacts = cb => + fetch("https://addressbook-api.herokuapp.com/contacts") + .then(res => res.json()) + .then(data => { + cb(null, data.contacts); + }, cb); + +export default fetchContacts; diff --git a/subjects/14-Server-Rendering/exercise/server.js b/subjects/14-Server-Rendering/exercise/server.js new file mode 100644 index 000000000..3030ea754 --- /dev/null +++ b/subjects/14-Server-Rendering/exercise/server.js @@ -0,0 +1,44 @@ +import http from "http"; +import React from "react"; +import ReactDOMServer from "react-dom/server"; + +import fetchContacts from "./fetchContacts"; +import App from "./App"; + +const webpackServer = "http://localhost:8080"; +const port = 8090; + +const createPage = () => ` + + + + + My Universal App + + + + +
    + + + + + +`; + +const app = http.createServer((req, res) => { + // TODO: We'd like to render the on the server + // instead of just sending a practically empty page. + const html = createPage(); + + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Length": html.length + }); + + res.end(html); +}); + +app.listen(port, () => { + console.log("\nOpen http://localhost:%s", port); +}); diff --git a/subjects/ServerRendering/solution.js b/subjects/14-Server-Rendering/solution.js similarity index 82% rename from subjects/ServerRendering/solution.js rename to subjects/14-Server-Rendering/solution.js index 64d504d79..4bbc6022c 100644 --- a/subjects/ServerRendering/solution.js +++ b/subjects/14-Server-Rendering/solution.js @@ -3,8 +3,8 @@ // // First, fire up the server: // -// 1. Run `npm run server-solution` from the root of this repository -// 2. Open http://localhost:8081 (not 8080) +// 1. Run `npm run ssr-solution` from the root of this repository +// 2. Open http://localhost:8090 (not 8080) // // Now let's write some code: // @@ -28,8 +28,13 @@ // Note: As you go through the steps, try using the "view source" feature of // your web browser to see the actual HTML you're rendering on the server. //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import { render } from 'react-dom' -import App from './solution/App' +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./solution/App"; -render(, document.getElementById('app')) +const contacts = window.__DATA__.contacts; + +ReactDOM.render( + , + document.getElementById("app") +); diff --git a/subjects/14-Server-Rendering/solution/App.js b/subjects/14-Server-Rendering/solution/App.js new file mode 100644 index 000000000..654177599 --- /dev/null +++ b/subjects/14-Server-Rendering/solution/App.js @@ -0,0 +1,31 @@ +import React from "react"; +import PropTypes from "prop-types"; + +class App extends React.Component { + static propTypes = { + contacts: PropTypes.array + }; + + render() { + const { contacts } = this.props; + + return ( +
    +

    ¡Universal App!

    + {contacts ? ( +
      + {contacts.map(contact => ( +
    • + {contact.first} {contact.last} +
    • + ))} +
    + ) : ( +

    Loading...

    + )} +
    + ); + } +} + +export default App; diff --git a/subjects/14-Server-Rendering/solution/fetchContacts.js b/subjects/14-Server-Rendering/solution/fetchContacts.js new file mode 100644 index 000000000..e89966433 --- /dev/null +++ b/subjects/14-Server-Rendering/solution/fetchContacts.js @@ -0,0 +1,10 @@ +import "isomorphic-fetch"; + +const fetchContacts = cb => + fetch("https://addressbook-api.herokuapp.com/contacts") + .then(res => res.json()) + .then(data => { + cb(null, data.contacts); + }, cb); + +export default fetchContacts; diff --git a/subjects/14-Server-Rendering/solution/server.js b/subjects/14-Server-Rendering/solution/server.js new file mode 100644 index 000000000..4a50cc956 --- /dev/null +++ b/subjects/14-Server-Rendering/solution/server.js @@ -0,0 +1,47 @@ +import http from "http"; +import React from "react"; +import ReactDOMServer from "react-dom/server"; + +import fetchContacts from "./fetchContacts"; +import App from "./App"; + +const webpackServer = "http://localhost:8080"; +const port = 8090; + +const createPage = (markup, data) => ` + + + + + My Universal App + + + +
    ${markup}
    + + + + + + +`; + +const app = http.createServer((req, res) => { + fetchContacts((error, contacts) => { + const markup = ReactDOMServer.renderToString( + + ); + const html = createPage(markup, { contacts }); + + res.writeHead(200, { + "Content-Type": "text/html", + "Content-Length": html.length + }); + + res.end(html); + }); +}); + +app.listen(port, () => { + console.log("\nOpen http://localhost:%s", port); +}); diff --git a/subjects/Redux/Redux.png b/subjects/15-Redux/Redux.png similarity index 100% rename from subjects/Redux/Redux.png rename to subjects/15-Redux/Redux.png diff --git a/subjects/15-Redux/exercise.js b/subjects/15-Redux/exercise.js new file mode 100644 index 000000000..39e5932c1 --- /dev/null +++ b/subjects/15-Redux/exercise.js @@ -0,0 +1 @@ +import "./exercise/index.js"; diff --git a/subjects/15-Redux/exercise/actions.js b/subjects/15-Redux/exercise/actions.js new file mode 100644 index 000000000..f6b56c6d3 --- /dev/null +++ b/subjects/15-Redux/exercise/actions.js @@ -0,0 +1,23 @@ +import { fetchContacts, deleteContactById } from "./utils/api"; + +export const ADD_CONTACT = "ADD_CONTACT"; +export const LOAD_CONTACTS = "LOAD_CONTACTS"; +export const CONTACTS_WERE_LOADED = "CONTACTS_WERE_LOADED"; + +export function addContact(contact) { + return { + type: ADD_CONTACT, + contact + }; +} + +export function loadContacts(dispatch) { + dispatch({ type: LOAD_CONTACTS }); + + fetchContacts((error, contacts) => { + dispatch({ + type: CONTACTS_WERE_LOADED, + contacts + }); + }); +} diff --git a/subjects/15-Redux/exercise/components/App.js b/subjects/15-Redux/exercise/components/App.js new file mode 100644 index 000000000..a0ff01f2e --- /dev/null +++ b/subjects/15-Redux/exercise/components/App.js @@ -0,0 +1,19 @@ +import React from "react"; +import { Provider } from "react-redux"; +import store from "../store"; +import ContactList from "./ContactList"; + +class App extends React.Component { + render() { + return ( + +
    +

    Contacts!

    + +
    +
    + ); + } +} + +export default App; diff --git a/subjects/15-Redux/exercise/components/ContactList.js b/subjects/15-Redux/exercise/components/ContactList.js new file mode 100644 index 000000000..7018f8d0d --- /dev/null +++ b/subjects/15-Redux/exercise/components/ContactList.js @@ -0,0 +1,45 @@ +import React from "react"; +import { connect } from "react-redux"; +import { addContact, loadContacts } from "../actions"; +import CreateContactForm from "./CreateContactForm"; + +class ContactList extends React.Component { + static defaultProps = { + contacts: [], + dispatch: () => { + console.log( + "Dispatch failed; you still need to connect() your ContactList" + ); + } + }; + + componentDidMount() { + loadContacts(this.props.dispatch); + } + + createContact(contact) { + this.props.dispatch(addContact(contact)); + } + + render() { + const { contacts } = this.props; + + return ( +
      + {contacts.map(contact => ( +
    • + {contact.first}{" "} + {contact.last} +
    • + ))} +
    • + this.createContact(contact)} + /> +
    • +
    + ); + } +} + +export default ContactList; diff --git a/subjects/15-Redux/exercise/components/CreateContactForm.js b/subjects/15-Redux/exercise/components/CreateContactForm.js new file mode 100644 index 000000000..c0f2bf171 --- /dev/null +++ b/subjects/15-Redux/exercise/components/CreateContactForm.js @@ -0,0 +1,60 @@ +import React from "react"; +import PropTypes from "prop-types"; +import serializeForm from "form-serialize"; + +const transparentGif = + "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + +function generateId() { + return Math.random() + .toString(36) + .substring(7); +} + +class CreateContactForm extends React.Component { + static propTypes = { + onCreate: PropTypes.func.isRequired + }; + + handleSubmit = event => { + event.preventDefault(); + const contact = serializeForm(event.target, { hash: true }); + contact.id = generateId(); + this.props.onCreate(contact); + event.target.reset(); + }; + + render() { + return ( +
    + {" "} + + + + +
    + ); + } +} + +export default CreateContactForm; diff --git a/subjects/Redux/exercise.js b/subjects/15-Redux/exercise/index.js similarity index 74% rename from subjects/Redux/exercise.js rename to subjects/15-Redux/exercise/index.js index 1223a0807..d6b428599 100644 --- a/subjects/Redux/exercise.js +++ b/subjects/15-Redux/exercise/index.js @@ -3,9 +3,10 @@ The goal of this exercise is to gain some hands-on experience using Redux to manage the state of a React application. In the process, we'll also learn how to use Redux to communicate changes to a real API server. - - Make the update its state so it displays the contacts when the store - changes (hint: use store.subscribe). If you open the console, you'll see the - contacts are already being loaded from the API. + - Get the the props it needs by using connect() to connect it to + the in its parent, the . + - Once it's connected, if you open the console you'll see the contacts being + loaded from the API. - Add a delete + )} + + ))} +
  • + this.createContact(contact)} + /> +
  • + + ); + } +} + +export default connect(state => state)(ContactList); diff --git a/subjects/15-Redux/solution/components/CreateContactForm.js b/subjects/15-Redux/solution/components/CreateContactForm.js new file mode 100644 index 000000000..c0f2bf171 --- /dev/null +++ b/subjects/15-Redux/solution/components/CreateContactForm.js @@ -0,0 +1,60 @@ +import React from "react"; +import PropTypes from "prop-types"; +import serializeForm from "form-serialize"; + +const transparentGif = + "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + +function generateId() { + return Math.random() + .toString(36) + .substring(7); +} + +class CreateContactForm extends React.Component { + static propTypes = { + onCreate: PropTypes.func.isRequired + }; + + handleSubmit = event => { + event.preventDefault(); + const contact = serializeForm(event.target, { hash: true }); + contact.id = generateId(); + this.props.onCreate(contact); + event.target.reset(); + }; + + render() { + return ( +
    + {" "} + + + + +
    + ); + } +} + +export default CreateContactForm; diff --git a/subjects/Redux/solution.js b/subjects/15-Redux/solution/index.js similarity index 74% rename from subjects/Redux/solution.js rename to subjects/15-Redux/solution/index.js index ed67a13f5..d6b428599 100644 --- a/subjects/Redux/solution.js +++ b/subjects/15-Redux/solution/index.js @@ -3,9 +3,10 @@ The goal of this exercise is to gain some hands-on experience using Redux to manage the state of a React application. In the process, we'll also learn how to use Redux to communicate changes to a real API server. - - Make the update its state so it displays the contacts when the store - changes (hint: use store.subscribe). If you open the console, you'll see the - contacts are already being loaded from the API. + - Get the the props it needs by using connect() to connect it to + the in its parent, the . + - Once it's connected, if you open the console you'll see the contacts being + loaded from the API. - Add a delete
    - +
    - ) + ); } } -render(, document.getElementById('app')) +render(, document.getElementById("app")); diff --git a/subjects/ChatApp/solution.js b/subjects/16-Chat-App/solution.js similarity index 52% rename from subjects/ChatApp/solution.js rename to subjects/16-Chat-App/solution.js index b7e225410..d4a305022 100644 --- a/subjects/ChatApp/solution.js +++ b/subjects/16-Chat-App/solution.js @@ -5,21 +5,21 @@ // // Need some ideas? // -// - Cause the message list to automatically scroll as new -// messages come in +// - Cause the message list to automatically scroll as new messages come in // - Highlight messages from you to make them easy to find // - Highlight messages that mention you by your GitHub username // - Group subsequent messages from the same sender // - Create a filter that lets you filter messages in the chat by // sender and/or content //////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import { render } from 'react-dom' -import { login, sendMessage, subscribeToMessages } from './utils/ChatUtils' -import './styles' +import "./styles.css"; + +import React from "react"; +import { render } from "react-dom"; +import { login, sendMessage, subscribeToMessages } from "./utils"; /* -Here's how to use the ChatUtils: +Here's how to use the utils: login((error, auth) => { // hopefully the error is `null` and you have a auth.github object @@ -44,33 +44,32 @@ The world is your oyster! class SmartScroller extends React.Component { componentDidMount() { - this.autoScroll = true - this.scrollToBottom() + this.autoScroll = true; + this.scrollToBottom(); } componentDidUpdate() { - if (this.autoScroll) - this.scrollToBottom() + if (this.autoScroll) this.scrollToBottom(); } scrollToBottom() { - this.node.scrollTop = this.node.scrollHeight + this.node.scrollTop = this.node.scrollHeight; } - handleScroll() { - const { scrollTop, scrollHeight, clientHeight } = this.node - const distanceToBottom = scrollHeight - (scrollTop + clientHeight) - this.autoScroll = distanceToBottom < 10 - } + handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = this.node; + const distanceToBottom = scrollHeight - (scrollTop + clientHeight); + this.autoScroll = distanceToBottom < 10; + }; render() { return (
    this.node = node} + ref={node => (this.node = node)} onScroll={this.handleScroll} /> - ) + ); } } @@ -78,86 +77,93 @@ class Chat extends React.Component { state = { auth: null, messages: [] - } + }; componentDidMount() { login((error, auth) => { - this.setState({ auth }) + this.setState({ auth }); subscribeToMessages(messages => { - this.setState({ messages }) - }) - }) + this.setState({ messages }); + }); + }); } - handleSubmit = (event) => { - event.preventDefault() + handleSubmit = event => { + event.preventDefault(); - const { auth } = this.state - const messageText = this.messageInput.value + const { auth } = this.state; + const messageText = this.messageInput.value; - if ((/\S/).test(messageText)) { + if (/\S/.test(messageText)) { sendMessage( - auth.uid, // the auth.uid string - auth.github.username, // the username - auth.github.profileImageURL, // the user's profile image - messageText // the text of the message - ) + auth.uid, // the auth.uid string + auth.github.username, // the username + auth.github.profileImageURL, // the user's profile image + messageText // the text of the message + ); // Clear the form. - event.target.reset() + event.target.reset(); } - } + }; render() { - const { auth, messages } = this.state + const { auth, messages } = this.state; - if (auth == null) - return

    Loading...

    + if (auth == null) return

    Loading...

    ; // Array of arrays of messages grouped by user. const messageGroups = messages.reduce((groups, message) => { - const prevGroup = groups.length && groups[groups.length - 1] + const prevGroup = groups.length && groups[groups.length - 1]; if (prevGroup && prevGroup[0].uid === message.uid) { - prevGroup.push(message) + prevGroup.push(message); } else { - groups.push([ message ]) + groups.push([message]); } - return groups - }, []) + return groups; + }, []); return (

    HipReact

    -

    # messages: {messages.length}

    +

    + # messages: {messages.length} +

      - {messageGroups.map(messages => ( -
    1. -
      - -
      -
        - {messages.map(message => ( -
      1. {message.text}
      2. - ))} -
      -
    2. - ))} + {messageGroups.map((messages, index) => ( +
    3. +
      + +
      +
        + {messages.map((message, index) => ( +
      1. + {message.text} +
      2. + ))} +
      +
    4. + ))}
    - this.messageInput = node} type="text" placeholder="say something..."/> + (this.messageInput = node)} + type="text" + placeholder="say something..." + />
    - ) + ); } } -render(, document.getElementById('app')) +render(, document.getElementById("app")); diff --git a/subjects/ChatApp/styles.css b/subjects/16-Chat-App/styles.css similarity index 92% rename from subjects/ChatApp/styles.css rename to subjects/16-Chat-App/styles.css index fcb917f7a..104215bfc 100644 --- a/subjects/ChatApp/styles.css +++ b/subjects/16-Chat-App/styles.css @@ -1,10 +1,6 @@ html { - box-sizing: border-box; overflow: hidden; /* prevent elastic scrolling in WebKit */ } -*, *:before, *:after { - box-sizing: inherit; -} body { margin: 0; @@ -12,7 +8,7 @@ body { } .chat { - height: 100%; + height: 100vh; display: flex; flex-direction: column; } @@ -35,7 +31,6 @@ body { font-size: 1.5em; } - .messages { overflow: auto; flex: 1; @@ -73,6 +68,7 @@ ol.messages { .message-group-avatar { position: absolute; background-color: lightblue; + line-height: 0; } .message-group-avatar img { height: 40px; diff --git a/subjects/16-Chat-App/utils.js b/subjects/16-Chat-App/utils.js new file mode 100644 index 000000000..49afa4370 --- /dev/null +++ b/subjects/16-Chat-App/utils.js @@ -0,0 +1,84 @@ +import Firebase from "firebase/lib/firebase-web"; + +const baseRef = new Firebase("https://hip-react.firebaseio.com"); +const messagesRef = baseRef.child("messages"); +const usersRef = baseRef.child("users"); + +const reservedRefNameChars = /[\.#\$\[\]]/g; + +function escapeKey(name) { + return name.replace(reservedRefNameChars, "_"); +} + +function escapeValue(rawValue) { + const value = + rawValue && typeof rawValue.toJSON === "function" + ? rawValue.toJSON() + : rawValue; + + if (value == null) return null; // Remove undefined values + + if (Array.isArray(value)) return value.map(escapeValue); + + if (typeof value === "object") { + return Object.keys(value).reduce((memo, key) => { + memo[escapeKey(key)] = escapeValue(value[key]); + return memo; + }, {}); + } + + return value; +} + +function saveAuth(auth) { + usersRef.child(auth.uid).set(escapeValue(auth)); +} + +export function login(callback) { + const auth = baseRef.getAuth(); + + if (auth) { + saveAuth(auth); + callback(null, auth); + } else { + baseRef.authWithOAuthPopup("github", (error, auth) => { + if (auth) saveAuth(auth); + callback(error, auth); + }); + } +} + +export function subscribeToMessages(callback) { + const handleValue = snapshot => { + const messages = []; + + snapshot.forEach(s => { + const message = s.val(); + message._key = s.key(); + messages.push(message); + }); + + callback(messages); + }; + + messagesRef.on("value", handleValue); + + return () => { + messagesRef.off("value", handleValue); + }; +} + +let serverTimeOffset = 0; +baseRef.child(".info/serverTimeOffset").on("value", s => { + serverTimeOffset = s.val(); +}); + +export function sendMessage(uid, username, avatarURL, text) { + messagesRef.push({ + uid, + timestamp: Date.now() + serverTimeOffset, + username, + avatarURL, + text + }); +} diff --git a/subjects/17-Mini-Router/exercise.js b/subjects/17-Mini-Router/exercise.js new file mode 100644 index 000000000..fa30740af --- /dev/null +++ b/subjects/17-Mini-Router/exercise.js @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Implement the core components of React Router to make this app work: +// +// +// 1. Add some state w/ `location` as a key +// 2. Set `location`'s initial value to `this.history.location` +// 3. Listen to `history` and put the location into state +// 4. Use context to provide API to descendants +// +// +// 1. Get the location from context +// 2. Figure out if the path matches location.pathname +// (hint: location.pathname.startsWith(...) +// 3. If there is a match, figure out which prop to render +// `component` or `render` +// 4. If there is no match, render null +// +// +// 1. Get a "push" method from context +// 2. Use `push(...)` with the `to` prop +// +// Got extra time? +// +// - Implement or +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; + +// You will be working in mini-router.js. This file is just an example app that +// uses the components from that library. +import { Router, Route, Link } from "./exercise/mini-router"; + +function App() { + return ( + +
    +
      +
    • + Dashboard +
    • +
    • + About +
    • +
    • + Topics +
    • +
    + +
    + + ( +
    +

    Dashboard

    +
    + )} + /> + + +
    +
    + ); +} + +function About() { + return ( +
    +

    About

    +
    + ); +} + +function Topics() { + return ( +
    +

    Topics

    +
      +
    • Rendering with React
    • +
    • Components
    • +
    • Props v. State
    • +
    +
    + ); +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/17-Mini-Router/exercise/mini-router.js b/subjects/17-Mini-Router/exercise/mini-router.js new file mode 100644 index 000000000..2bb8dccaf --- /dev/null +++ b/subjects/17-Mini-Router/exercise/mini-router.js @@ -0,0 +1,49 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { createHashHistory } from "history"; + +/* +How to use the history library: + +// read the current URL +history.location + +// listen for changes to the URL +history.listen(() => { + history.location // is now different +}) + +// change the URL +history.push('/something') +*/ + +class Router extends React.Component { + history = createHashHistory(); + + render() { + return this.props.children; + } +} + +class Route extends React.Component { + render() { + const { path, render, component: Component } = this.props; + return null; + } +} + +class Link extends React.Component { + handleClick = event => { + event.preventDefault(); + }; + + render() { + return ( + + {this.props.children} + + ); + } +} + +export { Router, Route, Link }; diff --git a/subjects/17-Mini-Router/solution.js b/subjects/17-Mini-Router/solution.js new file mode 100644 index 000000000..7fd225abf --- /dev/null +++ b/subjects/17-Mini-Router/solution.js @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Implement the core components of React Router to make this app work: +// +// +// 1. Add some state w/ `location` as a key +// 2. Set `location`'s initial value to `this.history.location` +// 3. Listen to `history` and put the location into state +// 4. Use context to provide API to descendants +// +// +// 1. Get the location from context +// 2. Figure out if the path matches location.pathname +// (hint: location.pathname.startsWith(...) +// 3. If there is a match, figure out which prop to render +// `component` or `render` +// 4. If there is no match, render null +// +// +// 1. Get a "push" method from context +// 2. Use `push(...)` with the `to` prop +// +// Got extra time? +// +// - Implement or +//////////////////////////////////////////////////////////////////////////////// +import React from "react"; +import ReactDOM from "react-dom"; + +// You will be working in mini-router.js. This file is just an example app that +// uses the components from that library. +import { Router, Route, Link } from "./solution/mini-router"; + +function App() { + return ( + +
    +
      +
    • + Dashboard +
    • +
    • + About +
    • +
    • + Topics +
    • +
    + +
    + + ( +
    +

    Dashboard

    +
    + )} + /> + + +
    +
    + ); +} + +function About() { + return ( +
    +

    About

    +
    + ); +} + +function Topics() { + return ( +
    +

    Topics

    +
      +
    • Rendering with React
    • +
    • Components
    • +
    • Props v. State
    • +
    +
    + ); +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/17-Mini-Router/solution/mini-router.js b/subjects/17-Mini-Router/solution/mini-router.js new file mode 100644 index 000000000..82b9d11d7 --- /dev/null +++ b/subjects/17-Mini-Router/solution/mini-router.js @@ -0,0 +1,90 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { createHashHistory } from "history"; + +/* +How to use the history library: + +// read the current URL +history.location + +// listen for changes to the URL +history.listen(() => { + history.location // is now different +}) + +// change the URL +history.push('/something') +*/ + +const RouterContext = React.createContext(); + +class Router extends React.Component { + history = createHashHistory(); + + state = { + location: this.history.location + }; + + componentDidMount() { + this.history.listen(location => { + this.setState({ location }); + }); + } + + handlePush = to => { + this.history.push(to); + }; + + render() { + return ( + + ); + } +} + +class Route extends React.Component { + render() { + return ( + + {router => { + const { path, render, component: Component } = this.props; + + if (router.location.pathname.startsWith(path)) { + if (render) return render(); + if (Component) return ; + } + + return null; + }} + + ); + } +} + +class Link extends React.Component { + handleClick = (event, router) => { + event.preventDefault(); + router.push(this.props.to); + }; + + render() { + return ( + + {router => ( + this.handleClick(event, router)} + > + {this.props.children} + + )} + + ); + } +} + +export { Router, Route, Link }; diff --git a/subjects/MiniRedux/exercise.js b/subjects/18-Mini-Redux/exercise.js similarity index 97% rename from subjects/MiniRedux/exercise.js rename to subjects/18-Mini-Redux/exercise.js index 1e927a3a2..69256c6f4 100644 --- a/subjects/MiniRedux/exercise.js +++ b/subjects/18-Mini-Redux/exercise.js @@ -28,4 +28,4 @@ If you run into problems, check the logger's output: - Is the next state what you'd expect? Check the reducer. */ -import './exercise/index' +import "./exercise/index"; diff --git a/subjects/18-Mini-Redux/exercise/components/App.js b/subjects/18-Mini-Redux/exercise/components/App.js new file mode 100644 index 000000000..a24c197a4 --- /dev/null +++ b/subjects/18-Mini-Redux/exercise/components/App.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import connect from "../mini-redux/connect"; + +class App extends React.Component { + increment = () => { + this.props.dispatch({ type: "INCREMENT" }); + }; + + decrement = () => { + this.props.dispatch({ type: "DECREMENT" }); + }; + + render() { + return ( +
    +

    Mini Redux!

    + {" "} + {this.props.counter}{" "} + +
    + ); + } +} + +const mapStateToProps = state => { + return { counter: state }; +}; + +export default connect(mapStateToProps)(App); diff --git a/subjects/MiniRedux/solution/index.js b/subjects/18-Mini-Redux/exercise/index.js similarity index 68% rename from subjects/MiniRedux/solution/index.js rename to subjects/18-Mini-Redux/exercise/index.js index f05793ae0..687a46296 100644 --- a/subjects/MiniRedux/solution/index.js +++ b/subjects/18-Mini-Redux/exercise/index.js @@ -16,24 +16,25 @@ // 3. Remove the "visibility: hidden" from the contact list in App // 4. Migrate the contacts from component state into the store // 5. Handle creating contacts with `dispatch` -import React from 'react' -import ReactDOM from 'react-dom' -import createStore from './mini-redux/createStore' -import Provider from './mini-redux/Provider' -import App from './components/App' +import React from "react"; +import ReactDOM from "react-dom"; +import createStore from "./mini-redux/createStore"; +import Provider from "./mini-redux/Provider"; +import App from "./components/App"; const store = createStore((state = 0, action) => { - if (action.type === 'INCREMENT') { - return state + 1 - } else if (action.type === 'DECREMENT') { - return state - 1 + if (action.type === "INCREMENT") { + return state + 1; + } else if (action.type === "DECREMENT") { + return state - 1; } else { - return state + return state; } -}) +}); -ReactDOM.render(( +ReactDOM.render( - - -), document.getElementById('app')) + + , + document.getElementById("app") +); diff --git a/subjects/18-Mini-Redux/exercise/mini-redux/Provider.js b/subjects/18-Mini-Redux/exercise/mini-redux/Provider.js new file mode 100644 index 000000000..088de54ba --- /dev/null +++ b/subjects/18-Mini-Redux/exercise/mini-redux/Provider.js @@ -0,0 +1,9 @@ +import React from "react"; + +class Provider extends React.Component { + render() { + return
    {this.props.children}
    ; + } +} + +export default Provider; diff --git a/subjects/18-Mini-Redux/exercise/mini-redux/connect.js b/subjects/18-Mini-Redux/exercise/mini-redux/connect.js new file mode 100644 index 000000000..4fd62c040 --- /dev/null +++ b/subjects/18-Mini-Redux/exercise/mini-redux/connect.js @@ -0,0 +1,9 @@ +import React from "react"; + +const connect = mapStateToProps => { + return Component => { + return Component; + }; +}; + +export default connect; diff --git a/subjects/18-Mini-Redux/exercise/mini-redux/createStore.js b/subjects/18-Mini-Redux/exercise/mini-redux/createStore.js new file mode 100644 index 000000000..baf748301 --- /dev/null +++ b/subjects/18-Mini-Redux/exercise/mini-redux/createStore.js @@ -0,0 +1,27 @@ +const createStore = reducer => { + let state = reducer(undefined, { type: "@INIT" }); + let listeners = []; + + const getState = () => state; + + const dispatch = action => { + state = reducer(state, action); + listeners.forEach(listener => listener()); + }; + + const subscribe = listener => { + listeners.push(listener); + + return () => { + listeners = listeners.filter(item => item !== listener); + }; + }; + + return { + getState, + dispatch, + subscribe + }; +}; + +export default createStore; diff --git a/subjects/MiniRedux/lecture.js b/subjects/18-Mini-Redux/lecture.js similarity index 94% rename from subjects/MiniRedux/lecture.js rename to subjects/18-Mini-Redux/lecture.js index db6ab19ca..f78347f7c 100644 --- a/subjects/MiniRedux/lecture.js +++ b/subjects/18-Mini-Redux/lecture.js @@ -14,4 +14,4 @@ * 6. exercise: edit contacts */ -import './lecture/index' +import "./lecture/index"; diff --git a/subjects/18-Mini-Redux/lecture/components/App.js b/subjects/18-Mini-Redux/lecture/components/App.js new file mode 100644 index 000000000..a03199983 --- /dev/null +++ b/subjects/18-Mini-Redux/lecture/components/App.js @@ -0,0 +1,52 @@ +import React from "react"; +import PropTypes from "prop-types"; +import CreateContactForm from "./CreateContactForm"; + +class App extends React.Component { + state = { + contacts: [ + { + id: "mj", + first: "Michael", + last: "Jackson", + avatar: + "https://pbs.twimg.com/profile_images/902276500107362304/vju-WV1i_400x400.jpg" + }, + { + id: "blee", + first: "Bruce", + last: "Lee", + avatar: + "https://pbs.twimg.com/profile_images/534863086276988928/bX3juDCC_400x400.jpeg" + } + ] + }; + + handleCreateContact = contact => { + this.setState({ + contacts: this.state.contacts.concat([contact]) + }); + }; + + render() { + return ( +
    +

    Contacts!

    + +
      + {this.state.contacts.map(contact => ( +
    • + {contact.first}{" "} + {contact.last} +
    • + ))} +
    • + +
    • +
    +
    + ); + } +} + +export default App; diff --git a/subjects/18-Mini-Redux/lecture/components/CreateContactForm.js b/subjects/18-Mini-Redux/lecture/components/CreateContactForm.js new file mode 100644 index 000000000..ba0a21c19 --- /dev/null +++ b/subjects/18-Mini-Redux/lecture/components/CreateContactForm.js @@ -0,0 +1,60 @@ +import React from "react"; +import PropTypes from "prop-types"; +import serializeForm from "form-serialize"; + +const transparentGif = + "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + +function generateId() { + return Math.random() + .toString(36) + .substring(7); +} + +class CreateContactForm extends React.Component { + static propTypes = { + onCreate: PropTypes.func.isRequired + }; + + handleSubmit = event => { + event.preventDefault(); + const contact = serializeForm(event.target, { hash: true }); + contact.id = generateId(); + this.props.onCreate(contact); + event.target.reset(); + }; + + render() { + return ( +
    + {" "} + + + + +
    + ); + } +} + +export default CreateContactForm; diff --git a/subjects/MiniRedux/lecture/index.js b/subjects/18-Mini-Redux/lecture/index.js similarity index 55% rename from subjects/MiniRedux/lecture/index.js rename to subjects/18-Mini-Redux/lecture/index.js index 87516e5c2..8c488b6a2 100644 --- a/subjects/MiniRedux/lecture/index.js +++ b/subjects/18-Mini-Redux/lecture/index.js @@ -1,8 +1,8 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import App from './components/App' +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./components/App"; -ReactDOM.render((), document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); //////////////////////////////////////////////////////////////////////////////// // - shared state (add sidebar) diff --git a/subjects/18-Mini-Redux/lecture/lib/contactsAPI.js b/subjects/18-Mini-Redux/lecture/lib/contactsAPI.js new file mode 100644 index 000000000..ba4ca6e3b --- /dev/null +++ b/subjects/18-Mini-Redux/lecture/lib/contactsAPI.js @@ -0,0 +1,13 @@ +import { getJSON, deleteJSON } from "./xhr"; + +const API = "http://addressbook-api.herokuapp.com"; + +export function fetchContacts(cb) { + getJSON(`${API}/contacts`, (error, res) => { + cb(error, res.contacts); + }); +} + +export function deleteContactById(contactId, cb) { + deleteJSON(`${API}/contacts/${contactId}`, cb); +} diff --git a/subjects/18-Mini-Redux/lecture/lib/xhr.js b/subjects/18-Mini-Redux/lecture/lib/xhr.js new file mode 100644 index 000000000..a03b97299 --- /dev/null +++ b/subjects/18-Mini-Redux/lecture/lib/xhr.js @@ -0,0 +1,47 @@ +localStorage.token = localStorage.token || Date.now() * Math.random(); + +function setToken(req) { + req.setRequestHeader("authorization", localStorage.token); +} + +export function getJSON(url, callback) { + const req = new XMLHttpRequest(); + req.onload = function() { + if (req.status === 404) { + callback(new Error("not found")); + } else { + callback(null, JSON.parse(req.response)); + } + }; + req.open("GET", url); + setToken(req); + req.send(); +} + +export function postJSON(url, obj, callback) { + const req = new XMLHttpRequest(); + req.onload = function() { + callback(JSON.parse(req.response)); + }; + req.open("POST", url); + req.setRequestHeader( + "Content-Type", + "application/json;charset=UTF-8" + ); + setToken(req); + req.send(JSON.stringify(obj)); +} + +export function deleteJSON(url, callback) { + const req = new XMLHttpRequest(); + req.onload = function() { + if (req.status === 500) { + callback(new Error(req.responseText)); + } else { + callback(null, req.responseText); + } + }; + req.open("DELETE", url); + setToken(req); + req.send(); +} diff --git a/subjects/Redux/notes.md b/subjects/18-Mini-Redux/notes.md similarity index 100% rename from subjects/Redux/notes.md rename to subjects/18-Mini-Redux/notes.md diff --git a/subjects/MiniRedux/solution.js b/subjects/18-Mini-Redux/solution.js similarity index 94% rename from subjects/MiniRedux/solution.js rename to subjects/18-Mini-Redux/solution.js index f56e96f3d..a64b8cb8e 100644 --- a/subjects/MiniRedux/solution.js +++ b/subjects/18-Mini-Redux/solution.js @@ -14,4 +14,4 @@ * 6. exercise: edit contacts */ -import './solution/index' +import "./solution/index"; diff --git a/subjects/18-Mini-Redux/solution/components/App.js b/subjects/18-Mini-Redux/solution/components/App.js new file mode 100644 index 000000000..a24c197a4 --- /dev/null +++ b/subjects/18-Mini-Redux/solution/components/App.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import connect from "../mini-redux/connect"; + +class App extends React.Component { + increment = () => { + this.props.dispatch({ type: "INCREMENT" }); + }; + + decrement = () => { + this.props.dispatch({ type: "DECREMENT" }); + }; + + render() { + return ( +
    +

    Mini Redux!

    + {" "} + {this.props.counter}{" "} + +
    + ); + } +} + +const mapStateToProps = state => { + return { counter: state }; +}; + +export default connect(mapStateToProps)(App); diff --git a/subjects/MiniRedux/exercise/index.js b/subjects/18-Mini-Redux/solution/index.js similarity index 68% rename from subjects/MiniRedux/exercise/index.js rename to subjects/18-Mini-Redux/solution/index.js index f05793ae0..687a46296 100644 --- a/subjects/MiniRedux/exercise/index.js +++ b/subjects/18-Mini-Redux/solution/index.js @@ -16,24 +16,25 @@ // 3. Remove the "visibility: hidden" from the contact list in App // 4. Migrate the contacts from component state into the store // 5. Handle creating contacts with `dispatch` -import React from 'react' -import ReactDOM from 'react-dom' -import createStore from './mini-redux/createStore' -import Provider from './mini-redux/Provider' -import App from './components/App' +import React from "react"; +import ReactDOM from "react-dom"; +import createStore from "./mini-redux/createStore"; +import Provider from "./mini-redux/Provider"; +import App from "./components/App"; const store = createStore((state = 0, action) => { - if (action.type === 'INCREMENT') { - return state + 1 - } else if (action.type === 'DECREMENT') { - return state - 1 + if (action.type === "INCREMENT") { + return state + 1; + } else if (action.type === "DECREMENT") { + return state - 1; } else { - return state + return state; } -}) +}); -ReactDOM.render(( +ReactDOM.render( - - -), document.getElementById('app')) + + , + document.getElementById("app") +); diff --git a/subjects/MiniRedux/solution/mini-redux/Provider.js b/subjects/18-Mini-Redux/solution/mini-redux/Provider.js similarity index 56% rename from subjects/MiniRedux/solution/mini-redux/Provider.js rename to subjects/18-Mini-Redux/solution/mini-redux/Provider.js index 0fc0da0b2..e80990d45 100644 --- a/subjects/MiniRedux/solution/mini-redux/Provider.js +++ b/subjects/18-Mini-Redux/solution/mini-redux/Provider.js @@ -1,19 +1,20 @@ -import React, { PropTypes } from 'react' +import React from "react"; +import PropTypes from "prop-types"; class Provider extends React.Component { static childContextTypes = { store: PropTypes.object - } + }; getChildContext() { return { store: this.props.store - } + }; } render() { - return
    {this.props.children}
    + return
    {this.props.children}
    ; } } -export default Provider +export default Provider; diff --git a/subjects/MiniRedux/solution/mini-redux/connect.js b/subjects/18-Mini-Redux/solution/mini-redux/connect.js similarity index 65% rename from subjects/MiniRedux/solution/mini-redux/connect.js rename to subjects/18-Mini-Redux/solution/mini-redux/connect.js index eb143e4a4..81ff58b25 100644 --- a/subjects/MiniRedux/solution/mini-redux/connect.js +++ b/subjects/18-Mini-Redux/solution/mini-redux/connect.js @@ -1,24 +1,25 @@ -import React, { PropTypes } from 'react' +import React from "react"; +import PropTypes from "prop-types"; -const connect = (mapStateToProps) => { - return (Component) => { +const connect = mapStateToProps => { + return Component => { return class extends React.Component { static contextTypes = { store: PropTypes.object - } + }; componentDidMount() { this.unsubscribe = this.context.store.subscribe(() => { - this.forceUpdate() - }) + this.forceUpdate(); + }); } componentWillUnmount() { - this.unsubscribe() + this.unsubscribe(); } render() { - const props = mapStateToProps(this.context.store.getState()) + const props = mapStateToProps(this.context.store.getState()); return ( { {...props} dispatch={this.context.store.dispatch} /> - ) + ); } - } - } -} + }; + }; +}; -export default connect +export default connect; diff --git a/subjects/18-Mini-Redux/solution/mini-redux/createStore.js b/subjects/18-Mini-Redux/solution/mini-redux/createStore.js new file mode 100644 index 000000000..baf748301 --- /dev/null +++ b/subjects/18-Mini-Redux/solution/mini-redux/createStore.js @@ -0,0 +1,27 @@ +const createStore = reducer => { + let state = reducer(undefined, { type: "@INIT" }); + let listeners = []; + + const getState = () => state; + + const dispatch = action => { + state = reducer(state, action); + listeners.forEach(listener => listener()); + }; + + const subscribe = listener => { + listeners.push(listener); + + return () => { + listeners = listeners.filter(item => item !== listener); + }; + }; + + return { + getState, + dispatch, + subscribe + }; +}; + +export default createStore; diff --git a/subjects/Select/exercise.js b/subjects/19-Select/exercise.js similarity index 55% rename from subjects/Select/exercise.js rename to subjects/19-Select/exercise.js index 63a9087a0..08aec1fe7 100644 --- a/subjects/Select/exercise.js +++ b/subjects/19-Select/exercise.js @@ -1,83 +1,83 @@ -import React, { PropTypes } from 'react' -import { render } from 'react-dom' -import './styles.css' - -const { func, any } = PropTypes - - //////////////////////////////////////////////////////////////////////////////// -// Requirements +// Exercise: // -// Make this work like a normal +// Make this work like a normal this.setState({ selectValue })} - > + -

    Uncontrolled

    - this.setState({ selectValue: value })} + >
    - ) + ); } } -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/19-Select/solution.js b/subjects/19-Select/solution.js new file mode 100644 index 000000000..c447cc698 --- /dev/null +++ b/subjects/19-Select/solution.js @@ -0,0 +1,137 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// Make this work like a normal component!" + ); + } + } + + render() { + const { value } = this.isControlled() ? this.props : this.state; + + let label; + const children = React.Children.map(this.props.children, child => { + if (child.props.value === value) { + label = child.props.children; + } + + return React.cloneElement(child, { + onSelect: () => { + if (this.isControlled()) { + if (this.props.onChange) { + this.props.onChange(child.props.value); + } + } else { + this.setState({ value: child.props.value }, () => { + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + }); + } + } + }); + }); + + return ( +
    +
    + {label} +
    + {this.state.showOptions && ( +
    {children}
    + )} +
    + ); + } +} + +class Option extends React.Component { + render() { + return ( +
    + {this.props.children} +
    + ); + } +} + +class App extends React.Component { + state = { + selectValue: "dosa" + }; + + setToMintChutney = () => { + this.setState({ selectValue: "mint-chutney" }); + }; + + render() { + return ( +
    +

    Select

    + +

    Uncontrolled

    + + + +

    Controlled

    + +
    {JSON.stringify(this.state, null, 2)}
    +

    + +

    + + +
    + ); + } +} + +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Select/styles.css b/subjects/19-Select/styles.css similarity index 100% rename from subjects/Select/styles.css rename to subjects/19-Select/styles.css diff --git a/subjects/Slider/exercise.js b/subjects/20-Slider/exercise.js similarity index 58% rename from subjects/Slider/exercise.js rename to subjects/20-Slider/exercise.js index 55aa6e102..302ba4ec6 100644 --- a/subjects/Slider/exercise.js +++ b/subjects/20-Slider/exercise.js @@ -1,7 +1,8 @@ -import React, { PropTypes } from 'react' -import { render } from 'react-dom' -import CSSTransitionGroup from 'react-addons-css-transition-group' -import './styles.css' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import CSSTransitionGroup from "react-addons-css-transition-group"; +import "./styles.css"; class Slider extends React.Component { static propTypes = { @@ -9,71 +10,59 @@ class Slider extends React.Component { autoplay: PropTypes.bool, onTogglePlay: PropTypes.func, duration: PropTypes.number - } + }; static defaultProps = { autoplay: false, duration: 5000, initialIndex: 0 - } + }; render() { - return ( -
    - ) + return
    ; } } class SliderStage extends React.Component { render() { - return ( -
    - ) + return
    ; } } class Slide extends React.Component { render() { - return + return ; } } class SliderControls extends React.Component { render() { - return ( -
    - ) + return
    ; } } class SliderPrevious extends React.Component { render() { - return ( -
    - ) + ); } } -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Slider/slides/chicken-nuggets.png b/subjects/20-Slider/slides/chicken-nuggets.png similarity index 100% rename from subjects/Slider/slides/chicken-nuggets.png rename to subjects/20-Slider/slides/chicken-nuggets.png diff --git a/subjects/Slider/slides/hamburger.png b/subjects/20-Slider/slides/hamburger.png similarity index 100% rename from subjects/Slider/slides/hamburger.png rename to subjects/20-Slider/slides/hamburger.png diff --git a/subjects/Slider/slides/mcmuffin.png b/subjects/20-Slider/slides/mcmuffin.png similarity index 100% rename from subjects/Slider/slides/mcmuffin.png rename to subjects/20-Slider/slides/mcmuffin.png diff --git a/subjects/Slider/solution.js b/subjects/20-Slider/solution.js similarity index 58% rename from subjects/Slider/solution.js rename to subjects/20-Slider/solution.js index 9b551eb95..f0b363713 100644 --- a/subjects/Slider/solution.js +++ b/subjects/20-Slider/solution.js @@ -1,7 +1,8 @@ -import React, { PropTypes } from 'react' -import { render } from 'react-dom' -import CSSTransitionGroup from 'react-addons-css-transition-group' -import './styles.css' +import React from "react"; +import ReactDOM from "react-dom"; +import PropTypes from "prop-types"; +import CSSTransitionGroup from "react-addons-css-transition-group"; +import "./styles.css"; class Slider extends React.Component { static propTypes = { @@ -9,13 +10,13 @@ class Slider extends React.Component { autoplay: PropTypes.bool, onTogglePlay: PropTypes.func, duration: PropTypes.number - } + }; static defaultProps = { autoplay: false, duration: 5000, initialIndex: 0 - } + }; static childContextTypes = { currentIndex: PropTypes.number, @@ -23,69 +24,64 @@ class Slider extends React.Component { prev: PropTypes.func, registerCount: PropTypes.func, toggleAutoPlay: PropTypes.func - } + }; state = { currentIndex: this.props.initialIndex || 0 - } + }; - slideCount = null - interval = null + slideCount = null; + interval = null; getChildContext() { return { currentIndex: this.state.currentIndex, next: this.next, prev: this.prev, - registerCount: (count) => this.slideCount = count, + registerCount: count => (this.slideCount = count), toggleAutoPlay: () => this.toggleAutoPlay() - } + }; } componentDidMount() { - if (this.props.autoPlay) - this.startAutoPlay() + if (this.props.autoPlay) this.startAutoPlay(); } toggleAutoPlay() { - if (this.interval) - this.stopAutoPlay() - else - this.startAutoPlay() + if (this.interval) this.stopAutoPlay(); + else this.startAutoPlay(); - this.props.onTogglePlay(!!this.interval) + this.props.onTogglePlay(!!this.interval); } startAutoPlay() { - this.interval = setInterval(this.next, this.props.duration) + this.interval = setInterval(this.next, this.props.duration); } stopAutoPlay() { - clearInterval(this.interval) - this.interval = null + clearInterval(this.interval); + this.interval = null; } prev = () => { - let { currentIndex } = this.state + let { currentIndex } = this.state; - currentIndex-- + currentIndex--; - if (currentIndex < 0) - currentIndex = this.slideCount - 1 + if (currentIndex < 0) currentIndex = this.slideCount - 1; - this.setState({ currentIndex }) - } + this.setState({ currentIndex }); + }; next = () => { - let { currentIndex } = this.state + let { currentIndex } = this.state; - currentIndex++ + currentIndex++; - if (currentIndex === this.slideCount) - currentIndex = 0 + if (currentIndex === this.slideCount) currentIndex = 0; - this.setState({ currentIndex }) - } + this.setState({ currentIndex }); + }; render() { const { @@ -94,10 +90,8 @@ class Slider extends React.Component { onTogglePlay, duration, ...props - } = this.props - return ( -
    - ) + } = this.props; + return
    ; } } @@ -105,16 +99,16 @@ class SliderStage extends React.Component { static contextTypes = { currentIndex: PropTypes.number.isRequired, registerCount: PropTypes.func.isRequired - } + }; componentDidMount() { this.context.registerCount( React.Children.count(this.props.children) - ) + ); } render() { - const style = { ...this.props.style, position: 'relative' } + const style = { ...this.props.style, position: "relative" }; return (
    @@ -123,74 +117,71 @@ class SliderStage extends React.Component { transitionEnterTimeout={600} transitionLeaveTimeout={600} > - {React.cloneElement(this.props.children[this.context.currentIndex], { - key: this.context.currentIndex - })} + {React.cloneElement( + this.props.children[this.context.currentIndex], + { + key: this.context.currentIndex + } + )}
    - ) + ); } } class Slide extends React.Component { componentWillMount() { // preload 'em - new Image().src = this.props.src + new Image().src = this.props.src; } render() { - return + return ; } } class SliderControls extends React.Component { render() { - return ( -
    - ) + return
    ; } } class SliderPrevious extends React.Component { static contextTypes = { prev: PropTypes.func.isRequired - } + }; render() { - return ( -
    - ) + ); } } -render(, document.getElementById('app')) +ReactDOM.render(, document.getElementById("app")); diff --git a/subjects/Slider/styles.css b/subjects/20-Slider/styles.css similarity index 100% rename from subjects/Slider/styles.css rename to subjects/20-Slider/styles.css diff --git a/subjects/21-Testing/components/ContentToggle.css b/subjects/21-Testing/components/ContentToggle.css new file mode 100644 index 000000000..95728edc9 --- /dev/null +++ b/subjects/21-Testing/components/ContentToggle.css @@ -0,0 +1,30 @@ +.content-toggle { + border: 1px solid #ccc; + margin: 10px 0; + background: #fff; +} + +.content-toggle-summary { + cursor: pointer; + border: none; + background: inherit; + display: block; + width: 100%; + text-align: left; + margin: 0; + padding: 10px; + font: inherit; + border-bottom: solid 1px transparent; +} +.content-toggle-summary:before { + display: inline-block; + width: 1em; + content: "▸"; +} +.content-toggle-summary-open:before { + content: "▾"; +} + +.content-toggle-details { + padding: 0 10px; +} diff --git a/subjects/21-Testing/components/ContentToggle.js b/subjects/21-Testing/components/ContentToggle.js new file mode 100644 index 000000000..445e8d6ac --- /dev/null +++ b/subjects/21-Testing/components/ContentToggle.js @@ -0,0 +1,32 @@ +import "./ContentToggle.css"; + +import React from "react"; + +class ContentToggle extends React.Component { + handleClick = () => { + if (this.props.onToggle) { + this.props.onToggle(!this.props.isOpen); + } + }; + + render() { + let summaryClassName = "content-toggle-summary"; + + if (this.props.isOpen) { + summaryClassName += " content-toggle-summary-open"; + } + + return ( +
    + +
    + {this.props.isOpen && this.props.children} +
    +
    + ); + } +} + +export default ContentToggle; diff --git a/subjects/21-Testing/components/Droppable.js b/subjects/21-Testing/components/Droppable.js new file mode 100644 index 000000000..c9989a7e7 --- /dev/null +++ b/subjects/21-Testing/components/Droppable.js @@ -0,0 +1,93 @@ +import React from "react"; + +const style = { + border: "3px solid #ccc", + padding: 50, + margin: 10, + width: 200, + textAlign: "center", + display: "inline-block" +}; + +function readFilesFromEvent(event, cb) { + const files = []; + let needToLoadCounter = 0; + + for (let i = 0; i < event.dataTransfer.files.length; i++) { + let file = event.dataTransfer.files[i]; + if (!file.type.match("image.*")) continue; + needToLoadCounter++; + let reader = new FileReader(); + reader.onload = fileEvent => { + needToLoadCounter--; + files.push({ + name: file.name, + data: fileEvent.target.result + }); + maybeFinish(); + }; + reader.readAsDataURL(file); + } + + maybeFinish(); + + function maybeFinish() { + if (needToLoadCounter === 0) cb(files); + } +} + +class Droppable extends React.Component { + state = { + acceptDrop: false, + files: null + }; + + handleDragOver = event => { + if (event.dataTransfer.types[0] === "Files") { + event.preventDefault(); + this.setState({ + acceptDrop: true + }); + } + }; + + handleDrop = event => { + event.stopPropagation(); + event.preventDefault(); + this.setState({ + acceptDrop: false + }); + readFilesFromEvent(event, files => { + this.setState({ files }); + }); + }; + + render() { + const { acceptDrop, files } = this.state; + + return ( +
    + {acceptDrop ? "Drop it!" : "Drag a file here"} + {files && + files.map(file => ( +
    +

    + {file.name} +

    + +
    + ))} +
    + ); + } +} + +export default Droppable; diff --git a/subjects/21-Testing/components/StatefulContentToggle.js b/subjects/21-Testing/components/StatefulContentToggle.js new file mode 100644 index 000000000..fbb6fb8e5 --- /dev/null +++ b/subjects/21-Testing/components/StatefulContentToggle.js @@ -0,0 +1,18 @@ +import React from "react"; +import ContentToggle from "./ContentToggle"; + +class StatefulContentToggle extends React.Component { + state = { isOpen: false }; + + render() { + return ( + this.setState({ isOpen })} + /> + ); + } +} + +export default StatefulContentToggle; diff --git a/subjects/21-Testing/components/Tabs.js b/subjects/21-Testing/components/Tabs.js new file mode 100644 index 000000000..07ebed478 --- /dev/null +++ b/subjects/21-Testing/components/Tabs.js @@ -0,0 +1,76 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const tabType = PropTypes.shape({ + label: PropTypes.string.isRequired, + content: PropTypes.string.isRequired +}); + +const styles = {}; + +styles.tab = { + display: "inline-block", + padding: 10, + margin: 10, + borderBottom: "4px solid", + borderBottomColor: "#ccc", + cursor: "pointer" +}; + +styles.activeTab = { + ...styles.tab, + borderBottomColor: "#000" +}; + +styles.panel = { + padding: 10 +}; + +class Tabs extends React.Component { + static propTypes = { + data: PropTypes.arrayOf(tabType) + }; + + state = { + activeTabIndex: 0 + }; + + selectTabIndex(activeTabIndex) { + this.setState({ activeTabIndex }); + } + + render() { + const { data } = this.props; + const { activeTabIndex } = this.state; + + const tabs = data.map((tab, index) => { + const isActive = index === activeTabIndex; + const style = isActive ? styles.activeTab : styles.tab; + + return ( +
    this.selectTabIndex(index)} + > + {tab.label} +
    + ); + }); + + const activeTab = data[activeTabIndex]; + const content = activeTab && activeTab.content; + + return ( +
    + {tabs} +
    + {content} +
    +
    + ); + } +} + +export default Tabs; diff --git a/subjects/21-Testing/exercise.js b/subjects/21-Testing/exercise.js new file mode 100644 index 000000000..bf34ce558 --- /dev/null +++ b/subjects/21-Testing/exercise.js @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Fill in the test stubs to make the tests pass +//////////////////////////////////////////////////////////////////////////////// +import "./mocha-setup"; + +import React from "react"; +import ReactDOM from "react-dom"; +import { Simulate } from "react-dom/test-utils"; +import expect from "expect"; + +import Tabs from "./components/Tabs"; + +const FixtureData = [ + { + label: "USA", + content: "Land of the Free, Home of the brave" + }, + { label: "Brazil", content: "Sunshine, beaches, and Carnival" }, + { label: "Russia", content: "World Cup 2018!" } +]; + +describe("when is rendered", () => { + let node; + beforeEach(() => { + node = document.createElement("div"); + ReactDOM.render(, node); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(node); + }); + + it("renders the USA tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[0].innerText).toEqual( + FixtureData[0].label, + "USA tab was not rendered" + ); + }); + + it("renders the Brazil tab"); + + it("renders the Russia tab"); + + it("activates the first tab"); + + it("does not activate the second tab"); + + describe("after clicking the second tab", () => { + beforeEach(() => { + // TODO: simulate a click on the second tab + }); + + it("activates the second tab"); + + it("deactivates the first tab"); + + it("puts the correct content in the panel"); + }); +}); diff --git a/subjects/21-Testing/lecture.js b/subjects/21-Testing/lecture.js new file mode 100644 index 000000000..ab20b3b1e --- /dev/null +++ b/subjects/21-Testing/lecture.js @@ -0,0 +1,122 @@ +import "./mocha-setup"; + +import React from "react"; +import ReactDOM from "react-dom"; +import ReactDOMServer from "react-dom/server"; +import { Simulate } from "react-dom/test-utils"; +import expect from "expect"; + +import ContentToggle from "./components/ContentToggle"; +import StatefulContentToggle from "./components/StatefulContentToggle"; +import Tabs from "./components/Tabs"; +import Droppable from "./components/Droppable"; + +// describe("ContentToggle", () => { +// let div; +// beforeEach(() => { +// div = document.createElement("div"); +// }); + +// it("displays the summary", () => { +// ReactDOM.render(, div); + +// expect(div.innerHTML).toMatch( +// /The Summary/, +// '"The Summary" was not found in HTML' +// ); +// }); + +// describe("isOpen prop", () => { +// it("does not display children when false", () => { +// ReactDOM.render( +// +//

    Cheers

    +//
    , +// div +// ); + +// expect(div.innerHTML).toNotMatch( +// /Cheers/, +// '"Cheers" was found in HTML' +// ); +// }); + +// it("defaults to false", () => { +// ReactDOM.render( +// +//

    Cheers

    +//
    , +// div +// ); + +// expect(div.innerHTML).toNotMatch( +// /Cheers/, +// '"Cheers" was found in HTML' +// ); +// }); + +// it("displays children when true", () => { +// ReactDOM.render( +// +//

    Cheers

    +//
    , +// div +// ); + +// expect(div.innerHTML).toMatch( +// /Cheers/, +// '"Cheers" was not found in HTML' +// ); +// }); +// }); +// }); + +// describe("StatefulContentToggle", () => { +// let div; +// beforeEach(() => { +// div = document.createElement("div"); +// }); + +// it("opens when clicked", () => { +// ReactDOM.render( +// +//

    The Content

    +//
    , +// div +// ); + +// Simulate.click(div.querySelector("button")); + +// expect(div.innerHTML).toMatch( +// /The Content/, +// '"The Content" was not found in HTML' +// ); +// }); +// }); + +// describe("Droppable", () => { +// let div; +// beforeEach(() => { +// div = document.createElement("div"); +// }); + +// it("accepts files", () => { +// ReactDOM.render(, div); +// Simulate.dragOver(div.querySelector("div.Droppable"), { +// dataTransfer: { types: ["Files"] } +// }); +// expect(div.innerHTML).toMatch( +// /Drop it!/, +// '"Drop it!" was not found in HTML' +// ); +// }); +// }); + +// - render to a node that isn't in the dom +// - match innerHTML +// - renderToString +// - Simulate +// - actually render something +// - getDefaultProps for application modules +// - shallow renderer +// - assert on vdom diff --git a/subjects/21-Testing/mocha-browser.js b/subjects/21-Testing/mocha-browser.js new file mode 100644 index 000000000..483e0dc42 --- /dev/null +++ b/subjects/21-Testing/mocha-browser.js @@ -0,0 +1,7108 @@ +(function() { + // CommonJS require() + + function require(p) { + var path = require.resolve(p), + mod = require.modules[path]; + if (!mod) throw new Error('failed to require "' + p + '"'); + if (!mod.exports) { + mod.exports = {}; + mod.call(mod.exports, mod, mod.exports, require.relative(path)); + } + return mod.exports; + } + + require.modules = {}; + + require.resolve = function(path) { + var orig = path, + reg = path + ".js", + index = path + "/index.js"; + return ( + (require.modules[reg] && reg) || + (require.modules[index] && index) || + orig + ); + }; + + require.register = function(path, fn) { + require.modules[path] = fn; + }; + + require.relative = function(parent) { + return function(p) { + if ("." != p.charAt(0)) return require(p); + + var path = parent.split("/"), + segs = p.split("/"); + path.pop(); + + for (var i = 0; i < segs.length; i++) { + var seg = segs[i]; + if (".." == seg) path.pop(); + else if ("." != seg) path.push(seg); + } + + return require(path.join("/")); + }; + }; + + require.register("browser/debug.js", function( + module, + exports, + require + ) { + module.exports = function(type) { + return function() {}; + }; + }); // module: browser/debug.js + + require.register("browser/diff.js", function( + module, + exports, + require + ) { + /* See LICENSE file for terms of use */ + + /* + * Text diff implementation. + * + * This library supports the following APIS: + * JsDiff.diffChars: Character by character diff + * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace + * JsDiff.diffLines: Line based diff + * + * JsDiff.diffCss: Diff targeted at CSS content + * + * These methods are based on the implementation proposed in + * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 + */ + var JsDiff = (function() { + /*jshint maxparams: 5*/ + function clonePath(path) { + return { + newPos: path.newPos, + components: path.components.slice(0) + }; + } + function removeEmpty(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + function escapeHTML(s) { + var n = s; + n = n.replace(/&/g, "&"); + n = n.replace(//g, ">"); + n = n.replace(/"/g, """); + + return n; + } + + var Diff = function(ignoreWhitespace) { + this.ignoreWhitespace = ignoreWhitespace; + }; + Diff.prototype = { + diff: function(oldString, newString) { + // Handle the identity case (this is due to unrolling editLength == 0 + if (newString === oldString) { + return [{ value: newString }]; + } + if (!newString) { + return [{ value: oldString, removed: true }]; + } + if (!oldString) { + return [{ value: newString, added: true }]; + } + + newString = this.tokenize(newString); + oldString = this.tokenize(oldString); + + var newLen = newString.length, + oldLen = oldString.length; + var maxEditLength = newLen + oldLen; + var bestPath = [{ newPos: -1, components: [] }]; + + // Seed editLength = 0 + var oldPos = this.extractCommon( + bestPath[0], + newString, + oldString, + 0 + ); + if ( + bestPath[0].newPos + 1 >= newLen && + oldPos + 1 >= oldLen + ) { + return bestPath[0].components; + } + + for ( + var editLength = 1; + editLength <= maxEditLength; + editLength++ + ) { + for ( + var diagonalPath = -1 * editLength; + diagonalPath <= editLength; + diagonalPath += 2 + ) { + var basePath; + var addPath = bestPath[diagonalPath - 1], + removePath = bestPath[diagonalPath + 1]; + oldPos = + (removePath ? removePath.newPos : 0) - diagonalPath; + if (addPath) { + // No one else is going to attempt to use this value, clear it + bestPath[diagonalPath - 1] = undefined; + } + + var canAdd = addPath && addPath.newPos + 1 < newLen; + var canRemove = + removePath && 0 <= oldPos && oldPos < oldLen; + if (!canAdd && !canRemove) { + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the new string is the farthest from the origin + // and does not pass the bounds of the diff graph + if ( + !canAdd || + (canRemove && addPath.newPos < removePath.newPos) + ) { + basePath = clonePath(removePath); + this.pushComponent( + basePath.components, + oldString[oldPos], + undefined, + true + ); + } else { + basePath = clonePath(addPath); + basePath.newPos++; + this.pushComponent( + basePath.components, + newString[basePath.newPos], + true, + undefined + ); + } + + var oldPos = this.extractCommon( + basePath, + newString, + oldString, + diagonalPath + ); + + if ( + basePath.newPos + 1 >= newLen && + oldPos + 1 >= oldLen + ) { + return basePath.components; + } else { + bestPath[diagonalPath] = basePath; + } + } + } + }, + + pushComponent: function(components, value, added, removed) { + var last = components[components.length - 1]; + if ( + last && + last.added === added && + last.removed === removed + ) { + // We need to clone here as the component clone operation is just + // as shallow array clone + components[components.length - 1] = { + value: this.join(last.value, value), + added: added, + removed: removed + }; + } else { + components.push({ + value: value, + added: added, + removed: removed + }); + } + }, + extractCommon: function( + basePath, + newString, + oldString, + diagonalPath + ) { + var newLen = newString.length, + oldLen = oldString.length, + newPos = basePath.newPos, + oldPos = newPos - diagonalPath; + while ( + newPos + 1 < newLen && + oldPos + 1 < oldLen && + this.equals(newString[newPos + 1], oldString[oldPos + 1]) + ) { + newPos++; + oldPos++; + + this.pushComponent( + basePath.components, + newString[newPos], + undefined, + undefined + ); + } + basePath.newPos = newPos; + return oldPos; + }, + + equals: function(left, right) { + var reWhitespace = /\S/; + if ( + this.ignoreWhitespace && + !reWhitespace.test(left) && + !reWhitespace.test(right) + ) { + return true; + } else { + return left === right; + } + }, + join: function(left, right) { + return left + right; + }, + tokenize: function(value) { + return value; + } + }; + + var CharDiff = new Diff(); + + var WordDiff = new Diff(true); + var WordWithSpaceDiff = new Diff(); + WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) { + return removeEmpty(value.split(/(\s+|\b)/)); + }; + + var CssDiff = new Diff(true); + CssDiff.tokenize = function(value) { + return removeEmpty(value.split(/([{}:;,]|\s+)/)); + }; + + var LineDiff = new Diff(); + LineDiff.tokenize = function(value) { + var retLines = [], + lines = value.split(/^/m); + + for (var i = 0; i < lines.length; i++) { + var line = lines[i], + lastLine = lines[i - 1]; + + // Merge lines that may contain windows new lines + if ( + line == "\n" && + lastLine && + lastLine[lastLine.length - 1] === "\r" + ) { + retLines[retLines.length - 1] += "\n"; + } else if (line) { + retLines.push(line); + } + } + + return retLines; + }; + + return { + Diff: Diff, + + diffChars: function(oldStr, newStr) { + return CharDiff.diff(oldStr, newStr); + }, + diffWords: function(oldStr, newStr) { + return WordDiff.diff(oldStr, newStr); + }, + diffWordsWithSpace: function(oldStr, newStr) { + return WordWithSpaceDiff.diff(oldStr, newStr); + }, + diffLines: function(oldStr, newStr) { + return LineDiff.diff(oldStr, newStr); + }, + + diffCss: function(oldStr, newStr) { + return CssDiff.diff(oldStr, newStr); + }, + + createPatch: function( + fileName, + oldStr, + newStr, + oldHeader, + newHeader + ) { + var ret = []; + + ret.push("Index: " + fileName); + ret.push( + "===================================================================" + ); + ret.push( + "--- " + + fileName + + (typeof oldHeader === "undefined" ? "" : "\t" + oldHeader) + ); + ret.push( + "+++ " + + fileName + + (typeof newHeader === "undefined" ? "" : "\t" + newHeader) + ); + + var diff = LineDiff.diff(oldStr, newStr); + if (!diff[diff.length - 1].value) { + diff.pop(); // Remove trailing newline add + } + diff.push({ value: "", lines: [] }); // Append an empty value to make cleanup easier + + function contextLines(lines) { + return lines.map(function(entry) { + return " " + entry; + }); + } + function eofNL(curRange, i, current) { + var last = diff[diff.length - 2], + isLast = i === diff.length - 2, + isLastOfType = + i === diff.length - 3 && + (current.added !== last.added || + current.removed !== last.removed); + + // Figure out if this is the last line for the given file and missing NL + if ( + !/\n$/.test(current.value) && + (isLast || isLastOfType) + ) { + curRange.push("\\ No newline at end of file"); + } + } + + var oldRangeStart = 0, + newRangeStart = 0, + curRange = [], + oldLine = 1, + newLine = 1; + for (var i = 0; i < diff.length; i++) { + var current = diff[i], + lines = + current.lines || + current.value.replace(/\n$/, "").split("\n"); + current.lines = lines; + + if (current.added || current.removed) { + if (!oldRangeStart) { + var prev = diff[i - 1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + + if (prev) { + curRange = contextLines(prev.lines.slice(-4)); + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + curRange.push.apply( + curRange, + lines.map(function(entry) { + return (current.added ? "+" : "-") + entry; + }) + ); + eofNL(curRange, i, current); + + if (current.added) { + newLine += lines.length; + } else { + oldLine += lines.length; + } + } else { + if (oldRangeStart) { + // Close out any changes that have been output (or join overlapping) + if (lines.length <= 8 && i < diff.length - 2) { + // Overlapping + curRange.push.apply(curRange, contextLines(lines)); + } else { + // end the range and output + var contextSize = Math.min(lines.length, 4); + ret.push( + "@@ -" + + oldRangeStart + + "," + + (oldLine - oldRangeStart + contextSize) + + " +" + + newRangeStart + + "," + + (newLine - newRangeStart + contextSize) + + " @@" + ); + ret.push.apply(ret, curRange); + ret.push.apply( + ret, + contextLines(lines.slice(0, contextSize)) + ); + if (lines.length <= 4) { + eofNL(ret, i, current); + } + + oldRangeStart = 0; + newRangeStart = 0; + curRange = []; + } + } + oldLine += lines.length; + newLine += lines.length; + } + } + + return ret.join("\n") + "\n"; + }, + + applyPatch: function(oldStr, uniDiff) { + var diffstr = uniDiff.split("\n"); + var diff = []; + var remEOFNL = false, + addEOFNL = false; + + for ( + var i = diffstr[0][0] === "I" ? 4 : 0; + i < diffstr.length; + i++ + ) { + if (diffstr[i][0] === "@") { + var meh = diffstr[i].split( + /@@ -(\d+),(\d+) \+(\d+),(\d+) @@/ + ); + diff.unshift({ + start: meh[3], + oldlength: meh[2], + oldlines: [], + newlength: meh[4], + newlines: [] + }); + } else if (diffstr[i][0] === "+") { + diff[0].newlines.push(diffstr[i].substr(1)); + } else if (diffstr[i][0] === "-") { + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if (diffstr[i][0] === " ") { + diff[0].newlines.push(diffstr[i].substr(1)); + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if (diffstr[i][0] === "\\") { + if (diffstr[i - 1][0] === "+") { + remEOFNL = true; + } else if (diffstr[i - 1][0] === "-") { + addEOFNL = true; + } + } + } + + var str = oldStr.split("\n"); + for (var i = diff.length - 1; i >= 0; i--) { + var d = diff[i]; + for (var j = 0; j < d.oldlength; j++) { + if (str[d.start - 1 + j] !== d.oldlines[j]) { + return false; + } + } + Array.prototype.splice.apply( + str, + [d.start - 1, +d.oldlength].concat(d.newlines) + ); + } + + if (remEOFNL) { + while (!str[str.length - 1]) { + str.pop(); + } + } else if (addEOFNL) { + str.push(""); + } + return str.join("\n"); + }, + + convertChangesToXML: function(changes) { + var ret = []; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + if (change.added) { + ret.push(""); + } else if (change.removed) { + ret.push(""); + } + + ret.push(escapeHTML(change.value)); + + if (change.added) { + ret.push(""); + } else if (change.removed) { + ret.push(""); + } + } + return ret.join(""); + }, + + // See: http://code.google.com/p/google-diff-match-patch/wiki/API + convertChangesToDMP: function(changes) { + var ret = [], + change; + for (var i = 0; i < changes.length; i++) { + change = changes[i]; + ret.push([ + change.added ? 1 : change.removed ? -1 : 0, + change.value + ]); + } + return ret; + } + }; + })(); + + if (typeof module !== "undefined") { + module.exports = JsDiff; + } + }); // module: browser/diff.js + + require.register("browser/escape-string-regexp.js", function( + module, + exports, + require + ) { + "use strict"; + + var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; + + module.exports = function(str) { + if (typeof str !== "string") { + throw new TypeError("Expected a string"); + } + + return str.replace(matchOperatorsRe, "\\$&"); + }; + }); // module: browser/escape-string-regexp.js + + require.register("browser/events.js", function( + module, + exports, + require + ) { + /** + * Module exports. + */ + + exports.EventEmitter = EventEmitter; + + /** + * Check if `obj` is an array. + */ + + function isArray(obj) { + return "[object Array]" == {}.toString.call(obj); + } + + /** + * Event emitter constructor. + * + * @api public + */ + + function EventEmitter() {} + + /** + * Adds a listener. + * + * @api public + */ + + EventEmitter.prototype.on = function(name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + /** + * Adds a volatile listener. + * + * @api public + */ + + EventEmitter.prototype.once = function(name, fn) { + var self = this; + + function on() { + self.removeListener(name, on); + fn.apply(this, arguments); + } + + on.listener = fn; + this.on(name, on); + + return this; + }; + + /** + * Removes a listener. + * + * @api public + */ + + EventEmitter.prototype.removeListener = function(name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if ( + list[i] === fn || + (list[i].listener && list[i].listener === fn) + ) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if ( + list === fn || + (list.listener && list.listener === fn) + ) { + delete this.$events[name]; + } + } + + return this; + }; + + /** + * Removes all listeners for an event. + * + * @api public + */ + + EventEmitter.prototype.removeAllListeners = function(name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; + }; + + /** + * Gets all listeners for a certain event. + * + * @api public + */ + + EventEmitter.prototype.listeners = function(name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; + }; + + /** + * Emits an event. + * + * @api public + */ + + EventEmitter.prototype.emit = function(name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = [].slice.call(arguments, 1); + + if ("function" == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; + }; + }); // module: browser/events.js + + require.register("browser/fs.js", function( + module, + exports, + require + ) {}); // module: browser/fs.js + + require.register("browser/glob.js", function( + module, + exports, + require + ) {}); // module: browser/glob.js + + require.register("browser/path.js", function( + module, + exports, + require + ) {}); // module: browser/path.js + + require.register("browser/progress.js", function( + module, + exports, + require + ) { + /** + * Expose `Progress`. + */ + + module.exports = Progress; + + /** + * Initialize a new `Progress` indicator. + */ + + function Progress() { + this.percent = 0; + this.size(0); + this.fontSize(11); + this.font("helvetica, arial, sans-serif"); + } + + /** + * Set progress size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + + Progress.prototype.size = function(n) { + this._size = n; + return this; + }; + + /** + * Set text to `str`. + * + * @param {String} str + * @return {Progress} for chaining + * @api public + */ + + Progress.prototype.text = function(str) { + this._text = str; + return this; + }; + + /** + * Set font size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + + Progress.prototype.fontSize = function(n) { + this._fontSize = n; + return this; + }; + + /** + * Set font `family`. + * + * @param {String} family + * @return {Progress} for chaining + */ + + Progress.prototype.font = function(family) { + this._font = family; + return this; + }; + + /** + * Update percentage to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + */ + + Progress.prototype.update = function(n) { + this.percent = n; + return this; + }; + + /** + * Draw on `ctx`. + * + * @param {CanvasRenderingContext2d} ctx + * @return {Progress} for chaining + */ + + Progress.prototype.draw = function(ctx) { + try { + var percent = Math.min(this.percent, 100), + size = this._size, + half = size / 2, + x = half, + y = half, + rad = half - 1, + fontSize = this._fontSize; + + ctx.font = fontSize + "px " + this._font; + + var angle = Math.PI * 2 * (percent / 100); + ctx.clearRect(0, 0, size, size); + + // outer circle + ctx.strokeStyle = "#9f9f9f"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, angle, false); + ctx.stroke(); + + // inner circle + ctx.strokeStyle = "#eee"; + ctx.beginPath(); + ctx.arc(x, y, rad - 1, 0, angle, true); + ctx.stroke(); + + // text + var text = this._text || (percent | 0) + "%", + w = ctx.measureText(text).width; + + ctx.fillText(text, x - w / 2 + 1, y + fontSize / 2 - 1); + } catch (ex) {} //don't fail if we can't render progress + return this; + }; + }); // module: browser/progress.js + + require.register("browser/tty.js", function( + module, + exports, + require + ) { + exports.isatty = function() { + return true; + }; + + exports.getWindowSize = function() { + if ("innerHeight" in global) { + return [global.innerHeight, global.innerWidth]; + } else { + // In a Web Worker, the DOM Window is not available. + return [640, 480]; + } + }; + }); // module: browser/tty.js + + require.register("context.js", function(module, exports, require) { + /** + * Expose `Context`. + */ + + module.exports = Context; + + /** + * Initialize a new `Context`. + * + * @api private + */ + + function Context() {} + + /** + * Set or get the context `Runnable` to `runnable`. + * + * @param {Runnable} runnable + * @return {Context} + * @api private + */ + + Context.prototype.runnable = function(runnable) { + if (0 == arguments.length) return this._runnable; + this.test = this._runnable = runnable; + return this; + }; + + /** + * Set test timeout `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + + Context.prototype.timeout = function(ms) { + if (arguments.length === 0) return this.runnable().timeout(); + this.runnable().timeout(ms); + return this; + }; + + /** + * Set test timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Context} self + * @api private + */ + + Context.prototype.enableTimeouts = function(enabled) { + this.runnable().enableTimeouts(enabled); + return this; + }; + + /** + * Set test slowness threshold `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + + Context.prototype.slow = function(ms) { + this.runnable().slow(ms); + return this; + }; + + /** + * Mark a test as skipped. + * + * @return {Context} self + * @api private + */ + + Context.prototype.skip = function() { + this.runnable().skip(); + return this; + }; + + /** + * Inspect the context void of `._runnable`. + * + * @return {String} + * @api private + */ + + Context.prototype.inspect = function() { + return JSON.stringify( + this, + function(key, val) { + if ("_runnable" == key) return; + if ("test" == key) return; + return val; + }, + 2 + ); + }; + }); // module: context.js + + require.register("hook.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var Runnable = require("./runnable"); + + /** + * Expose `Hook`. + */ + + module.exports = Hook; + + /** + * Initialize a new `Hook` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + + function Hook(title, fn) { + Runnable.call(this, title, fn); + this.type = "hook"; + } + + /** + * Inherit from `Runnable.prototype`. + */ + + function F() {} + F.prototype = Runnable.prototype; + Hook.prototype = new F(); + Hook.prototype.constructor = Hook; + + /** + * Get or set the test `err`. + * + * @param {Error} err + * @return {Error} + * @api public + */ + + Hook.prototype.error = function(err) { + if (0 == arguments.length) { + var err = this._error; + this._error = null; + return err; + } + + this._error = err; + }; + }); // module: hook.js + + require.register("interfaces/bdd.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Suite = require("../suite"), + Test = require("../test"), + utils = require("../utils"), + escapeRe = require("browser/escape-string-regexp"); + + /** + * BDD-style interface: + * + * describe('Array', function(){ + * describe('#indexOf()', function(){ + * it('should return -1 when not present', function(){ + * + * }); + * + * it('should return the index when present', function(){ + * + * }); + * }); + * }); + * + */ + + module.exports = function(suite) { + var suites = [suite]; + + suite.on("pre-require", function(context, file, mocha) { + var common = require("./common")(suites, context); + + context.before = common.before; + context.after = common.after; + context.beforeEach = common.beforeEach; + context.afterEach = common.afterEach; + context.run = mocha.options.delay && common.runWithSuite(suite); + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.describe = context.context = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending describe. + */ + + context.xdescribe = context.xcontext = context.describe.skip = function( + title, + fn + ) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive suite. + */ + + context.describe.only = function(title, fn) { + var suite = context.describe(title, fn); + mocha.grep(suite.fullTitle()); + return suite; + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.it = context.specify = function(title, fn) { + var suite = suites[0]; + if (suite.pending) fn = null; + var test = new Test(title, fn); + test.file = file; + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.it.only = function(title, fn) { + var test = context.it(title, fn); + var reString = "^" + escapeRe(test.fullTitle()) + "$"; + mocha.grep(new RegExp(reString)); + return test; + }; + + /** + * Pending test case. + */ + + context.xit = context.xspecify = context.it.skip = function( + title + ) { + context.it(title); + }; + }); + }; + }); // module: interfaces/bdd.js + + require.register("interfaces/common.js", function( + module, + exports, + require + ) { + /** + * Functions common to more than one interface + * @module lib/interfaces/common + */ + + "use strict"; + + module.exports = function(suites, context) { + return { + /** + * This is only present if flag --delay is passed into Mocha. It triggers + * root suite execution. Returns a function which runs the root suite. + */ + runWithSuite: function runWithSuite(suite) { + return function run() { + suite.run(); + }; + }, + + /** + * Execute before running tests. + */ + before: function(name, fn) { + suites[0].beforeAll(name, fn); + }, + + /** + * Execute after running tests. + */ + after: function(name, fn) { + suites[0].afterAll(name, fn); + }, + + /** + * Execute before each test case. + */ + beforeEach: function(name, fn) { + suites[0].beforeEach(name, fn); + }, + + /** + * Execute after each test case. + */ + afterEach: function(name, fn) { + suites[0].afterEach(name, fn); + }, + + test: { + /** + * Pending test case. + */ + skip: function(title) { + context.test(title); + } + } + }; + }; + }); // module: interfaces/common.js + + require.register("interfaces/exports.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Suite = require("../suite"), + Test = require("../test"); + + /** + * TDD-style interface: + * + * exports.Array = { + * '#indexOf()': { + * 'should return -1 when the value is not present': function(){ + * + * }, + * + * 'should return the correct index when the value is present': function(){ + * + * } + * } + * }; + * + */ + + module.exports = function(suite) { + var suites = [suite]; + + suite.on("require", visit); + + function visit(obj, file) { + var suite; + for (var key in obj) { + if ("function" == typeof obj[key]) { + var fn = obj[key]; + switch (key) { + case "before": + suites[0].beforeAll(fn); + break; + case "after": + suites[0].afterAll(fn); + break; + case "beforeEach": + suites[0].beforeEach(fn); + break; + case "afterEach": + suites[0].afterEach(fn); + break; + default: + var test = new Test(key, fn); + test.file = file; + suites[0].addTest(test); + } + } else { + suite = Suite.create(suites[0], key); + suites.unshift(suite); + visit(obj[key]); + suites.shift(); + } + } + } + }; + }); // module: interfaces/exports.js + + require.register("interfaces/index.js", function( + module, + exports, + require + ) { + exports.bdd = require("./bdd"); + exports.tdd = require("./tdd"); + exports.qunit = require("./qunit"); + exports.exports = require("./exports"); + }); // module: interfaces/index.js + + require.register("interfaces/qunit.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Suite = require("../suite"), + Test = require("../test"), + escapeRe = require("browser/escape-string-regexp"), + utils = require("../utils"); + + /** + * QUnit-style interface: + * + * suite('Array'); + * + * test('#length', function(){ + * var arr = [1,2,3]; + * ok(arr.length == 3); + * }); + * + * test('#indexOf()', function(){ + * var arr = [1,2,3]; + * ok(arr.indexOf(1) == 0); + * ok(arr.indexOf(2) == 1); + * ok(arr.indexOf(3) == 2); + * }); + * + * suite('String'); + * + * test('#length', function(){ + * ok('foo'.length == 3); + * }); + * + */ + + module.exports = function(suite) { + var suites = [suite]; + + suite.on("pre-require", function(context, file, mocha) { + var common = require("./common")(suites, context); + + context.before = common.before; + context.after = common.after; + context.beforeEach = common.beforeEach; + context.afterEach = common.afterEach; + context.run = mocha.options.delay && common.runWithSuite(suite); + /** + * Describe a "suite" with the given `title`. + */ + + context.suite = function(title) { + if (suites.length > 1) suites.shift(); + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + return suite; + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn) { + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn) { + var test = new Test(title, fn); + test.file = file; + suites[0].addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn) { + var test = context.test(title, fn); + var reString = "^" + escapeRe(test.fullTitle()) + "$"; + mocha.grep(new RegExp(reString)); + }; + + context.test.skip = common.test.skip; + }); + }; + }); // module: interfaces/qunit.js + + require.register("interfaces/tdd.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Suite = require("../suite"), + Test = require("../test"), + escapeRe = require("browser/escape-string-regexp"), + utils = require("../utils"); + + /** + * TDD-style interface: + * + * suite('Array', function(){ + * suite('#indexOf()', function(){ + * suiteSetup(function(){ + * + * }); + * + * test('should return -1 when not present', function(){ + * + * }); + * + * test('should return the index when present', function(){ + * + * }); + * + * suiteTeardown(function(){ + * + * }); + * }); + * }); + * + */ + + module.exports = function(suite) { + var suites = [suite]; + + suite.on("pre-require", function(context, file, mocha) { + var common = require("./common")(suites, context); + + context.setup = common.beforeEach; + context.teardown = common.afterEach; + context.suiteSetup = common.before; + context.suiteTeardown = common.after; + context.run = mocha.options.delay && common.runWithSuite(suite); + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.suite = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.file = file; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending suite. + */ + context.suite.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn) { + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn) { + var suite = suites[0]; + if (suite.pending) fn = null; + var test = new Test(title, fn); + test.file = file; + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn) { + var test = context.test(title, fn); + var reString = "^" + escapeRe(test.fullTitle()) + "$"; + mocha.grep(new RegExp(reString)); + }; + + context.test.skip = common.test.skip; + }); + }; + }); // module: interfaces/tdd.js + + require.register("mocha.js", function(module, exports, require) { + /*! + * mocha + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + + /** + * Module dependencies. + */ + + var path = require("browser/path"), + escapeRe = require("browser/escape-string-regexp"), + utils = require("./utils"); + + /** + * Expose `Mocha`. + */ + + exports = module.exports = Mocha; + + /** + * To require local UIs and reporters when running in node. + */ + + if ( + typeof process !== "undefined" && + typeof process.cwd === "function" + ) { + var join = path.join, + cwd = process.cwd(); + module.paths.push(cwd, join(cwd, "node_modules")); + } + + /** + * Expose internals. + */ + + exports.utils = utils; + exports.interfaces = require("./interfaces"); + exports.reporters = require("./reporters"); + exports.Runnable = require("./runnable"); + exports.Context = require("./context"); + exports.Runner = require("./runner"); + exports.Suite = require("./suite"); + exports.Hook = require("./hook"); + exports.Test = require("./test"); + + /** + * Return image `name` path. + * + * @param {String} name + * @return {String} + * @api private + */ + + function image(name) { + return __dirname + "/../images/" + name + ".png"; + } + + /** + * Setup mocha with `options`. + * + * Options: + * + * - `ui` name "bdd", "tdd", "exports" etc + * - `reporter` reporter instance, defaults to `mocha.reporters.spec` + * - `globals` array of accepted globals + * - `timeout` timeout in milliseconds + * - `bail` bail on the first test failure + * - `slow` milliseconds to wait before considering a test slow + * - `ignoreLeaks` ignore global leaks + * - `fullTrace` display the full stack-trace on failing + * - `grep` string or regexp to filter tests with + * + * @param {Object} options + * @api public + */ + + function Mocha(options) { + options = options || {}; + this.files = []; + this.options = options; + if (options.grep) this.grep(new RegExp(options.grep)); + if (options.fgrep) this.grep(options.fgrep); + this.suite = new exports.Suite("", new exports.Context()); + this.ui(options.ui); + this.bail(options.bail); + this.reporter(options.reporter, options.reporterOptions); + if (null != options.timeout) this.timeout(options.timeout); + this.useColors(options.useColors); + if (options.enableTimeouts !== null) + this.enableTimeouts(options.enableTimeouts); + if (options.slow) this.slow(options.slow); + + this.suite.on("pre-require", function(context) { + exports.afterEach = context.afterEach || context.teardown; + exports.after = context.after || context.suiteTeardown; + exports.beforeEach = context.beforeEach || context.setup; + exports.before = context.before || context.suiteSetup; + exports.describe = context.describe || context.suite; + exports.it = context.it || context.test; + exports.setup = context.setup || context.beforeEach; + exports.suiteSetup = context.suiteSetup || context.before; + exports.suiteTeardown = context.suiteTeardown || context.after; + exports.suite = context.suite || context.describe; + exports.teardown = context.teardown || context.afterEach; + exports.test = context.test || context.it; + exports.run = context.run; + }); + } + + /** + * Enable or disable bailing on the first failure. + * + * @param {Boolean} [bail] + * @api public + */ + + Mocha.prototype.bail = function(bail) { + if (0 == arguments.length) bail = true; + this.suite.bail(bail); + return this; + }; + + /** + * Add test `file`. + * + * @param {String} file + * @api public + */ + + Mocha.prototype.addFile = function(file) { + this.files.push(file); + return this; + }; + + /** + * Set reporter to `reporter`, defaults to "spec". + * + * @param {String|Function} reporter name or constructor + * @param {Object} reporterOptions optional options + * @api public + */ + Mocha.prototype.reporter = function(reporter, reporterOptions) { + if ("function" == typeof reporter) { + this._reporter = reporter; + } else { + reporter = reporter || "spec"; + var _reporter; + try { + _reporter = require("./reporters/" + reporter); + } catch (err) {} + if (!_reporter) + try { + _reporter = require(reporter); + } catch (err) { + err.message.indexOf("Cannot find module") !== -1 + ? console.warn('"' + reporter + '" reporter not found') + : console.warn( + '"' + + reporter + + '" reporter blew up with error:\n' + + err.stack + ); + } + if (!_reporter && reporter === "teamcity") + console.warn( + "The Teamcity reporter was moved to a package named " + + "mocha-teamcity-reporter " + + "(https://npmjs.org/package/mocha-teamcity-reporter)." + ); + if (!_reporter) + throw new Error('invalid reporter "' + reporter + '"'); + this._reporter = _reporter; + } + this.options.reporterOptions = reporterOptions; + return this; + }; + + /** + * Set test UI `name`, defaults to "bdd". + * + * @param {String} bdd + * @api public + */ + + Mocha.prototype.ui = function(name) { + name = name || "bdd"; + this._ui = exports.interfaces[name]; + if (!this._ui) + try { + this._ui = require(name); + } catch (err) {} + if (!this._ui) + throw new Error('invalid interface "' + name + '"'); + this._ui = this._ui(this.suite); + return this; + }; + + /** + * Load registered files. + * + * @api private + */ + + Mocha.prototype.loadFiles = function(fn) { + var self = this; + var suite = this.suite; + var pending = this.files.length; + this.files.forEach(function(file) { + file = path.resolve(file); + suite.emit("pre-require", global, file, self); + suite.emit("require", require(file), file, self); + suite.emit("post-require", global, file, self); + --pending || (fn && fn()); + }); + }; + + /** + * Enable growl support. + * + * @api private + */ + + Mocha.prototype._growl = function(runner, reporter) { + var notify = require("growl"); + + runner.on("end", function() { + var stats = reporter.stats; + if (stats.failures) { + var msg = + stats.failures + " of " + runner.total + " tests failed"; + notify(msg, { + name: "mocha", + title: "Failed", + image: image("error") + }); + } else { + notify( + stats.passes + " tests passed in " + stats.duration + "ms", + { + name: "mocha", + title: "Passed", + image: image("ok") + } + ); + } + }); + }; + + /** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @param {RegExp|String} re + * @return {Mocha} + * @api public + */ + + Mocha.prototype.grep = function(re) { + this.options.grep = + "string" == typeof re ? new RegExp(escapeRe(re)) : re; + return this; + }; + + /** + * Invert `.grep()` matches. + * + * @return {Mocha} + * @api public + */ + + Mocha.prototype.invert = function() { + this.options.invert = true; + return this; + }; + + /** + * Ignore global leaks. + * + * @param {Boolean} ignore + * @return {Mocha} + * @api public + */ + + Mocha.prototype.ignoreLeaks = function(ignore) { + this.options.ignoreLeaks = !!ignore; + return this; + }; + + /** + * Enable global leak checking. + * + * @return {Mocha} + * @api public + */ + + Mocha.prototype.checkLeaks = function() { + this.options.ignoreLeaks = false; + return this; + }; + + /** + * Display long stack-trace on failing + * + * @return {Mocha} + * @api public + */ + + Mocha.prototype.fullTrace = function() { + this.options.fullStackTrace = true; + return this; + }; + + /** + * Enable growl support. + * + * @return {Mocha} + * @api public + */ + + Mocha.prototype.growl = function() { + this.options.growl = true; + return this; + }; + + /** + * Ignore `globals` array or string. + * + * @param {Array|String} globals + * @return {Mocha} + * @api public + */ + + Mocha.prototype.globals = function(globals) { + this.options.globals = (this.options.globals || []).concat( + globals + ); + return this; + }; + + /** + * Emit color output. + * + * @param {Boolean} colors + * @return {Mocha} + * @api public + */ + + Mocha.prototype.useColors = function(colors) { + if (colors !== undefined) { + this.options.useColors = colors; + } + return this; + }; + + /** + * Use inline diffs rather than +/-. + * + * @param {Boolean} inlineDiffs + * @return {Mocha} + * @api public + */ + + Mocha.prototype.useInlineDiffs = function(inlineDiffs) { + this.options.useInlineDiffs = + arguments.length && inlineDiffs != undefined + ? inlineDiffs + : false; + return this; + }; + + /** + * Set the timeout in milliseconds. + * + * @param {Number} timeout + * @return {Mocha} + * @api public + */ + + Mocha.prototype.timeout = function(timeout) { + this.suite.timeout(timeout); + return this; + }; + + /** + * Set slowness threshold in milliseconds. + * + * @param {Number} slow + * @return {Mocha} + * @api public + */ + + Mocha.prototype.slow = function(slow) { + this.suite.slow(slow); + return this; + }; + + /** + * Enable timeouts. + * + * @param {Boolean} enabled + * @return {Mocha} + * @api public + */ + + Mocha.prototype.enableTimeouts = function(enabled) { + this.suite.enableTimeouts( + arguments.length && enabled !== undefined ? enabled : true + ); + return this; + }; + + /** + * Makes all tests async (accepting a callback) + * + * @return {Mocha} + * @api public + */ + + Mocha.prototype.asyncOnly = function() { + this.options.asyncOnly = true; + return this; + }; + + /** + * Disable syntax highlighting (in browser). + * @returns {Mocha} + * @api public + */ + Mocha.prototype.noHighlighting = function() { + this.options.noHighlighting = true; + return this; + }; + + /** + * Delay root suite execution. + * @returns {Mocha} + * @api public + */ + Mocha.prototype.delay = function delay() { + this.options.delay = true; + return this; + }; + + /** + * Run tests and invoke `fn()` when complete. + * + * @param {Function} fn + * @return {Runner} + * @api public + */ + Mocha.prototype.run = function(fn) { + if (this.files.length) this.loadFiles(); + var suite = this.suite; + var options = this.options; + options.files = this.files; + var runner = new exports.Runner(suite, options.delay); + var reporter = new this._reporter(runner, options); + runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.fullStackTrace = options.fullStackTrace; + runner.asyncOnly = options.asyncOnly; + if (options.grep) runner.grep(options.grep, options.invert); + if (options.globals) runner.globals(options.globals); + if (options.growl) this._growl(runner, reporter); + if (options.useColors !== undefined) { + exports.reporters.Base.useColors = options.useColors; + } + exports.reporters.Base.inlineDiffs = options.useInlineDiffs; + + function done(failures) { + if (reporter.done) { + reporter.done(failures, fn); + } else fn && fn(failures); + } + + return runner.run(done); + }; + }); // module: mocha.js + + require.register("ms.js", function(module, exports, require) { + /** + * Helpers. + */ + + var s = 1000; + var m = s * 60; + var h = m * 60; + var d = h * 24; + var y = d * 365.25; + + /** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} options + * @return {String|Number} + * @api public + */ + + module.exports = function(val, options) { + options = options || {}; + if ("string" == typeof val) return parse(val); + return options["long"] ? longFormat(val) : shortFormat(val); + }; + + /** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + + function parse(str) { + var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec( + str + ); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || "ms").toLowerCase(); + switch (type) { + case "years": + case "year": + case "y": + return n * y; + case "days": + case "day": + case "d": + return n * d; + case "hours": + case "hour": + case "h": + return n * h; + case "minutes": + case "minute": + case "m": + return n * m; + case "seconds": + case "second": + case "s": + return n * s; + case "ms": + return n; + } + } + + /** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function shortFormat(ms) { + if (ms >= d) return Math.round(ms / d) + "d"; + if (ms >= h) return Math.round(ms / h) + "h"; + if (ms >= m) return Math.round(ms / m) + "m"; + if (ms >= s) return Math.round(ms / s) + "s"; + return ms + "ms"; + } + + /** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + + function longFormat(ms) { + return ( + plural(ms, d, "day") || + plural(ms, h, "hour") || + plural(ms, m, "minute") || + plural(ms, s, "second") || + ms + " ms" + ); + } + + /** + * Pluralization helper. + */ + + function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + " " + name; + return Math.ceil(ms / n) + " " + name + "s"; + } + }); // module: ms.js + + require.register("pending.js", function(module, exports, require) { + /** + * Expose `Pending`. + */ + + module.exports = Pending; + + /** + * Initialize a new `Pending` error with the given message. + * + * @param {String} message + */ + + function Pending(message) { + this.message = message; + } + }); // module: pending.js + + require.register("reporters/base.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var tty = require("browser/tty"), + diff = require("browser/diff"), + ms = require("../ms"), + utils = require("../utils"), + supportsColor = process.env ? require("supports-color") : null; + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Check if both stdio streams are associated with a tty. + */ + + var isatty = tty.isatty(1) && tty.isatty(2); + + /** + * Expose `Base`. + */ + + exports = module.exports = Base; + + /** + * Enable coloring by default, except in the browser interface. + */ + + exports.useColors = process.env + ? supportsColor || process.env.MOCHA_COLORS !== undefined + : false; + + /** + * Inline diffs instead of +/- + */ + + exports.inlineDiffs = false; + + /** + * Default color map. + */ + + exports.colors = { + pass: 90, + fail: 31, + "bright pass": 92, + "bright fail": 91, + "bright yellow": 93, + pending: 36, + suite: 0, + "error title": 0, + "error message": 31, + "error stack": 90, + checkmark: 32, + fast: 90, + medium: 33, + slow: 31, + green: 32, + light: 90, + "diff gutter": 90, + "diff added": 42, + "diff removed": 41 + }; + + /** + * Default symbol map. + */ + + exports.symbols = { + ok: "✓", + err: "✖", + dot: "․" + }; + + // With node.js on Windows: use symbols available in terminal default fonts + if ("win32" == process.platform) { + exports.symbols.ok = "\u221A"; + exports.symbols.err = "\u00D7"; + exports.symbols.dot = "."; + } + + /** + * Color `str` with the given `type`, + * allowing colors to be disabled, + * as well as user-defined color + * schemes. + * + * @param {String} type + * @param {String} str + * @return {String} + * @api private + */ + + var color = (exports.color = function(type, str) { + if (!exports.useColors) return String(str); + return "\u001b[" + exports.colors[type] + "m" + str + "\u001b[0m"; + }); + + /** + * Expose term window size, with some + * defaults for when stderr is not a tty. + */ + + exports.window = { + width: isatty + ? process.stdout.getWindowSize + ? process.stdout.getWindowSize(1)[0] + : tty.getWindowSize()[1] + : 75 + }; + + /** + * Expose some basic cursor interactions + * that are common among reporters. + */ + + exports.cursor = { + hide: function() { + isatty && process.stdout.write("\u001b[?25l"); + }, + + show: function() { + isatty && process.stdout.write("\u001b[?25h"); + }, + + deleteLine: function() { + isatty && process.stdout.write("\u001b[2K"); + }, + + beginningOfLine: function() { + isatty && process.stdout.write("\u001b[0G"); + }, + + CR: function() { + if (isatty) { + exports.cursor.deleteLine(); + exports.cursor.beginningOfLine(); + } else { + process.stdout.write("\r"); + } + } + }; + + /** + * Outut the given `failures` as a list. + * + * @param {Array} failures + * @api public + */ + + exports.list = function(failures) { + console.log(); + failures.forEach(function(test, i) { + // format + var fmt = + color("error title", " %s) %s:\n") + + color("error message", " %s") + + color("error stack", "\n%s\n"); + + // msg + var err = test.err, + message = err.message || "", + stack = err.stack || message, + index = stack.indexOf(message), + actual = err.actual, + expected = err.expected, + escape = true; + if (index === -1) { + msg = message; + } else { + index += message.length; + msg = stack.slice(0, index); + // remove msg from stack + stack = stack.slice(index + 1); + } + + // uncaught + if (err.uncaught) { + msg = "Uncaught " + msg; + } + // explicitly show diff + if ( + err.showDiff !== false && + sameType(actual, expected) && + expected !== undefined + ) { + if ("string" !== typeof actual) { + escape = false; + err.actual = actual = utils.stringify(actual); + err.expected = expected = utils.stringify(expected); + } + + fmt = + color("error title", " %s) %s:\n%s") + + color("error stack", "\n%s\n"); + var match = message.match(/^([^:]+): expected/); + msg = + "\n " + color("error message", match ? match[1] : msg); + + if (exports.inlineDiffs) { + msg += inlineDiff(err, escape); + } else { + msg += unifiedDiff(err, escape); + } + } + + // indent stack trace + stack = stack.replace(/^/gm, " "); + + console.log(fmt, i + 1, test.fullTitle(), msg, stack); + }); + }; + + /** + * Initialize a new `Base` reporter. + * + * All other reporters generally + * inherit from this reporter, providing + * stats such as test duration, number + * of tests passed / failed etc. + * + * @param {Runner} runner + * @api public + */ + + function Base(runner) { + var self = this, + stats = (this.stats = { + suites: 0, + tests: 0, + passes: 0, + pending: 0, + failures: 0 + }), + failures = (this.failures = []); + + if (!runner) return; + this.runner = runner; + + runner.stats = stats; + + runner.on("start", function() { + stats.start = new Date(); + }); + + runner.on("suite", function(suite) { + stats.suites = stats.suites || 0; + suite.root || stats.suites++; + }); + + runner.on("test end", function(test) { + stats.tests = stats.tests || 0; + stats.tests++; + }); + + runner.on("pass", function(test) { + stats.passes = stats.passes || 0; + + var medium = test.slow() / 2; + test.speed = + test.duration > test.slow() + ? "slow" + : test.duration > medium ? "medium" : "fast"; + + stats.passes++; + }); + + runner.on("fail", function(test, err) { + stats.failures = stats.failures || 0; + stats.failures++; + test.err = err; + failures.push(test); + }); + + runner.on("end", function() { + stats.end = new Date(); + stats.duration = new Date() - stats.start; + }); + + runner.on("pending", function() { + stats.pending++; + }); + } + + /** + * Output common epilogue used by many of + * the bundled reporters. + * + * @api public + */ + + Base.prototype.epilogue = function() { + var stats = this.stats; + var tests; + var fmt; + + console.log(); + + // passes + fmt = + color("bright pass", " ") + + color("green", " %d passing") + + color("light", " (%s)"); + + console.log(fmt, stats.passes || 0, ms(stats.duration)); + + // pending + if (stats.pending) { + fmt = color("pending", " ") + color("pending", " %d pending"); + + console.log(fmt, stats.pending); + } + + // failures + if (stats.failures) { + fmt = color("fail", " %d failing"); + + console.log(fmt, stats.failures); + + Base.list(this.failures); + console.log(); + } + + console.log(); + }; + + /** + * Pad the given `str` to `len`. + * + * @param {String} str + * @param {String} len + * @return {String} + * @api private + */ + + function pad(str, len) { + str = String(str); + return Array(len - str.length + 1).join(" ") + str; + } + + /** + * Returns an inline diff between 2 strings with coloured ANSI output + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + + function inlineDiff(err, escape) { + var msg = errorDiff(err, "WordsWithSpace", escape); + + // linenos + var lines = msg.split("\n"); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines + .map(function(str, i) { + return pad(++i, width) + " |" + " " + str; + }) + .join("\n"); + } + + // legend + msg = + "\n" + + color("diff removed", "actual") + + " " + + color("diff added", "expected") + + "\n\n" + + msg + + "\n"; + + // indent + msg = msg.replace(/^/gm, " "); + return msg; + } + + /** + * Returns a unified diff between 2 strings + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + + function unifiedDiff(err, escape) { + var indent = " "; + function cleanUp(line) { + if (escape) { + line = escapeInvisibles(line); + } + if (line[0] === "+") + return indent + colorLines("diff added", line); + if (line[0] === "-") + return indent + colorLines("diff removed", line); + if (line.match(/\@\@/)) return null; + if (line.match(/\\ No newline/)) return null; + else return indent + line; + } + function notBlank(line) { + return line != null; + } + var msg = diff.createPatch("string", err.actual, err.expected); + var lines = msg.split("\n").splice(4); + return ( + "\n " + + colorLines("diff added", "+ expected") + + " " + + colorLines("diff removed", "- actual") + + "\n\n" + + lines + .map(cleanUp) + .filter(notBlank) + .join("\n") + ); + } + + /** + * Return a character diff for `err`. + * + * @param {Error} err + * @return {String} + * @api private + */ + + function errorDiff(err, type, escape) { + var actual = escape ? escapeInvisibles(err.actual) : err.actual; + var expected = escape + ? escapeInvisibles(err.expected) + : err.expected; + return diff["diff" + type](actual, expected) + .map(function(str) { + if (str.added) return colorLines("diff added", str.value); + if (str.removed) return colorLines("diff removed", str.value); + return str.value; + }) + .join(""); + } + + /** + * Returns a string with all invisible characters in plain text + * + * @param {String} line + * @return {String} + * @api private + */ + function escapeInvisibles(line) { + return line + .replace(/\t/g, "") + .replace(/\r/g, "") + .replace(/\n/g, "\n"); + } + + /** + * Color lines for `str`, using the color `name`. + * + * @param {String} name + * @param {String} str + * @return {String} + * @api private + */ + + function colorLines(name, str) { + return str + .split("\n") + .map(function(str) { + return color(name, str); + }) + .join("\n"); + } + + /** + * Check that a / b have the same type. + * + * @param {Object} a + * @param {Object} b + * @return {Boolean} + * @api private + */ + + function sameType(a, b) { + a = Object.prototype.toString.call(a); + b = Object.prototype.toString.call(b); + return a == b; + } + }); // module: reporters/base.js + + require.register("reporters/doc.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + utils = require("../utils"); + + /** + * Expose `Doc`. + */ + + exports = module.exports = Doc; + + /** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Doc(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total, + indents = 2; + + function indent() { + return Array(indents).join(" "); + } + + runner.on("suite", function(suite) { + if (suite.root) return; + ++indents; + console.log('%s
    ', indent()); + ++indents; + console.log( + "%s

    %s

    ", + indent(), + utils.escape(suite.title) + ); + console.log("%s
    ", indent()); + }); + + runner.on("suite end", function(suite) { + if (suite.root) return; + console.log("%s
    ", indent()); + --indents; + console.log("%s
    ", indent()); + --indents; + }); + + runner.on("pass", function(test) { + console.log( + "%s
    %s
    ", + indent(), + utils.escape(test.title) + ); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log( + "%s
    %s
    ", + indent(), + code + ); + }); + + runner.on("fail", function(test, err) { + console.log( + '%s
    %s
    ', + indent(), + utils.escape(test.title) + ); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log( + '%s
    %s
    ', + indent(), + code + ); + console.log( + '%s
    %s
    ', + indent(), + utils.escape(err) + ); + }); + } + }); // module: reporters/doc.js + + require.register("reporters/dot.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + color = Base.color; + + /** + * Expose `Dot`. + */ + + exports = module.exports = Dot; + + /** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + + function Dot(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + n = -1; + + runner.on("start", function() { + process.stdout.write("\n"); + }); + + runner.on("pending", function(test) { + if (++n % width == 0) process.stdout.write("\n "); + process.stdout.write(color("pending", Base.symbols.dot)); + }); + + runner.on("pass", function(test) { + if (++n % width == 0) process.stdout.write("\n "); + if ("slow" == test.speed) { + process.stdout.write( + color("bright yellow", Base.symbols.dot) + ); + } else { + process.stdout.write(color(test.speed, Base.symbols.dot)); + } + }); + + runner.on("fail", function(test, err) { + if (++n % width == 0) process.stdout.write("\n "); + process.stdout.write(color("fail", Base.symbols.dot)); + }); + + runner.on("end", function() { + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + Dot.prototype = new F(); + Dot.prototype.constructor = Dot; + }); // module: reporters/dot.js + + require.register("reporters/html-cov.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var JSONCov = require("./json-cov"), + fs = require("browser/fs"); + + /** + * Expose `HTMLCov`. + */ + + exports = module.exports = HTMLCov; + + /** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @api public + */ + + function HTMLCov(runner) { + var jade = require("jade"), + file = __dirname + "/templates/coverage.jade", + str = fs.readFileSync(file, "utf8"), + fn = jade.compile(str, { filename: file }), + self = this; + + JSONCov.call(this, runner, false); + + runner.on("end", function() { + process.stdout.write( + fn({ + cov: self.cov, + coverageClass: coverageClass + }) + ); + }); + } + + /** + * Return coverage class for `n`. + * + * @return {String} + * @api private + */ + + function coverageClass(n) { + if (n >= 75) return "high"; + if (n >= 50) return "medium"; + if (n >= 25) return "low"; + return "terrible"; + } + }); // module: reporters/html-cov.js + + require.register("reporters/html.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + utils = require("../utils"), + Progress = require("../browser/progress"), + escape = utils.escape; + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Expose `HTML`. + */ + + exports = module.exports = HTML; + + /** + * Stats template. + */ + + var statsTemplate = + '"; + + /** + * Initialize a new `HTML` reporter. + * + * @param {Runner} runner + * @api public + */ + + function HTML(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total, + stat = fragment(statsTemplate), + items = stat.getElementsByTagName("li"), + passes = items[1].getElementsByTagName("em")[0], + passesLink = items[1].getElementsByTagName("a")[0], + failures = items[2].getElementsByTagName("em")[0], + failuresLink = items[2].getElementsByTagName("a")[0], + duration = items[3].getElementsByTagName("em")[0], + canvas = stat.getElementsByTagName("canvas")[0], + report = fragment('
      '), + stack = [report], + progress, + ctx, + root = document.getElementById("mocha"); + + if (canvas.getContext) { + var ratio = window.devicePixelRatio || 1; + canvas.style.width = canvas.width; + canvas.style.height = canvas.height; + canvas.width *= ratio; + canvas.height *= ratio; + ctx = canvas.getContext("2d"); + ctx.scale(ratio, ratio); + progress = new Progress(); + } + + if (!root) + return error("#mocha div missing, add it to your document"); + + // pass toggle + on(passesLink, "click", function() { + unhide(); + var name = /pass/.test(report.className) ? "" : " pass"; + report.className = + report.className.replace(/fail|pass/g, "") + name; + if (report.className.trim()) hideSuitesWithout("test pass"); + }); + + // failure toggle + on(failuresLink, "click", function() { + unhide(); + var name = /fail/.test(report.className) ? "" : " fail"; + report.className = + report.className.replace(/fail|pass/g, "") + name; + if (report.className.trim()) hideSuitesWithout("test fail"); + }); + + root.appendChild(stat); + root.appendChild(report); + + if (progress) progress.size(40); + + runner.on("suite", function(suite) { + if (suite.root) return; + + // suite + var url = self.suiteURL(suite); + var el = fragment( + '
    • %s

    • ', + url, + escape(suite.title) + ); + + // container + stack[0].appendChild(el); + stack.unshift(document.createElement("ul")); + el.appendChild(stack[0]); + }); + + runner.on("suite end", function(suite) { + if (suite.root) return; + stack.shift(); + }); + + runner.on("fail", function(test, err) { + if ("hook" == test.type) runner.emit("test end", test); + }); + + runner.on("test end", function(test) { + // TODO: add to stats + var percent = (stats.tests / this.total * 100) | 0; + if (progress) progress.update(percent).draw(ctx); + + // update stats + var ms = new Date() - stats.start; + text(passes, stats.passes); + text(failures, stats.failures); + text(duration, (ms / 1000).toFixed(2)); + + // test + if ("passed" == test.state) { + var url = self.testURL(test); + var el = fragment( + '
    • %e%ems

    • ', + test.speed, + test.title, + test.duration, + url + ); + } else if (test.pending) { + var el = fragment( + '
    • %e

    • ', + test.title + ); + } else { + var el = fragment( + '
    • %e

    • ', + test.title, + self.testURL(test) + ); + var str = test.err.stack || test.err.toString(); + + // FF / Opera do not add the message + if (!~str.indexOf(test.err.message)) { + str = test.err.message + "\n" + str; + } + + // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we + // check for the result of the stringifying. + if ("[object Error]" == str) str = test.err.message; + + // Safari doesn't give you a stack. Let's at least provide a source line. + if ( + !test.err.stack && + test.err.sourceURL && + test.err.line !== undefined + ) { + str += + "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; + } + + el.appendChild(fragment('
      %e
      ', str)); + } + + // toggle code + // TODO: defer + if (!test.pending) { + var h2 = el.getElementsByTagName("h2")[0]; + + on(h2, "click", function() { + pre.style.display = + "none" == pre.style.display ? "block" : "none"; + }); + + var pre = fragment( + "
      %e
      ", + utils.clean(test.fn.toString()) + ); + el.appendChild(pre); + pre.style.display = "none"; + } + + // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. + if (stack[0]) stack[0].appendChild(el); + }); + } + + /** + * Makes a URL, preserving querystring ("search") parameters. + * @param {string} s + * @returns {string} your new URL + */ + var makeUrl = function makeUrl(s) { + var search = window.location.search; + + // Remove previous grep query parameter if present + if (search) { + search = search + .replace(/[?&]grep=[^&\s]*/g, "") + .replace(/^&/, "?"); + } + + return ( + window.location.pathname + + (search ? search + "&" : "?") + + "grep=" + + encodeURIComponent(s) + ); + }; + + /** + * Provide suite URL + * + * @param {Object} [suite] + */ + HTML.prototype.suiteURL = function(suite) { + return makeUrl(suite.fullTitle()); + }; + + /** + * Provide test URL + * + * @param {Object} [test] + */ + + HTML.prototype.testURL = function(test) { + return makeUrl(test.fullTitle()); + }; + + /** + * Display error `msg`. + */ + + function error(msg) { + document.body.appendChild( + fragment('
      %s
      ', msg) + ); + } + + /** + * Return a DOM fragment from `html`. + */ + + function fragment(html) { + var args = arguments, + div = document.createElement("div"), + i = 1; + + div.innerHTML = html.replace(/%([se])/g, function(_, type) { + switch (type) { + case "s": + return String(args[i++]); + case "e": + return escape(args[i++]); + } + }); + + return div.firstChild; + } + + /** + * Check for suites that do not have elements + * with `classname`, and hide them. + */ + + function hideSuitesWithout(classname) { + var suites = document.getElementsByClassName("suite"); + for (var i = 0; i < suites.length; i++) { + var els = suites[i].getElementsByClassName(classname); + if (0 == els.length) suites[i].className += " hidden"; + } + } + + /** + * Unhide .hidden suites. + */ + + function unhide() { + var els = document.getElementsByClassName("suite hidden"); + for (var i = 0; i < els.length; ++i) { + els[i].className = els[i].className.replace( + "suite hidden", + "suite" + ); + } + } + + /** + * Set `el` text to `str`. + */ + + function text(el, str) { + if (el.textContent) { + el.textContent = str; + } else { + el.innerText = str; + } + } + + /** + * Listen on `event` with callback `fn`. + */ + + function on(el, event, fn) { + if (el.addEventListener) { + el.addEventListener(event, fn, false); + } else { + el.attachEvent("on" + event, fn); + } + } + }); // module: reporters/html.js + + require.register("reporters/index.js", function( + module, + exports, + require + ) { + exports.Base = require("./base"); + exports.Dot = require("./dot"); + exports.Doc = require("./doc"); + exports.TAP = require("./tap"); + exports.JSON = require("./json"); + exports.HTML = require("./html"); + exports.List = require("./list"); + exports.Min = require("./min"); + exports.Spec = require("./spec"); + exports.Nyan = require("./nyan"); + exports.XUnit = require("./xunit"); + exports.Markdown = require("./markdown"); + exports.Progress = require("./progress"); + exports.Landing = require("./landing"); + exports.JSONCov = require("./json-cov"); + exports.HTMLCov = require("./html-cov"); + exports.JSONStream = require("./json-stream"); + }); // module: reporters/index.js + + require.register("reporters/json-cov.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"); + + /** + * Expose `JSONCov`. + */ + + exports = module.exports = JSONCov; + + /** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @param {Boolean} output + * @api public + */ + + function JSONCov(runner, output) { + var self = this, + output = 1 == arguments.length ? true : output; + + Base.call(this, runner); + + var tests = [], + failures = [], + passes = []; + + runner.on("test end", function(test) { + tests.push(test); + }); + + runner.on("pass", function(test) { + passes.push(test); + }); + + runner.on("fail", function(test) { + failures.push(test); + }); + + runner.on("end", function() { + var cov = global._$jscoverage || {}; + var result = (self.cov = map(cov)); + result.stats = self.stats; + result.tests = tests.map(clean); + result.failures = failures.map(clean); + result.passes = passes.map(clean); + if (!output) return; + process.stdout.write(JSON.stringify(result, null, 2)); + }); + } + + /** + * Map jscoverage data to a JSON structure + * suitable for reporting. + * + * @param {Object} cov + * @return {Object} + * @api private + */ + + function map(cov) { + var ret = { + instrumentation: "node-jscoverage", + sloc: 0, + hits: 0, + misses: 0, + coverage: 0, + files: [] + }; + + for (var filename in cov) { + var data = coverage(filename, cov[filename]); + ret.files.push(data); + ret.hits += data.hits; + ret.misses += data.misses; + ret.sloc += data.sloc; + } + + ret.files.sort(function(a, b) { + return a.filename.localeCompare(b.filename); + }); + + if (ret.sloc > 0) { + ret.coverage = ret.hits / ret.sloc * 100; + } + + return ret; + } + + /** + * Map jscoverage data for a single source file + * to a JSON structure suitable for reporting. + * + * @param {String} filename name of the source file + * @param {Object} data jscoverage coverage data + * @return {Object} + * @api private + */ + + function coverage(filename, data) { + var ret = { + filename: filename, + coverage: 0, + hits: 0, + misses: 0, + sloc: 0, + source: {} + }; + + data.source.forEach(function(line, num) { + num++; + + if (data[num] === 0) { + ret.misses++; + ret.sloc++; + } else if (data[num] !== undefined) { + ret.hits++; + ret.sloc++; + } + + ret.source[num] = { + source: line, + coverage: data[num] === undefined ? "" : data[num] + }; + }); + + ret.coverage = ret.hits / ret.sloc * 100; + + return ret; + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration + }; + } + }); // module: reporters/json-cov.js + + require.register("reporters/json-stream.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + color = Base.color; + + /** + * Expose `List`. + */ + + exports = module.exports = List; + + /** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function List(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + total = runner.total; + + runner.on("start", function() { + console.log(JSON.stringify(["start", { total: total }])); + }); + + runner.on("pass", function(test) { + console.log(JSON.stringify(["pass", clean(test)])); + }); + + runner.on("fail", function(test, err) { + test = clean(test); + test.err = err.message; + console.log(JSON.stringify(["fail", test])); + }); + + runner.on("end", function() { + process.stdout.write(JSON.stringify(["end", self.stats])); + }); + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration + }; + } + }); // module: reporters/json-stream.js + + require.register("reporters/json.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `JSON`. + */ + + exports = module.exports = JSONReporter; + + /** + * Initialize a new `JSON` reporter. + * + * @param {Runner} runner + * @api public + */ + + function JSONReporter(runner) { + var self = this; + Base.call(this, runner); + + var tests = [], + pending = [], + failures = [], + passes = []; + + runner.on("test end", function(test) { + tests.push(test); + }); + + runner.on("pass", function(test) { + passes.push(test); + }); + + runner.on("fail", function(test) { + failures.push(test); + }); + + runner.on("pending", function(test) { + pending.push(test); + }); + + runner.on("end", function() { + var obj = { + stats: self.stats, + tests: tests.map(clean), + pending: pending.map(clean), + failures: failures.map(clean), + passes: passes.map(clean) + }; + + runner.testResults = obj; + + process.stdout.write(JSON.stringify(obj, null, 2)); + }); + } + + /** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + + function clean(test) { + return { + title: test.title, + fullTitle: test.fullTitle(), + duration: test.duration, + err: errorJSON(test.err || {}) + }; + } + + /** + * Transform `error` into a JSON object. + * @param {Error} err + * @return {Object} + */ + + function errorJSON(err) { + var res = {}; + Object.getOwnPropertyNames(err).forEach(function(key) { + res[key] = err[key]; + }, err); + return res; + } + }); // module: reporters/json.js + + require.register("reporters/landing.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Landing`. + */ + + exports = module.exports = Landing; + + /** + * Airplane color. + */ + + Base.colors.plane = 0; + + /** + * Airplane crash color. + */ + + Base.colors["plane crash"] = 31; + + /** + * Runway color. + */ + + Base.colors.runway = 90; + + /** + * Initialize a new `Landing` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Landing(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + total = runner.total, + stream = process.stdout, + plane = color("plane", "✈"), + crashed = -1, + n = 0; + + function runway() { + var buf = Array(width).join("-"); + return " " + color("runway", buf); + } + + runner.on("start", function() { + stream.write("\n\n\n "); + cursor.hide(); + }); + + runner.on("test end", function(test) { + // check if the plane crashed + var col = -1 == crashed ? (width * ++n / total) | 0 : crashed; + + // show the crash + if ("failed" == test.state) { + plane = color("plane crash", "✈"); + crashed = col; + } + + // render landing strip + stream.write("\u001b[" + (width + 1) + "D\u001b[2A"); + stream.write(runway()); + stream.write("\n "); + stream.write(color("runway", Array(col).join("⋅"))); + stream.write(plane); + stream.write( + color("runway", Array(width - col).join("⋅") + "\n") + ); + stream.write(runway()); + stream.write("\u001b[0m"); + }); + + runner.on("end", function() { + cursor.show(); + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + Landing.prototype = new F(); + Landing.prototype.constructor = Landing; + }); // module: reporters/landing.js + + require.register("reporters/list.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `List`. + */ + + exports = module.exports = List; + + /** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function List(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + n = 0; + + runner.on("start", function() { + console.log(); + }); + + runner.on("test", function(test) { + process.stdout.write( + color("pass", " " + test.fullTitle() + ": ") + ); + }); + + runner.on("pending", function(test) { + var fmt = color("checkmark", " -") + color("pending", " %s"); + console.log(fmt, test.fullTitle()); + }); + + runner.on("pass", function(test) { + var fmt = + color("checkmark", " " + Base.symbols.dot) + + color("pass", " %s: ") + + color(test.speed, "%dms"); + cursor.CR(); + console.log(fmt, test.fullTitle(), test.duration); + }); + + runner.on("fail", function(test, err) { + cursor.CR(); + console.log(color("fail", " %d) %s"), ++n, test.fullTitle()); + }); + + runner.on("end", self.epilogue.bind(self)); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + List.prototype = new F(); + List.prototype.constructor = List; + }); // module: reporters/list.js + + require.register("reporters/markdown.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + utils = require("../utils"); + + /** + * Constants + */ + + var SUITE_PREFIX = "$"; + + /** + * Expose `Markdown`. + */ + + exports = module.exports = Markdown; + + /** + * Initialize a new `Markdown` reporter. + * + * @param {Runner} runner + * @api public + */ + + function Markdown(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + level = 0, + buf = ""; + + function title(str) { + return Array(level).join("#") + " " + str; + } + + function indent() { + return Array(level).join(" "); + } + + function mapTOC(suite, obj) { + var ret = obj, + key = SUITE_PREFIX + suite.title; + obj = obj[key] = obj[key] || { suite: suite }; + suite.suites.forEach(function(suite) { + mapTOC(suite, obj); + }); + return ret; + } + + function stringifyTOC(obj, level) { + ++level; + var buf = ""; + var link; + for (var key in obj) { + if ("suite" == key) continue; + if (key !== SUITE_PREFIX) { + link = " - [" + key.substring(1) + "]"; + link += + "(#" + utils.slug(obj[key].suite.fullTitle()) + ")\n"; + buf += Array(level).join(" ") + link; + } + buf += stringifyTOC(obj[key], level); + } + return buf; + } + + function generateTOC(suite) { + var obj = mapTOC(suite, {}); + return stringifyTOC(obj, 0); + } + + generateTOC(runner.suite); + + runner.on("suite", function(suite) { + ++level; + var slug = utils.slug(suite.fullTitle()); + buf += '' + "\n"; + buf += title(suite.title) + "\n"; + }); + + runner.on("suite end", function(suite) { + --level; + }); + + runner.on("pass", function(test) { + var code = utils.clean(test.fn.toString()); + buf += test.title + ".\n"; + buf += "\n```js\n"; + buf += code + "\n"; + buf += "```\n\n"; + }); + + runner.on("end", function() { + process.stdout.write("# TOC\n"); + process.stdout.write(generateTOC(runner.suite)); + process.stdout.write(buf); + }); + } + }); // module: reporters/markdown.js + + require.register("reporters/min.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"); + + /** + * Expose `Min`. + */ + + exports = module.exports = Min; + + /** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @param {Runner} runner + * @api public + */ + + function Min(runner) { + Base.call(this, runner); + + runner.on("start", function() { + // clear screen + process.stdout.write("\u001b[2J"); + // set cursor position + process.stdout.write("\u001b[1;3H"); + }); + + runner.on("end", this.epilogue.bind(this)); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + Min.prototype = new F(); + Min.prototype.constructor = Min; + }); // module: reporters/min.js + + require.register("reporters/nyan.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"); + + /** + * Expose `Dot`. + */ + + exports = module.exports = NyanCat; + + /** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + + function NyanCat(runner) { + Base.call(this, runner); + var self = this, + stats = this.stats, + width = (Base.window.width * 0.75) | 0, + rainbowColors = (this.rainbowColors = self.generateColors()), + colorIndex = (this.colorIndex = 0), + numerOfLines = (this.numberOfLines = 4), + trajectories = (this.trajectories = [[], [], [], []]), + nyanCatWidth = (this.nyanCatWidth = 11), + trajectoryWidthMax = (this.trajectoryWidthMax = + width - nyanCatWidth), + scoreboardWidth = (this.scoreboardWidth = 5), + tick = (this.tick = 0), + n = 0; + + runner.on("start", function() { + Base.cursor.hide(); + self.draw(); + }); + + runner.on("pending", function(test) { + self.draw(); + }); + + runner.on("pass", function(test) { + self.draw(); + }); + + runner.on("fail", function(test, err) { + self.draw(); + }); + + runner.on("end", function() { + Base.cursor.show(); + for (var i = 0; i < self.numberOfLines; i++) write("\n"); + self.epilogue(); + }); + } + + /** + * Draw the nyan cat + * + * @api private + */ + + NyanCat.prototype.draw = function() { + this.appendRainbow(); + this.drawScoreboard(); + this.drawRainbow(); + this.drawNyanCat(); + this.tick = !this.tick; + }; + + /** + * Draw the "scoreboard" showing the number + * of passes, failures and pending tests. + * + * @api private + */ + + NyanCat.prototype.drawScoreboard = function() { + var stats = this.stats; + + function draw(type, n) { + write(" "); + write(Base.color(type, n)); + write("\n"); + } + + draw("green", stats.passes); + draw("fail", stats.failures); + draw("pending", stats.pending); + write("\n"); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Append the rainbow. + * + * @api private + */ + + NyanCat.prototype.appendRainbow = function() { + var segment = this.tick ? "_" : "-"; + var rainbowified = this.rainbowify(segment); + + for (var index = 0; index < this.numberOfLines; index++) { + var trajectory = this.trajectories[index]; + if (trajectory.length >= this.trajectoryWidthMax) + trajectory.shift(); + trajectory.push(rainbowified); + } + }; + + /** + * Draw the rainbow. + * + * @api private + */ + + NyanCat.prototype.drawRainbow = function() { + var self = this; + + this.trajectories.forEach(function(line, index) { + write("\u001b[" + self.scoreboardWidth + "C"); + write(line.join("")); + write("\n"); + }); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Draw the nyan cat + * + * @api private + */ + + NyanCat.prototype.drawNyanCat = function() { + var self = this; + var startWidth = + this.scoreboardWidth + this.trajectories[0].length; + var dist = "\u001b[" + startWidth + "C"; + var padding = ""; + + write(dist); + write("_,------,"); + write("\n"); + + write(dist); + padding = self.tick ? " " : " "; + write("_|" + padding + "/\\_/\\ "); + write("\n"); + + write(dist); + padding = self.tick ? "_" : "__"; + var tail = self.tick ? "~" : "^"; + var face; + write(tail + "|" + padding + this.face() + " "); + write("\n"); + + write(dist); + padding = self.tick ? " " : " "; + write(padding + '"" "" '); + write("\n"); + + this.cursorUp(this.numberOfLines); + }; + + /** + * Draw nyan cat face. + * + * @return {String} + * @api private + */ + + NyanCat.prototype.face = function() { + var stats = this.stats; + if (stats.failures) { + return "( x .x)"; + } else if (stats.pending) { + return "( o .o)"; + } else if (stats.passes) { + return "( ^ .^)"; + } else { + return "( - .-)"; + } + }; + + /** + * Move cursor up `n`. + * + * @param {Number} n + * @api private + */ + + NyanCat.prototype.cursorUp = function(n) { + write("\u001b[" + n + "A"); + }; + + /** + * Move cursor down `n`. + * + * @param {Number} n + * @api private + */ + + NyanCat.prototype.cursorDown = function(n) { + write("\u001b[" + n + "B"); + }; + + /** + * Generate rainbow colors. + * + * @return {Array} + * @api private + */ + + NyanCat.prototype.generateColors = function() { + var colors = []; + + for (var i = 0; i < 6 * 7; i++) { + var pi3 = Math.floor(Math.PI / 3); + var n = i * (1.0 / 6); + var r = Math.floor(3 * Math.sin(n) + 3); + var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); + var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); + colors.push(36 * r + 6 * g + b + 16); + } + + return colors; + }; + + /** + * Apply rainbow to the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + NyanCat.prototype.rainbowify = function(str) { + if (!Base.useColors) return str; + var color = this.rainbowColors[ + this.colorIndex % this.rainbowColors.length + ]; + this.colorIndex += 1; + return "\u001b[38;5;" + color + "m" + str + "\u001b[0m"; + }; + + /** + * Stdout helper. + */ + + function write(string) { + process.stdout.write(string); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + NyanCat.prototype = new F(); + NyanCat.prototype.constructor = NyanCat; + }); // module: reporters/nyan.js + + require.register("reporters/progress.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Progress`. + */ + + exports = module.exports = Progress; + + /** + * General progress bar color. + */ + + Base.colors.progress = 90; + + /** + * Initialize a new `Progress` bar test reporter. + * + * @param {Runner} runner + * @param {Object} options + * @api public + */ + + function Progress(runner, options) { + Base.call(this, runner); + + var self = this, + options = options || {}, + stats = this.stats, + width = (Base.window.width * 0.5) | 0, + total = runner.total, + complete = 0, + max = Math.max, + lastN = -1; + + // default chars + options.open = options.open || "["; + options.complete = options.complete || "▬"; + options.incomplete = options.incomplete || Base.symbols.dot; + options.close = options.close || "]"; + options.verbose = false; + + // tests started + runner.on("start", function() { + console.log(); + cursor.hide(); + }); + + // tests complete + runner.on("test end", function() { + complete++; + var incomplete = total - complete, + percent = complete / total, + n = (width * percent) | 0, + i = width - n; + + if (lastN === n && !options.verbose) { + // Don't re-render the line if it hasn't changed + return; + } + lastN = n; + + cursor.CR(); + process.stdout.write("\u001b[J"); + process.stdout.write(color("progress", " " + options.open)); + process.stdout.write(Array(n).join(options.complete)); + process.stdout.write(Array(i).join(options.incomplete)); + process.stdout.write(color("progress", options.close)); + if (options.verbose) { + process.stdout.write( + color("progress", " " + complete + " of " + total) + ); + } + }); + + // tests are complete, output some stats + // and the failures if any + runner.on("end", function() { + cursor.show(); + console.log(); + self.epilogue(); + }); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + Progress.prototype = new F(); + Progress.prototype.constructor = Progress; + }); // module: reporters/progress.js + + require.register("reporters/spec.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `Spec`. + */ + + exports = module.exports = Spec; + + /** + * Initialize a new `Spec` test reporter. + * + * @param {Runner} runner + * @api public + */ + + function Spec(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + indents = 0, + n = 0; + + function indent() { + return Array(indents).join(" "); + } + + runner.on("start", function() { + console.log(); + }); + + runner.on("suite", function(suite) { + ++indents; + console.log(color("suite", "%s%s"), indent(), suite.title); + }); + + runner.on("suite end", function(suite) { + --indents; + if (1 == indents) console.log(); + }); + + runner.on("pending", function(test) { + var fmt = indent() + color("pending", " - %s"); + console.log(fmt, test.title); + }); + + runner.on("pass", function(test) { + if ("fast" == test.speed) { + var fmt = + indent() + + color("checkmark", " " + Base.symbols.ok) + + color("pass", " %s"); + cursor.CR(); + console.log(fmt, test.title); + } else { + var fmt = + indent() + + color("checkmark", " " + Base.symbols.ok) + + color("pass", " %s") + + color(test.speed, " (%dms)"); + cursor.CR(); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on("fail", function(test, err) { + cursor.CR(); + console.log( + indent() + color("fail", " %d) %s"), + ++n, + test.title + ); + }); + + runner.on("end", self.epilogue.bind(self)); + } + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + Spec.prototype = new F(); + Spec.prototype.constructor = Spec; + }); // module: reporters/spec.js + + require.register("reporters/tap.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + cursor = Base.cursor, + color = Base.color; + + /** + * Expose `TAP`. + */ + + exports = module.exports = TAP; + + /** + * Initialize a new `TAP` reporter. + * + * @param {Runner} runner + * @api public + */ + + function TAP(runner) { + Base.call(this, runner); + + var self = this, + stats = this.stats, + n = 1, + passes = 0, + failures = 0; + + runner.on("start", function() { + var total = runner.grepTotal(runner.suite); + console.log("%d..%d", 1, total); + }); + + runner.on("test end", function() { + ++n; + }); + + runner.on("pending", function(test) { + console.log("ok %d %s # SKIP -", n, title(test)); + }); + + runner.on("pass", function(test) { + passes++; + console.log("ok %d %s", n, title(test)); + }); + + runner.on("fail", function(test, err) { + failures++; + console.log("not ok %d %s", n, title(test)); + if (err.stack) console.log(err.stack.replace(/^/gm, " ")); + }); + + runner.on("end", function() { + console.log("# tests " + (passes + failures)); + console.log("# pass " + passes); + console.log("# fail " + failures); + }); + } + + /** + * Return a TAP-safe title of `test` + * + * @param {Object} test + * @return {String} + * @api private + */ + + function title(test) { + return test.fullTitle().replace(/#/g, ""); + } + }); // module: reporters/tap.js + + require.register("reporters/xunit.js", function( + module, + exports, + require + ) { + /** + * Module dependencies. + */ + + var Base = require("./base"), + utils = require("../utils"), + fs = require("browser/fs"), + escape = utils.escape; + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Expose `XUnit`. + */ + + exports = module.exports = XUnit; + + /** + * Initialize a new `XUnit` reporter. + * + * @param {Runner} runner + * @api public + */ + + function XUnit(runner, options) { + Base.call(this, runner); + var stats = this.stats, + tests = [], + self = this; + + if (options.reporterOptions && options.reporterOptions.output) { + if (!fs.createWriteStream) { + throw new Error("file output not supported in browser"); + } + self.fileStream = fs.createWriteStream( + options.reporterOptions.output + ); + } + + runner.on("pending", function(test) { + tests.push(test); + }); + + runner.on("pass", function(test) { + tests.push(test); + }); + + runner.on("fail", function(test) { + tests.push(test); + }); + + runner.on("end", function() { + self.write( + tag( + "testsuite", + { + name: "Mocha Tests", + tests: stats.tests, + failures: stats.failures, + errors: stats.failures, + skipped: stats.tests - stats.failures - stats.passes, + timestamp: new Date().toUTCString(), + time: stats.duration / 1000 || 0 + }, + false + ) + ); + + tests.forEach(function(t) { + self.test(t); + }); + self.write(""); + }); + } + + /** + * Override done to close the stream (if it's a file). + */ + XUnit.prototype.done = function(failures, fn) { + if (this.fileStream) { + this.fileStream.end(function() { + fn(failures); + }); + } else { + fn(failures); + } + }; + + /** + * Inherit from `Base.prototype`. + */ + + function F() {} + F.prototype = Base.prototype; + XUnit.prototype = new F(); + XUnit.prototype.constructor = XUnit; + + /** + * Write out the given line + */ + XUnit.prototype.write = function(line) { + if (this.fileStream) { + this.fileStream.write(line + "\n"); + } else { + console.log(line); + } + }; + + /** + * Output tag for the given `test.` + */ + + XUnit.prototype.test = function(test, ostream) { + var attrs = { + classname: test.parent.fullTitle(), + name: test.title, + time: test.duration / 1000 || 0 + }; + + if ("failed" == test.state) { + var err = test.err; + this.write( + tag( + "testcase", + attrs, + false, + tag( + "failure", + {}, + false, + cdata(escape(err.message) + "\n" + err.stack) + ) + ) + ); + } else if (test.pending) { + this.write( + tag("testcase", attrs, false, tag("skipped", {}, true)) + ); + } else { + this.write(tag("testcase", attrs, true)); + } + }; + + /** + * HTML tag helper. + */ + + function tag(name, attrs, close, content) { + var end = close ? "/>" : ">", + pairs = [], + tag; + + for (var key in attrs) { + pairs.push(key + '="' + escape(attrs[key]) + '"'); + } + + tag = + "<" + name + (pairs.length ? " " + pairs.join(" ") : "") + end; + if (content) tag += content + ""; + } + }); // module: reporters/xunit.js + + require.register("runnable.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require("browser/events").EventEmitter, + debug = require("browser/debug")("mocha:runnable"), + Pending = require("./pending"), + milliseconds = require("./ms"), + utils = require("./utils"); + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date, + setTimeout = global.setTimeout, + setInterval = global.setInterval, + clearTimeout = global.clearTimeout, + clearInterval = global.clearInterval; + + /** + * Object#toString(). + */ + + var toString = Object.prototype.toString; + + /** + * Expose `Runnable`. + */ + + module.exports = Runnable; + + /** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + + function Runnable(title, fn) { + this.title = title; + this.fn = fn; + this.async = fn && fn.length; + this.sync = !this.async; + this._timeout = 2000; + this._slow = 75; + this._enableTimeouts = true; + this.timedOut = false; + this._trace = new Error("done() called multiple times"); + } + + /** + * Inherit from `EventEmitter.prototype`. + */ + + function F() {} + F.prototype = EventEmitter.prototype; + Runnable.prototype = new F(); + Runnable.prototype.constructor = Runnable; + + /** + * Set & get timeout `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + + Runnable.prototype.timeout = function(ms) { + if (0 == arguments.length) return this._timeout; + if (ms === 0) this._enableTimeouts = false; + if ("string" == typeof ms) ms = milliseconds(ms); + debug("timeout %d", ms); + this._timeout = ms; + if (this.timer) this.resetTimeout(); + return this; + }; + + /** + * Set & get slow `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + + Runnable.prototype.slow = function(ms) { + if (0 === arguments.length) return this._slow; + if ("string" == typeof ms) ms = milliseconds(ms); + debug("timeout %d", ms); + this._slow = ms; + return this; + }; + + /** + * Set and & get timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Runnable|Boolean} enabled or self + * @api private + */ + + Runnable.prototype.enableTimeouts = function(enabled) { + if (arguments.length === 0) return this._enableTimeouts; + debug("enableTimeouts %s", enabled); + this._enableTimeouts = enabled; + return this; + }; + + /** + * Halt and mark as pending. + * + * @api private + */ + + Runnable.prototype.skip = function() { + throw new Pending(); + }; + + /** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + + Runnable.prototype.fullTitle = function() { + return this.parent.fullTitle() + " " + this.title; + }; + + /** + * Clear the timeout. + * + * @api private + */ + + Runnable.prototype.clearTimeout = function() { + clearTimeout(this.timer); + }; + + /** + * Inspect the runnable void of private properties. + * + * @return {String} + * @api private + */ + + Runnable.prototype.inspect = function() { + return JSON.stringify( + this, + function(key, val) { + if ("_" == key[0]) return; + if ("parent" == key) return "#"; + if ("ctx" == key) return "#"; + return val; + }, + 2 + ); + }; + + /** + * Reset the timeout. + * + * @api private + */ + + Runnable.prototype.resetTimeout = function() { + var self = this; + var ms = this.timeout() || 1e9; + + if (!this._enableTimeouts) return; + this.clearTimeout(); + this.timer = setTimeout(function() { + if (!self._enableTimeouts) return; + self.callback( + new Error( + "timeout of " + + ms + + "ms exceeded. Ensure the done() callback is being called in this test." + ) + ); + self.timedOut = true; + }, ms); + }; + + /** + * Whitelist these globals for this test run + * + * @api private + */ + Runnable.prototype.globals = function(arr) { + var self = this; + this._allowedGlobals = arr; + }; + + /** + * Run the test and invoke `fn(err)`. + * + * @param {Function} fn + * @api private + */ + + Runnable.prototype.run = function(fn) { + var self = this, + start = new Date(), + ctx = this.ctx, + finished, + emitted; + + // Some times the ctx exists but it is not runnable + if (ctx && ctx.runnable) ctx.runnable(this); + + // called multiple times + function multiple(err) { + if (emitted) return; + emitted = true; + self.emit( + "error", + err || + new Error( + "done() called multiple times; stacktrace may be inaccurate" + ) + ); + } + + // finished + function done(err) { + var ms = self.timeout(); + if (self.timedOut) return; + if (finished) return multiple(err || self._trace); + + // Discard the resolution if this test has already failed asynchronously + if (self.state) return; + + self.clearTimeout(); + self.duration = new Date() - start; + finished = true; + if (!err && self.duration > ms && self._enableTimeouts) + err = new Error( + "timeout of " + + ms + + "ms exceeded. Ensure the done() callback is being called in this test." + ); + fn(err); + } + + // for .resetTimeout() + this.callback = done; + + // explicit async with `done` argument + if (this.async) { + this.resetTimeout(); + + try { + this.fn.call(ctx, function(err) { + if ( + err instanceof Error || + toString.call(err) === "[object Error]" + ) + return done(err); + if (null != err) { + if ( + Object.prototype.toString.call(err) === + "[object Object]" + ) { + return done( + new Error( + "done() invoked with non-Error: " + + JSON.stringify(err) + ) + ); + } else { + return done( + new Error("done() invoked with non-Error: " + err) + ); + } + } + done(); + }); + } catch (err) { + done(utils.getError(err)); + } + return; + } + + if (this.asyncOnly) { + return done( + new Error( + "--async-only option in use without declaring `done()`" + ) + ); + } + + // sync or promise-returning + try { + if (this.pending) { + done(); + } else { + callFn(this.fn); + } + } catch (err) { + done(utils.getError(err)); + } + + function callFn(fn) { + var result = fn.call(ctx); + if (result && typeof result.then === "function") { + self.resetTimeout(); + result.then( + function() { + done(); + }, + function(reason) { + done( + reason || + new Error("Promise rejected with no or falsy reason") + ); + } + ); + } else { + done(); + } + } + }; + }); // module: runnable.js + + require.register("runner.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require("browser/events").EventEmitter, + debug = require("browser/debug")("mocha:runner"), + Pending = require("./pending"), + Test = require("./test"), + utils = require("./utils"), + filter = utils.filter, + keys = utils.keys, + type = utils.type, + stringify = utils.stringify, + stackFilter = utils.stackTraceFilter(); + + /** + * Non-enumerable globals. + */ + + var globals = [ + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "XMLHttpRequest", + "Date", + "setImmediate", + "clearImmediate" + ]; + + /** + * Expose `Runner`. + */ + + module.exports = Runner; + + /** + * Initialize a `Runner` for the given `suite`. + * + * Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * - `pending` (test) test pending + * + * @param {Suite} suite Root suite + * @param {boolean} [delay] Whether or not to delay execution of root suite + * until ready. + * @api public + */ + + function Runner(suite, delay) { + var self = this; + this._globals = []; + this._abort = false; + this._delay = delay; + this.suite = suite; + this.total = suite.total(); + this.failures = 0; + this.on("test end", function(test) { + self.checkGlobals(test); + }); + this.on("hook end", function(hook) { + self.checkGlobals(hook); + }); + this.grep(/.*/); + this.globals(this.globalProps().concat(extraGlobals())); + } + + /** + * Wrapper for setImmediate, process.nextTick, or browser polyfill. + * + * @param {Function} fn + * @api private + */ + + Runner.immediately = global.setImmediate || process.nextTick; + + /** + * Inherit from `EventEmitter.prototype`. + */ + + function F() {} + F.prototype = EventEmitter.prototype; + Runner.prototype = new F(); + Runner.prototype.constructor = Runner; + + /** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @param {RegExp} re + * @param {Boolean} invert + * @return {Runner} for chaining + * @api public + */ + + Runner.prototype.grep = function(re, invert) { + debug("grep %s", re); + this._grep = re; + this._invert = invert; + this.total = this.grepTotal(this.suite); + return this; + }; + + /** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @param {Suite} suite + * @return {Number} + * @api public + */ + + Runner.prototype.grepTotal = function(suite) { + var self = this; + var total = 0; + + suite.eachTest(function(test) { + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (match) total++; + }); + + return total; + }; + + /** + * Return a list of global properties. + * + * @return {Array} + * @api private + */ + + Runner.prototype.globalProps = function() { + var props = utils.keys(global); + + // non-enumerables + for (var i = 0; i < globals.length; ++i) { + if (~utils.indexOf(props, globals[i])) continue; + props.push(globals[i]); + } + + return props; + }; + + /** + * Allow the given `arr` of globals. + * + * @param {Array} arr + * @return {Runner} for chaining + * @api public + */ + + Runner.prototype.globals = function(arr) { + if (0 == arguments.length) return this._globals; + debug("globals %j", arr); + this._globals = this._globals.concat(arr); + return this; + }; + + /** + * Check for global variable leaks. + * + * @api private + */ + + Runner.prototype.checkGlobals = function(test) { + if (this.ignoreLeaks) return; + var ok = this._globals; + + var globals = this.globalProps(); + var leaks; + + if (test) { + ok = ok.concat(test._allowedGlobals || []); + } + + if (this.prevGlobalsLength == globals.length) return; + this.prevGlobalsLength = globals.length; + + leaks = filterLeaks(ok, globals); + this._globals = this._globals.concat(leaks); + + if (leaks.length > 1) { + this.fail( + test, + new Error("global leaks detected: " + leaks.join(", ") + "") + ); + } else if (leaks.length) { + this.fail(test, new Error("global leak detected: " + leaks[0])); + } + }; + + /** + * Fail the given `test`. + * + * @param {Test} test + * @param {Error} err + * @api private + */ + + Runner.prototype.fail = function(test, err) { + ++this.failures; + test.state = "failed"; + + if (!(err instanceof Error)) { + err = new Error( + "the " + + type(err) + + " " + + stringify(err) + + " was thrown, throw an Error :)" + ); + } + + err.stack = + this.fullStackTrace || !err.stack + ? err.stack + : stackFilter(err.stack); + + this.emit("fail", test, err); + }; + + /** + * Fail the given `hook` with `err`. + * + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and subsuites, but executes other `after each` + * hooks + * + * @param {Hook} hook + * @param {Error} err + * @api private + */ + + Runner.prototype.failHook = function(hook, err) { + this.fail(hook, err); + if (this.suite.bail()) { + this.emit("end"); + } + }; + + /** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @param {String} name + * @param {Function} function + * @api private + */ + + Runner.prototype.hook = function(name, fn) { + var suite = this.suite, + hooks = suite["_" + name], + self = this, + timer; + + function next(i) { + var hook = hooks[i]; + if (!hook) return fn(); + self.currentRunnable = hook; + + hook.ctx.currentTest = self.test; + + self.emit("hook", hook); + + hook.on("error", function(err) { + self.failHook(hook, err); + }); + + hook.run(function(err) { + hook.removeAllListeners("error"); + var testError = hook.error(); + if (testError) self.fail(self.test, testError); + if (err) { + if (err instanceof Pending) { + suite.pending = true; + } else { + self.failHook(hook, err); + + // stop executing hooks, notify callee of hook err + return fn(err); + } + } + self.emit("hook end", hook); + delete hook.ctx.currentTest; + next(++i); + }); + } + + Runner.immediately(function() { + next(0); + }); + }; + + /** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err, errSuite)`. + * + * @param {String} name + * @param {Array} suites + * @param {Function} fn + * @api private + */ + + Runner.prototype.hooks = function(name, suites, fn) { + var self = this, + orig = this.suite; + + function next(suite) { + self.suite = suite; + + if (!suite) { + self.suite = orig; + return fn(); + } + + self.hook(name, function(err) { + if (err) { + var errSuite = self.suite; + self.suite = orig; + return fn(err, errSuite); + } + + next(suites.pop()); + }); + } + + next(suites.pop()); + }; + + /** + * Run hooks from the top level down. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + + Runner.prototype.hookUp = function(name, fn) { + var suites = [this.suite].concat(this.parents()).reverse(); + this.hooks(name, suites, fn); + }; + + /** + * Run hooks from the bottom up. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + + Runner.prototype.hookDown = function(name, fn) { + var suites = [this.suite].concat(this.parents()); + this.hooks(name, suites, fn); + }; + + /** + * Return an array of parent Suites from + * closest to furthest. + * + * @return {Array} + * @api private + */ + + Runner.prototype.parents = function() { + var suite = this.suite, + suites = []; + while ((suite = suite.parent)) suites.push(suite); + return suites; + }; + + /** + * Run the current test and callback `fn(err)`. + * + * @param {Function} fn + * @api private + */ + + Runner.prototype.runTest = function(fn) { + var test = this.test, + self = this; + + if (this.asyncOnly) test.asyncOnly = true; + + try { + test.on("error", function(err) { + self.fail(test, err); + }); + test.run(fn); + } catch (err) { + fn(err); + } + }; + + /** + * Run tests in the given `suite` and invoke + * the callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + + Runner.prototype.runTests = function(suite, fn) { + var self = this, + tests = suite.tests.slice(), + test; + + function hookErr(err, errSuite, after) { + // before/after Each hook for errSuite failed: + var orig = self.suite; + + // for failed 'after each' hook start from errSuite parent, + // otherwise start from errSuite itself + self.suite = after ? errSuite.parent : errSuite; + + if (self.suite) { + // call hookUp afterEach + self.hookUp("afterEach", function(err2, errSuite2) { + self.suite = orig; + // some hooks may fail even now + if (err2) return hookErr(err2, errSuite2, true); + // report error suite + fn(errSuite); + }); + } else { + // there is no need calling other 'after each' hooks + self.suite = orig; + fn(errSuite); + } + } + + function next(err, errSuite) { + // if we bail after first err + if (self.failures && suite._bail) return fn(); + + if (self._abort) return fn(); + + if (err) return hookErr(err, errSuite, true); + + // next test + test = tests.shift(); + + // all done + if (!test) return fn(); + + // grep + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (!match) return next(); + + // pending + if (test.pending) { + self.emit("pending", test); + self.emit("test end", test); + return next(); + } + + // execute test and hook(s) + self.emit("test", (self.test = test)); + self.hookDown("beforeEach", function(err, errSuite) { + if (suite.pending) { + self.emit("pending", test); + self.emit("test end", test); + return next(); + } + if (err) return hookErr(err, errSuite, false); + + self.currentRunnable = self.test; + self.runTest(function(err) { + test = self.test; + + if (err) { + if (err instanceof Pending) { + self.emit("pending", test); + } else { + self.fail(test, err); + } + self.emit("test end", test); + + if (err instanceof Pending) { + return next(); + } + + return self.hookUp("afterEach", next); + } + + test.state = "passed"; + self.emit("pass", test); + self.emit("test end", test); + self.hookUp("afterEach", next); + }); + }); + } + + this.next = next; + next(); + }; + + /** + * Run the given `suite` and invoke the + * callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + + Runner.prototype.runSuite = function(suite, fn) { + var total = this.grepTotal(suite), + self = this, + i = 0; + + debug("run suite %s", suite.fullTitle()); + + if (!total) return fn(); + + this.emit("suite", (this.suite = suite)); + + function next(errSuite) { + if (errSuite) { + // current suite failed on a hook from errSuite + if (errSuite == suite) { + // if errSuite is current suite + // continue to the next sibling suite + return done(); + } else { + // errSuite is among the parents of current suite + // stop execution of errSuite and all sub-suites + return done(errSuite); + } + } + + if (self._abort) return done(); + + var curr = suite.suites[i++]; + if (!curr) return done(); + self.runSuite(curr, next); + } + + function done(errSuite) { + self.suite = suite; + self.hook("afterAll", function() { + self.emit("suite end", suite); + fn(errSuite); + }); + } + + this.hook("beforeAll", function(err) { + if (err) return done(); + self.runTests(suite, next); + }); + }; + + /** + * Handle uncaught exceptions. + * + * @param {Error} err + * @api private + */ + + Runner.prototype.uncaught = function(err) { + if (err) { + debug( + "uncaught exception %s", + err !== + function() { + return this; + }.call(err) + ? err + : err.message || err + ); + } else { + debug("uncaught undefined exception"); + err = utils.undefinedError(); + } + err.uncaught = true; + + var runnable = this.currentRunnable; + if (!runnable) return; + + runnable.clearTimeout(); + + // Ignore errors if complete + if (runnable.state) return; + this.fail(runnable, err); + + // recover from test + if ("test" == runnable.type) { + this.emit("test end", runnable); + this.hookUp("afterEach", this.next); + return; + } + + // bail on hooks + this.emit("end"); + }; + + /** + * Run the root suite and invoke `fn(failures)` + * on completion. + * + * @param {Function} fn + * @return {Runner} for chaining + * @api public + */ + + Runner.prototype.run = function(fn) { + var self = this, + rootSuite = this.suite; + + fn = fn || function() {}; + + function uncaught(err) { + self.uncaught(err); + } + + function start() { + self.emit("start"); + self.runSuite(rootSuite, function() { + debug("finished running"); + self.emit("end"); + }); + } + + debug("start"); + + // callback + this.on("end", function() { + debug("end"); + process.removeListener("uncaughtException", uncaught); + fn(self.failures); + }); + + // uncaught exception + process.on("uncaughtException", uncaught); + + if (this._delay) { + // for reporters, I guess. + // might be nice to debounce some dots while we wait. + this.emit("waiting", rootSuite); + rootSuite.once("run", start); + } else { + start(); + } + + return this; + }; + + /** + * Cleanly abort execution + * + * @return {Runner} for chaining + * @api public + */ + Runner.prototype.abort = function() { + debug("aborting"); + this._abort = true; + }; + + /** + * Filter leaks with the given globals flagged as `ok`. + * + * @param {Array} ok + * @param {Array} globals + * @return {Array} + * @api private + */ + + function filterLeaks(ok, globals) { + return filter(globals, function(key) { + // Firefox and Chrome exposes iframes as index inside the window object + if (/^d+/.test(key)) return false; + + // in firefox + // if runner runs in an iframe, this iframe's window.getInterface method not init at first + // it is assigned in some seconds + if (global.navigator && /^getInterface/.test(key)) return false; + + // an iframe could be approached by window[iframeIndex] + // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak + if (global.navigator && /^\d+/.test(key)) return false; + + // Opera and IE expose global variables for HTML element IDs (issue #243) + if (/^mocha-/.test(key)) return false; + + var matched = filter(ok, function(ok) { + if (~ok.indexOf("*")) + return 0 == key.indexOf(ok.split("*")[0]); + return key == ok; + }); + return ( + matched.length == 0 && + (!global.navigator || "onerror" !== key) + ); + }); + } + + /** + * Array of globals dependent on the environment. + * + * @return {Array} + * @api private + */ + + function extraGlobals() { + if ( + typeof process === "object" && + typeof process.version === "string" + ) { + var nodeVersion = process.version + .split(".") + .reduce(function(a, v) { + return (a << 8) | v; + }); + + // 'errno' was renamed to process._errno in v0.9.11. + + if (nodeVersion < 0x00090b) { + return ["errno"]; + } + } + + return []; + } + }); // module: runner.js + + require.register("suite.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var EventEmitter = require("browser/events").EventEmitter, + debug = require("browser/debug")("mocha:suite"), + milliseconds = require("./ms"), + utils = require("./utils"), + Hook = require("./hook"); + + /** + * Expose `Suite`. + */ + + exports = module.exports = Suite; + + /** + * Create a new `Suite` with the given `title` + * and parent `Suite`. When a suite with the + * same title is already present, that suite + * is returned to provide nicer reporter + * and more flexible meta-testing. + * + * @param {Suite} parent + * @param {String} title + * @return {Suite} + * @api public + */ + + exports.create = function(parent, title) { + var suite = new Suite(title, parent.ctx); + suite.parent = parent; + if (parent.pending) suite.pending = true; + title = suite.fullTitle(); + parent.addSuite(suite); + return suite; + }; + + /** + * Initialize a new `Suite` with the given + * `title` and `ctx`. + * + * @param {String} title + * @param {Context} ctx + * @api private + */ + + function Suite(title, parentContext) { + this.title = title; + var context = function() {}; + context.prototype = parentContext; + this.ctx = new context(); + this.suites = []; + this.tests = []; + this.pending = false; + this._beforeEach = []; + this._beforeAll = []; + this._afterEach = []; + this._afterAll = []; + this.root = !title; + this._timeout = 2000; + this._enableTimeouts = true; + this._slow = 75; + this._bail = false; + this.delayed = false; + } + + /** + * Inherit from `EventEmitter.prototype`. + */ + + function F() {} + F.prototype = EventEmitter.prototype; + Suite.prototype = new F(); + Suite.prototype.constructor = Suite; + + /** + * Return a clone of this `Suite`. + * + * @return {Suite} + * @api private + */ + + Suite.prototype.clone = function() { + var suite = new Suite(this.title); + debug("clone"); + suite.ctx = this.ctx; + suite.timeout(this.timeout()); + suite.enableTimeouts(this.enableTimeouts()); + suite.slow(this.slow()); + suite.bail(this.bail()); + return suite; + }; + + /** + * Set timeout `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + + Suite.prototype.timeout = function(ms) { + if (0 == arguments.length) return this._timeout; + if (ms.toString() === "0") this._enableTimeouts = false; + if ("string" == typeof ms) ms = milliseconds(ms); + debug("timeout %d", ms); + this._timeout = parseInt(ms, 10); + return this; + }; + + /** + * Set timeout `enabled`. + * + * @param {Boolean} enabled + * @return {Suite|Boolean} self or enabled + * @api private + */ + + Suite.prototype.enableTimeouts = function(enabled) { + if (arguments.length === 0) return this._enableTimeouts; + debug("enableTimeouts %s", enabled); + this._enableTimeouts = enabled; + return this; + }; + + /** + * Set slow `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + + Suite.prototype.slow = function(ms) { + if (0 === arguments.length) return this._slow; + if ("string" == typeof ms) ms = milliseconds(ms); + debug("slow %d", ms); + this._slow = ms; + return this; + }; + + /** + * Sets whether to bail after first error. + * + * @param {Boolean} bail + * @return {Suite|Number} for chaining + * @api private + */ + + Suite.prototype.bail = function(bail) { + if (0 == arguments.length) return this._bail; + debug("bail %s", bail); + this._bail = bail; + return this; + }; + + /** + * Run `fn(test[, done])` before running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.beforeAll = function(title, fn) { + if (this.pending) return this; + if ("function" === typeof title) { + fn = title; + title = fn.name; + } + title = '"before all" hook' + (title ? ": " + title : ""); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeAll.push(hook); + this.emit("beforeAll", hook); + return this; + }; + + /** + * Run `fn(test[, done])` after running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.afterAll = function(title, fn) { + if (this.pending) return this; + if ("function" === typeof title) { + fn = title; + title = fn.name; + } + title = '"after all" hook' + (title ? ": " + title : ""); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterAll.push(hook); + this.emit("afterAll", hook); + return this; + }; + + /** + * Run `fn(test[, done])` before each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.beforeEach = function(title, fn) { + if (this.pending) return this; + if ("function" === typeof title) { + fn = title; + title = fn.name; + } + title = '"before each" hook' + (title ? ": " + title : ""); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeEach.push(hook); + this.emit("beforeEach", hook); + return this; + }; + + /** + * Run `fn(test[, done])` after each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.afterEach = function(title, fn) { + if (this.pending) return this; + if ("function" === typeof title) { + fn = title; + title = fn.name; + } + title = '"after each" hook' + (title ? ": " + title : ""); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.enableTimeouts(this.enableTimeouts()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterEach.push(hook); + this.emit("afterEach", hook); + return this; + }; + + /** + * Add a test `suite`. + * + * @param {Suite} suite + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.addSuite = function(suite) { + suite.parent = this; + suite.timeout(this.timeout()); + suite.enableTimeouts(this.enableTimeouts()); + suite.slow(this.slow()); + suite.bail(this.bail()); + this.suites.push(suite); + this.emit("suite", suite); + return this; + }; + + /** + * Add a `test` to this suite. + * + * @param {Test} test + * @return {Suite} for chaining + * @api private + */ + + Suite.prototype.addTest = function(test) { + test.parent = this; + test.timeout(this.timeout()); + test.enableTimeouts(this.enableTimeouts()); + test.slow(this.slow()); + test.ctx = this.ctx; + this.tests.push(test); + this.emit("test", test); + return this; + }; + + /** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + + Suite.prototype.fullTitle = function() { + if (this.parent) { + var full = this.parent.fullTitle(); + if (full) return full + " " + this.title; + } + return this.title; + }; + + /** + * Return the total number of tests. + * + * @return {Number} + * @api public + */ + + Suite.prototype.total = function() { + return ( + utils.reduce( + this.suites, + function(sum, suite) { + return sum + suite.total(); + }, + 0 + ) + this.tests.length + ); + }; + + /** + * Iterates through each suite recursively to find + * all tests. Applies a function in the format + * `fn(test)`. + * + * @param {Function} fn + * @return {Suite} + * @api private + */ + + Suite.prototype.eachTest = function(fn) { + utils.forEach(this.tests, fn); + utils.forEach(this.suites, function(suite) { + suite.eachTest(fn); + }); + return this; + }; + + /** + * This will run the root suite if we happen to be running in delayed mode. + */ + Suite.prototype.run = function run() { + if (this.root) { + this.emit("run"); + } + }; + }); // module: suite.js + + require.register("test.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var Runnable = require("./runnable"); + + /** + * Expose `Test`. + */ + + module.exports = Test; + + /** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + + function Test(title, fn) { + Runnable.call(this, title, fn); + this.pending = !fn; + this.type = "test"; + } + + /** + * Inherit from `Runnable.prototype`. + */ + + function F() {} + F.prototype = Runnable.prototype; + Test.prototype = new F(); + Test.prototype.constructor = Test; + }); // module: test.js + + require.register("utils.js", function(module, exports, require) { + /** + * Module dependencies. + */ + + var fs = require("browser/fs"), + path = require("browser/path"), + basename = path.basename, + exists = fs.existsSync || path.existsSync, + glob = require("browser/glob"), + join = path.join, + debug = require("browser/debug")("mocha:watch"); + + /** + * Ignored directories. + */ + + var ignore = ["node_modules", ".git"]; + + /** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ + + exports.escape = function(html) { + return String(html) + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); + }; + + /** + * Array#forEach (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + + exports.forEach = function(arr, fn, scope) { + for (var i = 0, l = arr.length; i < l; i++) + fn.call(scope, arr[i], i); + }; + + /** + * Array#map (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + + exports.map = function(arr, fn, scope) { + var result = []; + for (var i = 0, l = arr.length; i < l; i++) + result.push(fn.call(scope, arr[i], i, arr)); + return result; + }; + + /** + * Array#indexOf (<=IE8) + * + * @parma {Array} arr + * @param {Object} obj to find index of + * @param {Number} start + * @api private + */ + + exports.indexOf = function(arr, obj, start) { + for (var i = start || 0, l = arr.length; i < l; i++) { + if (arr[i] === obj) return i; + } + return -1; + }; + + /** + * Array#reduce (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} initial value + * @api private + */ + + exports.reduce = function(arr, fn, val) { + var rval = val; + + for (var i = 0, l = arr.length; i < l; i++) { + rval = fn(rval, arr[i], i, arr); + } + + return rval; + }; + + /** + * Array#filter (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @api private + */ + + exports.filter = function(arr, fn) { + var ret = []; + + for (var i = 0, l = arr.length; i < l; i++) { + var val = arr[i]; + if (fn(val, i, arr)) ret.push(val); + } + + return ret; + }; + + /** + * Object.keys (<=IE8) + * + * @param {Object} obj + * @return {Array} keys + * @api private + */ + + exports.keys = + Object.keys || + function(obj) { + var keys = [], + has = Object.prototype.hasOwnProperty; // for `window` on <=IE8 + + for (var key in obj) { + if (has.call(obj, key)) { + keys.push(key); + } + } + + return keys; + }; + + /** + * Watch the given `files` for changes + * and invoke `fn(file)` on modification. + * + * @param {Array} files + * @param {Function} fn + * @api private + */ + + exports.watch = function(files, fn) { + var options = { interval: 100 }; + files.forEach(function(file) { + debug("file %s", file); + fs.watchFile(file, options, function(curr, prev) { + if (prev.mtime < curr.mtime) fn(file); + }); + }); + }; + + /** + * Array.isArray (<=IE8) + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + var isArray = + Array.isArray || + function(obj) { + return "[object Array]" == {}.toString.call(obj); + }; + + /** + * @description + * Buffer.prototype.toJSON polyfill + * @type {Function} + */ + if (typeof Buffer !== "undefined" && Buffer.prototype) { + Buffer.prototype.toJSON = + Buffer.prototype.toJSON || + function() { + return Array.prototype.slice.call(this, 0); + }; + } + + /** + * Ignored files. + */ + + function ignored(path) { + return !~ignore.indexOf(path); + } + + /** + * Lookup files in the given `dir`. + * + * @return {Array} + * @api private + */ + + exports.files = function(dir, ext, ret) { + ret = ret || []; + ext = ext || ["js"]; + + var re = new RegExp("\\.(" + ext.join("|") + ")$"); + + fs + .readdirSync(dir) + .filter(ignored) + .forEach(function(path) { + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ext, ret); + } else if (path.match(re)) { + ret.push(path); + } + }); + + return ret; + }; + + /** + * Compute a slug from the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + exports.slug = function(str) { + return str + .toLowerCase() + .replace(/ +/g, "-") + .replace(/[^-\w]/g, ""); + }; + + /** + * Strip the function definition from `str`, + * and re-indent for pre whitespace. + */ + + exports.clean = function(str) { + str = str + .replace(/\r\n?|[\n\u2028\u2029]/g, "\n") + .replace(/^\uFEFF/, "") + .replace(/^function *\(.*\)\s*{|\(.*\) *=> *{?/, "") + .replace(/\s+\}$/, ""); + + var spaces = str.match(/^\n?( *)/)[1].length, + tabs = str.match(/^\n?(\t*)/)[1].length, + re = new RegExp( + "^\n?" + + (tabs ? "\t" : " ") + + "{" + + (tabs ? tabs : spaces) + + "}", + "gm" + ); + + str = str.replace(re, ""); + + return exports.trim(str); + }; + + /** + * Trim the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + exports.trim = function(str) { + return str.replace(/^\s+|\s+$/g, ""); + }; + + /** + * Parse the given `qs`. + * + * @param {String} qs + * @return {Object} + * @api private + */ + + exports.parseQuery = function(qs) { + return exports.reduce( + qs.replace("?", "").split("&"), + function(obj, pair) { + var i = pair.indexOf("="), + key = pair.slice(0, i), + val = pair.slice(++i); + + obj[key] = decodeURIComponent(val); + return obj; + }, + {} + ); + }; + + /** + * Highlight the given string of `js`. + * + * @param {String} js + * @return {String} + * @api private + */ + + function highlight(js) { + return js + .replace(//g, ">") + .replace(/\/\/(.*)/gm, '//$1') + .replace(/('.*?')/gm, '$1') + .replace(/(\d+\.\d+)/gm, '$1') + .replace(/(\d+)/gm, '$1') + .replace( + /\bnew[ \t]+(\w+)/gm, + 'new $1' + ) + .replace( + /\b(function|new|throw|return|var|if|else)\b/gm, + '$1' + ); + } + + /** + * Highlight the contents of tag `name`. + * + * @param {String} name + * @api private + */ + + exports.highlightTags = function(name) { + var code = document + .getElementById("mocha") + .getElementsByTagName(name); + for (var i = 0, len = code.length; i < len; ++i) { + code[i].innerHTML = highlight(code[i].innerHTML); + } + }; + + /** + * If a value could have properties, and has none, this function is called, which returns + * a string representation of the empty value. + * + * Functions w/ no properties return `'[Function]'` + * Arrays w/ length === 0 return `'[]'` + * Objects w/ no properties return `'{}'` + * All else: return result of `value.toString()` + * + * @param {*} value Value to inspect + * @param {string} [type] The type of the value, if known. + * @returns {string} + */ + var emptyRepresentation = function emptyRepresentation( + value, + type + ) { + type = type || exports.type(value); + + switch (type) { + case "function": + return "[Function]"; + case "object": + return "{}"; + case "array": + return "[]"; + default: + return value.toString(); + } + }; + + /** + * Takes some variable and asks `{}.toString()` what it thinks it is. + * @param {*} value Anything + * @example + * type({}) // 'object' + * type([]) // 'array' + * type(1) // 'number' + * type(false) // 'boolean' + * type(Infinity) // 'number' + * type(null) // 'null' + * type(new Date()) // 'date' + * type(/foo/) // 'regexp' + * type('type') // 'string' + * type(global) // 'global' + * @api private + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString + * @returns {string} + */ + exports.type = function type(value) { + if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { + return "buffer"; + } + return Object.prototype.toString + .call(value) + .replace(/^\[.+\s(.+?)\]$/, "$1") + .toLowerCase(); + }; + + /** + * @summary Stringify `value`. + * @description Different behavior depending on type of value. + * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. + * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. + * - If `value` is an *empty* object, function, or array, return result of function + * {@link emptyRepresentation}. + * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of + * JSON.stringify(). + * + * @see exports.type + * @param {*} value + * @return {string} + * @api private + */ + + exports.stringify = function(value) { + var type = exports.type(value); + + if (!~exports.indexOf(["object", "array", "function"], type)) { + if (type != "buffer") { + return jsonStringify(value); + } + var json = value.toJSON(); + // Based on the toJSON result + return jsonStringify( + json.data && json.type ? json.data : json, + 2 + ).replace(/,(\n|$)/g, "$1"); + } + + for (var prop in value) { + if (Object.prototype.hasOwnProperty.call(value, prop)) { + return jsonStringify(exports.canonicalize(value), 2).replace( + /,(\n|$)/g, + "$1" + ); + } + } + + return emptyRepresentation(value, type); + }; + + /** + * @description + * like JSON.stringify but more sense. + * @param {Object} object + * @param {Number=} spaces + * @param {number=} depth + * @returns {*} + * @private + */ + function jsonStringify(object, spaces, depth) { + if (typeof spaces == "undefined") return _stringify(object); // primitive types + + depth = depth || 1; + var space = spaces * depth, + str = isArray(object) ? "[" : "{", + end = isArray(object) ? "]" : "}", + length = object.length || exports.keys(object).length, + repeat = function(s, n) { + return new Array(n).join(s); + }; // `.repeat()` polyfill + + function _stringify(val) { + switch (exports.type(val)) { + case "null": + case "undefined": + val = "[" + val + "]"; + break; + case "array": + case "object": + val = jsonStringify(val, spaces, depth + 1); + break; + case "boolean": + case "regexp": + case "number": + val = + val === 0 && 1 / val === -Infinity // `-0` + ? "-0" + : val.toString(); + break; + case "date": + val = "[Date: " + val.toISOString() + "]"; + break; + case "buffer": + var json = val.toJSON(); + // Based on the toJSON result + json = json.data && json.type ? json.data : json; + val = "[Buffer: " + jsonStringify(json, 2, depth + 1) + "]"; + break; + default: + val = + val == "[Function]" || val == "[Circular]" + ? val + : '"' + val + '"'; //string + } + return val; + } + + for (var i in object) { + if (!object.hasOwnProperty(i)) continue; // not my business + --length; + str += + "\n " + + repeat(" ", space) + + (isArray(object) ? "" : '"' + i + '": ') + // key + _stringify(object[i]) + // value + (length ? "," : ""); // comma + } + + return ( + str + + (str.length != 1 // [], {} + ? "\n" + repeat(" ", --space) + end + : end) + ); + } + + /** + * Return if obj is a Buffer + * @param {Object} arg + * @return {Boolean} + * @api private + */ + exports.isBuffer = function(arg) { + return typeof Buffer !== "undefined" && Buffer.isBuffer(arg); + }; + + /** + * @summary Return a new Thing that has the keys in sorted order. Recursive. + * @description If the Thing... + * - has already been seen, return string `'[Circular]'` + * - is `undefined`, return string `'[undefined]'` + * - is `null`, return value `null` + * - is some other primitive, return the value + * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method + * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. + * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()` + * + * @param {*} value Thing to inspect. May or may not have properties. + * @param {Array} [stack=[]] Stack of seen values + * @return {(Object|Array|Function|string|undefined)} + * @see {@link exports.stringify} + * @api private + */ + + exports.canonicalize = function(value, stack) { + var canonicalizedObj, + type = exports.type(value), + prop, + withStack = function withStack(value, fn) { + stack.push(value); + fn(); + stack.pop(); + }; + + stack = stack || []; + + if (exports.indexOf(stack, value) !== -1) { + return "[Circular]"; + } + + switch (type) { + case "undefined": + case "buffer": + case "null": + canonicalizedObj = value; + break; + case "array": + withStack(value, function() { + canonicalizedObj = exports.map(value, function(item) { + return exports.canonicalize(item, stack); + }); + }); + break; + case "function": + for (prop in value) { + canonicalizedObj = {}; + break; + } + if (!canonicalizedObj) { + canonicalizedObj = emptyRepresentation(value, type); + break; + } + /* falls through */ + case "object": + canonicalizedObj = canonicalizedObj || {}; + withStack(value, function() { + exports.forEach(exports.keys(value).sort(), function(key) { + canonicalizedObj[key] = exports.canonicalize( + value[key], + stack + ); + }); + }); + break; + case "date": + case "number": + case "regexp": + case "boolean": + canonicalizedObj = value; + break; + default: + canonicalizedObj = value.toString(); + } + + return canonicalizedObj; + }; + + /** + * Lookup file names at the given `path`. + */ + exports.lookupFiles = function lookupFiles( + path, + extensions, + recursive + ) { + var files = []; + var re = new RegExp("\\.(" + extensions.join("|") + ")$"); + + if (!exists(path)) { + if (exists(path + ".js")) { + path += ".js"; + } else { + files = glob.sync(path); + if (!files.length) + throw new Error( + "cannot resolve path (or pattern) '" + path + "'" + ); + return files; + } + } + + try { + var stat = fs.statSync(path); + if (stat.isFile()) return path; + } catch (ignored) { + return; + } + + fs.readdirSync(path).forEach(function(file) { + file = join(path, file); + try { + var stat = fs.statSync(file); + if (stat.isDirectory()) { + if (recursive) { + files = files.concat( + lookupFiles(file, extensions, recursive) + ); + } + return; + } + } catch (ignored) { + return; + } + if ( + !stat.isFile() || + !re.test(file) || + basename(file)[0] === "." + ) + return; + files.push(file); + }); + + return files; + }; + + /** + * Generate an undefined error with a message warning the user. + * + * @return {Error} + */ + + exports.undefinedError = function() { + return new Error( + "Caught undefined error, did you throw without specifying what?" + ); + }; + + /** + * Generate an undefined error if `err` is not defined. + * + * @param {Error} err + * @return {Error} + */ + + exports.getError = function(err) { + return err || exports.undefinedError(); + }; + + /** + * @summary + * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) + * @description + * When invoking this function you get a filter function that get the Error.stack as an input, + * and return a prettify output. + * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace). + * @returns {Function} + */ + + exports.stackTraceFilter = function() { + var slash = "/", + is = + typeof document === "undefined" + ? { node: true } + : { browser: true }, + cwd = is.node + ? process.cwd() + slash + : location.href.replace(/\/[^\/]*$/, "/"); + + function isNodeModule(line) { + return ~line.indexOf("node_modules"); + } + + function isMochaInternal(line) { + return ( + ~line.indexOf("node_modules" + slash + "mocha") || + ~line.indexOf("components" + slash + "mochajs") || + ~line.indexOf("components" + slash + "mocha") + ); + } + + // node_modules, bower, componentJS + function isBrowserModule(line) { + return ( + ~line.indexOf("node_modules") || ~line.indexOf("components") + ); + } + + function isNodeInternal(line) { + return ( + ~line.indexOf("(timers.js:") || + ~line.indexOf("(events.js:") || + ~line.indexOf("(node.js:") || + ~line.indexOf("(module.js:") || + ~line.indexOf("GeneratorFunctionPrototype.next (native)") || + false + ); + } + + return function(stack) { + stack = stack.split("\n"); + + stack = exports.reduce( + stack, + function(list, line) { + if ( + is.node && + (isNodeModule(line) || + isMochaInternal(line) || + isNodeInternal(line)) + ) + return list; + + if (is.browser && isBrowserModule(line)) return list; + + // Clean up cwd(absolute) + list.push(line.replace(cwd, "")); + return list; + }, + [] + ); + + return stack.join("\n"); + }; + }; + }); // module: utils.js + // The global object is "self" in Web Workers. + var global = (function() { + return this; + })(); + + /** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + + var Date = global.Date; + var setTimeout = global.setTimeout; + var setInterval = global.setInterval; + var clearTimeout = global.clearTimeout; + var clearInterval = global.clearInterval; + + /** + * Node shims. + * + * These are meant only to allow + * mocha.js to run untouched, not + * to allow running node code in + * the browser. + */ + + var process = {}; + process.exit = function(status) {}; + process.stdout = {}; + + var uncaughtExceptionHandlers = []; + + var originalOnerrorHandler = global.onerror; + + /** + * Remove uncaughtException listener. + * Revert to original onerror handler if previously defined. + */ + + process.removeListener = function(e, fn) { + if ("uncaughtException" == e) { + if (originalOnerrorHandler) { + global.onerror = originalOnerrorHandler; + } else { + global.onerror = function() {}; + } + var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn); + if (i != -1) { + uncaughtExceptionHandlers.splice(i, 1); + } + } + }; + + /** + * Implements uncaughtException listener. + */ + + process.on = function(e, fn) { + if ("uncaughtException" == e) { + global.onerror = function(err, url, line) { + fn(new Error(err + " (" + url + ":" + line + ")")); + return true; + }; + uncaughtExceptionHandlers.push(fn); + } + }; + + /** + * Expose mocha. + */ + + var Mocha = (global.Mocha = require("mocha")), + mocha = (global.mocha = new Mocha({ reporter: "html" })); + + // The BDD UI is registered by default, but no UI will be functional in the + // browser without an explicit call to the overridden `mocha.ui` (see below). + // Ensure that this default UI does not expose its methods to the global scope. + mocha.suite.removeAllListeners("pre-require"); + + var immediateQueue = [], + immediateTimeout; + + function timeslice() { + var immediateStart = new Date().getTime(); + while ( + immediateQueue.length && + new Date().getTime() - immediateStart < 100 + ) { + immediateQueue.shift()(); + } + if (immediateQueue.length) { + immediateTimeout = setTimeout(timeslice, 0); + } else { + immediateTimeout = null; + } + } + + /** + * High-performance override of Runner.immediately. + */ + + Mocha.Runner.immediately = function(callback) { + immediateQueue.push(callback); + if (!immediateTimeout) { + immediateTimeout = setTimeout(timeslice, 0); + } + }; + + /** + * Function to allow assertion libraries to throw errors directly into mocha. + * This is useful when running tests in a browser because window.onerror will + * only receive the 'message' attribute of the Error. + */ + mocha.throwError = function(err) { + Mocha.utils.forEach(uncaughtExceptionHandlers, function(fn) { + fn(err); + }); + throw err; + }; + + /** + * Override ui to ensure that the ui functions are initialized. + * Normally this would happen in Mocha.prototype.loadFiles. + */ + + mocha.ui = function(ui) { + Mocha.prototype.ui.call(this, ui); + this.suite.emit("pre-require", global, null, this); + return this; + }; + + /** + * Setup mocha with the given setting options. + */ + + mocha.setup = function(opts) { + if ("string" == typeof opts) opts = { ui: opts }; + for (var opt in opts) this[opt](opts[opt]); + return this; + }; + + /** + * Run mocha, returning the Runner. + */ + + mocha.run = function(fn) { + var options = mocha.options; + mocha.globals("location"); + + var query = Mocha.utils.parseQuery(global.location.search || ""); + if (query.grep) mocha.grep(new RegExp(query.grep)); + if (query.fgrep) mocha.grep(query.fgrep); + if (query.invert) mocha.invert(); + + return Mocha.prototype.run.call(mocha, function(err) { + // The DOM Document is not available in Web Workers. + var document = global.document; + if ( + document && + document.getElementById("mocha") && + options.noHighlighting !== true + ) { + Mocha.utils.highlightTags("code"); + } + if (fn) fn(err); + }); + }; + + /** + * Expose the process shim. + */ + + Mocha.process = process; +})(); diff --git a/subjects/21-Testing/mocha-setup.js b/subjects/21-Testing/mocha-setup.js new file mode 100644 index 000000000..7cfd274a4 --- /dev/null +++ b/subjects/21-Testing/mocha-setup.js @@ -0,0 +1,7 @@ +const div = document.createElement("div"); +div.setAttribute("id", "mocha"); +document.body.appendChild(div); +import "mocha/mocha.css"; +const mocha = require("imports?global=>window!./mocha-browser"); +window.mocha.setup("bdd"); +setTimeout(() => window.mocha.run(), 0); diff --git a/subjects/21-Testing/solution.js b/subjects/21-Testing/solution.js new file mode 100644 index 000000000..32487686e --- /dev/null +++ b/subjects/21-Testing/solution.js @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Exercise: +// +// - Fill in the test stubs to make the tests pass +//////////////////////////////////////////////////////////////////////////////// +import "./mocha-setup"; + +import React from "react"; +import ReactDOM from "react-dom"; +import { Simulate } from "react-dom/test-utils"; +import expect from "expect"; + +import Tabs from "./components/Tabs"; + +const FixtureData = [ + { + label: "USA", + content: "Land of the Free, Home of the brave" + }, + { label: "Brazil", content: "Sunshine, beaches, and Carnival" }, + { label: "Russia", content: "World Cup 2018!" } +]; + +describe("when is rendered", () => { + let node, activeBorderBottomColor; + beforeEach(() => { + node = document.createElement("div"); + + const activeTab = document.createElement("div"); + activeTab.setAttribute("style", "border-bottom-color: #000"); + activeBorderBottomColor = activeTab.style.borderBottomColor; + + ReactDOM.render(, node); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(node); + }); + + it("renders the USA tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[0].innerText).toEqual( + FixtureData[0].label, + "USA tab was not rendered" + ); + }); + + it("renders the Brazil tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[1].innerText).toEqual( + FixtureData[1].label, + "Brazil tab was not rendered" + ); + }); + + it("renders the Russia tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[2].innerText).toEqual( + FixtureData[2].label, + "Russia tab was not rendered" + ); + }); + + it("activates the first tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[0].style.borderBottomColor).toEqual( + activeBorderBottomColor, + "First tab is not active" + ); + }); + + it("does not activate the second tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[1].style.borderBottomColor).not.toEqual( + activeBorderBottomColor, + "Second tab is active" + ); + }); + + describe("after clicking the second tab", () => { + beforeEach(() => { + const tabs = node.querySelectorAll(".Tab"); + Simulate.click(tabs[1]); + }); + + it("activates the second tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[1].style.borderBottomColor).toEqual( + activeBorderBottomColor, + "Second tab is not active" + ); + }); + + it("deactivates the first tab", () => { + const tabs = node.querySelectorAll(".Tab"); + expect(tabs[0].style.borderBottomColor).not.toEqual( + activeBorderBottomColor, + "First tab is active" + ); + }); + + it("puts the correct content in the panel", () => { + const panel = node.querySelector(".TabPanel"); + expect(panel.innerText).toEqual( + FixtureData[1].content, + "Correct content is not in the panel" + ); + }); + }); +}); diff --git a/subjects/Animation/exercise.js b/subjects/Animation/exercise.js deleted file mode 100644 index be87fb381..000000000 --- a/subjects/Animation/exercise.js +++ /dev/null @@ -1,109 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// - Use a to animate the transition of the red "marker" to its -// destination when it is dropped -// -// Got extra time? -// -// - If you didn't already, use a custom spring to give the animation -// an elastic, bouncy feel -// - Add a "drop hint" element that indicates which element will receive -// the marker when it is dropped to improve usability -//////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import { render, findDOMNode } from 'react-dom' -import { Motion, spring } from 'react-motion' -import Draggable from './utils/Draggable' -import './styles' - -const DropGrid = React.createClass({ - getInitialState() { - return { - isDraggingMarker: false, - startX: 0, - startY: 0, - mouseX: 0, - mouseY: 0 - } - }, - - getRelativeXY({ clientX, clientY }) { - const { offsetLeft, offsetTop } = findDOMNode(this) - - return { - x: clientX - offsetLeft, - y: clientY - offsetTop - } - }, - - handleDragStart(event) { - const { x, y } = this.getRelativeXY(event) - const { offsetLeft, offsetTop } = event.target - - // Prevent Chrome from displaying a text cursor - event.preventDefault() - - this.setState({ - isDraggingMarker: true, - startX: x - offsetLeft, - startY: y - offsetTop, - mouseX: x, - mouseY: y - }) - }, - - handleDrag(event) { - const { x, y } = this.getRelativeXY(event) - - this.setState({ - mouseX: x, - mouseY: y - }) - }, - - handleDrop() { - this.setState({ isDraggingMarker: false }) - }, - - render() { - const { isDraggingMarker, startX, startY, mouseX, mouseY } = this.state - - let markerLeft, markerTop - if (isDraggingMarker) { - markerLeft = mouseX - startX - markerTop = mouseY - startY - } else { - markerLeft = Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150 - markerTop = Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150 - } - - const markerStyle = { - left: markerLeft, - top: markerTop - } - - return ( -
      - -
      1
      -
      2
      -
      3
      -
      4
      -
      5
      -
      6
      -
      7
      -
      8
      -
      9
      -
      - ) - } -}) - -render(, document.getElementById('app')) diff --git a/subjects/Animation/solution.js b/subjects/Animation/solution.js deleted file mode 100644 index 20a3defcc..000000000 --- a/subjects/Animation/solution.js +++ /dev/null @@ -1,116 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// - Use a to animate the transition of the red "marker" to its -// destination when it is dropped -// -// Got extra time? -// -// - If you didn't already, use a custom spring to give the animation -// an elastic, bouncy feel -// - Add a "drop hint" element that indicates which element will receive -// the marker when it is dropped to improve usability -//////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import { render, findDOMNode } from 'react-dom' -import { Motion, spring } from 'react-motion' -import Draggable from './utils/Draggable' -import './styles' - -const DropGrid = React.createClass({ - getInitialState() { - return { - isDraggingMarker: false, - startX: 0, - startY: 0, - mouseX: 0, - mouseY: 0 - } - }, - - getRelativeXY({ clientX, clientY }) { - const { offsetLeft, offsetTop } = findDOMNode(this) - - return { - x: clientX - offsetLeft, - y: clientY - offsetTop - } - }, - - handleDragStart(event) { - const { x, y } = this.getRelativeXY(event) - const { offsetLeft, offsetTop } = event.target - - // Prevent Chrome from displaying a text cursor - event.preventDefault() - - this.setState({ - isDraggingMarker: true, - startX: x - offsetLeft, - startY: y - offsetTop, - mouseX: x, - mouseY: y - }) - }, - - handleDrag(event) { - const { x, y } = this.getRelativeXY(event) - - this.setState({ - mouseX: x, - mouseY: y - }) - }, - - handleDrop() { - this.setState({ isDraggingMarker: false }) - }, - - render() { - const { isDraggingMarker, startX, startY, mouseX, mouseY } = this.state - - let markerLeft, markerTop - if (isDraggingMarker) { - markerLeft = mouseX - startX - markerTop = mouseY - startY - } else { - markerLeft = Math.floor(Math.max(0, Math.min(449, mouseX)) / 150) * 150 - markerTop = Math.floor(Math.max(0, Math.min(449, mouseY)) / 150) * 150 - } - - const bouncySpring = (style) => - spring(style, { stiffness: 170, damping: 8 }) - - const markerStyle = { - left: isDraggingMarker ? markerLeft : bouncySpring(markerLeft), - top: isDraggingMarker ? markerTop : bouncySpring(markerTop) - } - - return ( -
      - - {style => ( - - )} - -
      1
      -
      2
      -
      3
      -
      4
      -
      5
      -
      6
      -
      7
      -
      8
      -
      9
      -
      - ) - } -}) - -render(, document.getElementById('app')) diff --git a/subjects/Animation/utils/Draggable.js b/subjects/Animation/utils/Draggable.js deleted file mode 100644 index 3b628954e..000000000 --- a/subjects/Animation/utils/Draggable.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { PropTypes } from 'react' - -const Draggable = React.createClass({ - propTypes: { - component: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func - ]).isRequired, - onDragStart: PropTypes.func, - onDrag: PropTypes.func, - onDrop: PropTypes.func - }, - - getDefaultProps() { - return { - component: 'div' - } - }, - - componentDidMount() { - this.isDragging = false - document.addEventListener('mouseup', this.handleMouseUp) - document.addEventListener('mousemove', this.handleMouseMove) - }, - - componentWillUnmount() { - document.removeEventListener('mousemove', this.handleMouseMove) - document.removeEventListener('mouseup', this.handleMouseUp) - }, - - handleMouseDown(event) { - if (!this.isDragging) { - this.isDragging = true - - // Prevent Chrome from displaying a text cursor - event.preventDefault() - - if (this.props.onDragStart) - this.props.onDragStart(event) - } - }, - - handleMouseMove(event) { - if (this.isDragging && this.props.onDrag) - this.props.onDrag(event) - }, - - handleMouseUp(event) { - if (this.isDragging) { - this.isDragging = false - - if (this.props.onDrop) - this.props.onDrop(event) - } - }, - - render() { - const { component, ...otherProps } = this.props - - return React.createElement(component, { - ...otherProps, - onMouseDown: this.handleMouseDown - }) - } -}) - -export default Draggable diff --git a/subjects/Animation/utils/Tone.js b/subjects/Animation/utils/Tone.js deleted file mode 100644 index 0742d342a..000000000 --- a/subjects/Animation/utils/Tone.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import createOscillator from './createOscillator' - -const { PropTypes } = React - -const Tone = React.createClass({ - propTypes: { - isPlaying: PropTypes.bool.isRequired, - pitch: PropTypes.number.isRequired, - volume: PropTypes.number.isRequired - }, - - componentDidMount() { - this.oscillator = createOscillator() - this.doImperativeWork() - }, - - componentDidUpdate() { - this.doImperativeWork() - }, - - doImperativeWork() { - if (this.props.isPlaying) { - this.oscillator.play() - } else { - this.oscillator.stop() - } - - this.oscillator.setPitchBend(this.props.pitch) - this.oscillator.setVolume(this.props.volume) - }, - - render() { - return null - } - -}) - -export default Tone diff --git a/subjects/Animation/utils/createOscillator.js b/subjects/Animation/utils/createOscillator.js deleted file mode 100644 index e5536ab50..000000000 --- a/subjects/Animation/utils/createOscillator.js +++ /dev/null @@ -1,63 +0,0 @@ -import './AudioContextMonkeyPatch' - -function Oscillator(audioContext) { - // TODO make more things not use this. - const oscillatorNode = audioContext.createOscillator() - oscillatorNode.start(0) - - const gainNode = audioContext.createGain() - this.pitchBase = 50 - this.pitchBend = 0 - this.pitchRange = 2000 - this.volume = 0.5 - this.maxVolume = 1 - this.frequency = this.pitchBase - - let hasConnected = false - let frequency = this.pitchBase - - this.play = function () { - oscillatorNode.connect(gainNode) - hasConnected = true - } - - this.stop = function () { - if (hasConnected) { - oscillatorNode.disconnect(gainNode) - hasConnected = false - } - } - - this.setType = function (type) { - oscillatorNode.type = type - } - - this.setPitchBend = function (v) { - this.pitchBend = v - frequency = this.pitchBase + this.pitchBend * this.pitchRange - oscillatorNode.frequency.value = frequency - this.frequency = frequency - } - - this.setVolume = function (v) { - this.volume = this.maxVolume * v - gainNode.gain.value = this.volume - } - - this.connect = function (output) { - gainNode.connect(output) - } - - return this -} - -function createOscillator() { - const audioContext = new AudioContext() - const theremin = new Oscillator(audioContext) - - theremin.connect(audioContext.destination) - - return theremin -} - -export default createOscillator diff --git a/subjects/Calculator/exercise.js b/subjects/Calculator/exercise.js deleted file mode 100644 index a36ac7917..000000000 --- a/subjects/Calculator/exercise.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import './styles.css' - -class Calculator extends React.Component { - render() { - return ( -
      -
      0
      -
      -
      -
      - - - -
      -
      - - - - - - - - - - - -
      -
      -
      - - - - - -
      -
      -
      - ) - } -} - -ReactDOM.render( -
      - -
      , - document.getElementById('app') -) diff --git a/subjects/Calculator/solution.js b/subjects/Calculator/solution.js deleted file mode 100644 index d67f4a6cc..000000000 --- a/subjects/Calculator/solution.js +++ /dev/null @@ -1,276 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import ReactPoint from 'react-point' -import './styles.css' - -class AutoScalingText extends React.Component { - state = { - scale: 1 - } - - componentDidUpdate() { - const { scale } = this.state - - const node = this.node - const parentNode = node.parentNode - - const availableWidth = parentNode.offsetWidth - const actualWidth = node.offsetWidth - const actualScale = availableWidth / actualWidth - - if (scale === actualScale) - return - - if (actualScale < 1) { - this.setState({ scale: actualScale }) - } else if (scale < 1) { - this.setState({ scale: 1 }) - } - } - - render() { - const { scale } = this.state - - return ( -
      this.node = node} - >{this.props.children}
      - ) - } -} - -class CalculatorDisplay extends React.Component { - render() { - const { value, ...props } = this.props - - const language = navigator.language || 'en-US' - let formattedValue = parseFloat(value).toLocaleString(language, { - useGrouping: true, - maximumFractionDigits: 6 - }) - - // Add back missing .0 in e.g. 12.0 - const match = value.match(/\.\d*?(0*)$/) - - if (match) - formattedValue += (/[1-9]/).test(match[0]) ? match[1] : match[0] - - return ( -
      - {formattedValue} -
      - ) - } -} - -class CalculatorKey extends React.Component { - render() { - const { onPress, className, ...props } = this.props - - return ( - - - {isOpen && ( -
      -

      A taco is a traditional Mexican dish composed of a corn or wheat tortilla folded or rolled around a filling.

      -
      - )} -
      - ) -} - -function updateThePage() { - ReactDOM.render(, document.getElementById('app')) -} - -updateThePage() - -//////////////////////////////////////////////////////////////////////////////// -// Let's encapsulate state in an object and call it what it really is. Then, add -// a setState function that we can use to update state and automatically update -// the page any time the state changes. - -////////////////////////////////////////////////////////////////////////////////// -// React gives us setState and automatically re-renders as the state changes. - -//////////////////////////////////////////////////////////////////////////////// -// Let's make re-usable and render a few of them. Title and -// children are properties we can pass in from the parent component. - -//////////////////////////////////////////////////////////////////////////////// -// Wrap a few s in a that tracks the # of times -// it has been toggled and shows a counter. gets an onToggle -// handler, declared as a function in propTypes. - -//////////////////////////////////////////////////////////////////////////////// -// But we just got finished making generic, and now -// is not! Can we make it generic as well? React.cloneElement -// can help us pass props to elements that weren't initially provided. -// -// Side note: Be careful to use the React.Children utility methods. -// this.props.children is opaque! - -//class ContentToggle extends React.Component { -// state = { -// isOpen: false -// } -// -// handleClick() { -// this.setState({ -// isOpen: !this.state.isOpen -// }) -// -// if (this.props.onToggle) -// this.props.onToggle() -// } -// -// render() { -// let summaryClassName = 'ContentToggle__Summary' -// -// if (this.state.isOpen) -// summaryClassName += ' ContentToggle__Summary--is-open' -// -// return ( -//
      -// -// {this.state.isOpen && ( -//
      -// {this.props.children} -//
      -// )} -//
      -// ) -// } -//} -// -//class ToggleTracker extends React.Component { -// state = { -// numToggles: 0 -// } -// -// handleToggle() { -// this.setState({ -// numToggles: this.state.numToggles + 1 -// }) -// } -// -// render() { -// let { children } = this.props -// -// children = React.Children.map(children, (child) => ( -// React.cloneElement(child, { -// onToggle: this.handleToggle -// }) -// )) -// -// return ( -//
      -//
      {JSON.stringify(this.state, null, 2)}
      -// {children} -//
      -// ) -// } -//} -// -//ReactDOM.render(( -// -// -//

      A taco is a traditional Mexican dish composed of a corn or wheat tortilla folded or rolled around a filling.

      -//
      -// -//

      A burrito is a type of Mexican and Tex-Mex food, consisting of a wheat flour tortilla wrapped or folded into a cylindrical shape to completely enclose the filling (in contrast to a taco, which is generally formed by simply folding a tortilla in half around a filling, leaving the semicircular perimeter open).

      -//
      -//
      -//), document.getElementById('app')) diff --git a/subjects/Components/solution.js b/subjects/Components/solution.js deleted file mode 100644 index 10412bf61..000000000 --- a/subjects/Components/solution.js +++ /dev/null @@ -1,97 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// - Render a tab for each country with its name in the tab -// - Make it so that you can click on a tab and it will appear active -// while the others appear inactive -// - Make it so the panel renders the correct content for the selected tab -// -// Got extra time? -// -// - Make generic so that it doesn't know anything about -// country data (Hint: good propTypes help) -//////////////////////////////////////////////////////////////////////////////// -import React from 'react' -import ReactDOM from 'react-dom' - -const styles = {} - -styles.tab = { - display: 'inline-block', - padding: 10, - margin: 10, - borderBottom: '4px solid', - borderBottomColor: '#ccc', - cursor: 'pointer' -} - -styles.activeTab = { - ...styles.tab, - borderBottomColor: '#000' -} - -styles.panel = { - padding: 10 -} - -class Tabs extends React.Component { - state = { - activeTabIndex: 0 - } - - selectTabIndex(activeTabIndex) { - this.setState({ activeTabIndex }) - } - - render() { - const { data } = this.props - const { activeTabIndex } = this.state - - const tabs = data.map((country, index) => { - const isActive = index === activeTabIndex - const style = isActive ? styles.activeTab : styles.tab - - return ( -
      this.selectTabIndex(index)} - >{country.name}
      - ) - }) - - const activeCountry = data[activeTabIndex] - const content = activeCountry && activeCountry.description - - return ( -
      - {tabs} -
      - {content} -
      -
      - ) - } -} - -class App extends React.Component { - render() { - return ( -
      -

      Countries

      - -
      - ) - } -} - -const DATA = [ - { id: 1, name: 'USA', description: 'Land of the Free, Home of the brave' }, - { id: 2, name: 'Brazil', description: 'Sunshine, beaches, and Carnival' }, - { id: 3, name: 'Russia', description: 'World Cup 2018!' } -] - -ReactDOM.render(, document.getElementById('app'), function () { - require('./tests').run(this) -}) diff --git a/subjects/Components/tests.js b/subjects/Components/tests.js deleted file mode 100644 index b73bfe8fe..000000000 --- a/subjects/Components/tests.js +++ /dev/null @@ -1,44 +0,0 @@ -/*eslint-disable no-console */ -import { findDOMNode } from 'react-dom' -import { Simulate } from 'react-addons-test-utils' -import assert from '../assert' - -export function run(component) { - const node = findDOMNode(component) - const html = node.innerHTML - const tabs = node.querySelectorAll('.Tab') - const panel = node.querySelector('.TabPanel') - - const borderFixture = document.createElement('div') - borderFixture.setAttribute('style', 'border-bottom-color: #000') - - console.log('on first render') - assert(!!html.match(/USA/), 'render USA tab') - assert(!!html.match(/Brazil/), 'render Brazil tab') - assert(!!html.match(/Russia/), 'render Russia tab') - assert( - tabs[0] && tabs[0].style.borderBottomColor === borderFixture.style.borderBottomColor, - 'first tab is active' - ) - assert( - tabs[1] && tabs[1].style.borderBottomColor !== borderFixture.style.borderBottomColor, - 'second tab is inactive' - ) - - console.log('after clicking the second tab...') - Simulate.click(tabs[1]) - assert( - tabs[1] && tabs[1].style.borderBottomColor === borderFixture.style.borderBottomColor, - 'second tab is active' - ) - assert( - tabs[0] && tabs[0].style.borderBottomColor !== borderFixture.style.borderBottomColor, - 'first tab is inactive' - ) - assert( - panel.textContent.trim() === 'Sunshine, beaches, and Carnival', - 'panel has the correct content' - ) - - Simulate.click(tabs[0]) -} diff --git a/subjects/CompoundComponents/lecture.js b/subjects/CompoundComponents/lecture.js deleted file mode 100644 index 906354577..000000000 --- a/subjects/CompoundComponents/lecture.js +++ /dev/null @@ -1,428 +0,0 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' -import * as styles from './lib/styles' - -class Tabs extends React.Component { - state = { - activeIndex: 0 - } - - selectTabIndex(activeIndex) { - this.setState({ activeIndex }) - } - - renderTabs() { - return this.props.data.map((tab, index) => { - const isActive = this.state.activeIndex === index - return ( -
      this.selectTabIndex(index)} - >{tab.label}
      - ) - }) - } - - renderPanel() { - const tab = this.props.data[this.state.activeIndex] - return ( -
      -

      {tab.description}

      -
      - ) - } - - render() { - return ( -
      -
      - {this.renderTabs()} -
      -
      - {this.renderPanel()} -
      -
      - ) - } -} - -class App extends React.Component { - render() { - const tabData = [ - { label: 'Tacos', - description:

      Tacos are delicious

      - }, - { label: 'Burritos', - description:

      Sometimes a burrito is what you really need

      - }, - { label: 'Coconut Korma', - description:

      Might be your best option

      - } - ] - - return ( -
      - -
      - ) - } -} - -ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// What if I wanted tabs on the bottom? - -//class Tabs extends React.Component { -// static defaultProps = { -// tabsPlacement: 'top' -// } -// -// state = { -// activeIndex: 0 -// } -// -// selectTabIndex(activeIndex) { -// this.setState({ activeIndex }) -// } -// -// renderTabs() { -// return this.props.data.map((tab, index) => { -// const isActive = this.state.activeIndex === index -// return ( -//
      this.selectTabIndex(index)} -// >{tab.label}
      -// ) -// }) -// } -// -// renderPanel() { -// const tab = this.props.data[this.state.activeIndex] -// return ( -//
      -//

      {tab.description}

      -//
      -// ) -// } -// -// render() { -// const tabs = ( -//
      -// {this.renderTabs()} -//
      -// ) -// const panel = ( -//
      -// {this.renderPanel()} -//
      -// ) -// return ( -//
      -// {this.props.tabsPlacement === 'top' ? -// [tabs, panel] : -// [panel, tabs] -// } -//
      -// ) -// } -//} -// -//class App extends React.Component { -// render() { -// const tabData = [ -// { label: 'Tacos', -// description:

      Tacos are delicious

      -// }, -// { label: 'Burritos', -// description:

      Sometimes a burrito is what you really need

      -// }, -// { label: 'Coconut Korma', -// description:

      Might be your best option

      -// } -// ] -// -// return ( -//
      -// -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// That wasn't too bad, but it added a lot of complexity for something that -// didn't seem to warrant that much of a change -// -// - render is less obvious -// - have to use keys, or wrap stuff in extra divs -// - adding another option that has to do with rendering will add even more -// complexity - -//////////////////////////////////////////////////////////////////////////////// -// Lets add "disabled" to a tab, what does jQuery UI do? -// https://api.jqueryui.com/tabs/#option-disabled - -//class Tabs extends React.Component { -// static defaultProps = { -// tabsPlacement: 'top', -// disabled: [] -// } -// -// state = { -// activeIndex: 0 -// } -// -// selectTabIndex(activeIndex) { -// this.setState({ activeIndex }) -// } -// -// renderTabs() { -// return this.props.data.map((tab, index) => { -// const isActive = this.state.activeIndex === index -// const isDisabled = this.props.disabled.indexOf(index) !== -1 -// const props = { -// key: tab.label, -// style: isDisabled ? styles.disabledTab : ( -// isActive ? styles.activeTab : styles.tab -// ) -// } -// if (!isDisabled) -// props.onClick = () => this.selectTabIndex(index) -// return
      {tab.label}
      -// }) -// } -// -// renderPanel() { -// const tab = this.props.data[this.state.activeIndex] -// return ( -//
      -//

      {tab.description}

      -//
      -// ) -// } -// -// render() { -// const tabs = ( -//
      -// {this.renderTabs()} -//
      -// ) -// const panel = ( -//
      -// {this.renderPanel()} -//
      -// ) -// return ( -//
      -// {this.props.tabsPlacement === 'top' ? -// [tabs, panel] : -// [panel, tabs] -// } -//
      -// ) -// } -//} -// -//class App extends React.Component { -// render() { -// const tabData = [ -// { label: 'Tacos', -// description:

      Tacos are delicious

      -// }, -// { label: 'Burritos', -// description:

      Sometimes a burrito is what you really need

      -// }, -// { label: 'Coconut Korma', -// description:

      Might be your best option

      -// } -// ] -// -// return ( -//
      -// -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Feels weird ... whenever your options affect rendering, its a great -// opportunity to create child components instead - -//class TabList extends React.Component { -// render() { -// const children = React.Children.map(this.props.children, (child, index) => { -// return React.cloneElement(child, { -// isActive: index === this.props.activeIndex, -// onClick: () => this.props.onActivate(index) -// }) -// }) -// -// return
      {children}
      -// } -//} -// -//class Tab extends React.Component { -// render() { -// return ( -//
      -// {this.props.children} -//
      -// ) -// } -//} -// -//class TabPanels extends React.Component { -// render() { -// return ( -//
      -// {this.props.children[this.props.activeIndex]} -//
      -// ) -// } -//} -// -//class TabPanel extends React.Component { -// render() { -// return
      {this.props.children}
      -// } -//} -// -//class Tabs extends React.Component { -// state = { -// activeIndex: 0 -// } -// -// render() { -// const children = React.Children.map(this.props.children, (child, index) => { -// if (child.type === TabPanels) { -// return React.cloneElement(child, { -// activeIndex: this.state.activeIndex -// }) -// } else if (child.type === TabList) { -// return React.cloneElement(child, { -// activeIndex: this.state.activeIndex, -// onActivate: (activeIndex) => this.setState({ activeIndex }) -// }) -// } else { -// return child -// } -// }) -// -// return
      {children}
      -// } -//} -// -//class App extends React.Component { -// render() { -// return ( -//
      -// -// -// Tacos -// Burritos -// Coconut Korma -// -// -// -// -//

      Tacos are delicious

      -//
      -// -//

      Sometimes a burrito is what you really need

      -//
      -// -//

      Might be your best option

      -//
      -//
      -//
      -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Now this is really flexible -// -// - can change order of panels v. tabs -// - can pass in our own styles to tabs -// - can even have unrelated elements inside -// - in other words, we now have control over rendering while -// Tabs handles the interaction -// -// Oh but you really loved the old tabs yeah? - -//class DataTabs extends React.Component { -// static defaultProps = { -// disabled: [] -// } -// -// render() { -// return ( -// -// -// {this.props.data.map((item, index) => ( -// -// {item.label} -// -// ))} -// -// -// -// {this.props.data.map((item) => ( -// {item.description} -// ))} -// -// -// ) -// } -//} -// -//class App extends React.Component { -// render() { -// const tabData = [ -// { label: 'Tacos', -// description:

      Tacos are delicious

      -// }, -// { label: 'Burritos', -// description:

      Sometimes a burrito is what you really need

      -// }, -// { label: 'Coconut Korma', -// description:

      Might be your best option

      -// } -// ] -// -// return ( -//
      -// -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Instead of creating a handful of options, compose several components together -// and then compose them together into their own components. -// -// A really awesome library that does this is react-soundplayer diff --git a/subjects/CompoundComponents/lib/styles.js b/subjects/CompoundComponents/lib/styles.js deleted file mode 100644 index fab419082..000000000 --- a/subjects/CompoundComponents/lib/styles.js +++ /dev/null @@ -1,25 +0,0 @@ -export const tabs = {} - -export const tab = { - display: 'inline-block', - padding: 10, - margin: 10, - borderBottom: '4px solid', - borderBottomColor: '#ccc', - cursor: 'pointer' -} - -export const activeTab = { - ...tab, - borderBottomColor: '#000' -} - -export const disabledTab = { - ...tab, - opacity: 0.25, - cursor: 'default' -} - -export const tabPanels = { - padding: 10 -} diff --git a/subjects/Context/lecture.js b/subjects/Context/lecture.js deleted file mode 100644 index e8f277960..000000000 --- a/subjects/Context/lecture.js +++ /dev/null @@ -1,314 +0,0 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' -import * as styles from './styles' - -//////////////////////////////////////////////////////////////////////////////// -// Sometimes you don't want to specify how deep in the view tree the child -// components need to be, our current implementation expects TabList/TabPanels -// to be immediate children of Tabs, also Tab and TabPanel are required to be -// immediate children of their parent components. We really only care about the -// interactivity between the components, not their hierarchy. -// -// We could recursively check children with each render, which seems like a bad -// plan, so instead we can use a feature called "context". - -class TabList extends React.Component { - render() { - const children = React.Children.map(this.props.children, (child, index) => { - return React.cloneElement(child, { - isActive: index === this.props.activeIndex, - onClick: () => this.props.onActivate(index) - }) - }) - - return
      {children}
      - } -} - -class Tab extends React.Component { - render() { - return ( -
      - {this.props.children} -
      - ) - } -} - -class TabPanels extends React.Component { - render() { - return ( -
      - {this.props.children[this.props.activeIndex]} -
      - ) - } -} - -class TabPanel extends React.Component { - render() { - return
      {this.props.children}
      - } -} - -class Tabs extends React.Component { - state = { - activeIndex: 0 - } - - render() { - const children = React.Children.map(this.props.children, (child, index) => { - if (child.type === TabPanels) { - return React.cloneElement(child, { - activeIndex: this.state.activeIndex - }) - } else if (child.type === TabList) { - return React.cloneElement(child, { - activeIndex: this.state.activeIndex, - onActivate: (activeIndex) => this.setState({ activeIndex }) - }) - } else { - return child - } - }) - - return
      {children}
      - } -} - -class App extends React.Component { - render () { - return ( -
      - - - Tacos - Burritos - Coconut Korma - - -

      Tacos are delicious

      -

      Sometimes a burrito is what you really need

      -

      Might be your best option

      -
      -
      -
      - ) - } -} - -ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Wrapping in a div breaks everything! Instead of using -// cloneElement, let's use context. - -//class TabList extends React.Component { -// render() { -// const children = React.Children.map(this.props.children, (child, index) => { -// return React.cloneElement(child, { -// isActive: index === this.props.activeIndex, -// onClick: () => this.props.onActivate(index) -// }) -// }) -// return
      {children}
      -// } -//} -// -//class Tab extends React.Component { -// render() { -// return ( -//
      -// {this.props.children} -//
      -// ) -// } -//} -// -//class TabPanels extends React.Component { -// static contextTypes = { -// activeIndex: PropTypes.number -// } -// -// render() { -// return ( -//
      -// {this.props.children[this.context.activeIndex]} -//
      -// ) -// } -//} -// -//class TabPanel extends React.Component { -// render() { -// return
      {this.props.children}
      -// } -//} -// -//class Tabs extends React.Component { -// static childContextTypes = { -// activeIndex: PropTypes.number -// } -// -// getChildContext () { -// return { -// activeIndex: this.state.activeIndex -// } -// } -// -// state = { -// activeIndex: 0 -// } -// -// render() { -// const children = React.Children.map(this.props.children, (child, index) => { -// if (child.type === TabList) { -// return React.cloneElement(child, { -// activeIndex: this.state.activeIndex, -// onActivate: (activeIndex) => this.setState({ activeIndex }) -// }) -// } else { -// return child -// } -// }) -// -// return
      {children}
      -// } -//} -// -//class App extends React.Component { -// render () { -// return ( -//
      -// -// -// Tacos -// Burritos -// Coconut Korma -// -//
      -// -//

      Tacos are delicious

      -//

      Sometimes a burrito is what you really need

      -//

      Might be your best option

      -//
      -//
      -//
      -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Wrapping also breaks (no more active styles), lets check context -// for isActive and the click handler instead of props. - -//class TabList extends React.Component { -// static contextTypes = { -// activeIndex: PropTypes.number, -// onActivate: PropTypes.func -// } -// -// render() { -// const children = React.Children.map(this.props.children, (child, index) => ( -// React.cloneElement(child, { -// isActive: index === this.context.activeIndex, -// onClick: () => this.context.onActivate(index) -// }) -// )) -// -// return
      {children}
      -// } -//} -// -//class Tab extends React.Component { -// render() { -// return ( -//
      -// {this.props.children} -//
      -// ) -// } -//} -// -//class TabPanels extends React.Component { -// static contextTypes = { -// activeIndex: PropTypes.number -// } -// -// render() { -// return ( -//
      -// {this.props.children[this.context.activeIndex]} -//
      -// ) -// } -//} -// -//class Tabs extends React.Component { -// static childContextTypes = { -// activeIndex: PropTypes.number, -// onActivate: PropTypes.func -// } -// -// getChildContext() { -// return { -// activeIndex: this.state.activeIndex, -// onActivate: (activeIndex) => { -// this.setState({ activeIndex }) -// } -// } -// } -// -// state = { -// activeIndex: 0 -// } -// -// render() { -// return
      {this.props.children}
      -// } -//} -// -//class App extends React.Component { -// render() { -// return ( -//
      -// -//
      -// -// Tacos -// Burritos -// Coconut Korma -// -//
      -//
      -// -//

      Tacos are delicious

      -//

      Sometimes a burrito is what you really need

      -//

      Might be your best option

      -//
      -//
      -//
      -//
      -// ) -// } -//} -// -//ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/Context/solution.js b/subjects/Context/solution.js deleted file mode 100644 index 35a9b6faa..000000000 --- a/subjects/Context/solution.js +++ /dev/null @@ -1,99 +0,0 @@ -/*eslint-disable no-alert */ -//////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// Using context, implement the
      , , and -// components such that: -// -// - Clicking the "submits" the form -// - Hitting "Enter" while in a submits the form -// - Don't use a element, we're intentionally recreating the -// browser's built-in behavior -// -// Got extra time? -// -// - Send the values of all the s to the handler -// without using DOM traversal APIs -// - Implement a that resets the s in the form -// -//////////////////////////////////////////////////////////////////////////////// -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' - -class Form extends React.Component { - static childContextTypes = { - onFormSubmit: PropTypes.func - } - - getChildContext() { - return { - onFormSubmit: this.props.onSubmit - } - } - - render() { - return
      {this.props.children}
      - } -} - -class SubmitButton extends React.Component { - static contextTypes = { - onFormSubmit: React.PropTypes.func - } - - handleClick = () => { - this.context.onFormSubmit() - } - - render() { - return - } -} - -class TextInput extends React.Component { - static contextTypes = { - onFormSubmit: React.PropTypes.func - } - - handleKeyDown = (event) => { - if (event.key === 'Enter') - this.context.onFormSubmit() - } - - render() { - return ( - - ) - } -} - -class App extends React.Component { - handleSubmit = () => { - alert('YOU WIN!') - } - - render() { - return ( -
      -

      This isn’t even my final <Form/>!

      - - -

      - {' '} - -

      -

      - Submit -

      - -
      - ) - } -} - -ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/Context/styles.js b/subjects/Context/styles.js deleted file mode 100644 index fab419082..000000000 --- a/subjects/Context/styles.js +++ /dev/null @@ -1,25 +0,0 @@ -export const tabs = {} - -export const tab = { - display: 'inline-block', - padding: 10, - margin: 10, - borderBottom: '4px solid', - borderBottomColor: '#ccc', - cursor: 'pointer' -} - -export const activeTab = { - ...tab, - borderBottomColor: '#000' -} - -export const disabledTab = { - ...tab, - opacity: 0.25, - cursor: 'default' -} - -export const tabPanels = { - padding: 10 -} diff --git a/subjects/Cursors/lecture.js b/subjects/Cursors/lecture.js deleted file mode 100644 index d89786c88..000000000 --- a/subjects/Cursors/lecture.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import immstruct from 'immstruct' -import Immutable from 'immutable' - -const structure = immstruct.withHistory('app', { - points: [], - name: 'Drawing Pad' -}) - -const DrawingPad = React.createClass({ - getInitialState() { - return { - drawing: false - } - }, - - maybeDraw(e) { - if (this.state.drawing) - this.props.cursor.update('points', (points) => ( - points.push([ e.clientX, e.clientY ]) - )) - }, - - render() { - const points = this.props.cursor.get('points') - return ( -
      -
      this.setState({ drawing: true })} - onMouseUp={() => this.setState({ drawing: false })} - onMouseMove={this.maybeDraw} - > - {points.map((point, i) => ( -
      - ))} -
      -
      - ) - } -}) - -const App = React.createClass({ - handleSlider(e) { - setHistory(parseInt(e.target.value, 10)) - }, - - render() { - const { struct } = this.props - const cursor = struct.cursor() - const historyCount = struct.history.count() - return ( -
      -

      {cursor.get('name')}

      - cursor.update('name', () => e.target.value)}/> -
      - -
      - -
      - ) - } -}) - -structure.on('swap', render) -render() - -function setHistory(frame) { - const count = structure.history.count() - const current = structure._currentRevision - - if (frame > current) - redo(frame - current) - else if (frame < current) - undo(current - frame) -} - -function undo(amt) { - structure.undo(amt) - render() -} - -function redo(amt) { - structure.redo(amt) - render() -} - -function render() { - ReactDOM.render(, document.getElementById('app')) -} diff --git a/subjects/Flux/exercise.js b/subjects/Flux/exercise.js deleted file mode 100644 index 68ddba8f4..000000000 --- a/subjects/Flux/exercise.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { render } from 'react-dom' -import ContactList from './exercise/components/ContactList' - -/* -The goal of this exercise is to add a button beside each contact in the list -that can be used to delete that contact. To do this, you'll need to perform -the following steps: - -* Hint: Open up Flux.png and make your way around the Flux diagram as you go * - -- Make the component listen for changes to the ContactStore and - updates its state when the store changes -- Add a "delete" button next to each contact (components/ContactList.js) -- The delete button should create an action (actions/ViewActionCreators.js) that does two things: - - Sends a "delete contact" action through the dispatcher - - Sends a request to the server to actually delete the contact -- The server creates an action that sends a "contact was deleted" action through the dispatcher -- The ContactStore (stores/ContactStore.js) picks up the "delete contact" event, removes - the corresponding contact, and fires a change event -*/ - -render(, document.getElementById('app')) diff --git a/subjects/Flux/exercise/AppDispatcher.js b/subjects/Flux/exercise/AppDispatcher.js deleted file mode 100644 index bdbdfe048..000000000 --- a/subjects/Flux/exercise/AppDispatcher.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint no-console: 0 */ -import { Dispatcher } from 'flux' -import { PayloadSources } from './Constants' - -const AppDispatcher = new Dispatcher - -export function dispatchViewAction(action) { - console.log('dispatching view action', action) - AppDispatcher.dispatch({ - source: PayloadSources.VIEW_ACTION, - action: action - }) -} - -export function dispatchServerAction(action) { - console.log('dispatching server action', action) - AppDispatcher.dispatch({ - source: PayloadSources.SERVER_ACTION, - action: action - }) -} - -export default AppDispatcher diff --git a/subjects/Flux/exercise/Constants.js b/subjects/Flux/exercise/Constants.js deleted file mode 100644 index 4bb836132..000000000 --- a/subjects/Flux/exercise/Constants.js +++ /dev/null @@ -1,15 +0,0 @@ -import keyMirror from 'key-mirror' - -export default { - - ActionTypes: keyMirror({ - LOAD_CONTACTS: null, - CONTACTS_WERE_LOADED: null - }), - - PayloadSources: keyMirror({ - SERVER_ACTION: null, - VIEW_ACTION: null - }) - -} diff --git a/subjects/Flux/exercise/actions/ServerActionCreators.js b/subjects/Flux/exercise/actions/ServerActionCreators.js deleted file mode 100644 index af0eb9306..000000000 --- a/subjects/Flux/exercise/actions/ServerActionCreators.js +++ /dev/null @@ -1,9 +0,0 @@ -import { ActionTypes } from '../Constants' -import { dispatchServerAction } from '../AppDispatcher' - -export function contactsWereLoaded(contacts) { - dispatchServerAction({ - type: ActionTypes.CONTACTS_WERE_LOADED, - contacts - }) -} diff --git a/subjects/Flux/exercise/actions/ViewActionCreators.js b/subjects/Flux/exercise/actions/ViewActionCreators.js deleted file mode 100644 index 1274f666d..000000000 --- a/subjects/Flux/exercise/actions/ViewActionCreators.js +++ /dev/null @@ -1,11 +0,0 @@ -import { ActionTypes } from '../Constants' -import { dispatchViewAction } from '../AppDispatcher' -import * as APIUtils from '../utils/APIUtils' - -export function loadContacts() { - dispatchViewAction({ - type: ActionTypes.LOAD_CONTACTS - }) - - APIUtils.loadContacts() -} diff --git a/subjects/Flux/exercise/components/ContactList.js b/subjects/Flux/exercise/components/ContactList.js deleted file mode 100644 index 2b47ad90c..000000000 --- a/subjects/Flux/exercise/components/ContactList.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import { getState, addChangeListener } from '../stores/ContactStore' -import { loadContacts } from '../actions/ViewActionCreators' - -const ContactList = React.createClass({ - getInitialState() { - return getState() - }, - - componentDidMount() { - loadContacts() - }, - - render() { - const { contacts, loaded } = this.state - - if (!loaded) - return
      Loading...
      - - const items = contacts.map(contact => { - return ( -
    • - {contact.first} {contact.last} -
    • - ) - }) - - return ( -
      -
        {items}
      -
      - ) - } -}) - -export default ContactList diff --git a/subjects/Flux/exercise/lib/xhr.js b/subjects/Flux/exercise/lib/xhr.js deleted file mode 100644 index 1fde574c7..000000000 --- a/subjects/Flux/exercise/lib/xhr.js +++ /dev/null @@ -1,44 +0,0 @@ -localStorage.token = localStorage.token || (Date.now()*Math.random()) - -function setToken(req) { - req.setRequestHeader('authorization', localStorage.token) -} - -export function getJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 404) { - callback(new Error('not found')) - } else { - callback(null, JSON.parse(req.response)) - } - } - req.open('GET', url) - setToken(req) - req.send() -} - -export function postJSON(url, obj, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - callback(JSON.parse(req.response)) - } - req.open('POST', url) - req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') - setToken(req) - req.send(JSON.stringify(obj)) -} - -export function deleteJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 500) { - callback(new Error(req.responseText)) - } else { - callback(null, req.responseText) - } - } - req.open('DELETE', url) - setToken(req) - req.send() -} diff --git a/subjects/Flux/exercise/stores/ContactStore.js b/subjects/Flux/exercise/stores/ContactStore.js deleted file mode 100644 index 7a15ccc90..000000000 --- a/subjects/Flux/exercise/stores/ContactStore.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint no-console: 0 */ -import { EventEmitter } from 'events' -import AppDispatcher from '../AppDispatcher' -import { ActionTypes } from '../Constants' - -const CHANGE_EVENT = 'CHANGE' -const events = new EventEmitter - -const state = { - contacts: [], - loaded: false -} - -function setState(newState) { - Object.assign(state, newState) - events.emit(CHANGE_EVENT) - console.log('ContactStore state changed', state) -} - -export function addChangeListener(fn) { - events.addListener(CHANGE_EVENT, fn) -} - -export function removeChangeListener(fn) { - events.removeListener(CHANGE_EVENT, fn) -} - -export function getState() { - return state -} - -AppDispatcher.register(function (payload) { - const { action } = payload - - if (action.type === ActionTypes.CONTACTS_WERE_LOADED) { - setState({ - loaded: true, - contacts: action.contacts - }) - } -}) diff --git a/subjects/Flux/exercise/utils/APIUtils.js b/subjects/Flux/exercise/utils/APIUtils.js deleted file mode 100644 index 669e85d7d..000000000 --- a/subjects/Flux/exercise/utils/APIUtils.js +++ /dev/null @@ -1,16 +0,0 @@ -import { getJSON, deleteJSON } from '../lib/xhr' -import { contactsWereLoaded, contactWasDeleted } from '../actions/ServerActionCreators' - -const API = 'http://addressbook-api.herokuapp.com' - -export function loadContacts() { - getJSON(`${API}/contacts`, function (error, res) { - contactsWereLoaded(res.contacts) - }) -} - -export function deleteContact(contact) { - deleteJSON(`${API}/contacts/${contact.id}`, function (error, res) { - contactWasDeleted(contact) - }) -} diff --git a/subjects/Flux/lecture.js b/subjects/Flux/lecture.js deleted file mode 100644 index aed9ea12c..000000000 --- a/subjects/Flux/lecture.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -- Flux is an architecture, not a framework - - DO NOT START BUILDING STUFF WITH FLUX WHEN YOU'RE FIRST GETTING STARTED WITH REACT - - It can be difficult to understand why the patterns in Flux are useful if you haven't - already tried to solve problems w/out Flux - - You'll most likely hate Flux unless you're already fighting with your current JS - framework. If you're not, stick with what's working for you - -- Flux is good at: - - Making it easy to reason about changes to state - -- Remember our 2 questions: - - What state is there? - - When does it change? - -Open Flux.png - -- Views - - React components (see components) - - Create actions (see actions/ViewActionCreators.js) - -- Action Creators - - Create "actions" with meaningful names (e.g. "load contacts", "delete contact"). - These are the verbs. Ask yourself, "what actions can the user take?" - - Send actions through the dispatcher - - Possibly trigger API requests (side effect) - -- Dispatcher - - The only actual code Facebook's flux repo gives you! - - Synchronous dispatch of actions to ALL registered listeners (stores) - - Ensures only one action happens at a time - -- Stores - - Store the state of your app - - Listen for actions from the dispatcher, so they know when to update state - - Notify listeners when the state changes -*/ diff --git a/subjects/Flux/solution.js b/subjects/Flux/solution.js deleted file mode 100644 index f7cf405c4..000000000 --- a/subjects/Flux/solution.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { render } from 'react-dom' -import ContactList from './solution/components/ContactList' - -/* -The goal of this exercise is to add a button beside each contact in the list -that can be used to delete that contact. To do this, you'll need to perform -the following steps: - -* Hint: Open up Flux.png and make your way around the Flux diagram as you go * - -- Make the component listen for changes to the ContactStore and - updates its state when the store changes -- Add a "delete" button next to each contact (components/ContactList.js) -- The delete button should create an action (actions/ViewActionCreators.js) that does two things: - - Sends a "delete contact" action through the dispatcher - - Sends a request to the server to actually delete the contact -- The server creates an action that sends a "contact was deleted" action through the dispatcher -- The ContactStore (stores/ContactStore.js) picks up the "delete contact" event, removes - the corresponding contact, and fires a change event -*/ - -render(, document.getElementById('app')) diff --git a/subjects/Flux/solution/AppDispatcher.js b/subjects/Flux/solution/AppDispatcher.js deleted file mode 100644 index bdbdfe048..000000000 --- a/subjects/Flux/solution/AppDispatcher.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint no-console: 0 */ -import { Dispatcher } from 'flux' -import { PayloadSources } from './Constants' - -const AppDispatcher = new Dispatcher - -export function dispatchViewAction(action) { - console.log('dispatching view action', action) - AppDispatcher.dispatch({ - source: PayloadSources.VIEW_ACTION, - action: action - }) -} - -export function dispatchServerAction(action) { - console.log('dispatching server action', action) - AppDispatcher.dispatch({ - source: PayloadSources.SERVER_ACTION, - action: action - }) -} - -export default AppDispatcher diff --git a/subjects/Flux/solution/Constants.js b/subjects/Flux/solution/Constants.js deleted file mode 100644 index abb866dc5..000000000 --- a/subjects/Flux/solution/Constants.js +++ /dev/null @@ -1,18 +0,0 @@ -import keyMirror from 'key-mirror' - -export default { - - ActionTypes: keyMirror({ - LOAD_CONTACTS: null, - CONTACTS_WERE_LOADED: null, - DELETE_CONTACT: null, - ERROR_DELETING_CONTACT: null, - CONTACT_WAS_DELETED: null - }), - - PayloadSources: keyMirror({ - SERVER_ACTION: null, - VIEW_ACTION: null - }) - -} diff --git a/subjects/Flux/solution/actions/ServerActionCreators.js b/subjects/Flux/solution/actions/ServerActionCreators.js deleted file mode 100644 index 0f4d426b7..000000000 --- a/subjects/Flux/solution/actions/ServerActionCreators.js +++ /dev/null @@ -1,24 +0,0 @@ -import { ActionTypes } from '../Constants' -import { dispatchServerAction } from '../AppDispatcher' - -export function contactsWereLoaded(contacts) { - dispatchServerAction({ - type: ActionTypes.CONTACTS_WERE_LOADED, - contacts - }) -} - -export function contactWasDeleted(contact) { - dispatchServerAction({ - type: ActionTypes.CONTACT_WAS_DELETED, - contact - }) -} - -export function errorDeletingContact(error, contact) { - dispatchServerAction({ - type: ActionTypes.ERROR_DELETING_CONTACT, - error, - contact - }) -} diff --git a/subjects/Flux/solution/actions/ViewActionCreators.js b/subjects/Flux/solution/actions/ViewActionCreators.js deleted file mode 100644 index 2900d0690..000000000 --- a/subjects/Flux/solution/actions/ViewActionCreators.js +++ /dev/null @@ -1,22 +0,0 @@ -import { ActionTypes } from '../Constants' -import { dispatchViewAction } from '../AppDispatcher' -import * as APIUtils from '../utils/APIUtils' - -function loadContacts() { - dispatchViewAction({ - type: ActionTypes.LOAD_CONTACTS - }) - - APIUtils.loadContacts() -} - -function deleteContact(contact) { - dispatchViewAction({ - type: ActionTypes.DELETE_CONTACT, - contact - }) - - APIUtils.deleteContact(contact) -} - -export default { deleteContact, loadContacts } diff --git a/subjects/Flux/solution/components/ContactList.js b/subjects/Flux/solution/components/ContactList.js deleted file mode 100644 index 746489088..000000000 --- a/subjects/Flux/solution/components/ContactList.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react' -import ContactStore from '../stores/ContactStore' -import ViewActions from '../actions/ViewActionCreators' - -const ContactList = React.createClass({ - - getDefaultProps() { - return { - ContactStore, - ViewActions - } - }, - - getInitialState() { - return this.props.ContactStore.getState() - }, - - handleChange() { - this.setState(this.props.ContactStore.getState()) - }, - - componentDidMount() { - this.props.ContactStore.addChangeListener(this.handleChange) - this.props.ViewActions.loadContacts() - }, - - componentWillUnmount() { - this.props.ContactStore.removeChangeListener(this.handleChange) - }, - - deleteContact(contact) { - this.props.ViewActions.deleteContact(contact) - }, - - render() { - const { contacts, deletingContacts, errors, loaded } = this.state - - if (!loaded) - return
      Loading...
      - - const items = contacts.map(contact => { - const error = errors[contact.id] - const isDeleting = deletingContacts.indexOf(contact) !== -1 - - return ( -
    • - - {' '}{contact.first} {contact.last}{' '} - {error - ?

      {error.message}

      - : - } -
    • - ) - }) - - return ( -
      -
        {items}
      -
      - ) - } -}) - -export default ContactList diff --git a/subjects/Flux/solution/lib/xhr.js b/subjects/Flux/solution/lib/xhr.js deleted file mode 100644 index 1fde574c7..000000000 --- a/subjects/Flux/solution/lib/xhr.js +++ /dev/null @@ -1,44 +0,0 @@ -localStorage.token = localStorage.token || (Date.now()*Math.random()) - -function setToken(req) { - req.setRequestHeader('authorization', localStorage.token) -} - -export function getJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 404) { - callback(new Error('not found')) - } else { - callback(null, JSON.parse(req.response)) - } - } - req.open('GET', url) - setToken(req) - req.send() -} - -export function postJSON(url, obj, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - callback(JSON.parse(req.response)) - } - req.open('POST', url) - req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') - setToken(req) - req.send(JSON.stringify(obj)) -} - -export function deleteJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 500) { - callback(new Error(req.responseText)) - } else { - callback(null, req.responseText) - } - } - req.open('DELETE', url) - setToken(req) - req.send() -} diff --git a/subjects/Flux/solution/stores/ContactStore.js b/subjects/Flux/solution/stores/ContactStore.js deleted file mode 100644 index bf8332cba..000000000 --- a/subjects/Flux/solution/stores/ContactStore.js +++ /dev/null @@ -1,68 +0,0 @@ -import { EventEmitter } from 'events' -import AppDispatcher from '../AppDispatcher' -import { ActionTypes } from '../Constants' - -const CHANGE_EVENT = 'CHANGE' -const events = new EventEmitter - -const state = { - contacts: [], - deletingContacts: [], - errors: {}, - loaded: false -} - -function setState(newState) { - Object.assign(state, newState) - events.emit(CHANGE_EVENT) - console.log('ContactStore state changed', state) -} - -function addChangeListener(fn) { - events.addListener(CHANGE_EVENT, fn) -} - -function removeChangeListener(fn) { - events.removeListener(CHANGE_EVENT, fn) -} - -function getState() { - return state -} - -AppDispatcher.register(function (payload) { - const { action } = payload - - if (action.type === ActionTypes.CONTACTS_WERE_LOADED) { - setState({ - loaded: true, - contacts: action.contacts - }) - } - - if (action.type === ActionTypes.DELETE_CONTACT) { - setState({ - deletingContacts: state.deletingContacts.concat([ action.contact ]) - }) - } - - if (action.type === ActionTypes.ERROR_DELETING_CONTACT) { - const { errors } = state - errors[action.contact.id] = action.error - - setState({ - deletingContacts: state.deletingContacts.filter(c => c.id !== action.contact.id), - errors - }) - } - - if (action.type === ActionTypes.CONTACT_WAS_DELETED) { - setState({ - contacts: state.contacts.filter(c => c.id !== action.contact.id), - deletingContacts: state.deletingContacts.filter(c => c.id !== action.contact.id) - }) - } -}) - -export default { getState, removeChangeListener, addChangeListener } - diff --git a/subjects/Flux/solution/utils/APIUtils.js b/subjects/Flux/solution/utils/APIUtils.js deleted file mode 100644 index bb2064300..000000000 --- a/subjects/Flux/solution/utils/APIUtils.js +++ /dev/null @@ -1,26 +0,0 @@ -import { getJSON, deleteJSON } from '../lib/xhr' -import { contactsWereLoaded, contactWasDeleted, errorDeletingContact } from '../actions/ServerActionCreators' - -const API = 'http://addressbook-api.herokuapp.com' - -export function loadContacts() { - getJSON(`${API}/contacts`, function (error, res) { - contactsWereLoaded(res.contacts) - }) -} - -export function deleteContact(contact) { - deleteJSON(`${API}/contacts/${contact.id}`, function (error, res) { - fakeNetworkLatency(function () { - if (error) { - errorDeletingContact(error, contact) - } else { - contactWasDeleted(contact) - } - }) - }) -} - -function fakeNetworkLatency(callback) { - setTimeout(callback, Math.random() * 5000) -} diff --git a/subjects/Forms/lecture.js b/subjects/Forms/lecture.js deleted file mode 100644 index 091ec4820..000000000 --- a/subjects/Forms/lecture.js +++ /dev/null @@ -1,244 +0,0 @@ -import React from 'react' -import ReactDOM, { findDOMNode } from 'react-dom' -import serializeForm from 'form-serialize' - -//////////////////////////////////////////////////////////////////////////////// -// Here's a simple
      : - -class Forms extends React.Component { - render() { - return ( -
      -

      Forms

      - - - -
      - ) - } -} - -ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// Give the a default value. -//class Forms extends React.Component { -// render() { -// return ( -//
      -//

      Forms

      -//
      -// -//
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// Access the value using event.target. -//class Forms extends React.Component { -// handleChange = (event) => { -// console.log(event.target.value) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -//
      -// -//
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// Or use a ref. -//class Forms extends React.Component { -// handleChange = () => { -// console.log(findDOMNode(this.refs.someInput).value) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -//
      -// -//
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// Or you can "control" the and have its value in state. -// What happens if we don't have an `onChange` but provide a value? -//class Forms extends React.Component { -// state = { -// someInputValue: 'lol' -// } -// -// handleChange = () => { -// this.setState({ -// someInputValue: findDOMNode(this.refs.someInput).value -// }) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -//
      -// -//
      -//
      {JSON.stringify(this.state, null, 2)}
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// When it's controlled, we can set state elsewhere and it stays in sync -//class Forms extends React.Component { -// state = { -// someInputValue: 'lol' -// } -// -// handleChange = () => { -// this.setState({ -// someInputValue: findDOMNode(this.refs.someInput).value -// }) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -//
      -// -// -//
      -//
      {JSON.stringify(this.state, null, 2)}
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// Some forms are transactional, so modeling in state isn't necessary, just -// use DOM APIs to access the data, like when you need to save off some data -// somewhere and reset the form, but the values in the form are never -// important to `render`. -//class Forms extends React.Component { -// handleSubmit = (event) => { -// event.preventDefault() -// const values = serializeForm(event.target, { hash: true }) -// console.log(values) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -//
      -//

      -// -//

      -// -//

      -// -//

      -// -//

      -// -//

      -//
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// If we did want it all in state, we don't have to link up every single -// element to state, can use
      . However, updates won't be -// synchronized when other parts of the app manipulate the state like the -// button we had earlier. -//class Forms extends React.Component { -// state = { -// firstName: 'Ryan', -// lastName: 'Florence' -// } -// -// handleFormChange = (event) => { -// event.preventDefault() -// const form = findDOMNode(this.refs.form) -// const values = serializeForm(form, { hash: true }) -// this.setState(values) -// } -// -// render() { -// return ( -//
      -//

      Forms

      -// -//

      -// -//

      -// -//

      -// -//

      -// -//

      -// -//

      -// -//
      {JSON.stringify(this.state, null, 2)}
      -//
      -// ) -// } -//} - -//////////////////////////////////////////////////////////////////////////////// -// Use-cases: -// -// 1. Transactional forms, don't need anything in state, just use -// `defaultValue` and `onSubmit` -// 2. Some other part of the app needs the forms state to render, -// but nothing else needs to manipulate that state (one-way), -// use
      and DOM APIs to slurp form values into -// state -// 3. Multiple parts of the app manipulate the state, changes need -// to be reflected in the input (two-way), use `value` and -// `onChange` diff --git a/subjects/Forms/lib/swapi.js b/subjects/Forms/lib/swapi.js deleted file mode 100644 index ea585bdb7..000000000 --- a/subjects/Forms/lib/swapi.js +++ /dev/null @@ -1,43 +0,0 @@ -import { getJSON } from './xhr' - -const SWAPI = 'http://swapi.co/api' - -export function getFilms(cb) { - getJSON(`${SWAPI}/films/`, (err, data) => cb(err, data.results)) -} - -export function getPeople(filmURL, cb) { - getJSON(filmURL, (err, film) => { - const characters = film.characters.slice(0, 5) - let data = [] - characters.forEach((url) => { - getJSON(url, (err, person) => { - data.push(person) - if (data.length === characters.length) { - cb(null, data) - } - }) - }) - }) -} - -export function getVehicles(personURL, cb) { - getJSON(personURL, (err, person) => { - const vehicles = person.vehicles.slice(0, 5) - let data = [] - if (person.vehicles.length === 0) { - cb(null, data) - } else { - vehicles.forEach((url) => { - getJSON(url, (err, vehicle) => { - data.push(vehicle) - if (data.length === vehicles.length) { - cb(null, data) - } - }) - }) - } - }) -} - - diff --git a/subjects/Forms/lib/xhr.js b/subjects/Forms/lib/xhr.js deleted file mode 100644 index 1fde574c7..000000000 --- a/subjects/Forms/lib/xhr.js +++ /dev/null @@ -1,44 +0,0 @@ -localStorage.token = localStorage.token || (Date.now()*Math.random()) - -function setToken(req) { - req.setRequestHeader('authorization', localStorage.token) -} - -export function getJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 404) { - callback(new Error('not found')) - } else { - callback(null, JSON.parse(req.response)) - } - } - req.open('GET', url) - setToken(req) - req.send() -} - -export function postJSON(url, obj, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - callback(JSON.parse(req.response)) - } - req.open('POST', url) - req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') - setToken(req) - req.send(JSON.stringify(obj)) -} - -export function deleteJSON(url, callback) { - const req = new XMLHttpRequest() - req.onload = function () { - if (req.status === 500) { - callback(new Error(req.responseText)) - } else { - callback(null, req.responseText) - } - } - req.open('DELETE', url) - setToken(req) - req.send() -} diff --git a/subjects/Forms/solution.js b/subjects/Forms/solution.js deleted file mode 100644 index 35b7262f8..000000000 --- a/subjects/Forms/solution.js +++ /dev/null @@ -1,109 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Exercise -// -// - When the checkbox is checked: -// - Fill in the shipping fields with the values from billing -// - Disable the shipping fields so they are not directly editable -// - Keep the shipping fields up to date as billing fields change -// - Hint: you can get the checkbox value from `event.target.checked` -// - When the form submits, console.log the values -// -// Got extra time? -// -// - If there are more than two characters in the "state" field, let the user -// know they should use the two-character abbreviation -// - If the user types something into shipping, then checks the checkbox, then -// unchecks the checkbox, ensure the field has the information from -// before clicking the checkbox the first time -import React from 'react' -import ReactDOM from 'react-dom' -import serializeForm from 'form-serialize' - -class CheckoutForm extends React.Component { - state = { - billingName: 'Michael Jackson', - billingState: 'CA', - shippingName: '', - shippingState: '', - shippingSameAsBilling: false - } - - handleSubmit = (event) => { - event.preventDefault() - - const values = serializeForm(event.target, { hash: true }) - - console.log(values) - } - - render() { - const { - billingName, - billingState, - shippingName, - shippingState, - shippingSameAsBilling - } = this.state - - return ( -
      -

      Checkout

      - -
      - Billing Address -

      - -

      -

      - -

      -
      - -
      - -
      - - Shipping Address -

      - -

      -

      - -

      -
      - -
      - ) - } -} - -ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/HelloWorld/lecture.js b/subjects/HelloWorld/lecture.js deleted file mode 100644 index 3d9e88f19..000000000 --- a/subjects/HelloWorld/lecture.js +++ /dev/null @@ -1,371 +0,0 @@ -//import $ from 'jquery' -//import { search } from './lib/searchWikipedia' - -//const html = ` - //
      - //

      Wikipedia

      - //
      - // - // - //
      - //
      Loading...
      - //
      - //

      Results for:

      - //

      - // - //

      - //
      - //
        - //
        -//` - -//$('#app').html(html) // <-- component - -//$('#form').on('submit', (event) => { // <-- state change - //event.preventDefault() - //const term = $('#input').val() // <-- state - //$('#loading').show() // <-- time - //$('#meta').hide() // <-- time - //$('#results').empty() // <-- time - //search(term, (err, results) => { - //$('#loading').hide() // <-- time - //$('#meta').show() // <-- time - //$('#title').html(term) // <-- time - //results.forEach((result) => { - //const li = $('
      • ') - //const html = ` - //
        - //${result.title} - // - //
        - // - //` - //li.html(html) // <-- time - //if ($('#descending').is(':checked')) { // <-- state - //li.prependTo($('#results')) // <-- time - //} else { - //li.appendTo($('#results')) // <-- time - //} - //li.find('button').on('click', () => { // <-- component - //li.find('.toggler').toggle() // <-- time - //const isHidden = li.find('.toggler').is(':hidden') // <-- state - //li.find('button').html(isHidden ? 'show more' : 'hide') // <-- time - //}) - //}) - //}) -//}).trigger('submit') // <-- state change - -//$('#descending').on('click', (event) => { // <-- state change - //$('#results li').each((i, li) => { - //$('#results').prepend(li) // <-- time - //}) -//}) - -// What's awesome: -// -// I can still bang out this code even after not using jQuery for -// 4 years. -// -// What's not awesome: -// -// When our code... -// -// - is written as flows -// - doesn't call out state -// - has no entry point to change state -// -// ...it gets really hard to deal with. After you identify state, -// and how to change it, you must write code to connect every state -// to nearly every other state. Every feature requires changes to code -// in multiple places. Also, it's just too hard to think about for must -// of us leading to lot of bugs. - - -//////////////////////////////////////////////////////////////////////////////// -//import Backbone from 'backbone' -//import $ from 'jquery' -//import _ from 'underscore' -//import { search } from './lib/searchWikipedia' - -//const appTemplate = _.template(` - //
        - //

        <%= title %>

        - //
        - // - // - //
        - //<% if (loading) { %> - //
        Loading...
        - //<% } else { %> - //
        - //

        Results for: <%= term %>

        - //

        - // - //

        - //
        - //<% } %> - //
          - //<% results.forEach(function(result) { %> - //
        • - //<% }) %> - //
        - //
        -//`) - - -//const AppView = Backbone.View.extend({ - - //template: appTemplate, - - //events: { // <-- delegated state changes - //'submit #form': 'handleSubmit', - //'click #descending': 'handleDescending' - //}, - - //initialize() { - //this.listenTo(this.model, 'all', this.render) - //this.listenTo(this.model, 'change:term', this.search) - //this.render() - //this.search() - //}, - - //handleSubmit(event) { - //event.preventDefault() - //this.model.set('term', this.$el.find('#input').val()) // KVO Web - //}, - - //search() { - //this.model.set({ // KVO web - //loading: true, - //results: [], - //descending: false // cascading update! - //}) - //search(this.model.get('term'), (err, results) => { - //this.model.set({ // KVO web - //loading: false, - //results: results - //}) - //}) - //}, - - //handleDescending() { - //this.model.set( // <-- KVO web - //'descending', - //!this.model.get('descending') - //) - //}, - - //render() { - //const state = this.model.toJSON() - //if (state.descending) - //state.results = state.results.slice(0).reverse() - //this.$el.html(this.template(state)) // DOM Bomb! - //this.$el.find('#results li').each((index, el) => { - //new ToggleView({ // <-- imperative (re)composition! - //el: el, - //model: new Backbone.Model(state.results[index]) - //}).render() - //}) - //} -//}) - -//const ToggleView = Backbone.View.extend({ - //template: _.template(` - //
        - //<%= title %> - // - //
        - //<% if (isOpen) { %> - //
        - //

        <%= description %>

        - //
        - //<% } %> - //`), - - //events: { - //'click button': 'toggle' - //}, - - //initialize() { - //this.model.set('isOpen', false, { silent: true }) // <-- model ownership? - //this.listenTo(this.model, 'change:isOpen', this.render) - //}, - - //toggle() { - //this.model.set('isOpen', !this.model.get('isOpen')) // <-- KVO web - //}, - - //render() { - //this.$el.html(this.template(this.model.toJSON())) - //} -//}) - -//new AppView({ - //el: '#app', - //model: new Backbone.Model({ - //title: 'Wikipedia', - //loading: false, - //term: 'tacos', - //descending: false, - //results: [] - //}) -//}) - -// What's awesome -// -// - Moved state to models so we can identify what state changes -// the app. -// - Moved creating UI into templates, one step closer to being -// declarative -// -// What's not so awesome -// -// - DOM Bombs -// - kill focus for assistive devices -// - non-performant -// -// - KVO Web -// - can't predict what will happen if you change state -// > Events complect communication and flow of control. -// > ... their fundamental nature, ... is that upon an event -// > an arbitrary amount of other code is run -// > http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html -// -// - leads to cascading updates -// - non-performant -// - to fix leads to knowing how your app changes over time intimately -// -// - imperative composition -// - non-performant -// - to fix -// - have to know how your app changes over time intimately -// - lots of code to manage instances -// - lots of mistakes - -//////////////////////////////////////////////////////////////////////////////// -//import angular from 'angular' -//import { search } from './lib/searchWikipedia' - -//document.documentElement.setAttribute('ng-app', 'wikipedia') - -//document.getElementById('app').innerHTML = ` - //
        - //

        Wikipedia

        - //
        - // - // - //
        - //
        Loading...
        - //
        - //

        {{main.sortedResults().length}} results for: {{main.term}}

        - //

        - // - //

        - //
        - //
          - //
        • - // - //

          {{result.description}}

          - //
          - //
        • - //
        - //
        -//` - -//const app = angular.module('wikipedia', []) - -//app.controller('MainController', function ($rootScope) { - //const main = this - //main.term = 'taco' // <-- shared state! - //main.results = [] - //main.loading = false - //main.descending = false - - //main.getFriends = () => { - //return [ { name: 'Ryan' }, { name: 'Michael' } ] - //} - - //main.handleSubmit = () => { - //main.loading = true - //search(main.term, (err, results) => { - //main.results = results - //main.loading = false - //$rootScope.$digest() // <-- time! - //}) - //} - - //main.sortedResults = () => { - //return main.descending ? - //main.results.slice(0).reverse() : main.results - //} - - //main.handleSubmit() -//}) - -//app.directive('toggler', () => { // <-- Global! - //return { - //restrict: 'E', // WTH? - //scope: { - //title: '@' // WTH? - //}, - //controller($scope) { - //$scope.isOpen = false - //$scope.toggle = () => { - //$scope.isOpen = !$scope.isOpen - //} - //}, - //replace: true, - //transclude: true, // WTH? - //template: ` - //
        - //
        - //{{title}} - // - //
        - //
        - //
        - //` - //} -//}) - -// What's awesome -// -// - fully declarative templates -// - declarative component composition -// -// What's not so awesome -// -// - directives and filters are globals -// - have to think about time with $apply/$watch, etc. -// - rendering assumptions require you to keep object identity -// and therefore think about time -// - and the real kicker: shared mutable state -// -// > July 7, 2014 -// > -// > Vojta brought up some points that we don’t yet have plans to solve -// > some problems we see in larger apps. In particular, how developers -// > can reason about data flow within an app. -// > -// > Key points: scope hierarchy is a huge pile of shared state that many -// > components from the application because of two way data-binding it's -// > not clear what how the data flows because it can flow in all -// > directions (including from child components to parents) - this makes -// > it hard to understand the app and understand of impact of model -// > changes in one part of the app on another (seemingly unrelated) part -// > of it. -// https://twitter.com/teozaurus/status/518071391959388160 diff --git a/subjects/HelloWorld/lib/searchWikipedia.js b/subjects/HelloWorld/lib/searchWikipedia.js deleted file mode 100644 index 006eb3f56..000000000 --- a/subjects/HelloWorld/lib/searchWikipedia.js +++ /dev/null @@ -1,21 +0,0 @@ -import jsonp from 'jsonp' - -const API = 'https://en.wikipedia.org/w/api.php?action=opensearch&format=json' - -export function search(term, cb) { - jsonp(`${API}&search=${term}`, (err, data) => { - if (err) { - cb(err) - } else { - const [ searchTerm, titles, descriptions, urls ] = data - cb(null, titles.sort().map((title, index) => { - return { - title, - description: descriptions[index], - url: urls[index] - } - })) - } - }) -} - diff --git a/subjects/HigherOrderComponents/exercise.js b/subjects/HigherOrderComponents/exercise.js deleted file mode 100644 index c7f970d9c..000000000 --- a/subjects/HigherOrderComponents/exercise.js +++ /dev/null @@ -1,40 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// Make `withMousePosition` a "higher-order component" that sends the mouse -// position to the component as props. -// -// Hint: use `event.clientX` and `event.clientY` -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' - -const withMousePosition = (Component) => { - return Component -} - -class App extends React.Component { - static propTypes = { - mouse: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired - }).isRequired - } - - render() { - const { mouse } = this.props - - return ( -
        - {mouse ? ( -

        The mouse position is ({mouse.x}, {mouse.y})

        - ) : ( -

        We don't know the mouse position yet :(

        - )} -
        - ) - } -} - -const AppWithMouse = withMousePosition(App) - -ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/HigherOrderComponents/lecture.js b/subjects/HigherOrderComponents/lecture.js deleted file mode 100644 index d2bba4efb..000000000 --- a/subjects/HigherOrderComponents/lecture.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' -import createMediaListener from './utils/createMediaListener' - -const media = createMediaListener({ - big: '(min-width : 1000px)', - tiny: '(max-width: 400px)' -}) - -class App extends React.Component { - state = { - media: media.getState() - } - - componentDidMount() { - media.listen(media => this.setState({ media })) - } - - componentWillUnmount() { - media.dispose() - } - - render() { - const { media } = this.state - - return media.big ? ( -

        Hey, this is a big screen

        - ) : media.tiny ? ( -
        tiny tiny tiny
        - ) : ( -

        Somewhere in between

        - ) - } -} - -ReactDOM.render(, document.getElementById('app')) - -//////////////////////////////////////////////////////////////////////////////// -// We can move all of that code into a higher-order component. A higher-order -// component (HoC) is a function that takes a `Component` as an argument, -// and returns a new component renders the `Component` with some extra props. - -//const mediaComponent = (Component, mediaQueries) => { -// const media = createMediaListener(mediaQueries) -// -// return class extends React.Component { -// state = { -// media: media.getState() -// } -// -// componentDidMount() { -// media.listen(media => this.setState({ media })) -// } -// -// componentWillUnmount() { -// media.dispose() -// } -// -// render() { -// return -// } -// } -//} -// -//class App extends React.Component { -// static propTypes = { -// media: PropTypes.shape({ -// big: PropTypes.bool, -// tiny: PropTypes.bool -// }) -// } -// -// render() { -// const { media } = this.props -// -// return media.big ? ( -//

        Hey, this is a big screen

        -// ) : media.tiny ? ( -//
        tiny tiny tiny
        -// ) : ( -//

        Somewhere in between

        -// ) -// } -//} -// -//const AppWithMedia = mediaComponent(App, { -// big: '(min-width : 1000px)', -// tiny: '(max-width: 400px)' -//}) -// -//ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/HigherOrderComponents/solution.js b/subjects/HigherOrderComponents/solution.js deleted file mode 100644 index db9b9645b..000000000 --- a/subjects/HigherOrderComponents/solution.js +++ /dev/null @@ -1,57 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Exercise: -// -// Make `withMousePosition` a "higher-order component" that sends the mouse -// position to the component as props. -// -// Hint: use `event.clientX` and `event.clientY` -import React, { PropTypes } from 'react' -import ReactDOM from 'react-dom' - -const withMousePosition = (Component) => { - return class extends React.Component { - state = { x: 0, y: 0 } - - handleMouseMove = (event) => { - this.setState({ - x: event.clientX, - y: event.clientY - }) - } - - render() { - return ( -
        - -
        - ) - } - } -} - -class App extends React.Component { - static propTypes = { - mouse: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired - }).isRequired - } - - render() { - const { mouse } = this.props - - return ( -
        - {mouse ? ( -

        The mouse position is ({mouse.x}, {mouse.y})

        - ) : ( -

        We don't know the mouse position yet :(

        - )} -
        - ) - } -} - -const AppWithMouse = withMousePosition(App) - -ReactDOM.render(, document.getElementById('app')) diff --git a/subjects/HigherOrderComponents/utils/createMediaListener.js b/subjects/HigherOrderComponents/utils/createMediaListener.js deleted file mode 100644 index a88c54a25..000000000 --- a/subjects/HigherOrderComponents/utils/createMediaListener.js +++ /dev/null @@ -1,61 +0,0 @@ -/* -const listener = createMediaListener({ - mobile: "(max-width: 767px)", - small: "(max-width: 568px), (max-height: 400px)" -}) - -listener.listen((state) => {}) -listener.getState() -listenter.dispose() -*/ - -export default (media) => { - let transientListener = null - - const mediaKeys = Object.keys(media) - - const queryLists = mediaKeys.reduce((queryLists, key) => { - queryLists[key] = window.matchMedia(media[key]) - return queryLists - }, {}) - - const mediaState = mediaKeys.reduce((state, key) => { - state[key] = queryLists[key].matches - return state - }, {}) - - const mutateMediaState = (key, val) => { - mediaState[key] = val - notify() - } - - const notify = () => { - if (transientListener != null) - transientListener(mediaState) - } - - const listeners = mediaKeys.reduce((listeners, key) => { - listeners[key] = (event) => { - mutateMediaState(key, event.matches) - } - return listeners - }, {}) - - const listen = (listener) => { - transientListener = listener - mediaKeys.forEach((key) => { - queryLists[key].addListener(listeners[key]) - }) - } - - const dispose = () => { - transientListener = null - mediaKeys.forEach((key) => { - queryLists[key].removeListener(listeners[key]) - }) - } - - const getState = () => mediaState - - return { listen, dispose, getState } -} diff --git a/subjects/ImperativeToDeclarative/utils/createOscillator.js b/subjects/ImperativeToDeclarative/utils/createOscillator.js deleted file mode 100644 index e5536ab50..000000000 --- a/subjects/ImperativeToDeclarative/utils/createOscillator.js +++ /dev/null @@ -1,63 +0,0 @@ -import './AudioContextMonkeyPatch' - -function Oscillator(audioContext) { - // TODO make more things not use this. - const oscillatorNode = audioContext.createOscillator() - oscillatorNode.start(0) - - const gainNode = audioContext.createGain() - this.pitchBase = 50 - this.pitchBend = 0 - this.pitchRange = 2000 - this.volume = 0.5 - this.maxVolume = 1 - this.frequency = this.pitchBase - - let hasConnected = false - let frequency = this.pitchBase - - this.play = function () { - oscillatorNode.connect(gainNode) - hasConnected = true - } - - this.stop = function () { - if (hasConnected) { - oscillatorNode.disconnect(gainNode) - hasConnected = false - } - } - - this.setType = function (type) { - oscillatorNode.type = type - } - - this.setPitchBend = function (v) { - this.pitchBend = v - frequency = this.pitchBase + this.pitchBend * this.pitchRange - oscillatorNode.frequency.value = frequency - this.frequency = frequency - } - - this.setVolume = function (v) { - this.volume = this.maxVolume * v - gainNode.gain.value = this.volume - } - - this.connect = function (output) { - gainNode.connect(output) - } - - return this -} - -function createOscillator() { - const audioContext = new AudioContext() - const theremin = new Oscillator(audioContext) - - theremin.connect(audioContext.destination) - - return theremin -} - -export default createOscillator diff --git a/subjects/JSONTable/exercise.js b/subjects/JSONTable/exercise.js deleted file mode 100644 index 5d7063e99..000000000 --- a/subjects/JSONTable/exercise.js +++ /dev/null @@ -1,33 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Requirements -// -// - fetch the src with getJSON((error, payload) => {}) -// - render the content of the th's from the field names (hint: use -// the field names from the first record) -// - render each result as a row in -import 'purecss/build/pure.css' -import React from 'react' -import { render } from 'react-dom' -import getJSON from './lib/getJSON' - -const JSONTable = React.createClass({ - render() { - return
        ...
        - } -}) - -const App = React.createClass({ - render() { - return ( -
        -

        JSONTable

        - payload.contacts} - /> -
        - ) - } -}) - -render(, document.getElementById('app')) diff --git a/subjects/JSONTable/lib/getJSON.js b/subjects/JSONTable/lib/getJSON.js deleted file mode 100644 index 17634c3cf..000000000 --- a/subjects/JSONTable/lib/getJSON.js +++ /dev/null @@ -1,20 +0,0 @@ -localStorage.token = localStorage.token || (Date.now()*Math.random()); - -function setToken(req) { - req.setRequestHeader('authorization', localStorage.token); -} - -export default function getJSON(url, cb) { - var req = new XMLHttpRequest(); - req.onload = function () { - if (req.status === 404) { - cb(new Error('not found')); - } else { - cb(null, JSON.parse(req.response)); - } - }; - req.open('GET', url); - setToken(req); - req.send(); -} - diff --git a/subjects/JSONTable/solution.js b/subjects/JSONTable/solution.js deleted file mode 100644 index ab5ca0ac4..000000000 --- a/subjects/JSONTable/solution.js +++ /dev/null @@ -1,111 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Requirements -// -// - fetch the src with getJSON((error, payload) => {}) -// - render the content of the th's from the field names (hint: use -// the field names from the first record) -// - render each result as a row in -import 'purecss/build/pure.css' -import React from 'react' -import { render } from 'react-dom' -import getJSON from './lib/getJSON' - -function isURL(content) { - return (/^https?:\/\//).test(content) -} - -function isImageURL(content) { - return isURL(content) && (/\.(jpe?g|gif|png)$/).test(content) -} - -const JSONTable = React.createClass({ - propTypes: { - src: React.PropTypes.string.isRequired, - getData: React.PropTypes.func.isRequired, - getKey: React.PropTypes.func.isRequired - }, - - getDefaultProps() { - return { - getKey: (item) => item.id - } - }, - - getInitialState() { - return { - data: null - } - }, - - componentDidMount() { - getJSON(this.props.src, (error, payload) => { - this.setState({ - data: this.props.getData(payload) - }) - }) - }, - - formatContent(content) { - if (Array.isArray(content)) - return content.map(this.formatContent) - - if (isImageURL(content)) - return

        - - if (isURL(content)) - return

        {content}

        - - return content - }, - - render() { - const { data } = this.state - - if (data == null || data.length === 0) - return null - - const fields = Object.keys(data[0]) - - return ( - - - - {fields.map(field => )} - - - - {data.map(item => ( - - {fields.map(field => ( - - ))} - - ))} - -
        {field}
        {this.formatContent(item[field])}
        - ) - } -}) - -const App = React.createClass({ - render() { - return ( -
        -

        JSONTable

        - payload.contacts} - /> - {/* - payload.results} - getKey={item => item.url} - /> - */} -
        - ) - } -}) - -render(, document.getElementById('app')) diff --git a/subjects/MigratingToReact/backbone-todomvc/.gitignore b/subjects/MigratingToReact/backbone-todomvc/.gitignore deleted file mode 100644 index 7f3beee79..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -node_modules/backbone/* -!node_modules/backbone/backbone.js - -node_modules/backbone.localstorage/* -!node_modules/backbone.localstorage/backbone.localStorage.js - -node_modules/jquery/* -!node_modules/jquery/dist -node_modules/jquery/dist/* -!node_modules/jquery/dist/jquery.js - -node_modules/todomvc-app-css/* -!node_modules/todomvc-app-css/index.css - -node_modules/todomvc-common/* -!node_modules/todomvc-common/base.css -!node_modules/todomvc-common/base.js - -node_modules/underscore/* -!node_modules/underscore/underscore.js diff --git a/subjects/MigratingToReact/backbone-todomvc/index.html b/subjects/MigratingToReact/backbone-todomvc/index.html deleted file mode 100644 index 73bc7383f..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/index.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - Backbone.js • TodoMVC - - - - -
        - -
        - - -
          -
          -
          -
          - - - - - - - - - - - - - - - diff --git a/subjects/MigratingToReact/backbone-todomvc/js/app.js b/subjects/MigratingToReact/backbone-todomvc/js/app.js deleted file mode 100644 index cf6b8b78c..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/app.js +++ /dev/null @@ -1,16 +0,0 @@ -/*global $ */ -/*jshint unused:false */ -var app = app || {}; -var ENTER_KEY = 13; -var ESC_KEY = 27; - -$(function () { - 'use strict'; - - // sorry, there's no build, jsx loader is async, - // so we're just hacking for now to get the point across - setTimeout(function () { - // kick things off by creating the `App` - new app.AppView(); - }, 100); -}); diff --git a/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js b/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js deleted file mode 100644 index a0bd4170c..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/collections/todos.js +++ /dev/null @@ -1,41 +0,0 @@ -/*global Backbone */ -var app = app || {}; - -(function () { - 'use strict'; - - // Todo Collection - // --------------- - - // The collection of todos is backed by *localStorage* instead of a remote - // server. - var Todos = Backbone.Collection.extend({ - // Reference to this collection's model. - model: app.Todo, - - // Save all of the todo items under the `"todos"` namespace. - localStorage: new Backbone.LocalStorage('todos-backbone'), - - // Filter down the list of all todo items that are finished. - completed: function () { - return this.where({completed: true}); - }, - - // Filter down the list to only todo items that are still not finished. - remaining: function () { - return this.where({completed: false}); - }, - - // We keep the Todos in sequential order, despite being saved by unordered - // GUID in the database. This generates the next order number for new items. - nextOrder: function () { - return this.length ? this.last().get('order') + 1 : 1; - }, - - // Todos are sorted by their original insertion order. - comparator: 'order' - }); - - // Create our global collection of **Todos**. - app.todos = new Todos(); -})(); diff --git a/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js b/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js deleted file mode 100644 index 3bc2c6253..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/models/todo.js +++ /dev/null @@ -1,26 +0,0 @@ -/*global Backbone */ -var app = app || {}; - -(function () { - 'use strict'; - - // Todo Model - // ---------- - - // Our basic **Todo** model has `title`, `order`, and `completed` attributes. - app.Todo = Backbone.Model.extend({ - // Default attributes for the todo - // and ensure that each todo created has `title` and `completed` keys. - defaults: { - title: '', - completed: false - }, - - // Toggle the `completed` state of this todo item. - toggle: function () { - this.save({ - completed: !this.get('completed') - }); - } - }); -})(); diff --git a/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js b/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js deleted file mode 100644 index f7d618f7a..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/routers/router.js +++ /dev/null @@ -1,26 +0,0 @@ -/*global Backbone */ -var app = app || {}; - -(function () { - 'use strict'; - - // Todo Router - // ---------- - var TodoRouter = Backbone.Router.extend({ - routes: { - '*filter': 'setFilter' - }, - - setFilter: function (param) { - // Set the current filter to be used - app.TodoFilter = param || ''; - - // Trigger a collection filter event, causing hiding/unhiding - // of Todo view items - app.todos.trigger('filter'); - } - }); - - app.TodoRouter = new TodoRouter(); - Backbone.history.start(); -})(); diff --git a/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js b/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js deleted file mode 100644 index 77f219446..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/views/app-view.js +++ /dev/null @@ -1,131 +0,0 @@ -/*global Backbone, jQuery, _, ENTER_KEY */ -var app = app || {}; - -(function ($) { - 'use strict'; - - // The Application - // --------------- - - // Our overall **AppView** is the top-level piece of UI. - app.AppView = Backbone.View.extend({ - - // Instead of generating a new element, bind to the existing skeleton of - // the App already present in the HTML. - el: '#todoapp', - - // Our template for the line of statistics at the bottom of the app. - statsTemplate: _.template($('#stats-template').html()), - - // Delegated events for creating new items, and clearing completed ones. - events: { - 'keypress #new-todo': 'createOnEnter', - 'click #clear-completed': 'clearCompleted', - 'click #toggle-all': 'toggleAllComplete' - }, - - // At initialization we bind to the relevant events on the `Todos` - // collection, when items are added or changed. Kick things off by - // loading any preexisting todos that might be saved in *localStorage*. - initialize: function () { - this.allCheckbox = this.$('#toggle-all')[0]; - this.$input = this.$('#new-todo'); - this.$footer = this.$('#footer'); - this.$main = this.$('#main'); - this.$list = $('#todo-list'); - - this.listenTo(app.todos, 'add', this.addOne); - this.listenTo(app.todos, 'reset', this.addAll); - this.listenTo(app.todos, 'change:completed', this.filterOne); - this.listenTo(app.todos, 'filter', this.filterAll); - this.listenTo(app.todos, 'all', this.render); - - // Suppresses 'add' events with {reset: true} and prevents the app view - // from being re-rendered for every model. Only renders when the 'reset' - // event is triggered at the end of the fetch. - app.todos.fetch({reset: true}); - }, - - // Re-rendering the App just means refreshing the statistics -- the rest - // of the app doesn't change. - render: function () { - var completed = app.todos.completed().length; - var remaining = app.todos.remaining().length; - - if (app.todos.length) { - this.$main.show(); - this.$footer.show(); - - this.$footer.html(this.statsTemplate({ - completed: completed, - remaining: remaining - })); - - this.$('#filters li a') - .removeClass('selected') - .filter('[href="#/' + (app.TodoFilter || '') + '"]') - .addClass('selected'); - } else { - this.$main.hide(); - this.$footer.hide(); - } - - this.allCheckbox.checked = !remaining; - }, - - // Add a single todo item to the list by creating a view for it, and - // appending its element to the `
            `. - addOne: function (todo) { - var view = new app.TodoView({ model: todo }); - this.$list.append(view.render().el); - }, - - // Add all items in the **Todos** collection at once. - addAll: function () { - this.$list.html(''); - app.todos.each(this.addOne, this); - }, - - filterOne: function (todo) { - todo.trigger('visible'); - }, - - filterAll: function () { - app.todos.each(this.filterOne, this); - }, - - // Generate the attributes for a new Todo item. - newAttributes: function () { - return { - title: this.$input.val().trim(), - order: app.todos.nextOrder(), - completed: false - }; - }, - - // If you hit return in the main input field, create new **Todo** model, - // persisting it to *localStorage*. - createOnEnter: function (e) { - if (e.which === ENTER_KEY && this.$input.val().trim()) { - app.todos.create(this.newAttributes()); - this.$input.val(''); - } - }, - - // Clear all completed todo items, destroying their models. - clearCompleted: function () { - _.invoke(app.todos.completed(), 'destroy'); - return false; - }, - - toggleAllComplete: function () { - var completed = this.allCheckbox.checked; - - app.todos.each(function (todo) { - todo.save({ - completed: completed - }); - }); - } - }); -})(jQuery); diff --git a/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js b/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js deleted file mode 100644 index 0b9e93d79..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/js/views/todo-view.js +++ /dev/null @@ -1,132 +0,0 @@ -/*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */ -var app = app || {}; - -(function ($) { - 'use strict'; - - // Todo Item View - // -------------- - - // The DOM element for a todo item... - app.TodoView = Backbone.View.extend({ - //... is a list tag. - tagName: 'li', - - // Cache the template function for a single item. - template: _.template($('#item-template').html()), - - // The DOM events specific to an item. - events: { - 'click .toggle': 'toggleCompleted', - 'dblclick label': 'edit', - 'click .destroy': 'clear', - 'keypress .edit': 'updateOnEnter', - 'keydown .edit': 'revertOnEscape', - 'blur .edit': 'close' - }, - - // The TodoView listens for changes to its model, re-rendering. Since - // there's a one-to-one correspondence between a **Todo** and a - // **TodoView** in this app, we set a direct reference on the model for - // convenience. - initialize: function () { - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model, 'destroy', this.remove); - this.listenTo(this.model, 'visible', this.toggleVisible); - }, - - // Re-render the titles of the todo item. - render: function () { - // Backbone LocalStorage is adding `id` attribute instantly after - // creating a model. This causes our TodoView to render twice. Once - // after creating a model and once on `id` change. We want to - // filter out the second redundant render, which is caused by this - // `id` change. It's known Backbone LocalStorage bug, therefore - // we've to create a workaround. - // https://github.com/tastejs/todomvc/issues/469 - if (this.model.changed.id !== undefined) { - return; - } - - this.$el.html(this.template(this.model.toJSON())); - this.$el.toggleClass('completed', this.model.get('completed')); - this.toggleVisible(); - this.$input = this.$('.edit'); - return this; - }, - - toggleVisible: function () { - this.$el.toggleClass('hidden', this.isHidden()); - }, - - isHidden: function () { - return this.model.get('completed') ? - app.TodoFilter === 'active' : - app.TodoFilter === 'completed'; - }, - - // Toggle the `"completed"` state of the model. - toggleCompleted: function () { - this.model.toggle(); - }, - - // Switch this view into `"editing"` mode, displaying the input field. - edit: function () { - this.$el.addClass('editing'); - this.$input.focus(); - }, - - // Close the `"editing"` mode, saving changes to the todo. - close: function () { - var value = this.$input.val(); - var trimmedValue = value.trim(); - - // We don't want to handle blur events from an item that is no - // longer being edited. Relying on the CSS class here has the - // benefit of us not having to maintain state in the DOM and the - // JavaScript logic. - if (!this.$el.hasClass('editing')) { - return; - } - - if (trimmedValue) { - this.model.save({ title: trimmedValue }); - - if (value !== trimmedValue) { - // Model values changes consisting of whitespaces only are - // not causing change to be triggered Therefore we've to - // compare untrimmed version with a trimmed one to check - // whether anything changed - // And if yes, we've to trigger change event ourselves - this.model.trigger('change'); - } - } else { - this.clear(); - } - - this.$el.removeClass('editing'); - }, - - // If you hit `enter`, we're through editing the item. - updateOnEnter: function (e) { - if (e.which === ENTER_KEY) { - this.close(); - } - }, - - // If you're pressing `escape` we revert your change by simply leaving - // the `editing` state. - revertOnEscape: function (e) { - if (e.which === ESC_KEY) { - this.$el.removeClass('editing'); - // Also reset the hidden input back to the original value. - this.$input.val(this.model.get('title')); - } - }, - - // Remove the item, destroy the model from *localStorage* and delete its view. - clear: function () { - this.model.destroy(); - } - }); -})(jQuery); diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json deleted file mode 100644 index 68ce04eb2..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.documentup.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Backbone.localStorage", - "twitter": "jeromegn" -} \ No newline at end of file diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore deleted file mode 100644 index 6a626034c..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -spec/ -examples/ -Makefile -bower.json -component.json -CNAME diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml deleted file mode 100644 index 7b74b286e..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: node_js -node_js: - - 0.10 -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" -script: make test \ No newline at end of file diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md deleted file mode 100644 index b17fe803e..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ -# Changelog - -## 1.1.16 - January 22, 2015 - -- Fix for #130. Uncaught TypeError: Cannot read property 'localStorage' of undefined - -## 1.1.15 - January 7, 2015 - -- You can now pass `{ajaxSync: true}` as an option to `fetch` and `save` to force remote syncing. (@adamterlson) -- Various fixes. Thanks to @nbergseng, @richardchen331, @eush77, @malept, @HenriqueSilverio - -## 1.1.14 - skipped due to some weirdness, inconsistent versioning. - -## 1.1.13 - August 5, 2014 - -- Forgot a comma, broke it for bower. - -## 1.1.10 - August 4, 2014 - -- Various fixes, thanks everybody - -## 1.1.7 - October 4, 2013 - -- Fix CommonJS requiring (Brandon Dimcheff) - -## 1.1.6 - July 2, 2013 - -- Don't assume `require` is defined when `exports` is defined (#100) - -## 1.1.5 - At some point... - -- Can't remember... - -## 1.1.4 - May 19, 2013 - -- #90 Check for localStorage (throw an error if not available) - -## 1.1.3 - May 12, 2013 - -- #79 Added CommonJS support - -## 1.1.2 - May 12, 2013 - -- #82 Upgraded Backbone to version 1.0 in test suite -- Fixed a few bugs after upgrading to 1.0... looks like Backbone is now triggering the events we were previously manually triggering. - -## 1.1.1 - May 11, 2013 - -- #87 Fixes Lodash v1.0.0-rc.1 removal of _#chain -- #86 Fix for when developer has specified in backbone which jQuery version to use by setting Backbone.$ - -## 1.1.0 - prior to May 11, 2013 - -- localStorage adapter for Backbone.js \ No newline at end of file diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md deleted file mode 100644 index 1991a3acd..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Backbone localStorage Adapter v1.1.16 - -[![Build Status](https://secure.travis-ci.org/jeromegn/Backbone.localStorage.png?branch=master)](http://travis-ci.org/jeromegn/Backbone.localStorage) - -Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database. - -[![Gittip](http://badgr.co/gittip/jeromegn.png)](https://www.gittip.com/jeromegn/) - -## Usage - -Include Backbone.localStorage after having included Backbone.js: - -```html - - -``` - -Create your collections like so: - -```javascript -window.SomeCollection = Backbone.Collection.extend({ - - localStorage: new Backbone.LocalStorage("SomeCollection"), // Unique name within your app. - - // ... everything else is normal. - -}); -``` - -If needed, you can use the default `Backbone.sync` (instead of local storage) by passing the `ajaxSync` option flag to any Backbone AJAX function, for example: - -```javascript -var myModel = new SomeModel(); -myModel.fetch({ ajaxSync: true }); -myModel.save({ new: "value" }, { ajaxSync: true }); -``` - -### RequireJS - -Include [RequireJS](http://requirejs.org): - -```html - -``` - -RequireJS config: -```javascript -require.config({ - paths: { - jquery: "lib/jquery", - underscore: "lib/underscore", - backbone: "lib/backbone", - localstorage: "lib/backbone.localStorage" - } -}); -``` - -Define your collection as a module: -```javascript -define("SomeCollection", ["localstorage"], function() { - var SomeCollection = Backbone.Collection.extend({ - localStorage: new Backbone.LocalStorage("SomeCollection") // Unique name within your app. - }); - - return SomeCollection; -}); -``` - -Require your collection: -```javascript -require(["SomeCollection"], function(SomeCollection) { - // ready to use SomeCollection -}); -``` - -### CommonJS - -If you're using [browserify](https://github.com/substack/node-browserify). - -Install using `npm install backbone.localstorage`, and require the module. - -```javascript -Backbone.LocalStorage = require("backbone.localstorage"); -``` - -##Support - -If you're having a problem with using the project, get help at CodersClan. - - - -## Contributing - -You'll need node and to `npm install` before being able to run the minification script. - -1. Fork; -2. Write code, with tests; -3. `make test` or `open spec/runner.html`; -4. Create a pull request. - -Have fun! - -## Acknowledgments - -- [Mark Woodall](https://github.com/llad): initial tests (now refactored); -- [Martin Häcker](https://github.com/dwt): many fixes and the test isolation. - -## License - -Licensed under MIT license - -Copyright (c) 2010 Jerome Gravel-Niquet - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js deleted file mode 100644 index 8ef0ccd71..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone.localstorage/backbone.localStorage-min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Backbone localStorage Adapter - * Version 1.1.16 - * - * https://github.com/jeromegn/Backbone.localStorage - */(function(a,b){typeof exports=="object"&&typeof require=="function"?module.exports=b(require("backbone")):typeof define=="function"&&define.amd?define(["backbone"],function(c){return b(c||a.Backbone)}):b(Backbone)})(this,function(a){function b(){return((1+Math.random())*65536|0).toString(16).substring(1)}function c(){return b()+b()+"-"+b()+"-"+b()+"-"+b()+"-"+b()+b()+b()}function d(a){return a===Object(a)}function e(a,b){var c=a.length;while(c--)if(a[c]===b)return!0;return!1}function f(a,b){for(var c in b)a[c]=b[c];return a}function g(a,b){if(a==null)return void 0;var c=a[b];return typeof c=="function"?a[b]():c}return a.LocalStorage=window.Store=function(a,b){if(!this.localStorage)throw"Backbone.localStorage: Environment does not support localStorage.";this.name=a,this.serializer=b||{serialize:function(a){return d(a)?JSON.stringify(a):a},deserialize:function(a){return a&&JSON.parse(a)}};var c=this.localStorage().getItem(this.name);this.records=c&&c.split(",")||[]},f(a.LocalStorage.prototype,{save:function(){this.localStorage().setItem(this.name,this.records.join(","))},create:function(a){return!a.id&&a.id!==0&&(a.id=c(),a.set(a.idAttribute,a.id)),this.localStorage().setItem(this._itemName(a.id),this.serializer.serialize(a)),this.records.push(a.id.toString()),this.save(),this.find(a)},update:function(a){this.localStorage().setItem(this._itemName(a.id),this.serializer.serialize(a));var b=a.id.toString();return e(this.records,b)||(this.records.push(b),this.save()),this.find(a)},find:function(a){return this.serializer.deserialize(this.localStorage().getItem(this._itemName(a.id)))},findAll:function(){var a=[];for(var b=0,c,d;b=1.4.0" - }, - "peerDependencies": { - "backbone": "~1.x.x" - }, - "keywords": [ - "backbone", - "localstorage", - "local", - "storage", - "cache", - "sync" - ], - "main": "./backbone.localStorage.js", - "engines": { - "node": "*" - }, - "gitHead": "728194adef60befefed1416825498d1b53cd517a", - "description": "[![Build Status](https://secure.travis-ci.org/jeromegn/Backbone.localStorage.png?branch=master)](http://travis-ci.org/jeromegn/Backbone.localStorage)", - "bugs": { - "url": "https://github.com/jeromegn/Backbone.localStorage/issues" - }, - "homepage": "https://github.com/jeromegn/Backbone.localStorage", - "_id": "backbone.localstorage@1.1.16", - "scripts": {}, - "_shasum": "3df5101d93abcee04f326035540f425dd0487916", - "_from": "backbone.localstorage@>=1.1.16 <2.0.0", - "_npmVersion": "2.1.17", - "_nodeVersion": "0.10.35", - "_npmUser": { - "name": "jeromegn", - "email": "jeromegn@gmail.com" - }, - "maintainers": [ - { - "name": "jeromegn", - "email": "jeromegn@gmail.com" - } - ], - "dist": { - "shasum": "3df5101d93abcee04f326035540f425dd0487916", - "tarball": "http://registry.npmjs.org/backbone.localstorage/-/backbone.localstorage-1.1.16.tgz" - }, - "directories": {}, - "_resolved": "https://registry.npmjs.org/backbone.localstorage/-/backbone.localstorage-1.1.16.tgz" -} diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore deleted file mode 100644 index 6bcb69f50..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -test/ -Rakefile -docs/ -raw/ -examples/ -index.html -.jshintrc diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml deleted file mode 100644 index 99dc7712c..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - 0.8 -notifications: - email: false diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME deleted file mode 100644 index dfadb7985..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CNAME +++ /dev/null @@ -1,2 +0,0 @@ -backbonejs.org - diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md deleted file mode 100644 index 55403616b..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/CONTRIBUTING.md +++ /dev/null @@ -1,22 +0,0 @@ -## How to Open a Backbone.js Ticket - -* Do not use tickets to ask for help with (debugging) your application. Ask on -the [mailing list](https://groups.google.com/forum/#!forum/backbonejs), -in the IRC channel (`#documentcloud` on Freenode), or if you understand your -specific problem, on [StackOverflow](http://stackoverflow.com/questions/tagged/backbone.js). - -* Before you open a ticket or send a pull request, -[search](https://github.com/jashkenas/backbone/issues) for previous -discussions about the same feature or issue. Add to the earlier ticket if you -find one. - -* Before sending a pull request for a feature or bug fix, be sure to have -[tests](http://backbonejs.org/test/). - -* Use the same coding style as the rest of the -[codebase](https://github.com/jashkenas/backbone/blob/master/backbone.js). - -* In your pull request, do not add documentation or rebuild the minified -`backbone-min.js` file. We'll do that before cutting a new release. - -* All pull requests should be made to the `master` branch. diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE deleted file mode 100644 index 3ffd97de0..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2010-2014 Jeremy Ashkenas, DocumentCloud - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md deleted file mode 100644 index 7b0482df7..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/README.md +++ /dev/null @@ -1,29 +0,0 @@ - ____ __ __ - /\ _`\ /\ \ /\ \ __ - \ \ \ \ \ __ ___\ \ \/'\\ \ \____ ___ ___ __ /\_\ ____ - \ \ _ <' /'__`\ /'___\ \ , < \ \ '__`\ / __`\ /' _ `\ /'__`\ \/\ \ /',__\ - \ \ \ \ \/\ \ \.\_/\ \__/\ \ \\`\\ \ \ \ \/\ \ \ \/\ \/\ \/\ __/ __ \ \ \/\__, `\ - \ \____/\ \__/.\_\ \____\\ \_\ \_\ \_,__/\ \____/\ \_\ \_\ \____\/\_\_\ \ \/\____/ - \/___/ \/__/\/_/\/____/ \/_/\/_/\/___/ \/___/ \/_/\/_/\/____/\/_/\ \_\ \/___/ - \ \____/ - \/___/ - (_'_______________________________________________________________________________'_) - (_.———————————————————————————————————————————————————————————————————————————————._) - - -Backbone supplies structure to JavaScript-heavy applications by providing models key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface. - -For Docs, License, Tests, pre-packed downloads, and everything else, really, see: -http://backbonejs.org - -To suggest a feature, report a bug, or general discussion: -http://github.com/jashkenas/backbone/issues - -Backbone is an open-sourced component of DocumentCloud: -https://github.com/documentcloud - -Many thanks to our contributors: -http://github.com/jashkenas/backbone/contributors - -Special thanks to Robert Kieffer for the original philosophy behind Backbone. -http://github.com/broofa diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js b/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js deleted file mode 100644 index 8ea4b13d3..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/backbone/backbone-min.js +++ /dev/null @@ -1,2 +0,0 @@ -(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\?([\\s\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\/]|\s+$/g;var O=/^\/+|\/+$/g;var P=/msie [\w.]+/;var C=/\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$(' -
            - - -
            -
            - -
            - - - - -
            - -
            - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            -
            -
            - -
            -
            hi there
            -
            -
            -
            -
            -
            -
            -
            - -
            -
            -
            - - -
            - - -
            - - -
            C
            -
            -
            - -
            -
              -
            1. Rice
            2. -
            3. Beans
            4. -
            5. Blinis
            6. -
            7. Tofu
            8. -
            - -
            I'm hungry. I should...
            - ...Eat lots of food... | - ...Eat a little food... | - ...Eat no food... - ...Eat a burger... - ...Eat some funyuns... - ...Eat some funyuns... -
            - -
            - - -
            - -
            - 1 - 2 - - - - - - - - -
            ​ -
          - -
          - - diff --git a/subjects/MigratingToReact/backbone-todomvc/node_modules/jquery/src/sizzle/test/jquery.js b/subjects/MigratingToReact/backbone-todomvc/node_modules/jquery/src/sizzle/test/jquery.js deleted file mode 100644 index 86a330515..000000000 --- a/subjects/MigratingToReact/backbone-todomvc/node_modules/jquery/src/sizzle/test/jquery.js +++ /dev/null @@ -1,9597 +0,0 @@ -/*! - * jQuery JavaScript Library v1.9.1 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-2-4 - */ -(function( window, undefined ) { - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -//"use strict"; -var - // The deferred used on DOM ready - readyList, - - // A central reference to the root jQuery(document) - rootjQuery, - - // Support: IE<9 - // For `typeof node.method` instead of `node.method !== undefined` - core_strundefined = typeof undefined, - - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - location = window.location, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // [[Class]] -> type pairs - class2type = {}, - - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], - - core_version = "1.9.1", - - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }, - - // The ready event handler - completed = function( event ) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: core_version, - - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return core_slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; - }, - - slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !core_hasOwn.call(obj, "constructor") && - !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || core_hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw new Error( msg ); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - if ( scripts ) { - jQuery( scripts ).remove(); - } - return jQuery.merge( [], parsed.childNodes ); - }, - - parseJSON: function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - } - } - } - - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? - function( text ) { - return text == null ? - "" : - core_trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - core_push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( core_indexOf ) { - return core_indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; - - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var retVal, - ret = [], - i = 0, - length = elems.length; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return core_concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var args, proxy, tmp; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = core_slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - } -}); - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - var length = obj.length, - type = jQuery.type( obj ); - - if ( jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( list && ( !fired || stack ) ) { - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = core_slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { - deferred.notifyWith( contexts, values ); - } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); -jQuery.support = (function() { - - var support, all, a, - input, select, fragment, - opt, eventName, isSupported, i, - div = document.createElement("div"); - - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = "
          a"; - - // Support tests won't run in some limited or non-browser environments - all = div.getElementsByTagName("*"); - a = div.getElementsByTagName("a")[ 0 ]; - if ( !all || !a || !all.length ) { - return {}; - } - - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px;float:left;opacity:.5"; - support = { - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.5/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - checkOn: !!input.value, - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Tests for enctype support on a form (#6743) - enctype: !!document.createElement("form").enctype, - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", - - // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode - boxModel: document.compatMode === "CSS1Compat", - - // Will be defined later - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true, - boxSizingReliable: true, - pixelPosition: false - }; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); - - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); - - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body - return; - } - - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
          t
          "; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - support.boxSizing = ( div.offsetWidth === 4 ); - support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); - } - - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
          "; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild( container ); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; -})(); - -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, ret, - internalKey = jQuery.expando, - getByName = typeof name === "string", - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - - // Avoids exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( getByName ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var i, l, thisCache, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - - for ( i = 0, l = name.length; i < l; i++ ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } -} - -jQuery.extend({ - cache: {}, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, - - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, - - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var attrs, name, - elem = this[0], - i = 0, - data = null; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; - - if ( !name.indexOf( "data-" ) ) { - name = jQuery.camelCase( name.slice(5) ); - - dataAttr( elem, name, data[ name ] ); - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - return jQuery.access( this, function( value ) { - - if ( value === undefined ) { - // Try to fetch any internally stored data first - return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; - } - - this.each(function() { - jQuery.data( this, key, value ); - }); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - hooks.cur = fn; - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); - - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.match( core_rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var ret, hooks, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val, - self = jQuery(this); - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var values = jQuery.makeArray( value ); - - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attr: function( elem, name, value ) { - var hooks, notxml, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( notxml ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - // In IE9+, Flash objects don't have .getAttribute (#12945) - // Support: IE9+ - if ( typeof elem.getAttribute !== core_strundefined ) { - ret = elem.getAttribute( name ); - } - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( rboolean.test( name ) ) { - // Set corresponding property to false for boolean attributes - // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 - if ( !getSetAttribute && ruseDefault.test( name ) ) { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } else { - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - return ( elem[ name ] = value ); - } - - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - return elem[ name ]; - } - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabindex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - } - } -}); - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - var - // Use .prop to determine if this attribute is understood as boolean - prop = jQuery.prop( elem, name ), - - // Fetch it accordingly - attr = typeof prop === "boolean" && elem.getAttribute( name ), - detail = typeof prop === "boolean" ? - - getSetInput && getSetAttribute ? - attr != null : - // oldIE fabricates an empty string for missing boolean attributes - // and conflates checked/selected into attroperties - ruseDefault.test( name ) ? - elem[ jQuery.camelCase( "default-" + name ) ] : - !!attr : - - // fetch an attribute node for properties not recognized as boolean - elem.getAttributeNode( name ); - - return detail && detail.value !== false ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; - -// fix oldIE value attroperty -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return jQuery.nodeName( elem, "input" ) ? - - // Ignore the value *property* by using defaultValue - elem.defaultValue : - - ret && ret.specified ? ret.value : undefined; - }, - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? - ret.value : - undefined; - }, - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute( name ) ? - value : - undefined; - } - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - get: nodeHook.get, - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }); - }); -} - - -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret == null ? undefined : ret; - } - }); - }); - - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }); -} - -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }); -}); -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - event.isTrigger = true; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - for ( ; cur != this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - } - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== document.activeElement && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === document.activeElement && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - - beforeunload: { - postDispatch: function( event ) { - - // Even when returnValue equals to undefined Firefox will still show alert - if ( event.result !== undefined ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === core_strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); -/*! - * Sizzle CSS Selector Engine - * Copyright 2012 jQuery Foundation and other contributors - * Released under the MIT license - * http://sizzlejs.com/ - */ -(function( window, undefined ) { - -var i, - cachedruns, - Expr, - getText, - isXML, - compile, - hasDuplicate, - outermostContext, - - // Local document vars - setDocument, - document, - docElem, - documentIsXML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - sortOrder, - - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - support = {}, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - // Array methods - arr = [], - pop = arr.pop, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; - } - } - return -1; - }, - - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - operators = "([*^$|!~]?=)", - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rsibling = /[\x20\t\r\n\f]*[+~]/, - - rnative = /^[^{]+\{\s*\[native code/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rescape = /'|\\/g, - rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, - funescape = function( _, escaped ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - return high !== high ? - escaped : - // BMP codepoint - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -// Use a stripped-down slice if we can't use a native one -try { - slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; -} catch ( e ) { - slice = function( i ) { - var elem, - results = []; - while ( (elem = this[i++]) ) { - results.push( elem ); - } - return results; - }; -} - -/** - * For feature detection - * @param {Function} fn The function to test for native support - */ -function isNative( fn ) { - return rnative.test( fn + "" ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var cache, - keys = []; - - return (cache = function( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key ] = value); - }); -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return fn( div ); - } catch (e) { - return false; - } finally { - // release memory in IE - div = null; - } -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } - - if ( !documentIsXML && !seed ) { - - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { - push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); - return results; - } - } - - // QSA path - if ( support.qsa && !rbuggyQSA.test(selector) ) { - old = true; - nid = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && context.parentNode || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, slice.call( newContext.querySelectorAll( - newSelector - ), 0 ) ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Detect xml - * @param {Element|Object} elem An element or a document - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsXML = isXML( doc ); - - // Check if getElementsByTagName("*") returns only elements - support.tagNameNoComments = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Check if attributes should be retrieved by attribute nodes - support.attributes = assert(function( div ) { - div.innerHTML = ""; - var type = typeof div.lastChild.getAttribute("multiple"); - // IE8 returns a string for some attributes even when not present - return type !== "boolean" && type !== "string"; - }); - - // Check if getElementsByClassName can be trusted - support.getByClassName = assert(function( div ) { - // Opera can't find a second classname (in 9.6) - div.innerHTML = ""; - if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { - return false; - } - - // Safari 3.2 caches class attributes and doesn't catch changes - div.lastChild.className = "e"; - return div.getElementsByClassName("e").length === 2; - }); - - // Check if getElementById returns elements by name - // Check if getElementsByName privileges form controls or returns elements by ID - support.getByName = assert(function( div ) { - // Inject content - div.id = expando + 0; - div.innerHTML = "
          "; - docElem.insertBefore( div, docElem.firstChild ); - - // Test - var pass = doc.getElementsByName && - // buggy browsers will return fewer than the correct 2 - doc.getElementsByName( expando ).length === 2 + - // buggy browsers will return more than the correct 0 - doc.getElementsByName( expando + 0 ).length; - support.getIdNotName = !doc.getElementById( expando ); - - // Cleanup - docElem.removeChild( div ); - - return pass; - }); - - // IE6/7 return modified attributes - Expr.attrHandle = assert(function( div ) { - div.innerHTML = ""; - return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && - div.firstChild.getAttribute("href") === "#"; - }) ? - {} : - { - "href": function( elem ) { - return elem.getAttribute( "href", 2 ); - }, - "type": function( elem ) { - return elem.getAttribute("type"); - } - }; - - // ID find and filter - if ( support.getIdNotName ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - - return m ? - m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? - [m] : - undefined : - []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.tagNameNoComments ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Name - Expr.find["NAME"] = support.getByName && function( tag, context ) { - if ( typeof context.getElementsByName !== strundefined ) { - return context.getElementsByName( name ); - } - }; - - // Class - Expr.find["CLASS"] = support.getByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { - return context.getElementsByClassName( className ); - } - }; - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21), - // no need to also add to buggyMatches since matches checks buggyQSA - // A support test would require too much code (would include document ready) - rbuggyQSA = [ ":focus" ]; - - if ( (support.qsa = isNative(doc.querySelectorAll)) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explictly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; - - // IE8 - Some boolean attributes are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function( div ) { - - // Opera 10-12/IE8 - ^= $= *= and empty values - // Should not select anything - div.innerHTML = ""; - if ( div.querySelectorAll("[i^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || - docElem.mozMatchesSelector || - docElem.webkitMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - // Document order sorting - sortOrder = docElem.compareDocumentPosition ? - function( a, b ) { - var compare; - - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { - if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { - if ( a === doc || contains( preferredDoc, a ) ) { - return -1; - } - if ( b === doc || contains( preferredDoc, b ) ) { - return 1; - } - return 0; - } - return compare & 4 ? -1 : 1; - } - - return a.compareDocumentPosition ? -1 : 1; - } : - function( a, b ) { - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - // Always assume the presence of duplicates if sort doesn't - // pass them to our comparison function (as in Google Chrome). - hasDuplicate = false; - [0, 0].sort( sortOrder ); - support.detectDuplicates = hasDuplicate; - - return document; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - // rbuggyQSA always contains :focus, so no need for an existence check - if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch(e) {} - } - - return Sizzle( expr, document, null, [elem] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - var val; - - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - if ( !documentIsXML ) { - name = name.toLowerCase(); - } - if ( (val = Expr.attrHandle[ name ]) ) { - return val( elem ); - } - if ( documentIsXML || support.attributes ) { - return elem.getAttribute( name ); - } - return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? - name : - val && val.specified ? val.value : null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -// Document sorting and removing duplicates -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - i = 1, - j = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( ; (elem = results[i]); i++ ) { - if ( elem === results[ i - 1 ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - return results; -}; - -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -// Returns a function to use in pseudos for input types -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -// Returns a function to use in pseudos for buttons -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -// Returns a function to use in pseudos for positionals -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[5] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[4] ) { - match[2] = match[4]; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeName ) { - if ( nodeName === "*" ) { - return function() { return true; }; - } - - nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifider - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsXML ? - elem.getAttribute("xml:lang") || elem.getAttribute("lang") : - elem.lang) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( tokens = [] ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push( { - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } - } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { - var elem, j, matcher, - setMatched = [], - matchedCount = 0, - i = "0", - unmatched = seed && [], - outermost = expandContext != null, - contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); - - if ( outermost ) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); - } - i = group.length; - while ( i-- ) { - cached = matcherFromTokens( group[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - } - return cached; -}; - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - match = tokenize( selector ); - - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - context.nodeType === 9 && !documentIsXML && - Expr.relative[ tokens[1].type ] ) { - - context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; - if ( !context ) { - return results; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, slice.call( seed, 0 ) ); - return results; - } - - break; - } - } - } - } - } - - // Compile and execute a filtering function - // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( - seed, - context, - documentIsXML, - results, - rsibling.test( selector ) - ); - return results; -} - -// Deprecated -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Easy API for creating new setFilters -function setFilters() {} -Expr.filters = setFilters.prototype = Expr.pseudos; -Expr.setFilters = new setFilters(); - -// Initialize with the default document -setDocument(); - -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})( window ); -var runtil = /Until$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - isSimple = /^.[^:#\[\.,]*$/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var i, ret, self, - len = this.length; - - if ( typeof selector !== "string" ) { - self = this; - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - ret = []; - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, this[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; - return ret; - }, - - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; - - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false) ); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true) ); - }, - - is: function( selector ) { - return !!selector && ( - typeof selector === "string" ? - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - rneedsContext.test( selector ) ? - jQuery( selector, this.context ).index( this[0] ) >= 0 : - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - ret = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - cur = this[i]; - - while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - } - cur = cur.parentNode; - } - } - - return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( jQuery.unique(all) ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -jQuery.fn.andSelf = jQuery.fn.addBack; - -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); - - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( this.length > 1 && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem ) { - return ( elem === qualifier ) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; - }); -} -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "" ], - legend: [ 1, "
          ", "
          " ], - area: [ 1, "", "" ], - param: [ 1, "", "" ], - thead: [ 1, "", "
          " ], - tr: [ 2, "", "
          " ], - col: [ 2, "", "
          " ], - td: [ 3, "", "
          " ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
          ", "
          " ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -jQuery.fn.extend({ - text: function( value ) { - return jQuery.access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - }, - - append: function() { - return this.domManip(arguments, true, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip(arguments, true, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.insertBefore( elem, this.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, false, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, false, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - // keepData is for internal use only--do not document - remove: function( selector, keepData ) { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function () { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return jQuery.access( this, function( value ) { - var elem = this[0] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1>" ); - - try { - for (; i < l; i++ ) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch(e) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function( value ) { - var isFunc = jQuery.isFunction( value ); - - // Make sure that the elements are removed from the DOM before they are inserted - // this can help fix replacing a parent with child elements - if ( !isFunc && typeof value !== "string" ) { - value = jQuery( value ).not( this ).detach(); - } - - return this.domManip( [ value ], true, function( elem ) { - var next = this.nextSibling, - parent = this.parentNode; - - if ( parent ) { - jQuery( this ).remove(); - parent.insertBefore( elem, next ); - } - }); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, table, callback ) { - - // Flatten any nested arrays - args = core_concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[0] = value.call( this, index, table ? self.html() : undefined ); - } - self.domManip( args, table, callback ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - table = table && jQuery.nodeName( first, "tr" ); - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( - table && jQuery.nodeName( this[i], "table" ) ? - findOrAppend( this[i], "tbody" ) : - this[i], - node, - i - ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Hope ajax is available... - jQuery.ajax({ - url: node.src, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); - } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } -}); - -function findOrAppend( elem, tag ) { - return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - var attr = elem.getAttributeNode("type"); - elem.type = ( attr && attr.specified ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - core_push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( manipulation_rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment( context ), - - nodes = [], - i = 0; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - - // Remove IE's autoinserted from table fragments - if ( !jQuery.support.tbody ) { - - // String was a , *may* have spurious - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare or - wrap[1] === "
          " && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !jQuery.support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = jQuery.support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; - - } else if ( typeof elem.removeAttribute !== core_strundefined ) { - elem.removeAttribute( internalKey ); - - } else { - elem[ internalKey ] = null; - } - - core_deletedIds.push( id ); - } - } - } - } - } -}); -var iframe, getStyles, curCSS, - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - rposition = /^(top|right|bottom|left)$/, - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rmargin = /^margin/, - rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), - rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), - elemdisplay = { BODY: "block" }, - - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: 0, - fontWeight: 400 - }, - - cssExpand = [ "Top", "Right", "Bottom", "Left" ], - cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; - -// return a css property mapped to a potentially vendor prefixed property -function vendorPropName( style, name ) { - - // shortcut for names that are not vendor prefixed - if ( name in style ) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in style ) { - return name; - } - } - - return origName; -} - -function isHidden( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); -} - -function showHide( elements, show ) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - values[ index ] = jQuery._data( elem, "olddisplay" ); - display = elem.style.display; - if ( show ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !values[ index ] && display === "none" ) { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); - } - } else { - - if ( !values[ index ] ) { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( index = 0; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - if ( !show || elem.style.display === "none" || elem.style.display === "" ) { - elem.style.display = show ? values[ index ] || "" : "none"; - } - } - - return elements; -} - -jQuery.fn.extend({ - css: function( name, value ) { - return jQuery.access( this, function( elem, name, value ) { - var len, styles, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - var bool = typeof state === "boolean"; - - return this.each(function() { - if ( bool ? state : isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - -jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Exclude the following css properties to add px - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if ( type === "string" && (ret = rrelNum.exec( value )) ) { - value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that NaN and null values aren't set. See: #7116 - if ( value == null || type === "number" && isNaN( value ) ) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( type === "number" && !jQuery.cssNumber[ origName ] ) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Wrapped to prevent IE from throwing errors when 'invalid' values are provided - // Fixes bug #5509 - try { - style[ name ] = value; - } catch(e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var num, val, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - //convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; - } - return val; - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } -}); - -// NOTE: we've included the "window" in window.getComputedStyle -// because jsdom on node.js will break without it. -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - return window.getComputedStyle( elem, null ); - }; - - curCSS = function( elem, name, _computed ) { - var width, minWidth, maxWidth, - computed = _computed || getStyles( elem ), - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, - style = elem.style; - - if ( computed ) { - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; - - curCSS = function( elem, name, _computed ) { - var left, rs, rsLeft, - computed = _computed || getStyles( elem ), - ret = computed ? computed[ name ] : undefined, - style = elem.style; - - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; - } - } - - return ret === "" ? "auto" : ret; - }; -} - -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -// Try to determine the default display value of an element -function css_defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - // Use the already-created iframe if possible - iframe = ( iframe || - jQuery("