From 559369016d822852c22af49c402a7b53c9551626 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Wed, 20 Apr 2016 20:02:37 -0400 Subject: [PATCH] initial commit (work in progress) --- CONTRIBUTING.md | 78 ++ README.md | 29 + bundler/README.md | 23 + bundler/bundler.js | 38 + examples/animation/flowers.jpg | Bin 0 -> 60035 bytes examples/animation/mosaic.html | 67 ++ examples/dbmonster/ENV.js | 211 ++++++ examples/dbmonster/bootstrap.min.css | 7 + examples/dbmonster/memory-stats.js | 101 +++ examples/dbmonster/mithril/app.js | 72 ++ examples/dbmonster/mithril/index.html | 20 + examples/dbmonster/monitor.js | 60 ++ examples/dbmonster/react/app.js | 84 +++ examples/dbmonster/react/index.html | 18 + examples/dbmonster/styles.css | 27 + examples/svg/clock.html | 70 ++ examples/svg/ring.html | 836 +++++++++++++++++++++ examples/svg/tiger.html | 746 ++++++++++++++++++ examples/threaditjs/app.js | 195 +++++ examples/threaditjs/colors.css | 6 + examples/threaditjs/index.html | 23 + examples/todomvc/app.css | 378 ++++++++++ examples/todomvc/base.css | 141 ++++ examples/todomvc/index.html | 24 + examples/todomvc/todomvc.js | 144 ++++ index.js | 8 + module/README.md | 64 ++ module/module.js | 29 + ospec/README.md | 389 ++++++++++ ospec/bin/ospec | 46 ++ ospec/bin/ospec.cmd | 1 + ospec/ospec.js | 187 +++++ ospec/tests/index.html | 12 + ospec/tests/test-ospec.js | 94 +++ package.json | 11 + querystring/build.js | 25 + querystring/parse.js | 42 ++ querystring/tests/index.html | 17 + querystring/tests/test-buildQueryString.js | 67 ++ querystring/tests/test-parseQueryString.js | 74 ++ render/hyperscript.js | 75 ++ render/normalizeChildren.js | 7 + render/render.js | 382 ++++++++++ render/tests/index.html | 37 + render/tests/test-attributes.js | 79 ++ render/tests/test-createElement.js | 82 ++ render/tests/test-createFragment.js | 50 ++ render/tests/test-createHTML.js | 54 ++ render/tests/test-createNodes.js | 62 ++ render/tests/test-createText.js | 71 ++ render/tests/test-event.js | 34 + render/tests/test-hyperscript.js | 379 ++++++++++ render/tests/test-input.js | 26 + render/tests/test-onbeforeremove.js | 159 ++++ render/tests/test-oncreate.js | 216 ++++++ render/tests/test-onremove.js | 103 +++ render/tests/test-onupdate.js | 192 +++++ render/tests/test-style.js | 0 render/tests/test-textContent.js | 200 +++++ render/tests/test-updateElement.js | 152 ++++ render/tests/test-updateFragment.js | 69 ++ render/tests/test-updateHTML.js | 49 ++ render/tests/test-updateNodes.js | 789 +++++++++++++++++++ render/tests/test-updateText.js | 114 +++ render/trust.js | 3 + request/request.js | 114 +++ request/tests/index.html | 21 + request/tests/test-ajax.js | 141 ++++ request/tests/test-jsonp.js | 55 ++ router/router.js | 87 +++ router/tests/index.html | 19 + router/tests/test-defineRoutes.js | 229 ++++++ test-utils/README.md | 12 + test-utils/ajaxMock.js | 77 ++ test-utils/callAsync.js | 3 + test-utils/domMock.js | 219 ++++++ test-utils/parseURL.js | 47 ++ test-utils/pushStateMock.js | 169 +++++ test-utils/tests/index.html | 23 + test-utils/tests/test-ajaxMock.js | 147 ++++ test-utils/tests/test-domMock.js | 580 ++++++++++++++ test-utils/tests/test-parseURL.js | 150 ++++ test-utils/tests/test-pushStateMock.js | 520 +++++++++++++ 83 files changed, 10461 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 bundler/README.md create mode 100644 bundler/bundler.js create mode 100644 examples/animation/flowers.jpg create mode 100644 examples/animation/mosaic.html create mode 100644 examples/dbmonster/ENV.js create mode 100644 examples/dbmonster/bootstrap.min.css create mode 100644 examples/dbmonster/memory-stats.js create mode 100644 examples/dbmonster/mithril/app.js create mode 100644 examples/dbmonster/mithril/index.html create mode 100644 examples/dbmonster/monitor.js create mode 100644 examples/dbmonster/react/app.js create mode 100644 examples/dbmonster/react/index.html create mode 100644 examples/dbmonster/styles.css create mode 100644 examples/svg/clock.html create mode 100644 examples/svg/ring.html create mode 100644 examples/svg/tiger.html create mode 100644 examples/threaditjs/app.js create mode 100644 examples/threaditjs/colors.css create mode 100644 examples/threaditjs/index.html create mode 100644 examples/todomvc/app.css create mode 100644 examples/todomvc/base.css create mode 100644 examples/todomvc/index.html create mode 100644 examples/todomvc/todomvc.js create mode 100644 index.js create mode 100644 module/README.md create mode 100644 module/module.js create mode 100644 ospec/README.md create mode 100644 ospec/bin/ospec create mode 100644 ospec/bin/ospec.cmd create mode 100644 ospec/ospec.js create mode 100644 ospec/tests/index.html create mode 100644 ospec/tests/test-ospec.js create mode 100644 package.json create mode 100644 querystring/build.js create mode 100644 querystring/parse.js create mode 100644 querystring/tests/index.html create mode 100644 querystring/tests/test-buildQueryString.js create mode 100644 querystring/tests/test-parseQueryString.js create mode 100644 render/hyperscript.js create mode 100644 render/normalizeChildren.js create mode 100644 render/render.js create mode 100644 render/tests/index.html create mode 100644 render/tests/test-attributes.js create mode 100644 render/tests/test-createElement.js create mode 100644 render/tests/test-createFragment.js create mode 100644 render/tests/test-createHTML.js create mode 100644 render/tests/test-createNodes.js create mode 100644 render/tests/test-createText.js create mode 100644 render/tests/test-event.js create mode 100644 render/tests/test-hyperscript.js create mode 100644 render/tests/test-input.js create mode 100644 render/tests/test-onbeforeremove.js create mode 100644 render/tests/test-oncreate.js create mode 100644 render/tests/test-onremove.js create mode 100644 render/tests/test-onupdate.js create mode 100644 render/tests/test-style.js create mode 100644 render/tests/test-textContent.js create mode 100644 render/tests/test-updateElement.js create mode 100644 render/tests/test-updateFragment.js create mode 100644 render/tests/test-updateHTML.js create mode 100644 render/tests/test-updateNodes.js create mode 100644 render/tests/test-updateText.js create mode 100644 render/trust.js create mode 100644 request/request.js create mode 100644 request/tests/index.html create mode 100644 request/tests/test-ajax.js create mode 100644 request/tests/test-jsonp.js create mode 100644 router/router.js create mode 100644 router/tests/index.html create mode 100644 router/tests/test-defineRoutes.js create mode 100644 test-utils/README.md create mode 100644 test-utils/ajaxMock.js create mode 100644 test-utils/callAsync.js create mode 100644 test-utils/domMock.js create mode 100644 test-utils/parseURL.js create mode 100644 test-utils/pushStateMock.js create mode 100644 test-utils/tests/index.html create mode 100644 test-utils/tests/test-ajaxMock.js create mode 100644 test-utils/tests/test-domMock.js create mode 100644 test-utils/tests/test-parseURL.js create mode 100644 test-utils/tests/test-pushStateMock.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6600c7f2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# FAQ + +## How do I go about contributing ideas or new features? + +Create an issue to suggest it and discuss first. Avoid submitting large changes. + + + +## How should I report bugs? + +Ideally, provide code to reproduce the issue (via jsfiddle, a gist, etc). Even better, submit a pull request with a fix and tests. If you don't know how to test your fix, or lint or whatever, submit anyways, and we can help you. + + + +## How do I run tests? + +Assuming you have forked this repo, you can open the `index.html` file in a module's `tests` folder and look at console output to see only tests for that module, or you can run `ospec/bin/ospec` from the command line to run all tests under a Node.js environment. Additionally, you can modify a test to use `o.only(description, test)` instead of `o(description, test)` if you wish to run only a specific test. + + + +## How do I build Mithril? + +Run `node bundler/bundler.js` from the command line to generate the bundled file. + + + +## Why do tests mock the browser APIs? + +Most notoriously, because it's impossible to test the router and some side effects properly otherwise. Also, mocks allow the tests to run under Node.js without requiring heavy dependencies like PhantomJS/ChromeDriver/JSDOM. + +Another important reason is that it allows us to document browser API quirks via code, through the tests for the mocks. + + + +## Why does Mithril use its own testing framework and not Mocha/Jasmine/Tape? + +Mainly to avoid requiring dependencies. ospec is customized to provide only essential information for common testing workflows (namely, no spamming ok's on pass, and accurate noiseless errors on failure) + + + +## Why do tests and examples use `module/module.js`? Why not use Browserify, Webpack or Rollup? + +Again, to avoid requiring dependencies. The Mithril codebase is written using a statically analyzable subset of CommonJS module definitions (as opposed to ES6 modules) because its syntax is backwards compatible with ES5, therefore making it possible to run source code unmodified in browsers without the need for a build tool or a file watcher. + +This simplifies the workflow for bug fixes, which means they can be fixed faster. + + + +## Why doesn't the Mithril codebase use ES6 via Babel? Would a PR to upgrade be welcome? + +Being able to run Mithril raw source code in IE is a requirement for all browser-related modules in this repo. + +In addition, ES6 features are usually less performant than equivalent ES5 code, and transpiled code is bulkier. + + + +## Why doesn't the Mithril codebase use trailing semi-colons? Would a PR to add them be welcome? + +I don't use them. Adding them means the semi-colon usage in the codebase will eventually become inconsistent. If you're not comfortable with ASI rules, bear in mind that this codebase is heavily optimized and large modifications will require an advanced level of javascript mastery. + + + +## Why does the Mithril codebase use a mix of `instanceof` and `typeof` checks instead of `Object.prototype.toString.call`, `Array.isArray`, etc? Would a PR to refactor those checks be welcome? + +Mithril avoids peeking at objects' [[class]] string for performance considerations. Many type checks are seemingly inconsistent, weird or convoluted because those specific constructs demonstrated the best performance profile in benchmarks compared to alternatives. + +Type checks are generally already irreducible expressions and having micro-modules for type checking subroutines would add maintenance overhead. + + + +## What should I know in advance when attempting a performance related contribution? + +You should be trying to reduce the number of DOM operations or reduce algorithmic complexity in a hot spot. Anything else is likely a waste of time. Specifically, micro-optimizations like caching array lengths, caching object property values and inlining functions won't have any positive impact in modern javascript engines. + +Keep object properties consistent (i.e. ensure the data objects always have the same properties and that properties are always in the same order) to allow the engine to keep using JIT'ed structs instead of hashmaps. Always place null checks first in compound type checking expressions to allow the Javascript engine to optimize to type-specific code paths. Prefer for loops over Array methods and try to pull conditionals out of loops if possible. + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..526ee4a0 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Mithril.js - A framework for building brilliant applications + +Note: This branch is a sneak peek for the upcoming version 1.0. It's a rewrite from the ground up and it's not backwards compatible with [Mithril 0.2.x](http://mithril.js.org) + +This rewrite aims to fix longstanding API design issues, significantly improve performance, and clean up the codebase. + +## Status + +Code still is in flux. Most notably, components and thunks (`{subtree: "retain"}`) are currently not implemented yet and there are several use cases that still need to be polished. DO NOT USE IN PRODUCTION YET! + +Some examples of usage can be found in the [examples](examples) folder. [ThreadItJS](examples/threaditjs/index.html) has the largest API surface coverage and comments indicating pending issues in framework usability. Note that the APIs those examples use may not become the final public API points in v1.0. + +## Performance + +Mithril's virtual DOM engine is now less than 400 lines of well organized code and it implements a modern search space reduction diff algorithm and a DOM recycling mechanism, which translate to top-of-class performance. See the [dbmon implementation](examples/dbmon/mithril/index.html) (for comparison, here's dbmon for [React v15.0.1](examples/dbmon/react/index.html)). + +## Lifecycle methods and Animation Support + +Mithril's `config` method is now replaced by several lifecycle methods to improve separation of concerns and allow better control over animations. + +## Robustness + +There are over 1800 assertions in the test suite, and tests cover even difficult-to-test things like `location.href`, `element.innerHTML` and `XMLHttpRequest` usage. + +## Modularity + +Despite the huge performance improvements, the new codebase is smaller than v0.2.x, currently clocking at 4.5kb min+gzip + +In addition, Mithril is now completely modular: you can import only the modules that you need and easily integrate 3rd party modules if you wish to use a different library for routing, ajax, and even rendering diff --git a/bundler/README.md b/bundler/README.md new file mode 100644 index 00000000..98711b42 --- /dev/null +++ b/bundler/README.md @@ -0,0 +1,23 @@ +# bundler.js + +Simplistic CommonJS module bundler + +Version: 0.1 +License: MIT + +## About + +This bundler attempts to aggressively bundle CommonJS modules by assuming the dependency tree is static, similar to what Rollup does for ES6 modules. + +Most browsers don't support ES6 `import/export` syntax, but we can achieve modularity by using CommonJS module syntax and employing [`module.js`](../module/README.md) in browser environments. + +Webpack is conservative and treats CommonJS modules as non-statically-analyzable since `require` and `module.exports` are legally allowed everywhere. Therefore, it must generate extra code to resolve dependencies at runtime (i.e. `__webpack_require()`). Rollup only works with ES6 modules. ES6 modules can be bundled more efficiently because they are statically analyzable, but some use cases are difficult to handle due to ES6's support for cyclic dependencies and hosting rules. This bundler assumes code is written in CommonJS style but follows a strict set of rules that emulate statically analyzable code and favors the usage of the factory pattern instead of relying on obscure corners of the Javascript language (hoisting rules and binding semantics). + +### Caveats + +- Only supports modules that have the `require` and `module.exports` statement declared at the top-level scope before all other code, i.e. it does not support CommonJS modules that rely on dynamic importing/exporting. This means modules should only export a pure function or export a factory function if there are multiple statements and/or internal module state. The factory function pattern allows easier dependency injection in stateful modules, thus making modules testable. +- Doesn't support scope pollution of module's top level scope. Specifically, it does not support same-name top-level variables in different modules. +- Changes the semantics of value/binding exporting between unbundled and bundled code, and therefore relying on those semantics is discouraged. Instead, it is recommended that module consumers inject dependencies via the factory function pattern +- Top level strictness is infectious (i.e. if entry file is in `"use strict"` mode, all modules inherit strict mode, and conversely, if the entry file is not in strict mode, all modules are pulled out of strict mode) +- Currently only supports assignments to `module.exports` (i.e. `module.exports.foo = bar` will not work) +- It is tiny and dependency-free because it uses regular expressions, and it only supports the narrow range of import/export declaration patterns outlined above. diff --git a/bundler/bundler.js b/bundler/bundler.js new file mode 100644 index 00000000..6c6009db --- /dev/null +++ b/bundler/bundler.js @@ -0,0 +1,38 @@ +"use strict" + +var fs = require("fs") +var path = require("path") + +var modules = {} + +function resolve(dir, data) { + var replacements = [] + data = data.replace(/((?:var|let|const|)\s*)([\w_$]+)(\s*=\s*)require\(([^\)]+)\)/g, function(match, def, variable, eq, dep) { + var filename = new Function("return " + dep).call() + var pathname = path.dirname(filename) + var normalized = path.normalize(dir + "/" + filename) + if (modules[normalized] === undefined) { + modules[normalized] = variable + return resolve(pathname, + fs.readFileSync(dir + "/" + filename + ".js", "utf8") + .replace(/"use strict"\s*/gm, "") + .replace(/module\.exports\s*=\s*/gm, def + variable + eq) + //.replace(/module\.exports(\.[\w_$]|\["[^\"]"\])/, def + variable + eq + "{}\n" + variable + "$1") + ) + } + else { + if (modules[normalized] !== variable) { + replacements.push({variable: variable, replacement: modules[normalized]}) + } + return "" + } + }) + if (replacements.length > 0) { + for (var i = 0; i < replacements.length; i++) { + data = data.replace(new RegExp("\\b" + replacements[i].variable + "\\b", "g"), replacements[i].replacement) + } + } + return data +} + +fs.writeFileSync("mithril.min.js", resolve(".", fs.readFileSync("index.js", "utf8")), "utf8") diff --git a/examples/animation/flowers.jpg b/examples/animation/flowers.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e604fbddbbd86e8a6cc8f0f59c76b590e3da6f25 GIT binary patch literal 60035 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 Do%{7t literal 0 HcmV?d00001 diff --git a/examples/animation/mosaic.html b/examples/animation/mosaic.html new file mode 100644 index 00000000..058f077c --- /dev/null +++ b/examples/animation/mosaic.html @@ -0,0 +1,67 @@ + + + + + Mosaic + + + +
+ + + + + + + \ No newline at end of file diff --git a/examples/dbmonster/ENV.js b/examples/dbmonster/ENV.js new file mode 100644 index 00000000..4bb153fa --- /dev/null +++ b/examples/dbmonster/ENV.js @@ -0,0 +1,211 @@ +var ENV = ENV || (function() { + + var first = true; + var counter = 0; + var data; + var _base; + (_base = String.prototype).lpad || (_base.lpad = function(padding, toLength) { + return padding.repeat((toLength - this.length) / padding.length).concat(this); + }); + + function formatElapsed(value) { + str = parseFloat(value).toFixed(2); + if (value > 60) { + minutes = Math.floor(value / 60); + comps = (value % 60).toFixed(2).split('.'); + seconds = comps[0].lpad('0', 2); + ms = comps[1]; + str = minutes + ":" + seconds + "." + ms; + } + return str; + } + + function getElapsedClassName(elapsed) { + var className = 'Query elapsed'; + if (elapsed >= 10.0) { + className += ' warn_long'; + } + else if (elapsed >= 1.0) { + className += ' warn'; + } + else { + className += ' short'; + } + return className; + } + + function countClassName(queries) { + var countClassName = "label"; + if (queries >= 20) { + countClassName += " label-important"; + } + else if (queries >= 10) { + countClassName += " label-warning"; + } + else { + countClassName += " label-success"; + } + return countClassName; + } + + function updateQuery(object) { + if (!object) { + object = {}; + } + var elapsed = Math.random() * 15; + object.elapsed = elapsed; + object.formatElapsed = formatElapsed(elapsed); + object.elapsedClassName = getElapsedClassName(elapsed); + object.query = "SELECT blah FROM something"; + object.waiting = Math.random() < 0.5; + if (Math.random() < 0.2) { + object.query = " in transaction"; + } + if (Math.random() < 0.1) { + object.query = "vacuum"; + } + return object; + } + + function cleanQuery(value) { + if (value) { + value.formatElapsed = ""; + value.elapsedClassName = ""; + value.query = ""; + value.elapsed = null; + value.waiting = null; + } else { + return { + query: "***", + formatElapsed: "", + elapsedClassName: "" + }; + } + } + + function generateRow(object, keepIdentity, counter) { + var nbQueries = Math.floor((Math.random() * 10) + 1); + if (!object) { + object = {}; + } + object.lastMutationId = counter; + object.nbQueries = nbQueries; + if (!object.lastSample) { + object.lastSample = {}; + } + if (!object.lastSample.topFiveQueries) { + object.lastSample.topFiveQueries = []; + } + if (keepIdentity) { + // for Angular optimization + if (!object.lastSample.queries) { + object.lastSample.queries = []; + for (var l = 0; l < 12; l++) { + object.lastSample.queries[l] = cleanQuery(); + } + } + for (var j in object.lastSample.queries) { + var value = object.lastSample.queries[j]; + if (j <= nbQueries) { + updateQuery(value); + } else { + cleanQuery(value); + } + } + } else { + object.lastSample.queries = []; + for (var j = 0; j < 12; j++) { + if (j < nbQueries) { + var value = updateQuery(cleanQuery()); + object.lastSample.queries.push(value); + } else { + object.lastSample.queries.push(cleanQuery()); + } + } + } + for (var i = 0; i < 5; i++) { + var source = object.lastSample.queries[i]; + object.lastSample.topFiveQueries[i] = source; + } + object.lastSample.nbQueries = nbQueries; + object.lastSample.countClassName = countClassName(nbQueries); + return object; + } + + function getData(keepIdentity) { + var oldData = data; + if (!keepIdentity) { // reset for each tick when !keepIdentity + data = []; + for (var i = 1; i <= ENV.rows; i++) { + data.push({ dbname: 'cluster' + i, query: "", formatElapsed: "", elapsedClassName: "" }); + data.push({ dbname: 'cluster' + i + ' slave', query: "", formatElapsed: "", elapsedClassName: "" }); + } + } + if (!data) { // first init when keepIdentity + data = []; + for (var i = 1; i <= ENV.rows; i++) { + data.push({ dbname: 'cluster' + i }); + data.push({ dbname: 'cluster' + i + ' slave' }); + } + oldData = data; + } + for (var i in data) { + var row = data[i]; + if (!keepIdentity && oldData && oldData[i]) { + row.lastSample = oldData[i].lastSample; + } + if (!row.lastSample || Math.random() < ENV.mutations()) { + counter = counter + 1; + if (!keepIdentity) { + row.lastSample = null; + } + generateRow(row, keepIdentity, counter); + } else { + data[i] = oldData[i]; + } + } + first = false; + return { + toArray: function() { + return data; + } + }; + } + + var mutationsValue = 0.5; + + function mutations(value) { + if (value) { + mutationsValue = value; + return mutationsValue; + } else { + return mutationsValue; + } + } + + var body = document.querySelector('body'); + var theFirstChild = body.firstChild; + + var sliderContainer = document.createElement( 'div' ); + sliderContainer.style.cssText = "display: flex"; + var slider = document.createElement('input'); + var text = document.createElement('label'); + text.innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%'; + text.id = "ratioval"; + slider.setAttribute("type", "range"); + slider.style.cssText = 'margin-bottom: 10px; margin-top: 5px'; + slider.addEventListener('change', function(e) { + ENV.mutations(e.target.value / 100); + document.querySelector('#ratioval').innerHTML = 'mutations : ' + (ENV.mutations() * 100).toFixed(0) + '%'; + }); + sliderContainer.appendChild( text ); + sliderContainer.appendChild( slider ); + body.insertBefore( sliderContainer, theFirstChild ); + + return { + generateData: getData, + rows: 50, + timeout: 0, + mutations: mutations + }; +})(); diff --git a/examples/dbmonster/bootstrap.min.css b/examples/dbmonster/bootstrap.min.css new file mode 100644 index 00000000..679272d2 --- /dev/null +++ b/examples/dbmonster/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/examples/dbmonster/memory-stats.js b/examples/dbmonster/memory-stats.js new file mode 100644 index 00000000..df75fb30 --- /dev/null +++ b/examples/dbmonster/memory-stats.js @@ -0,0 +1,101 @@ +/** + * @author mrdoob / http://mrdoob.com/ + * @author jetienne / http://jetienne.com/ + * @author paulirish / http://paulirish.com/ + */ +var MemoryStats = function (){ + + var msMin = 100; + var msMax = 0; + + var container = document.createElement( 'div' ); + container.id = 'stats'; + container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; + + var msDiv = document.createElement( 'div' ); + msDiv.id = 'ms'; + msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; + container.appendChild( msDiv ); + + var msText = document.createElement( 'div' ); + msText.id = 'msText'; + msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; + msText.innerHTML= 'Memory'; + msDiv.appendChild( msText ); + + var msGraph = document.createElement( 'div' ); + msGraph.id = 'msGraph'; + msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; + msDiv.appendChild( msGraph ); + + while ( msGraph.children.length < 74 ) { + + var bar = document.createElement( 'span' ); + bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; + msGraph.appendChild( bar ); + + } + + var updateGraph = function ( dom, height, color ) { + + var child = dom.appendChild( dom.firstChild ); + child.style.height = height + 'px'; + if( color ) child.style.backgroundColor = color; + + } + + var perf = window.performance || {}; + // polyfill usedJSHeapSize + if (!perf && !perf.memory){ + perf.memory = { usedJSHeapSize : 0 }; + } + if (perf && !perf.memory){ + perf.memory = { usedJSHeapSize : 0 }; + } + + // support of the API? + if( perf.memory.totalJSHeapSize === 0 ){ + console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .') + } + + // TODO, add a sanity check to see if values are bucketed. + // If so, reminde user to adopt the --enable-precise-memory-info flag. + // open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info + + var lastTime = Date.now(); + var lastUsedHeap= perf.memory.usedJSHeapSize; + return { + domElement: container, + + update: function () { + + // refresh only 30time per second + if( Date.now() - lastTime < 1000/30 ) return; + lastTime = Date.now() + + var delta = perf.memory.usedJSHeapSize - lastUsedHeap; + lastUsedHeap = perf.memory.usedJSHeapSize; + var color = delta < 0 ? '#830' : '#131'; + + var ms = perf.memory.usedJSHeapSize; + msMin = Math.min( msMin, ms ); + msMax = Math.max( msMax, ms ); + msText.textContent = "Mem: " + bytesToSize(ms, 2); + + var normValue = ms / (30*1024*1024); + var height = Math.min( 30, 30 - normValue * 30 ); + updateGraph( msGraph, height, color); + + function bytesToSize( bytes, nFractDigit ){ + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes == 0) return 'n/a'; + nFractDigit = nFractDigit !== undefined ? nFractDigit : 0; + var precision = Math.pow(10, nFractDigit); + var i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes*precision / Math.pow(1024, i))/precision + ' ' + sizes[i]; + }; + } + + } + +}; \ No newline at end of file diff --git a/examples/dbmonster/mithril/app.js b/examples/dbmonster/mithril/app.js new file mode 100644 index 00000000..88afbb58 --- /dev/null +++ b/examples/dbmonster/mithril/app.js @@ -0,0 +1,72 @@ +"use strict" + +var m = require("../../../render/hyperscript") +var render = require("../../../render/render")(window).render + +var data = [] + +var root = document.getElementById("app") +update() + +function update() { + data = ENV.generateData().toArray() + + Monitoring.renderRate.ping() + + render(root, [view()]) + + setTimeout(update, ENV.timeout) +} + +function view() { + return m("div", [ + m("table", { class: "table table-striped latest-data" }, [ + m("tbody", + data.map(function(db) { + return m("tr", {key: db.dbname}, [ + m("td", { class: "dbname" }, db.dbname), + m("td", { class: "query-count" }, [ + m("span", { class: db.lastSample.countclassName }, db.lastSample.nbQueries) + ]), + db.lastSample.topFiveQueries.map(function(query) { + return m("td", { class: query.elapsedclassName }, [ + m("span", query.formatElapsed), + m("div", { class: "popover left" }, [ + m("div", { class: "popover-content" }, query.query), + m("div", { class: "arrow" }) + ]) + ]) + }) + ]) + }) + ) + ]) + ]) +} +/* +function view() { + return m("div", [ + m("table.table.table-striped.latest-data", [ + m("tbody", + data.map(function(db) { + return m("tr", {key: db.dbname}, [ + m("td.dbname", db.dbname), + m("td.query-count", [ + m("span", { class: db.lastSample.countclassName }, db.lastSample.nbQueries) + ]), + db.lastSample.topFiveQueries.map(function(query) { + return m("td", { class: query.elapsedclassName }, [ + m("span", query.formatElapsed), + m("div.popover.left", [ + m("div.popover-content", query.query), + m("div.arrow") + ]) + ]) + }) + ]) + }) + ) + ]) + ]) +} +*/ \ No newline at end of file diff --git a/examples/dbmonster/mithril/index.html b/examples/dbmonster/mithril/index.html new file mode 100644 index 00000000..ec6c068b --- /dev/null +++ b/examples/dbmonster/mithril/index.html @@ -0,0 +1,20 @@ + + + + + + + dbmon (Mithril) + + +
+ + + + + + + + + + diff --git a/examples/dbmonster/monitor.js b/examples/dbmonster/monitor.js new file mode 100644 index 00000000..dccad19c --- /dev/null +++ b/examples/dbmonster/monitor.js @@ -0,0 +1,60 @@ +var Monitoring = Monitoring || (function() { + + var stats = new MemoryStats(); + stats.domElement.style.position = 'fixed'; + stats.domElement.style.right = '0px'; + stats.domElement.style.bottom = '0px'; + document.body.appendChild( stats.domElement ); + requestAnimationFrame(function rAFloop(){ + stats.update(); + requestAnimationFrame(rAFloop); + }); + + var RenderRate = function () { + var container = document.createElement( 'div' ); + container.id = 'stats'; + container.style.cssText = 'width:150px;opacity:0.9;cursor:pointer;position:fixed;right:80px;bottom:0px;'; + + var msDiv = document.createElement( 'div' ); + msDiv.id = 'ms'; + msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; + container.appendChild( msDiv ); + + var msText = document.createElement( 'div' ); + msText.id = 'msText'; + msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; + msText.innerHTML= 'Repaint rate: 0/sec'; + msDiv.appendChild( msText ); + + var bucketSize = 20; + var bucket = []; + var lastTime = Date.now(); + return { + domElement: container, + ping: function () { + var start = lastTime; + var stop = Date.now(); + var rate = 1000 / (stop - start); + bucket.push(rate); + if (bucket.length > bucketSize) { + bucket.shift(); + } + var sum = 0; + for (var i = 0; i < bucket.length; i++) { + sum = sum + bucket[i]; + } + msText.textContent = "Repaint rate: " + (sum / bucket.length).toFixed(2) + "/sec"; + lastTime = stop; + } + } + }; + + var renderRate = new RenderRate(); + document.body.appendChild( renderRate.domElement ); + + return { + memoryStats: stats, + renderRate: renderRate + }; + +})(); diff --git a/examples/dbmonster/react/app.js b/examples/dbmonster/react/app.js new file mode 100644 index 00000000..ad6aced1 --- /dev/null +++ b/examples/dbmonster/react/app.js @@ -0,0 +1,84 @@ +"use strict"; + +var h = React.createElement + +var Query = React.createClass({ + shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { + if (nextProps.elapsedClassName !== this.props.elapsedClassName) return true; + if (nextProps.formatElapsed !== this.props.formatElapsed) return true; + if (nextProps.query !== this.props.query) return true; + return false; + }, + render: function render() { + return h("td", { className: "Query " + this.props.elapsedClassName }, + this.props.formatElapsed, + h("div", { className: "popover left" }, + h("div", { className: "popover-content" }, this.props.query), + h("div", { className: "arrow" }) + ) + ); + } +}); + +var Database = React.createClass({ + shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { + if (nextProps.lastMutationId === this.props.lastMutationId) return false; + return true; + }, + render: function render() { + var lastSample = this.props.lastSample; + return h("tr", { key: this.props.dbname }, + h("td", { className: "dbname" }, this.props.dbname), + h("td", { className: "query-count" }, + h("span", { className: this.props.lastSample.countClassName }, this.props.lastSample.nbQueries) + ), + this.props.lastSample.topFiveQueries.map(function (query, index) { + return h(Query, { + key: index, + query: query.query, + elapsed: query.elapsed, + formatElapsed: query.formatElapsed, + elapsedClassName: query.elapsedClassName + }); + }) + ); + } +}); + +var DBMon = React.createClass({ + getInitialState: function getInitialState() { + return { + databases: [] + }; + }, + + loadSamples: function loadSamples() { + this.setState({ + databases: ENV.generateData(true).toArray() + }); + Monitoring.renderRate.ping(); + setTimeout(this.loadSamples, ENV.timeout); + }, + + componentDidMount: function componentDidMount() { + this.loadSamples(); + }, + + render: function render() { + return h("div", null, + h("table", { className: "table table-striped latest-data" }, + h("tbody", null, this.state.databases.map(function (database) { + return h(Database, { + key: database.dbname, + lastMutationId: database.lastMutationId, + dbname: database.dbname, + samples: database.samples, + lastSample: database.lastSample + }); + })) + ) + ); + } +}); + +ReactDOM.render(h(DBMon, null), document.getElementById('app')); \ No newline at end of file diff --git a/examples/dbmonster/react/index.html b/examples/dbmonster/react/index.html new file mode 100644 index 00000000..a7980043 --- /dev/null +++ b/examples/dbmonster/react/index.html @@ -0,0 +1,18 @@ + + + + + + + dbmon (React) + + +
+ + + + + + + + diff --git a/examples/dbmonster/styles.css b/examples/dbmonster/styles.css new file mode 100644 index 00000000..0ecbbbff --- /dev/null +++ b/examples/dbmonster/styles.css @@ -0,0 +1,27 @@ +body {color:#333;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;margin:0;} +label {display:inline-block;font-weight:700;margin-bottom:5px;} +input[type=range] {display:block;width:100%;} +table {border-collapse:collapse;border-spacing:0;} +:before,:after {box-sizing: border-box;} + +.table > thead > tr > th,.table > tbody > tr > th,.table > tfoot > tr > th,.table > thead > tr > td,.table > tbody > tr > td,.table > tfoot > tr > td {border-top:1px solid #ddd;line-height:1.42857143;padding:8px;vertical-align:top;} +.table {width:100%;} +.table-striped > tbody > tr:nth-child(odd) > td,.table-striped > tbody > tr:nth-child(odd) > th {background:#f9f9f9;} + +.label {border-radius:.25em;color:#fff;display:inline;font-size:75%;font-weight:700;line-height:1;padding:.2em .6em .3em;text-align:center;vertical-align:baseline;white-space:nowrap;} +.label-success {background-color:#5cb85c;} +.label-warning {background-color:#f0ad4e;} + +.popover {background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 5px 10px rgba(0,0,0,.2);display:none;left:0;max-width:276px;padding:1px;position:absolute;text-align:left;top:0;white-space:normal;z-index:1010;} +.popover>.arrow:after {border-width:10px;content:"";} +.popover.left {margin-left:-10px;} +.popover.left > .arrow {border-right-width:0;border-left-color:rgba(0,0,0,.25);margin-top:-11px;right:-11px;top:50%;} +.popover.left > .arrow:after {border-left-color:#fff;border-right-width:0;bottom:-10px;content:" ";right:1px;} +.popover > .arrow {border-width:11px;} +.popover > .arrow,.popover>.arrow:after {border-color:transparent;border-style:solid;display:block;height:0;position:absolute;width:0;} + +.popover-content {padding:9px 14px;} + +.Query {position:relative;} +.Query:hover .popover {display:block;left:-100%;width:100%;} + diff --git a/examples/svg/clock.html b/examples/svg/clock.html new file mode 100644 index 00000000..42b0c4ec --- /dev/null +++ b/examples/svg/clock.html @@ -0,0 +1,70 @@ + + + + + SVG Clock + + + +
+ + + + + + + diff --git a/examples/svg/ring.html b/examples/svg/ring.html new file mode 100644 index 00000000..4ff3c9d2 --- /dev/null +++ b/examples/svg/ring.html @@ -0,0 +1,836 @@ + + + + + SVG Ring + + + +
+ + + + + + + diff --git a/examples/svg/tiger.html b/examples/svg/tiger.html new file mode 100644 index 00000000..eb7f0c08 --- /dev/null +++ b/examples/svg/tiger.html @@ -0,0 +1,746 @@ + + + + + SVG Tiger + + + +
+ + + + + + + diff --git a/examples/threaditjs/app.js b/examples/threaditjs/app.js new file mode 100644 index 00000000..5273143d --- /dev/null +++ b/examples/threaditjs/app.js @@ -0,0 +1,195 @@ +T.time("Setup"); + +var request = require("../../request/request")(window, Promise, run).ajax +var m = require("../../render/hyperscript") +var trust = require("../../render/trust") +var render = require("../../render/render")(window, run).render +var router = require("../../router/router")(window, "#") + +//API calls +var api = { + home : function() { + T.timeEnd("Setup") + return request({ + method: "GET", + url: T.apiUrl + "/threads/", + }) + }, + thread : function(id) { + T.timeEnd("Setup") + return request({ + method: "GET", + url: T.apiUrl + "/comments/" + id, + }).then(T.transformResponse) + }, + newThread : function(text) { + return request({ + method: "POST", + url: T.apiUrl + "/threads/create", + data: {text: text}, + }); + }, + newComment : function(text, id) { + return request({ + url: T.apiUrl + "/comments/create", + method: "POST", + data: { + text: text, + parent: id, + } + }); + } +}; + +var threads = [], current = null, loaded = false, error = false, notFound = false +function loadThreads() { + loaded = false + api.home().then(function(response) { + document.title = "ThreaditJS: Mithril | Home" + threads = response.data + loaded = true + }, function() { + loaded = error = true + }) + .then(run) +} + +function loadThread(id) { + loaded = false + notFound = false + api.thread(id).then(function(response) { + document.title = "ThreaditJS: Mithril | " + T.trimTitle(response.root.text); + loaded = true + current = response + }, function(response) { + loaded = true + if (response.status === 404) notFound = true + else error = true + }) + .then(run) +} +function unloadThread() { + current = null +} + +function createThread() { + var threadText = document.getElementById("threadText") + api.newThread(threadText.value).then(function(response) { + threadText.value = ""; + threads.push(response.data); + }) + .then(run) + return false +} + +function showReplying(node) { + node.replying = true + node.newComment = "" + return false +} + +function submitComment(node) { + api.newComment(node.newComment, node.id).then(function(response) { + node.newComment = "" + node.replying = false + node.children.push(response.data) + }) + .then(run) + return false +} + +//shared +function header() { + return [ + m("p.head_links", [ + m("a[href='https://github.com/koglerjs/threaditjs/tree/master/examples/mithril']", "Source"), + " | ", + m("a[href='http://threaditjs.com']", "ThreaditJS Home"), + ]), + m("h2", [ + m("a[href='#/']", "ThreaditJS: Mithril"), + ]), + ] +} + +//home +function home() { + return {tag: "[", key: "home", attrs: {oncreate: loadThreads}, children: [ + header(), + m(".main", [ + loaded === false ? m("h2", "Loading") : + error ? m("h2", "Error! Try refreshing.") : + notFound ? m("h2", "Not found! Don't try refreshing!") : + [ + threads.map(threadListItem), + newThread(), + ] + ]) + ]} +} +function newThread() { + return m("form", {onsubmit: createThread}, [ + m("textarea#threadText"), + m("input", {type:"submit", value: "Post!"}), + ]) +} + +function threadListItem(thread) { + return [ + m("p", [ + m("a", {href: "#/thread/" + thread.id}, trust(T.trimTitle(thread.text))), + ]), + m("p.comment_count", thread.comment_count + " comment(s)"), + m("hr"), + ] +} + +//thread +function thread(args) { + if (current) T.time("Thread render") + return {tag: "[", key: args.id, attrs: {oncreate: function() {loadThread(args.id)}, onremove: unloadThread}, children: [ + header(), + current ? m(".main", {oncreate: function() {T.timeEnd("Thread render")}}, [ + threadNode({node: current.root}) + ]) : null + ]} +} +function threadNode(args) { + return m(".comment", [ + m("p", trust(args.node.text)), + m(".reply", reply(args)), + m(".children", [ + args.node.children.map(function(child) { + return threadNode({node: child}) + }) + ]) + ]) +} +function reply(args) { + return args.node.replying + ? m("form", {onsubmit: function() {return submitComment(args.node)}}, [ + m("textarea", { + value: args.node.newComment, //FIXME decouple UI state from data + oninput: function(e) { + args.node.newComment = e.target.value + }, + }), + m("input", {type:"submit", value: "Reply!"}), + m(".preview", trust(T.previewComment(args.node.newComment))), + ]) + : m("a", {onclick: function() {return showReplying(args.node)}}, "Reply!") +} + +//router +function run() { + replayRoute() +} + +var replayRoute = router.defineRoutes({ + "/thread/:id" : thread, + "/" : home +}, function(view, args) { + render(document.body, [view(args)]) +}, function() { + router.setPath("/") +}) diff --git a/examples/threaditjs/colors.css b/examples/threaditjs/colors.css new file mode 100644 index 00000000..e5716963 --- /dev/null +++ b/examples/threaditjs/colors.css @@ -0,0 +1,6 @@ +html {background-color:#f5f5f5;} + +a {color:#161;text-decoration:none;} +a:hover {text-decoration:underline;} + +input[type=submit] {background-color:#5A5;color:#fff;border:0;font-weight:bold;} \ No newline at end of file diff --git a/examples/threaditjs/index.html b/examples/threaditjs/index.html new file mode 100644 index 00000000..e51ee771 --- /dev/null +++ b/examples/threaditjs/index.html @@ -0,0 +1,23 @@ + + + + + Mithril • ThreadIt.js + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/todomvc/app.css b/examples/todomvc/app.css new file mode 100644 index 00000000..54d89abd --- /dev/null +++ b/examples/todomvc/app.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +#todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +#todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +#todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +#new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +#main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +#toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +#toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +#toggle-all:checked:before { + color: #737373; +} + +#todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +#todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +#todo-list li:last-child { + border-bottom: none; +} + +#todo-list li.editing { + border-bottom: none; + padding: 0; +} + +#todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +#todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +#todo-list li label { + white-space: pre; + word-break: break-word; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +#todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +#todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +#todo-list li .destroy:hover { + color: #af5b5e; +} + +#todo-list li .destroy:after { + content: '×'; +} + +#todo-list li:hover .destroy { + display: block; +} + +#todo-list li .edit { + display: none; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +#todo-count { + float: left; + text-align: left; +} + +#todo-count strong { + font-weight: 300; +} + +#filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +#filters li { + display: inline; +} + +#filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +#filters li a.selected, +#filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +#filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +#clear-completed, +html #clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +#clear-completed:hover { + text-decoration: underline; +} + +#info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +#info p { + line-height: 1; +} + +#info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +#info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + #toggle-all, + #todo-list li .toggle { + background: none; + } + + #todo-list li .toggle { + height: 40px; + } + + #toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + #footer { + height: 50px; + } + + #filters { + bottom: 10px; + } +} diff --git a/examples/todomvc/base.css b/examples/todomvc/base.css new file mode 100644 index 00000000..da65968a --- /dev/null +++ b/examples/todomvc/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/examples/todomvc/index.html b/examples/todomvc/index.html new file mode 100644 index 00000000..81cb2b5b --- /dev/null +++ b/examples/todomvc/index.html @@ -0,0 +1,24 @@ + + + + + Mithril • TodoMVC + + + + +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/examples/todomvc/todomvc.js b/examples/todomvc/todomvc.js new file mode 100644 index 00000000..78a646ec --- /dev/null +++ b/examples/todomvc/todomvc.js @@ -0,0 +1,144 @@ +var m = require("../../render/hyperscript") +var render = require("../../render/render")(window, run).render +var router = require("../../router/router")(window, "#") + +//model +var todos = loadData() +var editing = null +var showing = "all" + +function loadData() { + return JSON.parse(localStorage["todos-mithril"] || "[]") +} +function saveData() { + localStorage["todos-mithril"] = JSON.stringify(todos) +} + +function createTodo(title) { + todos.push({title: title.trim(), completed: false}) +} +function setStatuses(completed) { + for (var i = 0; i < todos.length; i++) todos[i].completed = completed +} +function setStatus(todo, completed) { + todo.completed = completed +} +function todosByStatus(todo) { + switch (showing) { + case "all": return true + case "active": return !todo.completed + case "completed": return todo.completed + } +} +function destroy(todo) { + var index = todos.indexOf(todo) + if (index > -1) todos.splice(index, 1) +} +function countRemaining() { + return todos.filter(function(todo) {return !todo.completed}).length +} +function clear() { + for (var i = 0; i < todos.length; i++) { + if (todos[i].completed) destroy(todos[i]) + } +} + +function edit(todo) { + editing = todo +} +function update(title) { + editing.title = title.trim() + if (editing.title === "") destroy(editing) + editing = null +} +function reset() { + editing = null +} + +function setFilter(filter) { + showing = filter +} + +//view +function add(e) { + if (e.keyCode === 13) { + createTodo(this.value) + this.value = "" + } +} +function toggleAll() { + setStatuses(document.getElementById("toggle-all").checked) +} +function toggle(todo) { + setStatus(todo, !todo.completed) +} +function focus(vnode, todo) { + if (todo === editing && vnode.dom !== document.activeElement) { + vnode.dom.value = todo.title + vnode.dom.focus() + vnode.dom.selectionStart = vnode.dom.selectionEnd = todo.title.length + } +} +function save(e) { + if (e.keyCode === 13 || e.type === "blur") update(this.value) + else if (e.keyCode === 27) reset() +} + +function view() { + var remaining = countRemaining() + return [ + m("header.header", [ + m("h1", "todos"), + m("input#new-todo[placeholder='What needs to be done?'][autofocus]", {onkeypress: add}), + ]), + m("section#main", {style: {display: todos.length > 0 ? "" : "none"}}, [ + m("input#toggle-all[type='checkbox']", {checked: remaining === 0, onclick: toggleAll}), + m("label[for='toggle-all']", {onclick: toggleAll}, "Mark all as complete"), + m("ul#todo-list", [ + todos.filter(todosByStatus).map(function(todo) { + return m("li", {class: (todo.completed ? "completed" : "") + " " + (todo === editing ? "editing" : "")}, [ + m(".view", [ + m("input.toggle[type='checkbox']", {checked: todo.completed, onclick: function() {toggle(todo)}}), + m("label", {ondblclick: function() {edit(todo)}}, todo.title), + m("button.destroy", {onclick: function() {destroy(todo)}}), + ]), + m("input.edit", {onupdate: function(vnode) {focus(vnode, todo)}, onkeypress: save, onblur: save}) + ]) + }), + ]), + ]), + todos.length ? m("footer#footer", [ + m("span#todo-count", [ + m("strong", remaining), + remaining === 1 ? " item left" : " items left", + ]), + m("ul#filters", [ + m("li", m("a[href='#/']", {class: showing === "all" ? "selected" : ""}, "All")), + m("li", m("a[href='#/active']", {class: showing === "active" ? "selected" : ""}, "Active")), + m("li", m("a[href='#/completed']", {class: showing === "completed" ? "selected" : ""}, "Completed")), + ]), + m("button#clear-completed", {onclick: clear}, "Clear completed"), + ]) : null, + ] +} + +var root = document.getElementById("todoapp") +var raf +function run() { + cancelAnimationFrame(raf) + raf = requestAnimationFrame(function() { + saveData() + render(root, view()) + }) +} + +router.defineRoutes({ + "/": "all", + "/active": "active", + "/completed": "completed", +}, function(filter) { + setFilter(filter) + run() +}, function() { + router.setPath("/") +}) diff --git a/index.js b/index.js new file mode 100644 index 00000000..2581d04b --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ +"use strict" + +//TODO expose properly +var Router = require("./router/router") +var Request = require("./request/request") +var Render = require("./render/render") +var m = require("./render/hyperscript") +var trust = require("./render/trust") \ No newline at end of file diff --git a/module/README.md b/module/README.md new file mode 100644 index 00000000..473391f4 --- /dev/null +++ b/module/README.md @@ -0,0 +1,64 @@ +# module.js + +[About](#about) | [Usage](#usage) | [API](#api) | [Goals](#goals) + +CommonJS module API polyfill for browsers + +Version: 1.0 +License: MIT + +## About + +- ~30 LOC +- allows running CommonJS modules in browsers without the need for a compile-time bundler like browserify or webpack +- doesn't separate module code into separate environment contexts, so it makes it possible to test private APIs + +### Caveats + +- pollutes global scope, thus making name collisions possible +- doesn't support out-of-order execution, so script tags for dependencies must appear before the modules that use them +- doesn't support circular dependencies +- runs code for all declared modules, regardless of whether they were `require`d +- only supports relative paths in `require` calls (i.e. won't resolve anything from `node_modules`) +- only supports files with `.js` extension + +## Usage + +```html + + + +``` + +```javascript +//file: bar.js +var foo = require("./foo") + +module.exports = {bar: "baz"} +``` + +--- + +## API + +### `any require(String module)` + +Imports a module. `module` should be the relative path of the module's javascript file, minus the '.js' extension + +--- + +### `any module.exports` + +A getter-setter. Assign to this to register a module for exporting + +--- + +## Goals + +The best code is no code at all. + +This micro-library exists to support a coding style that aims to achieve systematic terseness (in other words, a "less-is-more" philosophy). + +Browsers run javascript in a global environment that spans multiple files. Node.js runs each javascript file in a self-contained environment, which can communicate with other files via its CommonJS module API. Node.js' modular quality is desirable for writing scalable codebases, and we want to be able to use its module API both in the browser and in the server, with the ultimate goal of writing code that can run unmodified in both. Ideally this should also be possible without requiring a lot of extra machinery. + +Webpack and Browserify are commonly used solutions to solve this issue, but both require a large number of dependencies and a compilation/file watching tool. This micro-library provides an incomplete but usable subset of features of the CommonJS module API in order to aid in development tasks. The plan is to use Node.js' full CommonJS support for production-related tasks, and use this micro-library to run the same source code in-browser for development/testing tasks. diff --git a/module/module.js b/module/module.js new file mode 100644 index 00000000..44ee4764 --- /dev/null +++ b/module/module.js @@ -0,0 +1,29 @@ +"use strict" + +require.$$modules = {} +require.$$current = function() { + var href = window.location.href + var searchIndex = href.indexOf("?") + var hashIndex = href.indexOf("#") + var pathnameEnd = searchIndex > -1 ? searchIndex : hashIndex > -1 ? hashIndex : href.length + try {throw new Error} catch (e) {var error = e} + var src = error.stack.match(/^(?:(?!^Error|\/module\/module\.js).)*$/m).toString().match(/((?:file|https|http):\/\/.+)(?::\d+){2}/)[1] || href.slice(0, pathnameEnd) + var base = href.slice(0, href.lastIndexOf("/", pathnameEnd) + 1) + return src.replace(/\.js$/, "") +} + +var module = { + get exports() {return require.$$modules[require.$$current()]}, + set exports(value) {require.$$modules[require.$$current()] = value}, +} + +function require(name) { + var relative = require.$$current() + var slashIndex = relative.lastIndexOf("/") + var path = slashIndex > -1 ? relative.slice(0, slashIndex + 1) : "./" + var absolute = (path + name).replace(/\/\.\//g, "/") + var dotdot = /\/[^\/]+?\/\.{2}/ + while (dotdot.test(absolute)) absolute = absolute.replace(dotdot, "") + if (absolute in require.$$modules) return require.$$modules[absolute] + else throw new Error("Module does not exist: " + absolute) +} \ No newline at end of file diff --git a/ospec/README.md b/ospec/README.md new file mode 100644 index 00000000..9607f47a --- /dev/null +++ b/ospec/README.md @@ -0,0 +1,389 @@ +# ospec + +[About](#about) | [Usage](#usage) | [API](#api) | [Goals](#goals) + +Noiseless testing framework + +Version: 1.0 +License: MIT + +## About + +- ~180 LOC +- terser and faster test code than with mocha, jasmine or tape +- test code reads like bullet points +- assertion code follows [SVO](https://en.wikipedia.org/wiki/Subject–verb–object) structure in present tense for terseness and readability +- supports: + - test grouping + - assertions + - spies + - `equals`, `notEquals`, `deepEquals` and `notDeepEquals` assertion types + - `before`/`after`/`beforeEach`/`afterEach` hooks + - test exclusivity (i.e. `.only`) + - async tests and hooks +- explicitly disallows test-space configuration to encourage focus on testing, and to provide uniform test suites across projects + +## Usage + +### Single tests + +Both tests and assertions are declared via the `o` function. Tests should have a description and a body function. A test may have one or more assertions. Assertions should appear inside a test's body function and compare two values. + +```javascript +var o = require("ospec") + +o("addition", function() { + o(1 + 1).equals(2) +}) +o("subtraction", function() { + o(1 - 1).notEquals(2) +}) +``` + +Assertions may have descriptions: + +```javascript +o("addition", function() { + o(1 + 1).equals(2)("addition should work") + + /* in ES6, the following syntax is also possible + o(1 + 1).equals(2) `addition should work` + */ +}) +/* for a failing test, an assertion with a description outputs this: + +addition should work + +1 should equal 2 + +Error + at stacktrace/goes/here.js:1:1 +*/ +``` + +### Grouping tests + +Tests may be organized into logical groups using `o.spec` + +```javascript +o.spec("math", function() { + o("addition", function() { + o(1 + 1).equals(2) + }) + o("subtraction", function() { + o(1 - 1).notEquals(2) + }) +}) +``` + +Group names appear as a breadcrumb trail in test descriptions: `math > addition: 2 should equal 2` + +### Nested test groups + +Groups can be nested to further organize test groups. Note that tests cannot be nested inside other tests. + +```javascript +o.spec("math", function() { + o.spec("arithmetics", function() { + o("addition", function() { + o(1 + 1).equals(2) + }) + o("subtraction", function() { + o(1 - 1).notEquals(2) + }) + }) +}) +``` + +### Callback test + +The `o.spy()` method can be used to create a stub function that keeps track of its call count and received parameters + +```javascript +//code to be tested +function call(cb, arg) {cb(arg)} + +//test suite +var o = require("ospec") + +o.spec("call()", function() { + o("works", function() { + var spy = o.spy() + call(spy, 1) + + o(spy.callCount).equals(1) + o(spy.args[0]).equals(1) + }) +}) +``` + +### Asynchronous tests + +If a test body function declares a named argument, the test is assumed to be asynchronous, and the argument is a function that must be called exactly one time to signal that the test has completed. As a matter of convention, this argument is typically named `done`. + +```javascript +o("setTimeout calls callback", function(done) { + setTimeout(done, 10) +}) +``` + +By default, asynchronous tests time out after 20ms. This can be changed on a per-test basis using the `timeout` argument: + +```javascript +o("setTimeout calls callback", function(done, timeout) { + timeout(50) //wait 50ms before bailing out of the test + + setTimeout(done, 30) +}) +``` + +Note that the `timeout` function call must be the first statement in its test. + +Asynchronous tests generate an assertion that succeeds upon calling `done` or fails on timeout with the error message `async test timed out`. + +### `before`, `after`, `beforeEach`, `afterEach` hooks + +These hooks can be declared when it's necessary to setup and clean up state for a test or group of tests. The `before` and `after` hooks run once each per test group, whereas the `beforeEach` and `afterEach` hooks run for every test. + +```javascript +o.spec("math", function() { + var acc + o.beforeEach(function() { + acc = 0 + }) + + o("addition", function() { + acc += 1 + + o(acc).equals(1) + }) + o("subtraction", function() { + acc -= 1 + + o(acc).equals(-1) + }) +}) +``` + +It's strongly recommended to ensure that `beforeEach` hooks always overwrite all shared variables, and avoid `if/else` logic, memoization, undo routines inside `beforeEach` hooks. + +### Asynchronous hooks + +Like tests, hooks can also be asynchronous. Tests that are affected by asynchronous hooks will wait for the hooks to complete before running. + +```javascript +o.spec("math", function() { + var acc + o.beforeEach(function(done) { + setTimeout(function() { + acc = 0 + done() + }) + }) + + //tests only run after async hooks complete + o("addition", function() { + acc += 1 + + o(acc).equals(1) + }) + o("subtraction", function() { + acc -= 1 + + o(acc).equals(-1) + }) +}) +``` + +### Running only one test + +A test can be temporarily made to run exclusively by calling `o.only()` instead of `o`. This is useful when troubleshooting regressions, to zero-in on a failing test, and to avoid saturating console log w/ irrelevant debug information. + +```javascript +o.spec("math", function() { + o("addition", function() { + o(1 + 1).equals(2) + }) + + //only this test will be run, regardless of how many groups there are + o.only("subtraction", function() { + o(1 - 1).notEquals(2) + }) +}) +``` + +### Running the test suite + +```javascript +//define a test +o("addition", function() { + o(1 + 1).equals(2) +}) + +//run the suite +o.run() +``` + +### Running test suites concurrently + +The `o.new()` method can be used to create new instances of ospec, which can be run in parallel. Note that each instance will report independently, and there's no aggregation of results. + +```javascript +var _o = o.new() +_o("a test", function() { + _o(1).equals(1) +}) +_o.run() +``` + +--- + +## API + +*Square brackets denote optional arguments + +### void o.spec(String title, Function tests) + +Defines a group of tests. Groups are optional + +--- + +### void o(String title, Function([Function done [, Function timeout]]) assertions) + +Defines a test. + +If an argument is defined for the `assertions` function, the test is deemed to be asynchronous, and the argument is required to be called exactly one time. + +--- + +### Assertion o(any value) + +Starts an assertion. There are four types of assertion: `equals`, `notEquals`, `deepEquals` and `notDeepEquals`. + +Assertions have this form: + +``` +o(actualValue).equals(expectedValue) +``` + +As a matter of convention, the actual value should be the first argument and the expected value should be the second argument in an assertion. + +Assertions can also accept an optional description curried parameter: + +``` +o(actualValue).equals(expectedValue)("this is a description for this assertion") +``` + +Assertion descriptions can be simplified using ES6 tagged template string syntax: + +``` +o(actualValue).equals(expectedValue) `this is a description for this assertion` +``` + +#### Function(String description) o(any value).equals(any value) + +Asserts that two values are strictly equal (`===`) + +#### Function(String description) o(any value).notEquals(any value) + +Asserts that two values are strictly not equal (`!==`) + +#### Function(String description) o(any value).deepEquals(any value) + +Asserts that two values are recursively equal + +#### Function(String description) o(any value).notDeepEquals(any value) + +Asserts that two values are not recursively equal + +--- + +### void o.before(Function([Function done [, Function timeout]]) setup) + +Defines code to be run at the beginning of a test group + +If an argument is defined for the `setup` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. + +--- + +### void o.after(Function([Function done [, Function timeout]]) teardown) + +Defines code to be run at the end of a test group + +If an argument is defined for the `teardown` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. + +--- + +### void o.beforeEach(Function([Function done [, Function timeout]]) setup) + +Defines code to be run before each test in a group + +If an argument is defined for the `setup` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. + +--- + +### void o.afterEach(Function([Function done [, Function timeout]]) teardown) + +Defines code to be run after each test in a group + +If an argument is defined for the `teardown` function, this hook is deemed to be asynchronous, and the argument is required to be called exactly one time. + +--- + +### void o.only(String title, Function([Function done [, Function timeout]]) assertions) + +Declares that only a single test should be run, instead of all of them + +--- + +### Function o.spy() + +Returns a function that records the number of times it gets called, and its arguments + +#### Number o.spy().callCount + +The number of times the function has been called + +#### Array o.spy().args + +The arguments that were passed to the function in the last time it was called + +--- + +### void o.run() + +Runs the test suite + +--- + +### Function o.new() + +Returns a new instance of ospec. Useful if you want to run more than one test suite concurrently + +```javascript +var $o = o.new() +$o("a test", function() { + $o(1).equals(1) +}) +$o.run() +``` + +--- + +## Goals + +- Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies +- Disallow configuration in test-space: + - Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) + - Disallow ability to pick between different reporters + - Disallow ability to add custom assertion types +- Make assertion code terse, readable and self-descriptive +- Have as few assertion types as possible for a workable usage pattern + +Explicitly disallowing modularity and configuration in test-space has a few benefits: + +- tests always look the same, even across different projects and teams +- single source of documentation for entire testing API +- no need to hunt down plugins to figure out what they do, especially if they replace common javascript idioms with fuzzy spoken language constructs (e.g. what does `.is()` do?) +- no need to pollute project-space with ad-hoc configuration code +- discourages side-tracking and yak-shaving diff --git a/ospec/bin/ospec b/ospec/bin/ospec new file mode 100644 index 00000000..8d8bdd31 --- /dev/null +++ b/ospec/bin/ospec @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +var fs = require("fs") +var path = require("path") + +var o = require("../ospec") + +function traverseDirectory(pathname, callback) { + pathname = pathname.replace(/\\/g, "/") + return new Promise(function(resolve, reject) { + fs.lstat(pathname, function(err, stat) { + if (err) reject(err) + if (stat.isDirectory()) { + fs.readdir(pathname, function(err, pathnames) { + if (err) reject(err) + var promises = [] + for (var i = 0; i < pathnames.length; i++) { + pathnames[i] = path.join(pathname, pathnames[i]) + promises.push(traverseDirectory(pathnames[i], callback)) + } + callback(pathname, stat, pathnames) + resolve(Promise.all(promises)) + }) + } + else { + callback(pathname, stat) + resolve(pathname) + } + }) + }) +} + +traverseDirectory(".", function(pathname, stat, children) { + if (pathname.indexOf("node_modules") > -1) return + if (pathname.match(/(?:^|\/)tests\/.*\.js$/)) { + require("../../" + pathname) + } +}) +.then(o.run) +.catch(function(e) { + console.log(e.stack) +}) + +process.on("unhandledRejection", function(e) { + console.log("Uncaught (in promise) " + e.stack) +}) \ No newline at end of file diff --git a/ospec/bin/ospec.cmd b/ospec/bin/ospec.cmd new file mode 100644 index 00000000..fc8248a2 --- /dev/null +++ b/ospec/bin/ospec.cmd @@ -0,0 +1 @@ +node cli \ No newline at end of file diff --git a/ospec/ospec.js b/ospec/ospec.js new file mode 100644 index 00000000..39ac3a00 --- /dev/null +++ b/ospec/ospec.js @@ -0,0 +1,187 @@ +"use strict" + +module.exports = new function init() { + var spec = {}, subjects = [], results = [], only = null, ctx = spec, start, stack = 0, hasProcess = typeof process === "object" + + function o(subject, predicate) { + ctx[subject] = predicate + if (predicate === undefined) return new Assert(subject) + } + o.before = hook("__before") + o.after = hook("__after") + o.beforeEach = hook("__beforeEach") + o.afterEach = hook("__afterEach") + o.new = init + o.spec = function(subject, predicate) { + var parent = ctx + ctx = ctx[subject] = {} + predicate() + ctx = parent + } + o.only = function(subject, predicate) {o(subject, only = predicate)} + o.spy = function() { + var spy = function() { + spy.this = this + spy.args = [].slice.call(arguments) + spy.callCount++ + } + spy.args = [] + spy.callCount = 0 + return spy + } + o.run = function() { + start = new Date + test(spec, [], [], report) + + function test(spec, pre, post, finalize) { + pre = [].concat(pre, spec["__beforeEach"] || []) + post = [].concat(spec["__afterEach"] || [], post) + series([].concat(spec["__before"] || [], Object.keys(spec).map(function(key) { + return function(done, timeout) { + timeout(Infinity) + + if (key.slice(0, 2) === "__") return done() + if (only !== null && spec[key] !== only && typeof only === typeof spec[key]) return done() + subjects.push(key) + var type = typeof spec[key] + if (type === "object") test(spec[key], pre, post, pop) + if (type === "function") series([].concat(pre, spec[key], post, pop)) + + function pop() { + subjects.pop() + done() + } + } + }), spec["__after"] || [], finalize)) + } + + function series(fns) { + var cursor = 0 + next() + + function next() { + stack++ + if (cursor === fns.length) return + + var fn = fns[cursor++] + if (fn.length > 0) { + var timeout = 0, delay = 20, s = new Date + var isDone = false + var body = fn.toString() + var arg = (body.match(/\(([\w_$]+)/) || body.match(/([\w_$]+)\s*=>/) || []).pop() + if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once") + fn(function done() { + if (timeout !== undefined) { + timeout = clearTimeout(timeout) + if (delay !== Infinity) record(null) + if (!isDone) next() + else throw new Error("`" + arg + "()` should only be called once") + isDone = true + } + else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms") + }, function(t) {delay = t}) + if (timeout === 0) { + timeout = setTimeout(function() { + timeout = undefined + record("async test timed out") + next() + }, Math.min(delay, 2147483647)) + } + } + else { + fn() + if (stack < 5000) next() + else (hasProcess ? process.nextTick : setTimeout)(next, stack = 0) + } + } + } + } + function hook(name) { + return function(predicate) { + if (ctx[name]) throw new Error("This hook should be defined outside of a loop or inside a nested test group:\n" + predicate) + ctx[name] = predicate + } + } + + define("equals", "should equal", function(a, b) {return a === b}) + define("notEquals", "should not equal", function(a, b) {return a !== b}) + define("deepEquals", "should deep equal", deepEqual) + define("notDeepEquals", "should not deep equal", function(a, b) {return !deepEqual(a, b)}) + + function isArguments(a) { + if ("callee" in a) { + for (var i in a) if (i === "callee") return false + return true + } + } + function deepEqual(a, b) { + if (a === b) return true + if (a === null ^ b === null || a === undefined ^ b === undefined) return false + if (typeof a === "object" && typeof b === "object") { + var aIsArgs = isArguments(a), bIsArgs = isArguments(b) + if (a.constructor === Object && b.constructor === Object && !aIsArgs && !bIsArgs) { + for (var i in a) { + if (!deepEqual(a[i], b[i])) return false + } + for (var i in b) { + if (!(i in a)) return false + } + return true + } + if (a.length === b.length && (a instanceof Array && b instanceof Array || aIsArgs && bIsArgs)) { + for (var i = 0; i < a.length; i++) { + if (!deepEqual(a[i], b[i])) return false + } + return true + } + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime() + if (typeof Buffer === "function" && a instanceof Buffer && b instanceof Buffer) { + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true + } + if (a.valueOf() === b.valueOf()) return true + } + return false + } + + function Assert(value) {this.value = value} + function define(name, verb, compare) { + Assert.prototype[name] = function assert(value) { + if (compare(this.value, value)) record(null) + else record(serialize(this.value) + " " + verb + " " + serialize(value)) + return function(message) { + var result = results[results.length - 1] + result.message = message + "\n\n" + result.message + } + } + } + function record(message) { + var result = {pass: message === null} + if (result.pass === false) { + var error = new Error + if (error.stack === undefined) new function() {try {throw error} catch (e) {error = e}} + result.context = subjects.join(" > ") + result.message = message + result.error = error.stack + } + results.push(result) + } + function serialize(value) { + if (value === null || typeof value === "object") return String(value) + try {return JSON.stringify(value)} catch (e) {return String(value)} + } + function highlight(message) { + return hasProcess ? "\x1b[31m" + message + "\x1b[0m" : "%c" + message + "%c " + } + + function report() { + for (var i = 0, r; r = results[i]; i++) { + if (!r.pass) console.info(r.context + ": " + highlight(r.message) + "\n\n" + r.error.match(/^(?:(?!^Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + "\n\n", hasProcess ? "" : "color:red", hasProcess ? "" : "color:black") + } + console.log(results.length + " tests completed in " + Math.round(new Date - start) + "ms") + } + + return o +} \ No newline at end of file diff --git a/ospec/tests/index.html b/ospec/tests/index.html new file mode 100644 index 00000000..9b37de8b --- /dev/null +++ b/ospec/tests/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js new file mode 100644 index 00000000..374ff452 --- /dev/null +++ b/ospec/tests/test-ospec.js @@ -0,0 +1,94 @@ +"use strict" + +var callAsync = require("../../test-utils/callAsync") +var o = require("../ospec") + +new function(o) { + o = o.new() + + o.spec("ospec", function() { + o("skipped", function() { + o(1).equals(1) + }) + o.only(".only()", function() { + o(2).equals(2) + }) + }) + + o.run() +}(o) + +o.spec("ospec", function() { + o.spec("sync", function() { + var a = 0, b = 0 + + o.before(function test() {a = 1}) + o.after(function test() {a = 0}) + + o.beforeEach(function test() {b = 1}) + o.afterEach(function test() {b = 0}) + + o("assertions", function test() { + var spy = o.spy() + spy(a) + + o(a).equals(b) + o(a).notEquals(2) + o({a: [1, 2], b: 3}).deepEquals({a: [1, 2], b: 3}) + o([{a: 1, b: 2}, {c: 3}]).deepEquals([{a: 1, b: 2}, {c: 3}]) + + var values = ["a", "", 1, 0, true, false, null, undefined, Date(0), ["a"], [], function() {return arguments}.call(), new Uint8Array(), {a: 1}, {}] + for (var i = 0; i < values.length; i++) { + for (var j = 0; j < values.length; j++) { + if (i === j) o(values[i]).deepEquals(values[j]) + else o(values[i]).notDeepEquals(values[j]) + } + } + + o(spy.callCount).equals(1) + o(spy.args.length).equals(1) + o(spy.args[0]).equals(1) + }) + }) + o.spec("async", function() { + var a = 0, b = 0 + + o.before(function test(done) { + callAsync(function() { + a = 1 + done() + }) + }) + o.after(function test(done) { + callAsync(function() { + a = 0 + done() + }) + }) + + o.beforeEach(function test(done) { + callAsync(function() { + b = 1 + done() + }) + }) + o.afterEach(function test(done) { + callAsync(function() { + b = 0 + done() + }) + }) + + o("async hooks", function test(done) { + callAsync(function() { + var spy = o.spy() + spy(a) + + o(a).equals(b) + o(a).equals(1)("a and b should be initialized") + + done() + }) + }) + }) +}) diff --git a/package.json b/package.json new file mode 100644 index 00000000..d45f1b55 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "mithril", + "version": "1.0.0", + "description": "A framework for building brilliant applications", + "main": "index.js", + "scripts": { + "test": "node ospec/bin/ospec" + }, + "author": "Leo Horie", + "license": "MIT" +} diff --git a/querystring/build.js b/querystring/build.js new file mode 100644 index 00000000..6b8c1c0e --- /dev/null +++ b/querystring/build.js @@ -0,0 +1,25 @@ +"use strict" + +module.exports = function buildQueryString(object) { + if (Object.prototype.toString.call(object) !== "[object Object]") return "" + + var args = [] + for (var key in object) { + destructure(key, object[key]) + } + return args.join("&") + + function destructure(key, value) { + if (value instanceof Array) { + for (var i = 0; i < value.length; i++) { + destructure(key + "[" + i + "]", value[i]) + } + } + else if (Object.prototype.toString.call(value) === "[object Object]") { + for (var i in value) { + destructure(key + "[" + i + "]", value[i]) + } + } + else args.push(encodeURIComponent(key) + "=" + (value != null ? encodeURIComponent(value) : "")) + } +} \ No newline at end of file diff --git a/querystring/parse.js b/querystring/parse.js new file mode 100644 index 00000000..8bfc42ca --- /dev/null +++ b/querystring/parse.js @@ -0,0 +1,42 @@ +"use strict" + +module.exports = function parseQueryString(string) { + if (string === "" || string == null) return {} + if (string.charAt(0) === "?") string = string.slice(1) + + var entries = string.split("&"), data = {}, counters = {} + for (var i = 0; i < entries.length; i++) { + var entry = entries[i].split("=") + var key = decodeURIComponent(entry[0]) + var value = decodeURIComponent(entry[1]) + + //TODO refactor out + var number = Number(value) + if (value !== "" && !isNaN(number) || value === "NaN") value = number + else if (value === "true") value = true + else if (value === "false") value = false + else { + var date = new Date(value) + if (!isNaN(date.getTime())) value = date + } + + var levels = key.split(/\]\[?|\[/) + var cursor = data + if (key.indexOf("[") > -1) levels.pop() + for (var j = 0; j < levels.length; j++) { + var level = levels[j], nextLevel = levels[j + 1] + var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel)) + var isValue = j === levels.length - 1 + if (level === "") { + var key = levels.slice(0, j).join() + if (counters[key] == null) counters[key] = 0 + level = counters[key]++ + } + if (cursor[level] == null) { + cursor[level] = isValue ? value : isNumber ? [] : {} + } + cursor = cursor[level] + } + } + return data +} \ No newline at end of file diff --git a/querystring/tests/index.html b/querystring/tests/index.html new file mode 100644 index 00000000..d1058f33 --- /dev/null +++ b/querystring/tests/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/querystring/tests/test-buildQueryString.js b/querystring/tests/test-buildQueryString.js new file mode 100644 index 00000000..17f935fd --- /dev/null +++ b/querystring/tests/test-buildQueryString.js @@ -0,0 +1,67 @@ +"use strict" + +var o = require("../../ospec/ospec") +var buildQueryString = require("../../querystring/build") + +o.spec("buildQueryString", function() { + o("builds from flat object", function() { + var string = buildQueryString({a: "b", c: 1}) + + o(string).equals("a=b&c=1") + }) + o("builds from nested object", function() { + var string = buildQueryString({a: {b: 1, c: 2}}) + + o(string).equals("a%5Bb%5D=1&a%5Bc%5D=2") + }) + o("builds from deep nested object", function() { + var string = buildQueryString({a: {b: {c: 1, d: 2}}}) + + o(string).equals("a%5Bb%5D%5Bc%5D=1&a%5Bb%5D%5Bd%5D=2") + }) + o("builds from nested array", function() { + var string = buildQueryString({a: ["x", "y"]}) + + o(string).equals("a%5B0%5D=x&a%5B1%5D=y") + }) + o("builds from deep nested array", function() { + var string = buildQueryString({a: [["x", "y"]]}) + + o(string).equals("a%5B0%5D%5B0%5D=x&a%5B0%5D%5B1%5D=y") + }) + o("builds from deep nested array in object", function() { + var string = buildQueryString({a: {b: ["x", "y"]}}) + + o(string).equals("a%5Bb%5D%5B0%5D=x&a%5Bb%5D%5B1%5D=y") + }) + o("builds from deep nested object in array", function() { + var string = buildQueryString({a: [{b: 1, c: 2}]}) + + o(string).equals("a%5B0%5D%5Bb%5D=1&a%5B0%5D%5Bc%5D=2") + }) + o("builds date", function() { + var string = buildQueryString({a: new Date(0)}) + + o(string).equals("a=" + encodeURIComponent(new Date(0).toString())) + }) + o("builds null into empty string (like jQuery)", function() { + var string = buildQueryString({a: null}) + + o(string).equals("a=") + }) + o("builds undefined into empty string (like jQuery)", function() { + var string = buildQueryString({a: undefined}) + + o(string).equals("a=") + }) + o("builds zero", function() { + var string = buildQueryString({a: 0}) + + o(string).equals("a=0") + }) + o("builds false", function() { + var string = buildQueryString({a: false}) + + o(string).equals("a=false") + }) +}) diff --git a/querystring/tests/test-parseQueryString.js b/querystring/tests/test-parseQueryString.js new file mode 100644 index 00000000..2fe95add --- /dev/null +++ b/querystring/tests/test-parseQueryString.js @@ -0,0 +1,74 @@ +"use strict" + +var o = require("../../ospec/ospec") +var parseQueryString = require("../../querystring/parse") + +o.spec("parseQueryString", function() { + o("works", function() { + var data = parseQueryString("?aaa=bbb") + o(data).deepEquals({aaa: "bbb"}) + }) + o("parses flat object", function() { + var data = parseQueryString("?a=b&c=d") + o(data).deepEquals({a: "b", c: "d"}) + }) + o("parses without question mark", function() { + var data = parseQueryString("a=b&c=d") + o(data).deepEquals({a: "b", c: "d"}) + }) + o("parses nested object", function() { + var data = parseQueryString("a[b]=x&a[c]=y") + o(data).deepEquals({a: {b: "x", c: "y"}}) + }) + o("parses deep nested object", function() { + var data = parseQueryString("a[b][c]=x&a[b][d]=y") + o(data).deepEquals({a: {b: {c: "x", d: "y"}}}) + }) + o("parses nested array", function() { + var data = parseQueryString("a[0]=x&a[1]=y") + o(data).deepEquals({a: ["x", "y"]}) + }) + o("parses deep nested array", function() { + var data = parseQueryString("a[0][0]=x&a[0][1]=y") + o(data).deepEquals({a: [["x", "y"]]}) + }) + o("parses deep nested object in array", function() { + var data = parseQueryString("a[0][c]=x&a[0][d]=y") + o(data).deepEquals({a: [{c: "x", d: "y"}]}) + }) + o("parses deep nested array in object", function() { + var data = parseQueryString("a[b][0]=x&a[b][1]=y") + o(data).deepEquals({a: {b: ["x", "y"]}}) + }) + o("parses array without index", function() { + var data = parseQueryString("a[]=x&a[]=y&b[]=w&b[]=z") + o(data).deepEquals({a: ["x", "y"], b: ["w", "z"]}) + }) + /*TODO remove since build generates a[0]=b syntax + o("generates array for duplicate items", function() { + var data = parseQueryString("a=b&a=c&a=d") + o(data).deepEquals({a: ["b", "c", "d"]}) + }) + */ + o("casts booleans", function() { + var data = parseQueryString("a=true&b=false") + o(data).deepEquals({a: true, b: false}) + }) + o("casts numbers", function() { + var data = parseQueryString("a=1&b=-2.3&c=0x10&d=1e2&e=Infinity") + o(data).deepEquals({a: 1, b: -2.3, c: 16, d: 100, e: Infinity}) + }) + o("casts NaN", function() { + var data = parseQueryString("a=NaN") + o(isNaN(data.a)).equals(true) + }) + o("casts Date", function() { + var data = parseQueryString("a=" + new Date(0)) + o(data.a instanceof Date).equals(true) + o(data.a.getTime()).equals(0) + }) + o("does not cast empty string to number", function() { + var data = parseQueryString("a=") + o(data).deepEquals({a: ""}) + }) +}) diff --git a/render/hyperscript.js b/render/hyperscript.js new file mode 100644 index 00000000..331777d0 --- /dev/null +++ b/render/hyperscript.js @@ -0,0 +1,75 @@ +"use strict" + +var normalizeChildren = require("../render/normalizeChildren") + +var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:\s*=\s*("|'|)(.*?)\2)?\]/ +var selectorCache = {} +function hyperscript(selector) { + if (selectorCache[selector] === undefined) { + var match, tag, id, classes = [], attributes = {} + while (match = selectorParser.exec(selector)) { + var type = match[1], value = match[2] + if (type === "" && value !== "") tag = value + else if (type === "#") attributes.id = value + else if (type === ".") classes.push(value) + else if (match[3][0] === "[") { + var pair = attrParser.exec(match[3]) + attributes[pair[1]] = pair[3] || true + } + } + if (classes.length > 0) attributes.className = classes.join(" ") + selectorCache[selector] = function(attrs, children) { + var hasAttrs = false, childList, text + var className = attrs.className || attrs.class + for (var key in attributes) attrs[key] = attributes[key] + if (className !== undefined) { + if (attrs.class !== undefined) { + attrs.class = undefined + attrs.className = className + } + if (attributes.className !== undefined) attrs.className = attributes.className + " " + className + } + for (var key in attrs) { + if (key !== "key") { + hasAttrs = true + break + } + } + if (children instanceof Array && children.length == 1 && children[0] != null && children[0].tag === "#") text = children[0].children + else childList = children + return namespace({tag: tag || "div", key: attrs.key, attrs: hasAttrs ? attrs : undefined, children: childList, text: text}) + } + } + var attrs, children, childrenIndex + if (arguments[1] == null || typeof arguments[1] === "object" && arguments[1].tag === undefined && !(arguments[1] instanceof Array)) { + attrs = arguments[1] + childrenIndex = 2 + } + else childrenIndex = 1 + if (arguments.length === childrenIndex + 1) { + children = arguments[childrenIndex] instanceof Array ? arguments[childrenIndex] : [arguments[childrenIndex]] + } + else { + children = [] + for (var i = childrenIndex; i < arguments.length; i++) children.push(arguments[i]) + } + + return selectorCache[selector](attrs || {}, normalizeChildren(children)) +} + +function namespace(vnode) { + switch (vnode.tag) { + case "svg": changeNS("http://www.w3.org/2000/svg", vnode); break + case "math": changeNS("http://www.w3.org/1998/Math/MathML", vnode); break + } + return vnode +} + +function changeNS(ns, vnode) { + vnode.ns = ns + if (vnode.children != null) { + for (var i = 0; i < vnode.children.length; i++) changeNS(ns, vnode.children[i]) + } +} + +module.exports = hyperscript \ No newline at end of file diff --git a/render/normalizeChildren.js b/render/normalizeChildren.js new file mode 100644 index 00000000..fc974531 --- /dev/null +++ b/render/normalizeChildren.js @@ -0,0 +1,7 @@ +module.exports = function normalizeChildren(children) { + for (var i = 0; i < children.length; i++) { + if (children[i] instanceof Array) children[i] = {tag: "[", key: undefined, attrs: undefined, children: normalizeChildren(children[i]), text: undefined} + else if (children[i] != null && typeof children[i] !== "object") children[i] = {tag: "#", key: undefined, attrs: undefined, children: children[i], text: undefined} + } + return children +} \ No newline at end of file diff --git a/render/render.js b/render/render.js new file mode 100644 index 00000000..2429aee9 --- /dev/null +++ b/render/render.js @@ -0,0 +1,382 @@ +"use strict" + +var normalizeChildren = require("../render/normalizeChildren") + +module.exports = function($window, onevent) { + var $doc = $window.document + + //create + function createNodes(parent, vnodes, start, end, hooks, nextSibling) { + for (var i = start; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) { + insertNode(parent, createNode(vnode, hooks), nextSibling) + } + } + } + function createNode(vnode, hooks) { + var tag = vnode.tag + if (vnode.attrs && vnode.attrs.oncreate) { + hooks.push(vnode.attrs.oncreate.bind(vnode, vnode)) + } + switch (tag) { + case "#": return createText(vnode) + case "<": return createHTML(vnode) + case "[": return createFragment(vnode, hooks) + default: return createElement(vnode, hooks) + } + } + function createText(vnode) { + return vnode.dom = $doc.createTextNode(vnode.children) + } + function createHTML(vnode) { + var match = vnode.children.match(/^\s*?<(\w+)/im) || [] + var parent = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div" + var temp = $doc.createElement(parent) + + temp.innerHTML = vnode.children + vnode.dom = temp.firstChild + vnode.domSize = temp.childNodes.length + var fragment = $doc.createDocumentFragment() + var child + while (child = temp.firstChild) { + fragment.appendChild(child) + } + return fragment + } + function createFragment(vnode, hooks) { + var fragment = $doc.createDocumentFragment() + if (vnode.children != null) { + var children = vnode.children + createNodes(fragment, children, 0, children.length, hooks, null) + } + vnode.dom = fragment.firstChild + vnode.domSize = fragment.childNodes.length + return fragment + } + function createElement(vnode, hooks) { + var tag = vnode.tag + var ns = vnode.ns + + var attrs = vnode.attrs + var is = attrs && attrs.is + + var element = ns ? + is ? $doc.createElementNS(ns, tag, is) : $doc.createElementNS(ns, tag) : + is ? $doc.createElement(tag, is) : $doc.createElement(tag) + vnode.dom = element + + if (attrs != null) { + setAttrs(vnode, attrs) + } + + if (vnode.text != null) { + if (vnode.text !== "") element.textContent = vnode.text + else vnode.children = [{tag: "#", children: vnode.text}] + } + + if (vnode.children != null) { + var children = vnode.children + createNodes(element, children, 0, children.length, hooks, null) + } + return element + } + + //update + function updateNodes(parent, old, vnodes, hooks, nextSibling) { + if (old == null && vnodes == null) return + else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling) + else if (vnodes == null) removeNodes(parent, old, 0, old.length, vnodes) + else { + var recycling = isRecyclable(old, vnodes) + if (recycling) old = old.concat(old.pool) + + var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + while (oldEnd >= oldStart && end >= start) { + var o = old[oldStart], v = vnodes[start] + if (o === v) oldStart++, start++ + else if (o != null && v != null && o.key === v.key) { + oldStart++, start++ + updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling) + if (recycling) insertNode(parent, toFragment(v), nextSibling) + } + else { + var o = old[oldEnd] + if (o === v) oldEnd--, start++ + else if (o != null && v != null && o.key === v.key) { + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling) + insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) + oldEnd--, start++ + } + else break + } + } + while (oldEnd >= oldStart && end >= start) { + var o = old[oldEnd], v = vnodes[end] + if (o === v) oldEnd--, end-- + else if (o != null && v != null && o.key === v.key) { + updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling) + if (recycling) insertNode(parent, toFragment(v), nextSibling) + nextSibling = o.dom + oldEnd--, end-- + } + else { + if (!map) map = getKeyMap(old, oldEnd) + if (v != null) { + var oldIndex = map[v.key] + if (oldIndex != null) { + var movable = old[oldIndex] + updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling) + insertNode(parent, toFragment(movable), nextSibling) + old[oldIndex].skip = true + nextSibling = movable.dom + } + else { + var dom = createNode(v, hooks) + insertNode(parent, dom, nextSibling) + nextSibling = dom + } + } + end-- + } + if (end < start) break + } + createNodes(parent, vnodes, start, end + 1, hooks, nextSibling) + removeNodes(parent, old, oldStart, oldEnd + 1, vnodes) + } + } + function updateNode(parent, old, vnode, hooks, nextSibling, recycling) { + var oldTag = old.tag, tag = vnode.tag + if (oldTag === tag) { + if (recycling) { + if (vnode.attrs && vnode.attrs.oncreate) hooks.push(vnode.attrs.oncreate.bind(vnode, vnode)) + } + else if (vnode.attrs && vnode.attrs.onupdate) hooks.push(vnode.attrs.onupdate.bind(vnode, vnode)) + switch (oldTag) { + case "#": updateText(old, vnode); break + case "<": updateHTML(parent, old, vnode, nextSibling); break + case "[": updateFragment(parent, old, vnode, hooks, nextSibling); break + default: updateElement(old, vnode, hooks) + } + } + else { + removeNode(parent, old, null, false) + insertNode(parent, createNode(vnode, hooks), nextSibling) + } + } + function updateText(old, vnode) { + if (old.children.toString() !== vnode.children.toString()) { + old.dom.nodeValue = vnode.children + } + vnode.dom = old.dom + } + function updateHTML(parent, old, vnode, nextSibling) { + if (old.children !== vnode.children) { + toFragment(old) + insertNode(parent, createHTML(vnode), nextSibling) + } + else vnode.dom = old.dom + } + function updateFragment(parent, old, vnode, hooks, nextSibling) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling) + var domSize = 0, children = vnode.children + vnode.dom = null + if (children != null) { + for (var i = 0; i < children.length; i++) { + var child = children[i] + if (child != null) { + if (vnode.dom == null) vnode.dom = child.dom + domSize += child.domSize || 1 + } + } + if (domSize != 1) vnode.domSize = domSize + } + } + function updateElement(old, vnode, hooks) { + var element = vnode.dom = old.dom + updateAttrs(vnode, old.attrs, vnode.attrs) + if (old.text != null && vnode.text != null && vnode.text !== "") { + if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text + } + else { + if (old.text != null) old.children = [{tag: "#", children: old.text, dom: old.dom.firstChild}] + if (vnode.text != null) vnode.children = [{tag: "#", children: vnode.text}] + updateNodes(element, old.children, vnode.children, hooks, null) + } + } + function isRecyclable(old, vnodes) { + if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) { + var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0 + var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0 + var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0 + if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) { + return true + } + } + return false + } + function getKeyMap(vnodes, end) { + var map = {}, i = 0 + for (var i = 0; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) { + var key = vnode.key + if (key != null) map[key] = i + } + } + return map + } + function toFragment(vnode) { + var count = vnode.domSize + if (count != null) { + var fragment = $doc.createDocumentFragment() + if (count > 0) { + var dom = vnode.dom + while (--count) fragment.appendChild(dom.nextSibling) + fragment.insertBefore(dom, fragment.firstChild) + } + return fragment + } + else return vnode.dom + } + function getNextSibling(vnodes, i, nextSibling) { + for (; i < vnodes.length; i++) { + if (vnodes[i] != null) return vnodes[i].dom + } + return nextSibling + } + + function insertNode(parent, dom, nextSibling) { + if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling) + else parent.appendChild(dom) + } + + //remove + function removeNodes(parent, vnodes, start, end, context) { + for (var i = start; i < end; i++) { + var vnode = vnodes[i] + if (vnode != null) { + if (vnode.skip) vnode.skip = undefined + else removeNode(parent, vnode, context, false) + } + } + } + function removeNode(parent, vnode, context, deferred) { + if (vnode.attrs && vnode.attrs.onbeforeremove && deferred === false) { + vnode.attrs.onbeforeremove.call(vnode, vnode, function() {removeNode(parent, vnode, context, true)}) + return + } + + onremove(vnode) + if (vnode.dom) { + var count = vnode.domSize || 1 + if (count > 1) { + var dom = vnode.dom + while (--count) { + parent.removeChild(dom.nextSibling) + } + } + if (vnode.dom.parentNode != null) parent.removeChild(vnode.dom) + if (context != null && vnode.domSize == null) { //TODO test custom elements + if (!context.pool) context.pool = [vnode] + else context.pool.push(vnode) + } + } + } + function onremove(vnode) { + if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode, vnode) + + var children = vnode.children + if (children) { + for (var i = 0; i < children.length; i++) { + var child = children[i] + if (child != null) onremove(child) + } + } + } + + //attrs + function setAttrs(vnode, attrs) { + for (var key in attrs) { + setAttr(vnode, key, null, attrs[key]) + } + } + function setAttr(vnode, key, old, value) { + //TODO test input undo history + var element = vnode.dom + if (key === "key" || old === value || typeof value === "undefined" || isLifecycleMethod(key)) return + var nsLastIndex = key.indexOf(":") + if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") { + element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value) + } + else if (key[0] === "o" && key[1] === "n" && typeof value === "function") { + element[key] = function(e) { + var result = value.call(element, e) + if (typeof onevent === "function") onevent.call(element, e) + return result + } + } + else if (key === "style") updateStyle(element, old, value) + else if (key in element && !isAttribute(key) && vnode.ns === undefined) element[key] = value + else { + if (typeof value === "boolean") { + if (value) element.setAttribute(key, "") + else element.removeAttribute(key) + } + else element.setAttribute(key, value) + } + } + function updateAttrs(vnode, old, attrs) { + if (attrs != null) { + for (var key in attrs) { + setAttr(vnode, key, old[key], attrs[key]) + } + } + if (old != null) { + for (var key in old) { + if (attrs == null || !(key in attrs)) { + if (key !== "key") vnode.dom.removeAttribute(key) + } + } + } + } + function isLifecycleMethod(attr) { + return attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" + } + function isAttribute(attr) { + return attr === "href" || attr === "list" || attr === "form"// || attr === "type" || attr === "width" || attr === "height" + } + + //style + function updateStyle(element, old, style) { + if (style == null) element.style = "" + else if (typeof style === "string") element.style = style + else { + if (typeof old === "string") element.style = "" + for (var key in style) { + element.style[key] = style[key] + } + if (old != null && typeof old !== "string") { + for (var key in old) { + if (!(key in style)) element.style[key] = "" + } + } + } + } + + function render(dom, vnodes) { + //if (dom.lastRedraw + 16 > performance.now() && vnodes.length > 0) return + //dom.lastRedraw = performance.now() + var hooks = [] + var active = $doc.activeElement + if (!dom.vnodes) dom.vnodes = [] + + if (!(vnodes instanceof Array)) vnodes = [vnodes] + updateNodes(dom, dom.vnodes, normalizeChildren(vnodes), hooks, null) + for (var i = 0; i < hooks.length; i++) hooks[i]() + dom.vnodes = vnodes + if ($doc.activeElement !== active) active.focus() + } + + return {render: render} +} \ No newline at end of file diff --git a/render/tests/index.html b/render/tests/index.html new file mode 100644 index 00000000..5e7e7346 --- /dev/null +++ b/render/tests/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js new file mode 100644 index 00000000..4e43aa2a --- /dev/null +++ b/render/tests/test-attributes.js @@ -0,0 +1,79 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("attributes", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.body + render = vdom($window).render + }) + + o.spec("input readonly", function() { + o("when input readonly is true, attribute is present", function() { + var a = {tag: "input", attrs: {readonly: true}} + + render(root, [a]) + + o(a.dom.attributes["readonly"].nodeValue).equals("") + }) + o("when input readonly is false, attribute is not present", function() { + var a = {tag: "input", attrs: {readonly: false}} + + render(root, [a]) + + o(a.dom.attributes["readonly"]).equals(undefined) + }) + }) + o.spec("input checked", function() { + o("when input checked is true, attribute is not present", function() { + var a = {tag: "input", attrs: {checked: true}} + + render(root, [a]) + + o(a.dom.checked).equals(true) + o(a.dom.attributes["checked"]).equals(undefined) + }) + o("when input checked is false, attribute is not present", function() { + var a = {tag: "input", attrs: {checked: false}} + + render(root, [a]) + + o(a.dom.checked).equals(false) + o(a.dom.attributes["checked"]).equals(undefined) + }) + o("after input checked is changed by 3rd party, it can still be changed by render", function() { + var a = {tag: "input", attrs: {checked: false}} + var b = {tag: "input", attrs: {checked: true}} + + render(root, [a]) + + a.dom.checked = true //setting the javascript property makes the value no longer track the state of the attribute + a.dom.checked = false + + render(root, [b]) + + o(a.dom.checked).equals(true) + o(a.dom.attributes["checked"]).equals(undefined) + }) + }) + o.spec("link href", function() { + o("when link href is true, attribute is present", function() { + var a = {tag: "a", attrs: {href: true}} + + render(root, [a]) + + o(a.dom.attributes["href"]).notEquals(undefined) + }) + o("when link href is false, attribute is not present", function() { + var a = {tag: "a", attrs: {href: false}} + + render(root, [a]) + + o(a.dom.attributes["href"]).equals(undefined) + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js new file mode 100644 index 00000000..a037152c --- /dev/null +++ b/render/tests/test-createElement.js @@ -0,0 +1,82 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("createElement", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("creates element", function() { + var vnode = {tag: "div"} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("DIV") + }) + o("creates attr", function() { + var vnode = {tag: "div", attrs: {id: "a", title: "b"}} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("DIV") + o(vnode.dom.attributes["id"].nodeValue).equals("a") + o(vnode.dom.attributes["title"].nodeValue).equals("b") + }) + o("creates style", function() { + var vnode = {tag: "div", attrs: {style: {backgroundColor: "red"}}} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("DIV") + o(vnode.dom.style.backgroundColor).equals("red") + }) + o("creates children", function() { + var vnode = {tag: "div", children: [{tag: "a"}, {tag: "b"}]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("DIV") + o(vnode.dom.childNodes.length).equals(2) + o(vnode.dom.childNodes[0].nodeName).equals("A") + o(vnode.dom.childNodes[1].nodeName).equals("B") + }) + o("creates attrs and children", function() { + var vnode = {tag: "div", attrs: {id: "a", title: "b"}, children: [{tag: "a"}, {tag: "b"}]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("DIV") + o(vnode.dom.attributes["id"].nodeValue).equals("a") + o(vnode.dom.attributes["title"].nodeValue).equals("b") + o(vnode.dom.childNodes.length).equals(2) + o(vnode.dom.childNodes[0].nodeName).equals("A") + o(vnode.dom.childNodes[1].nodeName).equals("B") + }) + o("creates svg", function() { + var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}}]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("svg") + o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.dom.firstChild.nodeName).equals("a") + o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.dom.firstChild.attributes["href"].nodeValue).equals("javascript:;") + o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + }) + o("sets attributes correctly for svg", function() { + var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", attrs: {viewBox: "0 0 100 100"}} + render(root, [vnode]) + + o(vnode.dom.attributes["viewBox"].nodeValue).equals("0 0 100 100") + }) + o("creates mathml", function() { + var vnode = {tag: "math", ns: "http://www.w3.org/1998/Math/MathML", children: [{tag: "mrow", ns: "http://www.w3.org/1998/Math/MathML"}]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("math") + o(vnode.dom.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + o(vnode.dom.firstChild.nodeName).equals("mrow") + o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/1998/Math/MathML") + }) +}) diff --git a/render/tests/test-createFragment.js b/render/tests/test-createFragment.js new file mode 100644 index 00000000..9d629ab8 --- /dev/null +++ b/render/tests/test-createFragment.js @@ -0,0 +1,50 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("createFragment", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("creates fragment", function() { + var vnode = {tag: "[", children: [{tag: "a"}]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("A") + }) + o("handles empty fragment", function() { + var vnode = {tag: "[", children: []} + render(root, [vnode]) + + o(vnode.dom).equals(null) + o(vnode.domSize).equals(0) + }) + o("handles childless fragment", function() { + var vnode = {tag: "["} + render(root, [vnode]) + + o(vnode.dom).equals(null) + o(vnode.domSize).equals(0) + }) + o("handles multiple children", function() { + var vnode = {tag: "[", children: [{tag: "a"}, {tag: "b"}]} + render(root, [vnode]) + + o(vnode.domSize).equals(2) + o(vnode.dom.nodeName).equals("A") + o(vnode.dom.nextSibling.nodeName).equals("B") + }) + o("handles td", function() { + var vnode = {tag: "[", children: [{tag: "td"}]} + render(root, [vnode]) + + o(vnode.dom).notEquals(null) + o(vnode.dom.nodeName).equals("TD") + }) +}) diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js new file mode 100644 index 00000000..7188701d --- /dev/null +++ b/render/tests/test-createHTML.js @@ -0,0 +1,54 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("createHTML", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("creates HTML", function() { + var vnode = {tag: "<", children: ""} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("A") + }) + o("creates text HTML", function() { + var vnode = {tag: "<", children: "a"} + render(root, [vnode]) + + o(vnode.dom.nodeValue).equals("a") + }) + o("handles empty HTML", function() { + var vnode = {tag: "<", children: ""} + render(root, [vnode]) + + o(vnode.dom).equals(null) + o(vnode.domSize).equals(0) + }) + o("handles multiple children", function() { + var vnode = {tag: "<", children: ""} + render(root, [vnode]) + + o(vnode.domSize).equals(2) + o(vnode.dom.nodeName).equals("A") + o(vnode.dom.nextSibling.nodeName).equals("B") + }) + o("handles valid html tags", function() { + //FIXME body,head,html,frame,frameset are not supported + //FIXME keygen is broken in Firefox + var tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blockquote", /*"body",*/ "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", /*"frame", "frameset",*/ "h1", "h2", "h3", "h4", "h5", "h6", /*"head",*/ "header", "hr", /*"html",*/ "i", "iframe", "img", "input", "ins", "kbd", /*"keygen", */"label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"] + + tags.forEach(function(tag) { + var vnode = {tag: "<", children: "<" + tag + " />"} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals(tag.toUpperCase()) + }) + }) +}) diff --git a/render/tests/test-createNodes.js b/render/tests/test-createNodes.js new file mode 100644 index 00000000..bf0fee18 --- /dev/null +++ b/render/tests/test-createNodes.js @@ -0,0 +1,62 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("createNodes", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("creates nodes", function() { + var vnodes = [ + {tag: "a"}, + {tag: "#", children: "b"}, + {tag: "<", children: "c"}, + {tag: "[", children: [{tag: "#", children: "d"}]}, + ] + render(root, vnodes) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeValue).equals("b") + o(root.childNodes[2].nodeValue).equals("c") + o(root.childNodes[3].nodeValue).equals("d") + }) + o("ignores null", function() { + var vnodes = [ + {tag: "a"}, + {tag: "#", children: "b"}, + null, + {tag: "<", children: "c"}, + {tag: "[", children: [{tag: "#", children: "d"}]}, + ] + render(root, vnodes) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeValue).equals("b") + o(root.childNodes[2].nodeValue).equals("c") + o(root.childNodes[3].nodeValue).equals("d") + }) + o("ignores undefined", function() { + var vnodes = [ + {tag: "a"}, + {tag: "#", children: "b"}, + undefined, + {tag: "<", children: "c"}, + {tag: "[", children: [{tag: "#", children: "d"}]}, + ] + render(root, vnodes) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeValue).equals("b") + o(root.childNodes[2].nodeValue).equals("c") + o(root.childNodes[3].nodeValue).equals("d") + }) +}) diff --git a/render/tests/test-createText.js b/render/tests/test-createText.js new file mode 100644 index 00000000..25f7cb68 --- /dev/null +++ b/render/tests/test-createText.js @@ -0,0 +1,71 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("createText", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("creates string", function() { + var vnode = {tag: "#", children: "a"} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("a") + }) + o("creates falsy string", function() { + var vnode = {tag: "#", children: ""} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("") + }) + o("creates number", function() { + var vnode = {tag: "#", children: 1} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("1") + }) + o("creates falsy number", function() { + var vnode = {tag: "#", children: 0} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("0") + }) + o("creates boolean", function() { + var vnode = {tag: "#", children: true} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("true") + }) + o("creates falsy boolean", function() { + var vnode = {tag: "#", children: false} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("false") + }) + o("creates spaces", function() { + var vnode = {tag: "#", children: " "} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals(" ") + }) + o("ignores html", function() { + var vnode = {tag: "#", children: "™"} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("#text") + o(vnode.dom.nodeValue).equals("™") + }) +}) diff --git a/render/tests/test-event.js b/render/tests/test-event.js new file mode 100644 index 00000000..e11c36b8 --- /dev/null +++ b/render/tests/test-event.js @@ -0,0 +1,34 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("event", function() { + var $window, root, onevent, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.body + onevent = o.spy() + render = vdom($window, onevent).render + }) + + o("handles onclick", function() { + var spy = o.spy() + var div = {tag: "div", attrs: {onclick: spy}} + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(root, [div]) + div.dom.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div.dom) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div.dom) + o(onevent.callCount).equals(1) + o(onevent.this).equals(div.dom) + o(onevent.args[0].type).equals("click") + o(onevent.args[0].target).equals(div.dom) + }) +}) \ No newline at end of file diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js new file mode 100644 index 00000000..8564de26 --- /dev/null +++ b/render/tests/test-hyperscript.js @@ -0,0 +1,379 @@ +var o = require("../../ospec/ospec") +var m = require("../../render/hyperscript") + +o.spec("hyperscript", function() { + o.spec("selector", function() { + o("handles tag in selector", function() { + var vnode = m("a") + + o(vnode.tag).equals("a") + }) + o("handles class in selector", function() { + var vnode = m(".a") + + o(vnode.tag).equals("div") + o(vnode.attrs.className).equals("a") + }) + o("handles many classes in selector", function() { + var vnode = m(".a.b.c") + + o(vnode.tag).equals("div") + o(vnode.attrs.className).equals("a b c") + }) + o("handles id in selector", function() { + var vnode = m("#a") + + o(vnode.tag).equals("div") + o(vnode.attrs.id).equals("a") + }) + o("handles attr in selector", function() { + var vnode = m("[a=b]") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + }) + o("handles many attrs in selector", function() { + var vnode = m("[a=b][c=d]") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + o(vnode.attrs.c).equals("d") + }) + o("handles attr w/ spaces in selector", function() { + var vnode = m("[a = b]") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + }) + o("handles attr w/ quotes in selector", function() { + var vnode = m("[a='b']") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + }) + o("handles attr w/ quotes and spaces in selector", function() { + var vnode = m("[a = 'b']") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + }) + o("handles many attr w/ quotes and spaces in selector", function() { + var vnode = m("[a = 'b'][c = 'd']") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + o(vnode.attrs.c).equals("d") + }) + o("handles tag, class, attrs in selector", function() { + var vnode = m("a.b[c = 'd']") + + o(vnode.tag).equals("a") + o(vnode.attrs.className).equals("b") + o(vnode.attrs.c).equals("d") + }) + o("handles tag, mixed classes, attrs in selector", function() { + var vnode = m("a.b[c = 'd'].e[f = 'g']") + + o(vnode.tag).equals("a") + o(vnode.attrs.className).equals("b e") + o(vnode.attrs.c).equals("d") + o(vnode.attrs.f).equals("g") + }) + o("handles attr without value", function() { + var vnode = m("[a]") + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals(true) + }) + }) + o.spec("attrs", function() { + o("handles string attr", function() { + var vnode = m("div", {a: "b"}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + }) + o("handles falsy string attr", function() { + var vnode = m("div", {a: ""}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("") + }) + o("handles number attr", function() { + var vnode = m("div", {a: 1}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals(1) + }) + o("handles falsy number attr", function() { + var vnode = m("div", {a: 0}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals(0) + }) + o("handles boolean attr", function() { + var vnode = m("div", {a: true}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals(true) + }) + o("handles falsy boolean attr", function() { + var vnode = m("div", {a: false}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals(false) + }) + o("handles many attrs", function() { + var vnode = m("div", {a: "b", c: "d"}) + + o(vnode.tag).equals("div") + o(vnode.attrs.a).equals("b") + o(vnode.attrs.c).equals("d") + }) + o("handles merging classes w/ class property", function() { + var vnode = m(".a", {class: "b"}) + + o(vnode.attrs.className).equals("a b") + }) + o("handles merging classes w/ className property", function() { + var vnode = m(".a", {className: "b"}) + + o(vnode.attrs.className).equals("a b") + }) + }) + o.spec("children", function() { + o("handles string single child", function() { + var vnode = m("div", {}, ["a"]) + + o(vnode.text).equals("a") + }) + o("handles falsy string single child", function() { + var vnode = m("div", {}, [""]) + + o(vnode.text).equals("") + }) + o("handles number single child", function() { + var vnode = m("div", {}, [1]) + + o(vnode.text).equals(1) + }) + o("handles falsy number single child", function() { + var vnode = m("div", {}, [0]) + + o(vnode.text).equals(0) + }) + o("handles boolean single child", function() { + var vnode = m("div", {}, [true]) + + o(vnode.text).equals(true) + }) + o("handles falsy boolean single child", function() { + var vnode = m("div", {}, [false]) + + o(vnode.text).equals(false) + }) + o("handles null single child", function() { + var vnode = m("div", {}, [null]) + + o(vnode.children[0]).equals(null) + }) + o("handles undefined single child", function() { + var vnode = m("div", {}, [undefined]) + + o(vnode.children[0]).equals(undefined) + }) + o("handles multiple string children", function() { + var vnode = m("div", {}, ["", "a"]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("a") + }) + o("handles multiple number children", function() { + var vnode = m("div", {}, [0, 1]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals(0) + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals(1) + }) + o("handles multiple boolean children", function() { + var vnode = m("div", {}, [false, true]) + + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals(false) + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals(true) + }) + o("handles multiple null/undefined child", function() { + var vnode = m("div", {}, [null, undefined]) + + o(vnode.children[0]).equals(null) + o(vnode.children[1]).equals(undefined) + }) + }) + o.spec("permutations", function() { + o("handles null attr and children", function() { + var vnode = m("div", null, [m("a"), m("b")]) + + o(vnode.children.length).equals(2) + o(vnode.children[0].tag).equals("a") + o(vnode.children[1].tag).equals("b") + }) + o("handles null attr and child unwrapped", function() { + var vnode = m("div", null, m("a")) + + o(vnode.children.length).equals(1) + o(vnode.children[0].tag).equals("a") + }) + o("handles null attr and children unwrapped", function() { + var vnode = m("div", null, m("a"), m("b")) + + o(vnode.children.length).equals(2) + o(vnode.children[0].tag).equals("a") + o(vnode.children[1].tag).equals("b") + }) + o("handles attr and children", function() { + var vnode = m("div", {a: "b"}, [m("i"), m("s")]) + + o(vnode.attrs.a).equals("b") + o(vnode.children[0].tag).equals("i") + o(vnode.children[1].tag).equals("s") + }) + o("handles attr and child unwrapped", function() { + var vnode = m("div", {a: "b"}, m("i")) + + o(vnode.attrs.a).equals("b") + o(vnode.children[0].tag).equals("i") + }) + o("handles attr and children unwrapped", function() { + var vnode = m("div", {a: "b"}, m("i"), m("s")) + + o(vnode.attrs.a).equals("b") + o(vnode.children[0].tag).equals("i") + o(vnode.children[1].tag).equals("s") + }) + o("handles attr and text children", function() { + var vnode = m("div", {a: "b"}, ["c", "d"]) + + o(vnode.attrs.a).equals("b") + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("c") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("d") + }) + o("handles attr and single string text child", function() { + var vnode = m("div", {a: "b"}, ["c"]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals("c") + }) + o("handles attr and single falsy string text child", function() { + var vnode = m("div", {a: "b"}, [""]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals("") + }) + o("handles attr and single number text child", function() { + var vnode = m("div", {a: "b"}, [1]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals(1) + }) + o("handles attr and single falsy number text child", function() { + var vnode = m("div", {a: "b"}, [0]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals(0) + }) + o("handles attr and single boolean text child", function() { + var vnode = m("div", {a: "b"}, [true]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals(true) + }) + o("handles attr and single falsy boolean text child", function() { + var vnode = m("div", {a: "b"}, [false]) + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals(false) + }) + o("handles attr and single text child unwrapped", function() { + var vnode = m("div", {a: "b"}, "c") + + o(vnode.attrs.a).equals("b") + o(vnode.text).equals("c") + }) + o("handles attr and text children unwrapped", function() { + var vnode = m("div", {a: "b"}, "c", "d") + + o(vnode.attrs.a).equals("b") + o(vnode.children[0].tag).equals("#") + o(vnode.children[0].children).equals("c") + o(vnode.children[1].tag).equals("#") + o(vnode.children[1].children).equals("d") + }) + o("handles children without attr", function() { + var vnode = m("div", [m("i"), m("s")]) + + o(vnode.attrs).equals(undefined) + o(vnode.children[0].tag).equals("i") + o(vnode.children[1].tag).equals("s") + }) + o("handles child without attr unwrapped", function() { + var vnode = m("div", m("i")) + + o(vnode.attrs).equals(undefined) + o(vnode.children[0].tag).equals("i") + }) + o("handles children without attr unwrapped", function() { + var vnode = m("div", m("i"), m("s")) + + o(vnode.attrs).equals(undefined) + o(vnode.children[0].tag).equals("i") + o(vnode.children[1].tag).equals("s") + }) + o("handles fragment children without attr unwrapped", function() { + var vnode = m("div", [m("i")], [m("s")]) + + o(vnode.children[0].tag).equals("[") + o(vnode.children[0].children[0].tag).equals("i") + o(vnode.children[1].tag).equals("[") + o(vnode.children[1].children[0].tag).equals("s") + }) + o("handles children with nested array", function() { + var vnode = m("div", [[m("i"), m("s")]]) + + o(vnode.children[0].tag).equals("[") + o(vnode.children[0].children[0].tag).equals("i") + o(vnode.children[0].children[1].tag).equals("s") + }) + o("handles children with deeply nested array", function() { + var vnode = m("div", [[[m("i"), m("s")]]]) + + o(vnode.children[0].tag).equals("[") + o(vnode.children[0].children[0].tag).equals("[") + o(vnode.children[0].children[0].children[0].tag).equals("i") + o(vnode.children[0].children[0].children[1].tag).equals("s") + }) + }) + o.spec("namespaced", function() { + o("handles svg ns", function() { + var vnode = m("svg", m("g")) + + o(vnode.tag).equals("svg") + o(vnode.ns).equals("http://www.w3.org/2000/svg") + o(vnode.children[0].tag).equals("g") + o(vnode.children[0].ns).equals("http://www.w3.org/2000/svg") + }) + o("handles mathml ns", function() { + var vnode = m("math", m("mrow")) + + o(vnode.tag).equals("math") + o(vnode.ns).equals("http://www.w3.org/1998/Math/MathML") + o(vnode.children[0].tag).equals("mrow") + o(vnode.children[0].ns).equals("http://www.w3.org/1998/Math/MathML") + }) + }) +}) \ No newline at end of file diff --git a/render/tests/test-input.js b/render/tests/test-input.js new file mode 100644 index 00000000..fef44b5a --- /dev/null +++ b/render/tests/test-input.js @@ -0,0 +1,26 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("input", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.body + render = vdom($window).render + }) + + o("maintains focus after move", function() { + var input = {tag: "input", key: 1} + var a = {tag: "a", key: 2} + var b = {tag: "b", key: 3} + + render(root, [input, a, b]) + input.dom.focus() + render(root, [a, input, b]) + + o($window.document.activeElement).equals(input.dom) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js new file mode 100644 index 00000000..b806b903 --- /dev/null +++ b/render/tests/test-onbeforeremove.js @@ -0,0 +1,159 @@ +"use strict" + +var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("onbeforeremove", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("does not call onbeforeremove when creating", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onbeforeremove: create}} + + render(root, [vnode]) + + o(create.callCount).equals(0) + }) + o("does not call onbeforeremove when updating", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onbeforeremove: create}} + var updated = {tag: "div", attrs: {onbeforeremove: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(0) + }) + o("calls onbeforeremove when removing element", function(done) { + var vnode = {tag: "div", attrs: {onbeforeremove: remove}} + + render(root, [vnode]) + render(root, []) + + function remove(node, complete) { + o(node).equals(vnode) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(vnode.dom) + + callAsync(function() { + o(root.childNodes.length).equals(1) + + complete() + + o(root.childNodes.length).equals(0) + + done() + }) + } + }) + o("calls onbeforeremove when removing text", function(done) { + var vnode = {tag: "#", attrs: {onbeforeremove: remove}, children: "a"} + + render(root, [vnode]) + render(root, []) + + function remove(node, complete) { + o(node).equals(vnode) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(vnode.dom) + + callAsync(function() { + o(root.childNodes.length).equals(1) + + complete() + + o(root.childNodes.length).equals(0) + + done() + }) + } + }) + o("calls onbeforeremove when removing fragment", function(done) { + var vnode = {tag: "[", attrs: {onbeforeremove: remove}, children: [{tag: "div"}]} + + render(root, [vnode]) + render(root, []) + + function remove(node, complete) { + o(node).equals(vnode) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(vnode.dom) + + callAsync(function() { + o(root.childNodes.length).equals(1) + + complete() + + o(root.childNodes.length).equals(0) + + done() + }) + } + }) + o("calls onbeforeremove when removing html", function(done) { + var vnode = {tag: "<", attrs: {onbeforeremove: remove}, children: "a"} + + render(root, [vnode]) + render(root, []) + + function remove(node, complete) { + o(node).equals(vnode) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(vnode.dom) + + callAsync(function() { + o(root.childNodes.length).equals(1) + + complete() + + o(root.childNodes.length).equals(0) + + done() + }) + } + }) + o("calls remove after onbeforeremove resolves", function(done) { + var spy = o.spy() + var vnode = {tag: "<", attrs: {onbeforeremove: remove, onremove: spy}, children: "a"} + + render(root, [vnode]) + render(root, []) + + function remove(node, complete) { + o(node).equals(vnode) + o(root.childNodes.length).equals(1) + o(root.firstChild).equals(vnode.dom) + + callAsync(function() { + o(root.childNodes.length).equals(1) + o(spy.callCount).equals(0) + + complete() + + o(root.childNodes.length).equals(0) + o(spy.callCount).equals(1) + + done() + }) + } + }) + o("does not set onbeforeremove as an event handler", function() { + var remove = o.spy() + var vnode = {tag: "div", attrs: {onbeforeremove: remove}, children: []} + + render(root, [vnode]) + + o(vnode.dom.onbeforeremove).equals(undefined) + o(vnode.dom.attributes["onbeforeremove"]).equals(undefined) + }) +}) \ No newline at end of file diff --git a/render/tests/test-oncreate.js b/render/tests/test-oncreate.js new file mode 100644 index 00000000..5077cb33 --- /dev/null +++ b/render/tests/test-oncreate.js @@ -0,0 +1,216 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("oncreate", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("calls oncreate when creating element", function() { + var callback = o.spy() + var vnode = {tag: "div", attrs: {oncreate: callback}} + + render(root, [vnode]) + + o(callback.callCount).equals(1) + o(callback.this).equals(vnode) + o(callback.args[0]).equals(vnode) + }) + o("calls oncreate when creating text", function() { + var callback = o.spy() + var vnode = {tag: "#", attrs: {oncreate: callback}, children: "a"} + + render(root, [vnode]) + + o(callback.callCount).equals(1) + o(callback.this).equals(vnode) + o(callback.args[0]).equals(vnode) + }) + o("calls oncreate when creating fragment", function() { + var callback = o.spy() + var vnode = {tag: "[", attrs: {oncreate: callback}, children: []} + + render(root, [vnode]) + + o(callback.callCount).equals(1) + o(callback.this).equals(vnode) + o(callback.args[0]).equals(vnode) + }) + o("calls oncreate when creating html", function() { + var callback = o.spy() + var vnode = {tag: "<", attrs: {oncreate: callback}, children: "a"} + + render(root, [vnode]) + + o(callback.callCount).equals(1) + o(callback.this).equals(vnode) + o(callback.args[0]).equals(vnode) + }) + o("calls oncreate when replacing keyed", function() { + var createDiv = o.spy() + var createA = o.spy() + var vnode = {tag: "div", key: 1, attrs: {oncreate: createDiv}} + var updated = {tag: "a", key: 1, attrs: {oncreate: createA}} + + render(root, [vnode]) + render(root, [updated]) + + o(createDiv.callCount).equals(1) + o(createDiv.this).equals(vnode) + o(createDiv.args[0]).equals(vnode) + o(createA.callCount).equals(1) + o(createA.this).equals(updated) + o(createA.args[0]).equals(updated) + }) + o("does not call oncreate when noop", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {oncreate: create}} + var updated = {tag: "div", attrs: {oncreate: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + o(update.callCount).equals(0) + }) + o("does not call oncreate when updating attr", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {oncreate: create}} + var updated = {tag: "div", attrs: {oncreate: update, id: "a"}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + o(update.callCount).equals(0) + }) + o("does not call oncreate when updating children", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {oncreate: create}, children: [{tag: "a"}]} + var updated = {tag: "div", attrs: {oncreate: update}, children: [{tag: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + o(update.callCount).equals(0) + }) + o("does not call oncreate when updating keyed", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", key: 1, attrs: {oncreate: create}} + var otherVnode = {tag: "a", key: 2} + var updated = {tag: "div", key: 1, attrs: {oncreate: update}} + var otherUpdated = {tag: "a", key: 2} + + render(root, [vnode, otherVnode]) + render(root, [otherUpdated, updated]) + + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + o(update.callCount).equals(0) + }) + o("does not call oncreate when removing", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {oncreate: create}} + + render(root, [vnode]) + render(root, []) + + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + }) + o("calls oncreate when recycling", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", key: 1, attrs: {oncreate: create}} + var updated = {tag: "div", key: 1, attrs: {oncreate: update}} + + render(root, [vnode]) + render(root, []) + render(root, [updated]) + + o(vnode.dom).equals(updated.dom) + o(create.callCount).equals(1) + o(create.this).equals(vnode) + o(create.args[0]).equals(vnode) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls oncreate at the same step as onupdate", function() { + var create = o.spy() + var update = o.spy() + var callback = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}, children: []} + var updated = {tag: "div", attrs: {onupdate: update}, children: [{tag: "a", attrs: {oncreate: callback}}]} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + o(callback.callCount).equals(1) + o(callback.this).equals(updated.children[0]) + o(callback.args[0]).equals(updated.children[0]) + }) + o("calls oncreate after full DOM creation", function() { + var created = false + var vnode = {tag: "div", children: [ + {tag: "a", attrs: {oncreate: create}, children: [ + {tag: "b"} + ]} + ]} + + render(root, [vnode]) + + function create(vnode) { + created = true + + o(vnode.dom.parentNode).notEquals(null) + o(vnode.dom.childNodes.length).equals(1) + } + o(created).equals(true) + }) + o("does not set oncreate as an event handler", function() { + var create = o.spy() + var vnode = {tag: "div", attrs: {oncreate: create}, children: []} + + render(root, [vnode]) + + o(vnode.dom.oncreate).equals(undefined) + o(vnode.dom.attributes["oncreate"]).equals(undefined) + }) + o("calls oncreate on recycle", function() { + var create = o.spy() + var vnodes = [{tag: "div", key: 1, attrs: {oncreate: create}}] + var temp = [] + var updated = [{tag: "div", key: 1, attrs: {oncreate: create}}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(create.callCount).equals(2) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js new file mode 100644 index 00000000..acc5dc8f --- /dev/null +++ b/render/tests/test-onremove.js @@ -0,0 +1,103 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("onremove", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("does not call onremove when creating", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onremove: create}} + var updated = {tag: "div", attrs: {onremove: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + }) + o("does not call onremove when updating", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onremove: create}} + var updated = {tag: "div", attrs: {onremove: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(0) + }) + o("calls onremove when removing element", function() { + var remove = o.spy() + var vnode = {tag: "div", attrs: {onremove: remove}} + + render(root, [vnode]) + render(root, []) + + o(remove.callCount).equals(1) + o(remove.this).equals(vnode) + o(remove.args[0]).equals(vnode) + }) + o("calls onremove when removing text", function() { + var remove = o.spy() + var vnode = {tag: "#", attrs: {onremove: remove}, children: "a"} + + render(root, [vnode]) + render(root, []) + + o(remove.callCount).equals(1) + o(remove.this).equals(vnode) + o(remove.args[0]).equals(vnode) + }) + o("calls onremove when removing fragment", function() { + var remove = o.spy() + var vnode = {tag: "[", attrs: {onremove: remove}, children: []} + + render(root, [vnode]) + render(root, []) + + o(remove.callCount).equals(1) + o(remove.this).equals(vnode) + o(remove.args[0]).equals(vnode) + }) + o("calls onremove when removing html", function() { + var remove = o.spy() + var vnode = {tag: "<", attrs: {onremove: remove}, children: "a"} + + render(root, [vnode]) + render(root, []) + + o(remove.callCount).equals(1) + o(remove.this).equals(vnode) + o(remove.args[0]).equals(vnode) + }) + o("does not set onremove as an event handler", function() { + var remove = o.spy() + var vnode = {tag: "div", attrs: {onremove: remove}, children: []} + + render(root, [vnode]) + + o(vnode.dom.onremove).equals(undefined) + o(vnode.dom.attributes["onremove"]).equals(undefined) + }) + o("calls onremove on recycle", function() { + var remove = o.spy() + var vnodes = [{tag: "div", key: 1}] + var temp = [{tag: "div", key: 2, attrs: {onremove: remove}}] + var updated = [{tag: "div", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(remove.callCount).equals(1) + }) +}) \ No newline at end of file diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js new file mode 100644 index 00000000..e3ee6a75 --- /dev/null +++ b/render/tests/test-onupdate.js @@ -0,0 +1,192 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("onupdate", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("does not call onupdate when creating element", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}} + var updated = {tag: "div", attrs: {onupdate: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("does not call onupdate when removing element", function() { + var create = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}} + + render(root, [vnode]) + render(root, []) + + o(create.callCount).equals(0) + }) + o("does not call onupdate when replacing keyed element", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", key: 1, attrs: {onupdate: create}} + var updated = {tag: "a", key: 1, attrs: {onupdate: update}} + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(0) + }) + o("does not call onupdate when recycling", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", key: 1, attrs: {onupdate: create}} + var updated = {tag: "div", key: 1, attrs: {onupdate: update}} + + render(root, [vnode]) + render(root, []) + render(root, [updated]) + + o(vnode.dom).equals(updated.dom) + o(create.callCount).equals(0) + o(update.callCount).equals(0) + }) + o("does not call old onupdate when removing the onupdate property in new vnode", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "a", attrs: {onupdate: create}} + var updated = {tag: "a"} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + }) + o("calls onupdate when noop", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}} + var updated = {tag: "div", attrs: {onupdate: update}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate when updating attr", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}} + var updated = {tag: "div", attrs: {onupdate: update, id: "a"}} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate when updating children", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "div", attrs: {onupdate: create}, children: [{tag: "a"}]} + var updated = {tag: "div", attrs: {onupdate: update}, children: [{tag: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate when updating text", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "#", attrs: {onupdate: create}, children: "a"} + var updated = {tag: "#", attrs: {onupdate: update}, children: "a"} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate when updating fragment", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "[", attrs: {onupdate: create}, children: []} + var updated = {tag: "[", attrs: {onupdate: update}, children: []} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate when updating html", function() { + var create = o.spy() + var update = o.spy() + var vnode = {tag: "<", attrs: {onupdate: create}, children: "a"} + var updated = {tag: "<", attrs: {onupdate: update}, children: "a"} + + render(root, [vnode]) + render(root, [updated]) + + o(create.callCount).equals(0) + o(update.callCount).equals(1) + o(update.this).equals(updated) + o(update.args[0]).equals(updated) + }) + o("calls onupdate after full DOM update", function() { + var called = false + var vnode = {tag: "div", attrs: {id: "1"}, children: [ + {tag: "a", attrs: {id: "2"}, children: [ + {tag: "b", attrs: {id: "3"}} + ]} + ]} + var updated = {tag: "div", attrs: {id: "11"}, children: [ + {tag: "a", attrs: {onupdate: update, id: "22"}, children: [ + {tag: "b", attrs: {id: "33"}} + ]} + ]} + + render(root, [vnode]) + render(root, [updated]) + + function update(vnode) { + called = true + + o(vnode.dom.parentNode.attributes["id"].nodeValue).equals("11") + o(vnode.dom.attributes["id"].nodeValue).equals("22") + o(vnode.dom.childNodes[0].attributes["id"].nodeValue).equals("33") + } + o(called).equals(true) + }) + o("does not set onupdate as an event handler", function() { + var update = o.spy() + var vnode = {tag: "div", attrs: {onupdate: update}, children: []} + + render(root, [vnode]) + + o(vnode.dom.onupdate).equals(undefined) + o(vnode.dom.attributes["onupdate"]).equals(undefined) + }) +}) \ No newline at end of file diff --git a/render/tests/test-style.js b/render/tests/test-style.js new file mode 100644 index 00000000..e69de29b diff --git a/render/tests/test-textContent.js b/render/tests/test-textContent.js new file mode 100644 index 00000000..c757968e --- /dev/null +++ b/render/tests/test-textContent.js @@ -0,0 +1,200 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("textContent", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("ignores null", function() { + var vnodes = [{tag: "a", text: null}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(0) + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("ignores undefined", function() { + var vnodes = [{tag: "a", text: undefined}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(0) + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates string", function() { + var vnodes = [{tag: "a", text: "a"}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("a") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates falsy string", function() { + var vnodes = [{tag: "a", text: ""}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates number", function() { + var vnodes = [{tag: "a", text: 1}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("1") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates falsy number", function() { + var vnodes = [{tag: "a", text: 0}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("0") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates boolean", function() { + var vnodes = [{tag: "a", text: true}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("true") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("creates falsy boolean", function() { + var vnodes = [{tag: "a", text: false}] + + render(root, vnodes) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("false") + o(vnodes[0].dom).equals(root.childNodes[0]) + }) + o("updates to string", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: "b"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("b") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates to falsy string", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: ""}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates to number", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("1") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates to falsy number", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: 0}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("0") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates to boolean", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: true}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("true") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates to falsy boolean", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a", text: false}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("false") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates with typecasting", function() { + var vnodes = [{tag: "a", text: "1"}] + var updated = [{tag: "a", text: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("1") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates from without text to with text", function() { + var vnodes = [{tag: "a"}] + var updated = [{tag: "a", text: "b"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes[0].nodeValue).equals("b") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("updates from with text to without text", function() { + var vnodes = [{tag: "a", text: "a"}] + var updated = [{tag: "a"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(vnodes[0].dom.childNodes.length).equals(0) + o(updated[0].dom).equals(root.childNodes[0]) + }) +}) \ No newline at end of file diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js new file mode 100644 index 00000000..d5de1080 --- /dev/null +++ b/render/tests/test-updateElement.js @@ -0,0 +1,152 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("updateElement", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("updates attr", function() { + var vnode = {tag: "a", attrs: {id: "b"}} + var updated = {tag: "a", attrs: {id: "c"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.attributes["id"].nodeValue).equals("c") + }) + o("adds attr", function() { + var vnode = {tag: "a", attrs: {id: "b"}} + var updated = {tag: "a", attrs: {id: "c", title: "d"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.attributes["title"].nodeValue).equals("d") + }) + o("removes attr", function() { + var vnode = {tag: "a", attrs: {id: "b", title: "d"}} + var updated = {tag: "a", attrs: {id: "c"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o("title" in updated.dom.attributes).equals(false) + }) + o("creates style object", function() { + var vnode = {tag: "a", attrs: {}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "green"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("creates style string", function() { + var vnode = {tag: "a", attrs: {}} + var updated = {tag: "a", attrs: {style: "background-color:green"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("updates style from object to object", function() { + var vnode = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "green"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("updates style from object to string", function() { + var vnode = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + var updated = {tag: "a", attrs: {style: "background-color:green;"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("updates style from string to object", function() { + var vnode = {tag: "a", attrs: {style: "background-color:red;"}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "green"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("updates style from string to string", function() { + var vnode = {tag: "a", attrs: {style: "background-color:red;"}} + var updated = {tag: "a", attrs: {style: "background-color:green;"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("green") + }) + o("removes style from object to object", function() { + var vnode = {tag: "a", attrs: {style: {backgroundColor: "red", border: "1px solid red"}}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("red") + o(updated.dom.style.border).equals("") + }) + o("removes style from string to object", function() { + var vnode = {tag: "a", attrs: {style: "background-color:red;border:1px solid red"}} + var updated = {tag: "a", attrs: {style: {backgroundColor: "red"}}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("red") + o(updated.dom.style.border).notEquals("1px solid red") + }) + o("removes style from object to string", function() { + var vnode = {tag: "a", attrs: {style: {backgroundColor: "red", border: "1px solid red"}}} + var updated = {tag: "a", attrs: {style: "background-color:red"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("red") + o(updated.dom.style.border).equals("") + }) + o("removes style from string to string", function() { + var vnode = {tag: "a", attrs: {style: "background-color:red;border:1px solid red"}} + var updated = {tag: "a", attrs: {style: "background-color:red"}} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom.style.backgroundColor).equals("red") + o(updated.dom.style.border).equals("") + }) + o("replaces el", function() { + var vnode = {tag: "a"} + var updated = {tag: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + }) +}) diff --git a/render/tests/test-updateFragment.js b/render/tests/test-updateFragment.js new file mode 100644 index 00000000..4b5bfd51 --- /dev/null +++ b/render/tests/test-updateFragment.js @@ -0,0 +1,69 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("updateFragment", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("updates fragment", function() { + var vnode = {tag: "[", children: [{tag: "a"}]} + var updated = {tag: "[", children: [{tag: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("B") + }) + o("adds els", function() { + var vnode = {tag: "[", children: []} + var updated = {tag: "[", children: [{tag: "a"}, {tag: "b"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(root.firstChild) + o(updated.domSize).equals(2) + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("removes els", function() { + var vnode = {tag: "[", children: [{tag: "a"}, {tag: "b"}]} + var updated = {tag: "[", children: []} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(null) + o(updated.domSize).equals(0) + o(root.childNodes.length).equals(0) + }) + o("updates from childless fragment", function() { + var vnode = {tag: "["} + var updated = {tag: "[", children: [{tag: "a"}]} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeName).equals("A") + }) + o("updates to childless fragment", function() { + var vnode = {tag: "[", children: [{tag: "a"}]} + var updated = {tag: "["} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(null) + o(root.childNodes.length).equals(0) + }) +}) diff --git a/render/tests/test-updateHTML.js b/render/tests/test-updateHTML.js new file mode 100644 index 00000000..72f77456 --- /dev/null +++ b/render/tests/test-updateHTML.js @@ -0,0 +1,49 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("updateHTML", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("updates html", function() { + var vnode = {tag: "<", children: "a"} + var updated = {tag: "<", children: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("b") + }) + o("adds html", function() { + var vnode = {tag: "<", children: ""} + var updated = {tag: "<", children: ""} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.domSize).equals(2) + o(updated.dom).equals(root.firstChild) + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("removes html", function() { + var vnode = {tag: "<", children: ""} + var updated = {tag: "<", children: ""} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(null) + o(updated.domSize).equals(0) + o(root.childNodes.length).equals(0) + }) +}) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js new file mode 100644 index 00000000..4511bb4e --- /dev/null +++ b/render/tests/test-updateNodes.js @@ -0,0 +1,789 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("updateNodes", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("handles el noop", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var updated = [{tag: "a", key: 1}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("handles el noop without key", function() { + var vnodes = [{tag: "a"}, {tag: "b"}] + var updated = [{tag: "a"}, {tag: "b"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("handles text noop", function() { + var vnodes = [{tag: "#", children: "a"}] + var updated = [{tag: "#", children: "a"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeValue).equals("a") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("handles text noop w/ type casting", function() { + var vnodes = [{tag: "#", children: 1}] + var updated = [{tag: "#", children: "1"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeValue).equals("1") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("handles falsy text noop w/ type casting", function() { + var vnodes = [{tag: "#", children: 0}] + var updated = [{tag: "#", children: "0"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeValue).equals("0") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("handles html noop", function() { + var vnodes = [{tag: "<", children: "a"}] + var updated = [{tag: "<", children: "a"}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeValue).equals("a") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("handles fragment noop", function() { + var vnodes = [{tag: "[", children: [{tag: "a"}]}] + var updated = [{tag: "[", children: [{tag: "a"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("handles fragment noop w/ text child", function() { + var vnodes = [{tag: "[", children: [{tag: "#", children: "a"}]}] + var updated = [{tag: "[", children: [{tag: "#", children: "a"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeValue).equals("a") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("reverses els w/ even count", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + var updated = [{tag: "s", key: 4}, {tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(4) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("B") + o(updated[2].dom).equals(root.childNodes[2]) + o(updated[3].dom.nodeName).equals("A") + o(updated[3].dom).equals(root.childNodes[3]) + }) + o("reverses els w/ odd count", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] + var updated = [{tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("A") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("creates el at start", function() { + var vnodes = [{tag: "a", key: 1}] + var updated = [{tag: "b", key: 2}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("B") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("creates el at end", function() { + var vnodes = [{tag: "a", key: 1}] + var updated = [{tag: "a", key: 1}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("creates el in middle", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var updated = [{tag: "a", key: 1}, {tag: "i", key: 3}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("B") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("creates el while reversing", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var updated = [{tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("B") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("A") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("deletes el at start", function() { + var vnodes = [{tag: "b", key: 2}, {tag: "a", key: 1}] + var updated = [{tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("deletes el at end", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var updated = [{tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("deletes el at middle", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "i", key: 3}, {tag: "b", key: 2}] + var updated = [{tag: "a", key: 1}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("deletes el while reversing", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "i", key: 3}, {tag: "b", key: 2}] + var updated = [{tag: "b", key: 2}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("B") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("creates, deletes, reverses els at same time", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "i", key: 3}, {tag: "b", key: 2}] + var updated = [{tag: "b", key: 2}, {tag: "a", key: 1}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("B") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("S") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("adds to empty array followed by el", function() { + var vnodes = [{tag: "[", key: 1, children: []}, {tag: "b", key: 2}] + var updated = [{tag: "[", key: 1, children: [{tag: "a"}]}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].children[0].dom.nodeName).equals("A") + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("reverses followed by el", function() { + var vnodes = [{tag: "[", key: 1, children: [{tag: "a", key: 2}, {tag: "b", key: 3}]}, {tag: "i", key: 4}] + var updated = [{tag: "[", key: 1, children: [{tag: "b", key: 3}, {tag: "a", key: 2}]}, {tag: "i", key: 4}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].children[0].dom.nodeName).equals("B") + o(updated[0].children[0].dom).equals(root.childNodes[0]) + o(updated[0].children[1].dom.nodeName).equals("A") + o(updated[0].children[1].dom).equals(root.childNodes[1]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[2]) + }) + o("updates empty array to html with same key", function() { + var vnodes = [{tag: "[", key: 1, children: []}] + var updated = [{tag: "<", key: 1, children: ""}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates empty html to array with same key", function() { + var vnodes = [{tag: "<", key: 1, children: ""}] + var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates empty array to html without key", function() { + var vnodes = [{tag: "[", children: []}] + var updated = [{tag: "<", children: ""}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates empty html to array without key", function() { + var vnodes = [{tag: "<", children: ""}] + var updated = [{tag: "[", children: [{tag: "a"}, {tag: "b"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates array to html with same key", function() { + var vnodes = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}] + var updated = [{tag: "<", key: 1, children: ""}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("S") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates html to array with same key", function() { + var vnodes = [{tag: "<", key: 1, children: ""}] + var updated = [{tag: "[", key: 1, children: [{tag: "i"}, {tag: "s"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("S") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates array to html without key", function() { + var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: "b"}]}] + var updated = [{tag: "<", children: ""}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("S") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates html to array without key", function() { + var vnodes = [{tag: "<", children: ""}] + var updated = [{tag: "[", children: [{tag: "i"}, {tag: "s"}]}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("S") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + }) + o("updates empty array to html with same key followed by el", function() { + var vnodes = [{tag: "[", key: 1, children: []}, {tag: "i", key: 2}] + var updated = [{tag: "<", key: 1, children: ""}, {tag: "i", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[2]) + }) + o("updates empty html to array with same key followed by el", function() { + var vnodes = [{tag: "[", key: 1, children: []}, {tag: "i", key: 2}] + var updated = [{tag: "<", key: 1, children: ""}, {tag: "i", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[2]) + }) + o("populates array followed by null then el", function() { + var vnodes = [{tag: "[", key: 1, children: []}, null, {tag: "i", key: 2}] + var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("I") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("populates childless array followed by el", function() { + var vnodes = [{tag: "[", key: 1}, {tag: "i", key: 2}] + var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, {tag: "i", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[2]) + }) + o("populates childless array followed by null then el", function() { + var vnodes = [{tag: "[", key: 1}, null, {tag: "i", key: 2}] + var updated = [{tag: "[", key: 1, children: [{tag: "a"}, {tag: "b"}]}, null, {tag: "i", key: 2}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[0].domSize).equals(2) + o(updated[0].dom.nextSibling.nodeName).equals("B") + o(updated[0].dom.nextSibling).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("I") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("moves from end to start", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + var updated = [{tag: "s", key: 4}, {tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(4) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("B") + o(updated[2].dom).equals(root.childNodes[2]) + o(updated[3].dom.nodeName).equals("I") + o(updated[3].dom).equals(root.childNodes[3]) + }) + o("moves from start to end", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + var updated = [{tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, updated) + + o(root.childNodes.length).equals(4) + o(updated[0].dom.nodeName).equals("B") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("S") + o(updated[2].dom).equals(root.childNodes[2]) + o(updated[3].dom.nodeName).equals("A") + o(updated[3].dom).equals(root.childNodes[3]) + }) + o("removes then recreate", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + var temp = [] + var updated = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(4) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("I") + o(updated[2].dom).equals(root.childNodes[2]) + o(updated[3].dom.nodeName).equals("S") + o(updated[3].dom).equals(root.childNodes[3]) + }) + o("removes then recreate reversed", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}, {tag: "s", key: 4}] + var temp = [] + var updated = [{tag: "s", key: 4}, {tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(4) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("B") + o(updated[2].dom).equals(root.childNodes[2]) + o(updated[3].dom.nodeName).equals("A") + o(updated[3].dom).equals(root.childNodes[3]) + }) + o("removes then recreate smaller", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "a", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("removes then recreate bigger", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("I") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("removes then create different", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "i", key: 3}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("S") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("removes then create different smaller", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "i", key: 3}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(1) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + }) + o("removes then create different bigger", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "i", key: 3}, {tag: "s", key: 4}, {tag: "div", key: 5}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("I") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("S") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("DIV") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("removes then create mixed", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "a", key: 1}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("S") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("removes then create mixed reversed", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "s", key: 4}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("removes then create mixed smaller", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] + var temp = [] + var updated = [{tag: "a", key: 1}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("S") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("removes then create mixed smaller reversed", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}] + var temp = [] + var updated = [{tag: "s", key: 4}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("A") + o(updated[1].dom).equals(root.childNodes[1]) + }) + o("removes then create mixed bigger", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "a", key: 1}, {tag: "i", key: 3}, {tag: "s", key: 4}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("S") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("removes then create mixed bigger reversed", function() { + var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] + var temp = [] + var updated = [{tag: "s", key: 4}, {tag: "i", key: 3}, {tag: "a", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(3) + o(updated[0].dom.nodeName).equals("S") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("I") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[2].dom.nodeName).equals("A") + o(updated[2].dom).equals(root.childNodes[2]) + }) + o("removes then recreates then reverses children", function() { + var vnodes = [{tag: "a", key: 1, children: [{tag: "i", key: 3}, {tag: "s", key: 4}]}, {tag: "b", key: 2}] + var temp1 = [] + var temp2 = [{tag: "a", key: 1, children: [{tag: "i", key: 3}, {tag: "s", key: 4}]}, {tag: "b", key: 2}] + var updated = [{tag: "a", key: 1, children: [{tag: "s", key: 4}, {tag: "i", key: 3}]}, {tag: "b", key: 2}] + + render(root, vnodes) + render(root, temp1) + render(root, temp2) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(updated[0].dom.nodeName).equals("A") + o(updated[0].dom).equals(root.childNodes[0]) + o(updated[1].dom.nodeName).equals("B") + o(updated[1].dom).equals(root.childNodes[1]) + o(updated[0].dom.childNodes.length).equals(2) + o(updated[0].dom.childNodes[0].nodeName).equals("S") + o(updated[0].dom.childNodes[1].nodeName).equals("I") + }) + o("removes then recreates nested", function() { + var vnodes = [{tag: "a", key: 1, children: [{tag: "a", key: 3, children: [{tag: "a", key: 5}]}, {tag: "a", key: 4, children: [{tag: "a", key: 5}]}]}, {tag: "a", key: 2}] + var temp = [] + var updated = [{tag: "a", key: 1, children: [{tag: "a", key: 3, children: [{tag: "a", key: 5}]}, {tag: "a", key: 4, children: [{tag: "a", key: 5}]}]}, {tag: "a", key: 2}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].childNodes.length).equals(2) + o(root.childNodes[0].childNodes[0].childNodes.length).equals(1) + o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) + o(root.childNodes[1].childNodes.length).equals(0) + }) + o("recycles", function() { + var vnodes = [{tag: "div", key: 1}] + var temp = [] + var updated = [{tag: "div", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(vnodes[0].dom).equals(updated[0].dom) + o(updated[0].dom.nodeName).equals("DIV") + }) + o("recycles when toggling", function() { + var vnodes = [{tag: "div", key: 1}] + var temp = [{tag: "div"}] + var updated = [{tag: "div", key: 1}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(vnodes[0].dom).equals(updated[0].dom) + o(updated[0].dom.nodeName).equals("DIV") + }) + o("recycles deep", function() { + var vnodes = [{tag: "div", children: [{tag: "a", key: 1}]}] + var temp = [{tag: "div"}] + var updated = [{tag: "div", children: [{tag: "a", key: 1}]}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(vnodes[0].dom.firstChild).equals(updated[0].dom.firstChild) + o(updated[0].dom.firstChild.nodeName).equals("A") + }) +}) diff --git a/render/tests/test-updateText.js b/render/tests/test-updateText.js new file mode 100644 index 00000000..e543eec4 --- /dev/null +++ b/render/tests/test-updateText.js @@ -0,0 +1,114 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") +var vdom = require("../../render/render") + +o.spec("updateText", function() { + var $window, root, render + o.beforeEach(function() { + $window = domMock() + root = $window.document.createElement("div") + render = vdom($window).render + }) + + o("updates to string", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("b") + }) + o("updates to falsy string", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: ""} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("") + }) + o("updates from falsy string", function() { + var vnode = {tag: "#", children: ""} + var updated = {tag: "#", children: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("b") + }) + o("updates to number", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: 1} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("1") + }) + o("updates to falsy number", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: 0} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("0") + }) + o("updates from falsy number", function() { + var vnode = {tag: "#", children: 0} + var updated = {tag: "#", children: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("b") + }) + o("updates to boolean", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: true} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("true") + }) + o("updates to falsy boolean", function() { + var vnode = {tag: "#", children: "a"} + var updated = {tag: "#", children: false} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("false") + }) + o("updates from falsy boolean", function() { + var vnode = {tag: "#", children: false} + var updated = {tag: "#", children: "b"} + + render(root, [vnode]) + render(root, [updated]) + + o(updated.dom).equals(vnode.dom) + o(updated.dom).equals(root.firstChild) + o(updated.dom.nodeValue).equals("b") + }) +}) diff --git a/render/trust.js b/render/trust.js new file mode 100644 index 00000000..9cf2f7ec --- /dev/null +++ b/render/trust.js @@ -0,0 +1,3 @@ +module.exports = function(html) { + return {tag: "<", key: undefined, attrs: undefined, children: html, text: undefined} +} \ No newline at end of file diff --git a/request/request.js b/request/request.js new file mode 100644 index 00000000..d5b9aa01 --- /dev/null +++ b/request/request.js @@ -0,0 +1,114 @@ +"use strict" + +var buildQueryString = require("../querystring/build") + +module.exports = function($window, Promise) { + var callbackCount = 0 + + function ajax(args) { + return new Promise(function(resolve, reject) { + var useBody = args.useBody != null ? args.useBody : args.method !== "GET" && args.method !== "TRACE" + + if (typeof args.serialize !== "function") args.serialize = JSON.stringify + if (typeof args.deserialize !== "function") args.deserialize = deserialize + if (typeof args.extract !== "function") args.extract = extract + + args.url = interpolate(args.url, args.data) + if (useBody) args.data = args.serialize(args.data) + else args.url = assemble(args.url, args.data) + + var xhr = new $window.XMLHttpRequest() + xhr.open(args.method, args.url, args.async || true, args.user, args.password) + + if (args.serialize === JSON.stringify && useBody) { + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") + } + if (args.deserialize === deserialize) { + xhr.setRequestHeader("Accept", "application/json, text/*") + } + + if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr + + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + try { + var response = args.deserialize(args.extract(xhr, args)) + if (xhr.status >= 200 && xhr.status < 300) { + if (typeof args.type === "function") { + if (response instanceof Array) { + for (var i = 0; i < response.length; i++) { + response[i] = new args.type(response[i]) + } + } + else response = new args.type(response[i]) + } + + resolve(response) + } + else reject(new Error(xhr.responseText)) + } + catch (e) { + reject(e) + } + } + } + + if (useBody) xhr.send(args.data) + else xhr.send() + }) + } + + function jsonp(args) { + return new Promise(function(resolve, reject) { + var callbackKey = "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ + var script = $window.document.createElement("script") + $window[callbackKey] = function(data) { + script.parentNode.removeChild(script) + resolve(data) + $window[callbackKey] = undefined + } + script.onerror = function(e) { + script.parentNode.removeChild(script) + reject(new Error("JSONP request failed")) + $window[callbackKey] = undefined + } + if (args.data == null) args.data = {} + args.url = interpolate(args.url, args.data) + args.data[args.callbackKey || "callback"] = callbackKey + script.src = assemble(args.url, args.data) + $window.document.documentElement.appendChild(script) + }) + } + + function interpolate(url, data) { + if (data == null) return url + + var tokens = url.match(/:[^\/]+/gi) || [] + for (var i = 0; i < tokens.length; i++) { + var key = tokens[i].slice(1) + if (data[key] != null) { + url = url.replace(tokens[i], data[key]) + delete data[key] + } + } + return url + } + + function assemble(url, data) { + var querystring = buildQueryString(data) + if (querystring !== "") { + var prefix = url.indexOf("?") < 0 ? "?" : "&" + url += prefix + querystring + } + return url + } + + function deserialize(data) { + try {return data !== "" ? JSON.parse(data) : null} + catch (e) {throw new Error(data)} + } + + function extract(xhr) {return xhr.responseText} + + return {ajax: ajax, jsonp: jsonp} +} \ No newline at end of file diff --git a/request/tests/index.html b/request/tests/index.html new file mode 100644 index 00000000..a4918fbd --- /dev/null +++ b/request/tests/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/request/tests/test-ajax.js b/request/tests/test-ajax.js new file mode 100644 index 00000000..22ff8f60 --- /dev/null +++ b/request/tests/test-ajax.js @@ -0,0 +1,141 @@ +"use strict" + +var o = require("../../ospec/ospec") +var ajaxMock = require("../../test-utils/ajaxMock") +var Request = require("../../request/request") + +o.spec("ajax", function() { + var mock, ajax + o.beforeEach(function() { + mock = ajaxMock() + ajax = new Request(mock, Promise).ajax + }) + + o.spec("success", function() { + o("works via GET", function(done) { + var s = new Date + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: JSON.stringify({a: 1})} + } + }) + ajax({method: "GET", url: "/item"}).then(function(data) { + o(data).deepEquals({a: 1}) + }).then(function() { + done() + }) + }) + o("works via POST", function(done) { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: JSON.stringify({a: 1})} + } + }) + ajax({method: "GET", url: "/item"}).then(function(data) { + o(data).deepEquals({a: 1}) + }).then(done) + }) + o("works w/ parameterized data via GET", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.query})} + } + }) + ajax({method: "GET", url: "/item", data: {x: "y"}}).then(function(data) { + o(data).deepEquals({a: "?x=y"}) + }).then(done) + }) + o("works w/ parameterized data via POST", function(done) { + mock.$defineRoutes({ + "POST /item": function(request) { + return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} + } + }) + ajax({method: "POST", url: "/item", data: {x: "y"}}).then(function(data) { + o(data).deepEquals({a: {x: "y"}}) + }).then(done) + }) + o("works w/ parameterized data containing colon via GET", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.query})} + } + }) + ajax({method: "GET", url: "/item", data: {x: ":y"}}).then(function(data) { + o(data).deepEquals({a: "?x=%3Ay"}) + }).then(done) + }) + o("works w/ parameterized data containing colon via POST", function(done) { + mock.$defineRoutes({ + "POST /item": function(request) { + return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} + } + }) + ajax({method: "POST", url: "/item", data: {x: ":y"}}).then(function(data) { + o(data).deepEquals({a: {x: ":y"}}) + }).then(done) + }) + o("works w/ parameterized url via GET", function(done) { + mock.$defineRoutes({ + "GET /item/y": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query})} + } + }) + ajax({method: "GET", url: "/item/:x", data: {x: "y"}}).then(function(data) { + o(data).deepEquals({a: "/item/y", b: {}}) + }).then(done) + }) + o("works w/ parameterized url via POST", function(done) { + mock.$defineRoutes({ + "POST /item/y": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})} + } + }) + ajax({method: "POST", url: "/item/:x", data: {x: "y"}}).then(function(data) { + o(data).deepEquals({a: "/item/y", b: {}}) + }).then(done) + }) + o("ignores unresolved parameter via GET", function(done) { + mock.$defineRoutes({ + "GET /item/:x": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.url})} + } + }) + ajax({method: "GET", url: "/item/:x"}).then(function(data) { + o(data).deepEquals({a: "/item/:x"}) + }).then(done) + }) + o("ignores unresolved parameter via POST", function(done) { + mock.$defineRoutes({ + "GET /item/:x": function(request) { + return {status: 200, responseText: JSON.stringify({a: request.url})} + } + }) + ajax({method: "GET", url: "/item/:x"}).then(function(data) { + o(data).deepEquals({a: "/item/:x"}) + }).then(done) + }) + }) + o.spec("failure", function() { + o("rejects on server error", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 500, responseText: JSON.stringify({error: "error"})} + } + }) + ajax({method: "GET", url: "/item"}).catch(function(e) { + o(e.message).equals(JSON.stringify({error: "error"})) + }).then(done) + }) + o("rejects on non-JSON server error", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + return {status: 500, responseText: "error"} + } + }) + ajax({method: "GET", url: "/item"}).catch(function(e) { + o(e.message).equals("error") + }).then(done) + }) + }) +}) \ No newline at end of file diff --git a/request/tests/test-jsonp.js b/request/tests/test-jsonp.js new file mode 100644 index 00000000..04b47c76 --- /dev/null +++ b/request/tests/test-jsonp.js @@ -0,0 +1,55 @@ +"use strict" + +var o = require("../../ospec/ospec") +var ajaxMock = require("../../test-utils/ajaxMock") +var Request = require("../../request/request") +var parseQueryString = require("../../querystring/parse") + +o.spec("jsonp", function() { + var mock, jsonp + o.beforeEach(function() { + mock = ajaxMock() + jsonp = new Request(mock, Promise).jsonp + }) + + o("works", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["callback"] + "(" + JSON.stringify({a: 1}) + ")"} + } + }) + jsonp({url: "/item"}).then(function(data) { + o(data).deepEquals({a: 1}) + }).then(done) + }) + o("works w/ other querystring params", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["callback"] + "(" + JSON.stringify(queryData) + ")"} + } + }) + jsonp({url: "/item", data: {a: "b", c: "d"}}).then(function(data) { + delete data["callback"] + o(data).deepEquals({a: "b", c: "d"}) + }).then(done) + }) + o("works w/ custom callbackKey", function(done) { + mock.$defineRoutes({ + "GET /item": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["cb"] + "(" + JSON.stringify({a: 2}) + ")"} + } + }) + jsonp({url: "/item", callbackKey: "cb"}).then(function(data) { + o(data).deepEquals({a: 2}) + }).then(done) + }) + o("handles error", function(done) { + jsonp({url: "/item", callbackKey: "cb"}).catch(function(e) { + o(e.message).equals("JSONP request failed") + done() + }) + }) +}) \ No newline at end of file diff --git a/router/router.js b/router/router.js new file mode 100644 index 00000000..90f539ab --- /dev/null +++ b/router/router.js @@ -0,0 +1,87 @@ +"use strict" + +var buildQueryString = require("../querystring/build") +var parseQueryString = require("../querystring/parse") + +module.exports = function($window, prefix) { + var supportsPushState = typeof $window.history.pushState === "function" && $window.location.protocol !== "file:" + + function parsePath(path) { + var params = {} + var queryIndex = path.indexOf("?") + var hashIndex = path.indexOf("#") + var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length + if (queryIndex > -1) { + var queryEnd = hashIndex > -1 ? hashIndex : path.length + var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd)) + for (var key in queryParams) params[key] = queryParams[key] + } + if (hashIndex > -1) { + var hashParams = parseQueryString(path.slice(hashIndex + 1)) + for (var key in hashParams) params[key] = hashParams[key] + } + return {name: path.slice(0, pathEnd), params: params} + } + + function getPath() { + var type = prefix.charAt(0) + switch (type) { + case "#": return $window.location.hash.slice(prefix.length) + case "?": return $window.location.search.slice(prefix.length) + $window.location.hash + default: return $window.location.pathname + $window.location.search + $window.location.hash + } + } + + function setPath(path, data, options) { + if (supportsPushState) { + var queryData = {} + if (data != null) { + for (var key in data) queryData[key] = data[key] + path = path.replace(/:([^\/]+)/g, function(match, token) { + delete queryData[token] + return data[token] + }) + } + + var query = buildQueryString(queryData) + if (query) path = path + "?" + query + + if (options && options.replace) $window.history.replaceState(null, null, prefix + path) + else $window.history.pushState(null, null, prefix + path) + $window.onpopstate() + } + else $window.location.href = prefix + path + } + + function defineRoutes(routes, resolve, reject) { + if (supportsPushState) $window.onpopstate = resolveRoute + else if (prefix.charAt(0) === "#") $window.onhashchange = resolveRoute + resolveRoute() + + function resolveRoute(e) { + var path = getPath() + var data = parsePath(path) + + for (var route in routes) { + var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") + + if (matcher.test(data.name)) { + data.name.replace(matcher, function() { + var keys = route.match(/:[^\/]+/g) || [] + var values = [].slice.call(arguments, 1, -2) + for (var i = 0; i < keys.length; i++) { + data.params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) + } + resolve(routes[route], data.params, path, route) + }) + return + } + } + + reject(path, data.params) + } + return resolveRoute + } + + return {getPath: getPath, setPath: setPath, defineRoutes: defineRoutes} +} \ No newline at end of file diff --git a/router/tests/index.html b/router/tests/index.html new file mode 100644 index 00000000..7d87aa12 --- /dev/null +++ b/router/tests/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/router/tests/test-defineRoutes.js b/router/tests/test-defineRoutes.js new file mode 100644 index 00000000..878999be --- /dev/null +++ b/router/tests/test-defineRoutes.js @@ -0,0 +1,229 @@ +"use strict" + +var o = require("../../ospec/ospec") +var pushStateMock = require("../../test-utils/pushStateMock") +var Router = require("../../router/router") + +o.spec("router", function() { + void ["#", "?", "", "#!", "?!"].forEach(function(prefix) { + o.spec("using prefix `" + prefix + "`", function() { + var $window, router, onRouteChange, onFail + + o.beforeEach(function() { + $window = pushStateMock() + router = new Router($window, prefix) + onRouteChange = o.spy() + onFail = o.spy() + }) + + o.spec("defineRoutes", function() { + o("resolves to route", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) + o(onFail.callCount).equals(0) + }) + + o("handles parameterized route", function() { + $window.location.href = prefix + "/test/x" + router.defineRoutes({"/test/:a": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "x"}, "/test/x", "/test/:a"]) + o(onFail.callCount).equals(0) + }) + + o("handles multi-parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + router.defineRoutes({"/test/:a/:b": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "x", b: "y"}, "/test/x/y", "/test/:a/:b"]) + o(onFail.callCount).equals(0) + }) + + o("handles rest parameterized route", function() { + $window.location.href = prefix + "/test/x/y" + router.defineRoutes({"/test/:a...": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "x/y"}, "/test/x/y", "/test/:a..."]) + o(onFail.callCount).equals(0) + }) + + o("handles route with search", function() { + $window.location.href = prefix + "/test?a=b&c=d" + router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b&c=d", "/test"]) + o(onFail.callCount).equals(0) + }) + + o("handles route with hash", function() { + $window.location.href = prefix + "/test#a=b&c=d" + router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test#a=b&c=d", "/test"]) + o(onFail.callCount).equals(0) + }) + + o("handles route with search and hash", function() { + $window.location.href = prefix + "/test?a=b#c=d" + router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {a: "b", c: "d"}, "/test?a=b#c=d", "/test"]) + o(onFail.callCount).equals(0) + }) + + o("calls reject", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/other": {data: 1}}, onRouteChange, onFail) + + o(onFail.callCount).equals(1) + o(onFail.args).deepEquals(["/test", {}]) + }) + + o("calls reject w/ search and hash", function() { + $window.location.href = prefix + "/test?a=b#c=d" + router.defineRoutes({"/other": {data: 1}}, onRouteChange, onFail) + + o(onFail.callCount).equals(1) + o(onFail.args).deepEquals(["/test?a=b#c=d", {a: "b", c: "d"}]) + }) + + o("handles out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + router.defineRoutes({"/z/y/x": {data: 1}, "/:a...": {data: 2}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) + }) + + o("handles reverse out of order routes", function() { + $window.location.href = prefix + "/z/y/x" + router.defineRoutes({"/:a...": {data: 2}, "/z/y/x": {data: 1}}, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) + }) + + o("handles dynamically added out of order routes", function() { + var routes = {} + routes["/z/y/x"] = {data: 1} + routes["/:a..."] = {data: 2} + + $window.location.href = prefix + "/z/y/x" + router.defineRoutes(routes, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) + }) + + o("handles reversed dynamically added out of order routes", function() { + var routes = {} + routes["/:a..."] = {data: 2} + routes["/z/y/x"] = {data: 1} + + $window.location.href = prefix + "/z/y/x" + router.defineRoutes(routes, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) + }) + + o("handles mixed out of order routes", function() { + var routes = {"/z/y/x": {data: 1}} + routes["/:a..."] = {data: 2} + + $window.location.href = prefix + "/z/y/x" + router.defineRoutes(routes, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 1}, {}, "/z/y/x", "/z/y/x"]) + }) + + o("handles reverse mixed out of order routes", function() { + var routes = {"/:a...": {data: 2}} + routes["/z/y/x"] = {data: 12} + + $window.location.href = prefix + "/z/y/x" + router.defineRoutes(routes, onRouteChange, onFail) + + o(onRouteChange.callCount).equals(1) + o(onRouteChange.args).deepEquals([{data: 2}, {a: "z/y/x"}, "/z/y/x", "/:a..."]) + }) + + o("replays", function() { + $window.location.href = prefix + "/test" + var replay = router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + replay() + + o(onRouteChange.callCount).equals(2) + o(onRouteChange.args).deepEquals([{data: 1}, {}, "/test", "/test"]) + o(onFail.callCount).equals(0) + }) + }) + + o.spec("getPath", function() { + o("gets route", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}}, onRouteChange, onFail) + + o(router.getPath()).equals("/test") + }) + o("gets route w/ params", function() { + $window.location.href = prefix + "/other/x/y/z?c=d#e=f" + router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail) + + o(router.getPath()).equals("/other/x/y/z?c=d#e=f") + }) + }) + + o.spec("setPath", function() { + o("sets route via API", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail) + router.setPath("/other/x/y/z?c=d#e=f") + + o(router.getPath()).equals("/other/x/y/z?c=d#e=f") + }) + o("sets route via pushState/onpopstate", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail) + $window.history.pushState(null, null, prefix + "/other/x/y/z?c=d#e=f") + $window.onpopstate() + + o(router.getPath()).equals("/other/x/y/z?c=d#e=f") + }) + o("sets parameterized route", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other/:a/:b...": {data: 2}}, onRouteChange, onFail) + router.setPath("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) + + o(router.getPath()).equals("/other/x/y/z?c=d&e=f") + }) + o("replace:true works", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail) + router.setPath("/other", null, {replace: true}) + $window.history.back() + + o($window.location.href).equals("http://localhost/") + }) + o("replace:false works", function() { + $window.location.href = prefix + "/test" + router.defineRoutes({"/test": {data: 1}, "/other": {data: 2}}, onRouteChange, onFail) + router.setPath("/other", null, {replace: false}) + $window.history.back() + + o($window.location.href).equals("http://localhost/" + (prefix ? prefix + "/" : "") + "test") + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test-utils/README.md b/test-utils/README.md new file mode 100644 index 00000000..a82cb9d2 --- /dev/null +++ b/test-utils/README.md @@ -0,0 +1,12 @@ +# Test utils + +Utilities for testing Mithril + +Version: 1.0 +License: MIT + +## About + +- pushStateMock - mock for `history.pushState` and `location` +- ajaxMock - mock for XMLHttpRequest and JSONP transporters +- parseURL - helper function for URL parsing \ No newline at end of file diff --git a/test-utils/ajaxMock.js b/test-utils/ajaxMock.js new file mode 100644 index 00000000..a8b68666 --- /dev/null +++ b/test-utils/ajaxMock.js @@ -0,0 +1,77 @@ +"use strict" + +var callAsync = require("../test-utils/callAsync") +var parseURL = require("../test-utils/parseURL") +var parseQueryString = require("../querystring/parse") + +module.exports = function() { + var routes = {} + var callback = "callback" + var serverErrorHandler = function() { + return {status: 500, responseText: "server error"} + } + + var $window = { + XMLHttpRequest: function XMLHttpRequest() { + var args = {} + this.setRequestHeader = function(header, value) {} + this.open = function(method, url, async, user, password) { + var urlData = parseURL(url, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) + args.method = method + args.pathname = urlData.pathname + args.search = urlData.search + args.async = async != null ? async : true + args.user = user + args.password = password + } + this.send = function(body) { + var self = this + var handler = routes[args.method + " " + args.pathname] || serverErrorHandler + var data = handler({url: args.pathname, query: args.search || {}, body: body || null}) + self.readyState = 4 + self.status = data.status + self.responseText = data.responseText + if (args.async === true) { + var s = new Date + callAsync(function() { + if (typeof self.onreadystatechange === "function") self.onreadystatechange() + }) + } + } + }, + document: { + createElement: function(tag) { + return {nodeName: tag.toUpperCase(), parentNode: null} + }, + documentElement: { + appendChild: function(element) { + element.parentNode = this + if (element.nodeName === "SCRIPT") { + var urlData = parseURL(element.src, {protocol: "http:", hostname: "localhost", port: "", pathname: "/"}) + var handler = routes["GET " + urlData.pathname] || serverErrorHandler + var data = handler({url: urlData.pathname, query: urlData.search, body: null}) + var query = parseQueryString(urlData.search) + callAsync(function() { + if (data.status === 200) { + new Function("$window", "with ($window) return " + data.responseText).call($window, $window) + } + else if (typeof element.onerror === "function") { + element.onerror({type: "error"}) + } + }) + } + }, + removeChild: function(element) { + element.parentNode = null + }, + }, + }, + $defineRoutes: function(rules) { + routes = rules + }, + $defineJSONPCallbackKey: function(key) { + callback = key + }, + } + return $window +} \ No newline at end of file diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js new file mode 100644 index 00000000..92604018 --- /dev/null +++ b/test-utils/callAsync.js @@ -0,0 +1,3 @@ +"use strict" + +module.exports = typeof process === "object" ? process.nextTick : window.setImmediate || window.setTimeout \ No newline at end of file diff --git a/test-utils/domMock.js b/test-utils/domMock.js new file mode 100644 index 00000000..f85b40bb --- /dev/null +++ b/test-utils/domMock.js @@ -0,0 +1,219 @@ +"use strict" + +module.exports = function() { + function appendChild(child) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var index = this.childNodes.indexOf(child) + if (index > -1) this.childNodes.splice(index, 1) + if (child.nodeType === 11) { + while (child.firstChild != null) this.appendChild(child.firstChild) + child.childNodes = [] + } + else { + this.childNodes.push(child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + } + function removeChild(child) { + var index = this.childNodes.indexOf(child) + if (index > -1) { + this.childNodes.splice(index, 1) + child.parentNode = null + } + else throw new TypeError("Failed to execute 'removeChild'") + } + function insertBefore(child, reference) { + var ancestor = this + while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode + if (ancestor === child) throw new Error("Node cannot be inserted at the specified point in the hierarchy") + + if (child.nodeType == null) throw new Error("Argument is not a DOM element") + + var refIndex = this.childNodes.indexOf(reference) + var index = this.childNodes.indexOf(child) + if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument") + if (index > -1) this.childNodes.splice(index, 1) + if (reference === null) this.appendChild(child) + else { + if (child.nodeType === 11) { + this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes)) + while (child.firstChild) { + var subchild = child.firstChild + child.removeChild(subchild) + subchild.parentNode = this + } + child.childNodes = [] + } + else { + this.childNodes.splice(refIndex, 0, child) + if (child.parentNode != null && child.parentNode !== this) child.parentNode.removeChild(child) + child.parentNode = this + } + } + } + function setAttribute(name, value) { + var nodeValue = String(value) + this.attributes[name] = { + namespaceURI: null, + get nodeValue() {return nodeValue}, + set nodeValue(value) {nodeValue = String(value)}, + } + } + function setAttributeNS(ns, name, value) { + this.setAttribute(name, value) + this.attributes[name].namespaceURI = ns + } + function removeAttribute(name) { + delete this.attributes[name] + } + var activeElement + var $window = { + document: { + createElement: function(tag, is) { + var style = {} + var element = { + nodeType: 1, + nodeName: tag.toUpperCase(), + namespaceURI: "http://www.w3.org/1999/xhtml", + appendChild: appendChild, + removeChild: removeChild, + insertBefore: insertBefore, + setAttribute: setAttribute, + setAttributeNS: setAttributeNS, + removeAttribute: removeAttribute, + parentNode: null, + childNodes: [], + attributes: {}, + get firstChild() { + return this.childNodes[0] || null + }, + get nextSibling() { + if (this.parentNode == null) return null + var index = this.parentNode.childNodes.indexOf(this) + if (index < 0) throw new TypeError("Parent's childNodes is out of sync") + return this.parentNode.childNodes[index + 1] || null + }, + set textContent(value) { + this.childNodes = [] + if (value !== "") this.appendChild($window.document.createTextNode(value)) + }, + set innerHTML(value) { + while (this.firstChild) this.removeChild(this.firstChild) + + var stack = [this], depth = 0, voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] + value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) { + if (startTag) { + var element = $window.document.createElement(startTag) + attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) { + var keyParts = key.split(":") + var name = keyParts.pop() + var ns = keyParts[0] + var value = doubleQuoted || singleQuoted || unquoted || "" + if (ns != null) element.setAttributeNS(ns, name, value) + else element.setAttribute(name, value) + }) + stack[depth].appendChild(element) + if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element + } + else if (endTag) { + depth-- + } + else if (text) { + stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities + } + }) + }, + get style() { + return style + }, + set style(value) { + if (typeof value === "string") { + for (var key in style) style[key] = "" + var rules = value.split(";") + for (var i = 0; i < rules.length; i++) { + var rule = rules[i] + var colonIndex = rule.indexOf(":") + if (colonIndex > -1) { + var key = rule.slice(0, colonIndex).trim().replace(/-\D/g, function(match) {return match[1].toUpperCase()}) + var value = rule.slice(colonIndex + 1).trim() + style[key] = value + } + } + } + }, + focus: function() {activeElement = this}, + dispatchEvent: function(e) { + e.target = this + if (typeof this["on" + e.type] === "function") this["on" + e.type](e) + }, + } + if (element.nodeName === "A") { + var href + Object.defineProperty(element, "href", { + get: function() {return this.attributes["href"] === undefined ? "" : "[FIXME implement]"}, + set: function(value) {this.setAttribute("href", value)}, + enumerable: true, + }) + } + if (element.nodeName === "INPUT") { + var checked + Object.defineProperty(element, "checked", { + get: function() {return checked === undefined ? this.attributes["checked"] !== undefined : checked}, + set: function(value) {checked = Boolean(value)}, + enumerable: true, + }) + } + return element + }, + createElementNS: function(ns, tag, is) { + var element = this.createElement(tag, is) + element.nodeName = tag + element.namespaceURI = ns + return element + }, + createTextNode: function(text) { + var nodeValue = String(text) + return { + nodeType: 3, + nodeName: "#text", + parentNode: null, + get nodeValue() {return nodeValue}, + set nodeValue(value) {nodeValue = String(value)}, + } + }, + createDocumentFragment: function() { + return { + nodeType: 11, + nodeName: "#document-fragment", + appendChild: appendChild, + insertBefore: insertBefore, + removeChild: removeChild, + parentNode: null, + childNodes: [], + get firstChild() { + return this.childNodes[0] || null + }, + } + }, + createEvent: function() { + return { + initEvent: function(type) {this.type = type}, + } + }, + get activeElement() {return activeElement}, + }, + } + $window.document.documentElement = $window.document.createElement("html") + $window.document.documentElement.appendChild($window.document.createElement("head")) + $window.document.body = $window.document.createElement("body") + $window.document.documentElement.appendChild($window.document.body) + activeElement = $window.document.body + + return $window +} \ No newline at end of file diff --git a/test-utils/parseURL.js b/test-utils/parseURL.js new file mode 100644 index 00000000..f5e76e8d --- /dev/null +++ b/test-utils/parseURL.js @@ -0,0 +1,47 @@ +"use strict" + +module.exports = function parseURL(url, root) { + var data = {} + var protocolIndex = url.indexOf("://") + var pathnameIndex = protocolIndex > - 1 ? url.indexOf("/", protocolIndex + 3) : url.indexOf("/") + var searchIndex = url.indexOf("?") + var hashIndex = url.indexOf("#") + if ((pathnameIndex > searchIndex && searchIndex > -1) || (pathnameIndex > hashIndex && hashIndex > -1)) pathnameIndex = -1 + if (searchIndex > hashIndex && hashIndex > -1) searchIndex = -1 + var pathnameEnd = searchIndex > -1 ? searchIndex : hashIndex > -1 ? hashIndex : url.length + if (protocolIndex > -1) { + //it's a full URL + if (pathnameIndex < 0) pathnameIndex = url.length + var portIndex = url.indexOf(":", protocolIndex + 1) + if (portIndex < 0) portIndex = pathnameIndex + data.protocol = url.slice(0, protocolIndex + 1) + data.hostname = url.slice(protocolIndex + 3, portIndex) + data.port = url.slice(portIndex + 1, pathnameIndex) + data.pathname = url.slice(pathnameIndex, pathnameEnd) || "/" + } + else { + data.protocol = root.protocol + data.hostname = root.hostname + data.port = root.port + if (pathnameIndex === 0) { + //it's an absolute path + data.pathname = url.slice(pathnameIndex, pathnameEnd) || "/" + } + else if (searchIndex !== 0 && hashIndex !== 0) { + //it's a relative path + var slashIndex = root.pathname.lastIndexOf("/") + var path = slashIndex > -1 ? root.pathname.slice(0, slashIndex + 1) : "./" + var normalized = url.slice(0, pathnameEnd).replace(/^\.$/, root.pathname.slice(slashIndex + 1)).replace(/^\.\//, "") + var dotdot = /\/[^\/]+?\/\.{2}/g + var pathname = path + normalized + pathname = path + normalized + while (dotdot.test(pathname)) pathname = pathname.replace(dotdot, "") + pathname = pathname.replace(/\/\.\//g, "/").replace(/^(\/\.{2})+/, "") || "/" + data.pathname = pathname + } + } + var searchEnd = hashIndex > -1 ? hashIndex : url.length + data.search = searchIndex > -1 ? url.slice(searchIndex, searchEnd) : "" + data.hash = hashIndex > -1 ? url.slice(hashIndex) : "" + return data +} diff --git a/test-utils/pushStateMock.js b/test-utils/pushStateMock.js new file mode 100644 index 00000000..1ab14c72 --- /dev/null +++ b/test-utils/pushStateMock.js @@ -0,0 +1,169 @@ +"use strict" + +var parseURL = require("../test-utils/parseURL") + +module.exports = function() { + var protocol = "http:" + var hostname = "localhost" + var port = "" + var pathname = "/" + var search = "" + var hash = "" + + var past = [], future = [] + + function getURL() { + if (protocol === "file:") return protocol + "//" + pathname + search + hash + return protocol + "//" + hostname + prefix(":", port) + pathname + search + hash + } + function setURL(value) { + var data = parseURL(value, {protocol: protocol, hostname: hostname, port: port, pathname: pathname}) + var isNew = false + if (data.protocol != null && data.protocol !== protocol) protocol = data.protocol, isNew = true + if (data.hostname != null && data.hostname !== hostname) hostname = data.hostname, isNew = true + if (data.port != null && data.port !== port) port = data.port, isNew = true + if (data.pathname != null && data.pathname !== pathname) pathname = data.pathname, isNew = true + if (data.search != null && data.search !== search) search = data.search, isNew = true + if (data.hash != null && data.hash !== hash) { + hash = data.hash + if (!isNew) hashchange() + } + return isNew + } + + function prefix(prefix, value) { + if (value === "") return "" + return (value.charAt(0) !== prefix ? prefix : "") + value + } + function hashchange() { + if (typeof $window.onhashchange === "function") $window.onhashchange({type: "hashchange"}) + } + function popstate() { + if (typeof $window.onpopstate === "function") $window.onpopstate({type: "popstate"}) + } + function unload() { + if (typeof $window.onunload === "function") $window.onunload({type: "unload"}) + } + var $window = { + location: { + get protocol() { + return protocol + }, + get hostname() { + return hostname + }, + get port() { + return port + }, + get pathname() { + return pathname + }, + get search() { + return search + }, + get hash() { + return hash + }, + get origin() { + if (protocol === "file:") return "null" + return protocol + "//" + hostname + prefix(":", port) + }, + get host() { + if (protocol === "file:") return "" + return hostname + prefix(":", port) + }, + get href() { + return getURL() + }, + + set protocol(value) { + throw new Error("Protocol is read-only") + }, + set hostname(value) { + unload() + past.push({url: getURL(), isNew: true}) + future = [] + hostname = value + }, + set port(value) { + if (protocol === "file:") throw new Error("Port is read-only under `file://` protocol") + unload() + past.push({url: getURL(), isNew: true}) + future = [] + port = value + }, + set pathname(value) { + if (protocol === "file:") throw new Error("Pathname is read-only under `file://` protocol") + unload() + past.push({url: getURL(), isNew: true}) + future = [] + pathname = prefix("/", value) + }, + set search(value) { + unload() + past.push({url: getURL(), isNew: true}) + future = [] + search = prefix("?", value) + }, + set hash(value) { + var oldHash = hash + past.push({url: getURL(), isNew: false}) + future = [] + hash = prefix("#", value) + if (oldHash != hash) hashchange() + }, + + set origin(value) { + console.warn("Origin is writable but ignored") + }, + set host(value) { + console.warn("Host is writable but ignored in Chrome") + }, + set href(value) { + var url = getURL() + var isNew = setURL(value) + if (isNew) { + setURL(url) + unload() + setURL(value) + } + past.push({url: url, isNew: isNew}) + future = [] + }, + }, + history: { + pushState: function(data, title, url) { + past.push({url: getURL(), isNew: false}) + future = [] + setURL(url) + }, + replaceState: function(data, title, url) { + future = [] + setURL(url) + }, + back: function() { + var entry = past.pop() + if (entry != null) { + if (entry.isNew) unload() + future.push({url: getURL(), isNew: false}) + setURL(entry.url) + if (!entry.isNew) popstate() + } + }, + forward: function() { + var entry = future.pop() + if (entry != null) { + if (entry.isNew) unload() + past.push({url: getURL(), isNew: false}) + setURL(entry.url) + if (!entry.isNew) popstate() + } + }, + }, + scrollTo: function(x, y) {}, + onpopstate: null, + onhashchange: null, + onunload: null, + } + return $window +} diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html new file mode 100644 index 00000000..1aa57186 --- /dev/null +++ b/test-utils/tests/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-utils/tests/test-ajaxMock.js b/test-utils/tests/test-ajaxMock.js new file mode 100644 index 00000000..8f5190be --- /dev/null +++ b/test-utils/tests/test-ajaxMock.js @@ -0,0 +1,147 @@ +"use strict" + +var o = require("../../ospec/ospec") +var ajaxMock = require("../../test-utils/ajaxMock") +var parseQueryString = require("../../querystring/parse") + +o.spec("ajaxMock", function() { + var $window, ajax + o.beforeEach(function() { + $window = ajaxMock() + }) + + o.spec("xhr", function() { + o("works", function(done, timeout) { + $window.$defineRoutes({ + "GET /item": function(request) { + o(request.url).equals("/item") + return {status: 200, responseText: "test"} + } + }) + var xhr = new $window.XMLHttpRequest() + xhr.open("GET", "/item") + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + o(xhr.status).equals(200) + o(xhr.responseText).equals("test") + done() + } + } + xhr.send() + }) + o("works w/ search", function(done, timeout) { + $window.$defineRoutes({ + "GET /item": function(request) { + o(request.query).equals("?a=b") + return {status: 200, responseText: "test"} + } + }) + var xhr = new $window.XMLHttpRequest() + xhr.open("GET", "/item?a=b") + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + done() + } + } + xhr.send() + }) + o("works w/ body", function(done, timeout) { + $window.$defineRoutes({ + "POST /item": function(request) { + o(request.body).equals("a=b") + return {status: 200, responseText: "test"} + } + }) + var xhr = new $window.XMLHttpRequest() + xhr.open("POST", "/item") + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + done() + } + } + xhr.send("a=b") + }) + o("handles routing error", function(done, timeout) { + var xhr = new $window.XMLHttpRequest() + xhr.open("GET", "/nonexistent") + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + o(xhr.status).equals(500) + done() + } + } + xhr.send("a=b") + }) + }) + o.spec("jsonp", function() { + o("works", function(done) { + $window.$defineRoutes({ + "GET /test": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["callback"] + "(" + JSON.stringify({a: 1}) + ")"} + } + }) + + $window["cb"] = finish + + var script = $window.document.createElement("script") + script.src = "/test?callback=cb" + $window.document.documentElement.appendChild(script) + + function finish(data) { + o(data).deepEquals({a: 1}) + done() + } + }) + o("works w/ custom callback key", function(done) { + $window.$defineRoutes({ + "GET /test": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["cb"] + "(" + JSON.stringify({a: 2}) + ")"} + } + }) + $window.$defineJSONPCallbackKey("cb") + + $window["customcb"] = finish2 + + var script = $window.document.createElement("script") + script.src = "/test?cb=customcb" + $window.document.documentElement.appendChild(script) + + function finish2(data) { + o(data).deepEquals({a: 2}) + done() + } + }) + o("works with other querystring params", function(done, timeout) { + $window.$defineRoutes({ + "GET /test": function(request) { + var queryData = parseQueryString(request.query) + return {status: 200, responseText: queryData["callback"] + "(" + JSON.stringify({a: 3}) + ")"} + } + }) + + $window["cbwithinparams"] = finish + + var script = $window.document.createElement("script") + script.src = "/test?a=b&callback=cbwithinparams&c=d" + $window.document.documentElement.appendChild(script) + + function finish(data) { + o(data).deepEquals({a: 3}) + done() + } + }) + o("handles error", function(done) { + var script = $window.document.createElement("script") + script.onerror = finish + script.src = "/test?cb=nonexistent" + $window.document.documentElement.appendChild(script) + + function finish(e) { + o(e.type).equals("error") + done() + } + }) + }) +}) \ No newline at end of file diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js new file mode 100644 index 00000000..0019b5ea --- /dev/null +++ b/test-utils/tests/test-domMock.js @@ -0,0 +1,580 @@ +"use strict" + +var o = require("../../ospec/ospec") +var domMock = require("../../test-utils/domMock") + +o.spec("domMock", function() { + var $document + o.beforeEach(function() { + $document = domMock().document + //$document = document //TODO clean up + }) + + o.spec("createElement", function() { + o("works", function() { + var node = $document.createElement("div") + + o(node.nodeType).equals(1) + o(node.nodeName).equals("DIV") + o(node.namespaceURI).equals("http://www.w3.org/1999/xhtml") + o(node.parentNode).equals(null) + o(node.childNodes.length).equals(0) + o(node.firstChild).equals(null) + o(node.nextSibling).equals(null) + }) + }) + + o.spec("createElementNS", function() { + o("works", function() { + var node = $document.createElementNS("http://www.w3.org/2000/svg", "svg") + + o(node.nodeType).equals(1) + o(node.nodeName).equals("svg") + o(node.namespaceURI).equals("http://www.w3.org/2000/svg") + o(node.parentNode).equals(null) + o(node.childNodes.length).equals(0) + o(node.firstChild).equals(null) + o(node.nextSibling).equals(null) + }) + }) + + o.spec("createTextNode", function() { + o("works", function() { + var node = $document.createTextNode("abc") + + o(node.nodeType).equals(3) + o(node.nodeName).equals("#text") + o(node.parentNode).equals(null) + o(node.nodeValue).equals("abc") + }) + o("works w/ number", function() { + var node = $document.createTextNode(123) + + o(node.nodeValue).equals("123") + }) + o("works w/ null", function() { + var node = $document.createTextNode(null) + + o(node.nodeValue).equals("null") + }) + o("works w/ undefined", function() { + var node = $document.createTextNode(undefined) + + o(node.nodeValue).equals("undefined") + }) + o("works w/ object", function() { + var node = $document.createTextNode({}) + + o(node.nodeValue).equals("[object Object]") + }) + o("does not unescape HTML", function() { + var node = $document.createTextNode("&") + + o(node.nodeValue).equals("&") + }) + o("nodeValue casts to string", function() { + var node = $document.createTextNode("a") + node.nodeValue = true + + o(node.nodeValue).equals("true") + }) + }) + + o.spec("createDocumentFragment", function() { + o("works", function() { + var node = $document.createDocumentFragment() + + o(node.nodeType).equals(11) + o(node.nodeName).equals("#document-fragment") + o(node.parentNode).equals(null) + o(node.childNodes.length).equals(0) + o(node.firstChild).equals(null) + }) + }) + + o.spec("appendChild", function() { + o("works", function() { + var parent = $document.createElement("div") + var child = $document.createElement("a") + parent.appendChild(child) + + o(parent.childNodes.length).equals(1) + o(parent.childNodes[0]).equals(child) + o(parent.firstChild).equals(child) + o(child.parentNode).equals(parent) + }) + o("moves existing", function() { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + parent.appendChild(b) + parent.appendChild(a) + + o(parent.childNodes.length).equals(2) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(a) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(a) + o(a.parentNode).equals(parent) + o(b.parentNode).equals(parent) + }) + o("removes from old parent", function() { + var parent = $document.createElement("div") + var source = $document.createElement("span") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + source.appendChild(b) + parent.appendChild(b) + + o(source.childNodes.length).equals(0) + }) + o("transfers from fragment", function() { + var parent = $document.createElement("div") + var a = $document.createDocumentFragment("a") + var b = $document.createElement("b") + var c = $document.createElement("c") + a.appendChild(b) + a.appendChild(c) + parent.appendChild(a) + + o(parent.childNodes.length).equals(2) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(c) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(c) + o(a.childNodes.length).equals(0) + o(a.firstChild).equals(null) + o(a.parentNode).equals(null) + o(b.parentNode).equals(parent) + o(c.parentNode).equals(parent) + }) + o("throws if appended to self", function(done) { + var div = $document.createElement("div") + try {div.appendChild(div)} + catch (e) {done()} + }) + o("throws if appended to child", function(done) { + var parent = $document.createElement("div") + var child = $document.createElement("a") + parent.appendChild(child) + try {child.appendChild(parent)} + catch (e) {done()} + }) + o("throws if child is not element", function(done) { + var parent = $document.createElement("div") + var child = 1 + try {parent.appendChild(child)} + catch (e) {done()} + }) + }) + + o.spec("removeChild", function() { + o("works", function() { + var parent = $document.createElement("div") + var child = $document.createElement("a") + parent.appendChild(child) + parent.removeChild(child) + + o(parent.childNodes.length).equals(0) + o(parent.firstChild).equals(null) + o(child.parentNode).equals(null) + }) + o("throws if not a child", function(done) { + var parent = $document.createElement("div") + var child = $document.createElement("a") + try {parent.removeChild(child)} + catch (e) {done()} + }) + }) + + o.spec("insertBefore", function() { + o("works", function() { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + parent.insertBefore(b, a) + + o(parent.childNodes.length).equals(2) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(a) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(a) + o(a.parentNode).equals(parent) + o(b.parentNode).equals(parent) + }) + o("moves existing", function() { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + parent.appendChild(b) + parent.insertBefore(b, a) + + o(parent.childNodes.length).equals(2) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(a) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(a) + o(a.parentNode).equals(parent) + o(b.parentNode).equals(parent) + }) + o("removes from old parent", function() { + var parent = $document.createElement("div") + var source = $document.createElement("span") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + source.appendChild(b) + parent.insertBefore(b, a) + + o(source.childNodes.length).equals(0) + }) + o("transfers from fragment", function() { + var parent = $document.createElement("div") + var ref = $document.createElement("span") + var a = $document.createDocumentFragment("a") + var b = $document.createElement("b") + var c = $document.createElement("c") + parent.appendChild(ref) + a.appendChild(b) + a.appendChild(c) + parent.insertBefore(a, ref) + + o(parent.childNodes.length).equals(3) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(c) + o(parent.childNodes[2]).equals(ref) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(c) + o(parent.firstChild.nextSibling.nextSibling).equals(ref) + o(a.childNodes.length).equals(0) + o(a.firstChild).equals(null) + o(a.parentNode).equals(null) + o(b.parentNode).equals(parent) + o(c.parentNode).equals(parent) + }) + o("appends if second arg is null", function() { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + parent.insertBefore(b, null) + + o(parent.childNodes.length).equals(2) + o(parent.childNodes[0]).equals(a) + o(parent.childNodes[1]).equals(b) + o(parent.firstChild).equals(a) + o(parent.firstChild.nextSibling).equals(b) + o(a.parentNode).equals(parent) + }) + o("throws if appended to self", function(done) { + var div = $document.createElement("div") + var a = $document.createElement("a") + div.appendChild(a) + try {div.isnertBefore(div, a)} + catch (e) {done()} + }) + o("throws if appended to child", function(done) { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + parent.appendChild(a) + a.appendChild(b) + try {a.insertBefore(parent, b)} + catch (e) {done()} + }) + o("throws if child is not element", function(done) { + var parent = $document.createElement("div") + var a = $document.createElement("a") + parent.appendChild(a) + try {parent.insertBefore(1, a)} + catch (e) {done()} + }) + o("throws if inserted before itself", function(done) { + var parent = $document.createElement("div") + var a = $document.createElement("a") + try {parent.insertBefore(a, a)} + catch (e) {done()} + }) + o("throws if second arg is undefined", function(done) { + var parent = $document.createElement("div") + var a = $document.createElement("a") + try {parent.insertBefore(a)} + catch (e) {done()} + }) + o("throws if reference is not child", function(done) { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + try {parent.insertBefore(a, b)} + catch (e) {done()} + }) + }) + + o.spec("setAttribute", function() { + o("works", function() { + var div = $document.createElement("div") + div.setAttribute("id", "aaa") + + o(div.attributes["id"].nodeValue).equals("aaa") + o(div.attributes["id"].namespaceURI).equals(null) + }) + o("works w/ number", function() { + var div = $document.createElement("div") + div.setAttribute("id", 123) + + o(div.attributes["id"].nodeValue).equals("123") + }) + o("works w/ null", function() { + var div = $document.createElement("div") + div.setAttribute("id", null) + + o(div.attributes["id"].nodeValue).equals("null") + }) + o("works w/ undefined", function() { + var div = $document.createElement("div") + div.setAttribute("id", undefined) + + o(div.attributes["id"].nodeValue).equals("undefined") + }) + o("works w/ object", function() { + var div = $document.createElement("div") + div.setAttribute("id", {}) + + o(div.attributes["id"].nodeValue).equals("[object Object]") + }) + }) + + o.spec("setAttributeNS", function() { + o("works", function() { + var div = $document.createElement("div") + div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa") + + o(div.attributes["href"].nodeValue).equals("aaa") + o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + }) + o("works w/ number", function() { + var div = $document.createElement("div") + div.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123) + + o(div.attributes["href"].nodeValue).equals("123") + o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + }) + }) + + o.spec("removeAttribute", function() { + o("works", function() { + var div = $document.createElement("div") + div.setAttribute("id", "aaa") + div.removeAttribute("id") + + o("id" in div.attributes).equals(false) + }) + }) + + o.spec("textContent", function() { + o("works", function() { + var div = $document.createElement("div") + var a = $document.createElement("a") + div.textContent = "aaa" + + o(div.childNodes.length).equals(1) + o(div.firstChild.nodeType).equals(3) + o(div.firstChild.nodeValue).equals("aaa") + }) + o("works with empty string", function() { + var div = $document.createElement("div") + var a = $document.createElement("a") + div.textContent = "" + + o(div.childNodes.length).equals(0) + }) + }) + + o.spec("innerHTML", function() { + o("works", function() { + var div = $document.createElement("div") + div.innerHTML = "
123234
345
" + o(div.childNodes.length).equals(2) + o(div.childNodes[0].nodeType).equals(1) + o(div.childNodes[0].nodeName).equals("BR") + o(div.childNodes[1].nodeType).equals(1) + o(div.childNodes[1].nodeName).equals("A") + o(div.childNodes[1].attributes["class"].nodeValue).equals("aaa") + o(div.childNodes[1].attributes["id"].nodeValue).equals("xyz") + o(div.childNodes[1].childNodes[0].nodeType).equals(3) + o(div.childNodes[1].childNodes[0].nodeValue).equals("123") + o(div.childNodes[1].childNodes[1].nodeType).equals(1) + o(div.childNodes[1].childNodes[1].nodeName).equals("B") + o(div.childNodes[1].childNodes[1].attributes["class"].nodeValue).equals("bbb") + o(div.childNodes[1].childNodes[2].nodeType).equals(3) + o(div.childNodes[1].childNodes[2].nodeValue).equals("234") + o(div.childNodes[1].childNodes[3].nodeType).equals(1) + o(div.childNodes[1].childNodes[3].nodeName).equals("BR") + o(div.childNodes[1].childNodes[3].attributes["class"].nodeValue).equals("ccc") + o(div.childNodes[1].childNodes[4].nodeType).equals(3) + o(div.childNodes[1].childNodes[4].nodeValue).equals("345") + }) + o("headers work", function() { + var div = $document.createElement("div") + div.innerHTML = "

" + o(div.childNodes.length).equals(6) + o(div.childNodes[0].nodeType).equals(1) + o(div.childNodes[0].nodeName).equals("H1") + o(div.childNodes[1].nodeType).equals(1) + o(div.childNodes[1].nodeName).equals("H2") + o(div.childNodes[2].nodeType).equals(1) + o(div.childNodes[2].nodeName).equals("H3") + o(div.childNodes[3].nodeType).equals(1) + o(div.childNodes[3].nodeName).equals("H4") + o(div.childNodes[4].nodeType).equals(1) + o(div.childNodes[4].nodeName).equals("H5") + o(div.childNodes[5].nodeType).equals(1) + o(div.childNodes[5].nodeName).equals("H6") + }) + o("detaches old elements", function() { + var div = $document.createElement("div") + var a = $document.createElement("a") + div.appendChild(a) + div.innerHTML = "" + + o(a.parentNode).equals(null) + }) + }) + o.spec("focus", function() { + o("body is active by default", function() { + o($document.documentElement.nodeName).equals("HTML") + o($document.body.nodeName).equals("BODY") + o($document.documentElement.firstChild.nodeName).equals("HEAD") + o($document.documentElement).equals($document.body.parentNode) + o($document.activeElement).equals($document.body) + }) + o("focus changes activeElement", function() { + var input = $document.createElement("input") + $document.body.appendChild(input) + input.focus() + + o($document.activeElement).equals(input) + + $document.body.removeChild(input) + }) + }) + o.spec("style", function() { + o("has style property", function() { + var div = $document.createElement("div") + + o(typeof div.style).equals("object") + }) + o("setting style string works", function() { + var div = $document.createElement("div") + div.style = "background-color: red; border-bottom: 1px solid red;" + + o(div.style.backgroundColor).equals("red") + o(div.style.borderBottom).equals("1px solid red") + }) + o("removing via setting style string works", function() { + var div = $document.createElement("div") + div.style = "background: red;" + div.style = "" + + o(div.style.background).equals("") + }) + }) + o.spec("events", function() { + var spy, div, e + o.beforeEach(function() { + spy = o.spy() + div = $document.createElement("div") + e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + $document.body.appendChild(div) + }) + o.afterEach(function() { + $document.body.removeChild(div) + }) + + o("click fires onclick", function() { + div.onclick = spy + div.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div) + }) + o("click without onclick doesn't throw", function(done) { + div.dispatchEvent(e) + done() + }) + }) + o.spec("attributes", function() { + o.spec("link href", function() { + o("is empty string if no attribute", function() { + var a = $document.createElement("a") + + o(a.href).equals("") + o(a.attributes["href"]).equals(undefined) + }) + o("is path if attribute is set", function() { + var a = $document.createElement("a") + a.setAttribute("href", "") + + o(a.href).notEquals("") + o(a.attributes["href"].nodeValue).equals("") + }) + o("is path if property is set", function() { + var a = $document.createElement("a") + a.href = "" + + o(a.href).notEquals("") + o(a.attributes["href"].nodeValue).equals("") + }) + }) + o.spec("input checked", function() { + o("only exists in input elements", function() { + var input = $document.createElement("input") + var a = $document.createElement("a") + + o("checked" in input).equals(true) + o("checked" in a).equals(false) + }) + o("tracks attribute value when unset", function() { + var input = $document.createElement("input") + input.setAttribute("type", "checkbox") + + o(input.checked).equals(false) + o(input.attributes["checked"]).equals(undefined) + + input.setAttribute("checked", "") + + o(input.checked).equals(true) + o(input.attributes["checked"].nodeValue).equals("") + + input.removeAttribute("checked") + + o(input.checked).equals(false) + o(input.attributes["checked"]).equals(undefined) + }) + o("does not track attribute value when set", function() { + var input = $document.createElement("input") + input.setAttribute("type", "checkbox") + input.checked = true + + o(input.checked).equals(true) + o(input.attributes["checked"]).equals(undefined) + + input.checked = false + input.setAttribute("checked", "") + + input.checked = true + input.removeAttribute("checked") + + o(input.checked).equals(true) + }) + }) + }) +}) diff --git a/test-utils/tests/test-parseURL.js b/test-utils/tests/test-parseURL.js new file mode 100644 index 00000000..0de794cc --- /dev/null +++ b/test-utils/tests/test-parseURL.js @@ -0,0 +1,150 @@ +"use strict" + +var o = require("../../ospec/ospec") +var parseURL = require("../../test-utils/parseURL") + +o.spec("parseURL", function() { + var root = {protocol: "http:", hostname: "localhost", port: "", pathname: "/"} + + o.spec("full URL", function() { + o("parses full URL", function() { + var data = parseURL("http://www.google.com:80/test?a=b#c") + o(data.protocol).equals("http:") + o(data.hostname).equals("www.google.com") + o(data.port).equals("80") + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("parses full URL omitting optionals", function() { + var data = parseURL("http://www.google.com") + o(data.protocol).equals("http:") + o(data.hostname).equals("www.google.com") + o(data.port).equals("") + o(data.pathname).equals("/") + o(data.search).equals("") + o(data.hash).equals("") + }) + }) + o.spec("absolute path", function() { + o("parses absolute path", function() { + var data = parseURL("/test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("parses absolute path omitting optionals", function() { + var data = parseURL("/test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + }) + o.spec("relative path", function() { + o("parses relative URL", function() { + var data = parseURL("test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("parses relative URL omitting optionals", function() { + var data = parseURL("test", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("") + o(data.hash).equals("") + }) + o("parses relative URL with dot", function() { + var data = parseURL("././test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("parses relative URL with dotdot", function() { + var data = parseURL("foo/bar/../../test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("clamps invalid dotdot", function() { + var data = parseURL("../../test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("clamps invalid dotdot after dot", function() { + var data = parseURL("./../../test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + o("clamps invalid dotdot after valid path", function() { + var data = parseURL("a/../../test?a=b#c", root) + o(data.protocol).equals(root.protocol) + o(data.hostname).equals(root.hostname) + o(data.port).equals(root.port) + o(data.pathname).equals("/test") + o(data.search).equals("?a=b") + o(data.hash).equals("#c") + }) + }) + o.spec("edge cases", function() { + o("handles hash w/ question mark", function() { + var data = parseURL("http://www.google.com/test#a?c") + o(data.pathname).equals("/test") + o(data.search).equals("") + o(data.hash).equals("#a?c") + }) + o("handles hash w/ slash", function() { + var data = parseURL("http://www.google.com/test#a/c") + o(data.pathname).equals("/test") + o(data.search).equals("") + o(data.hash).equals("#a/c") + }) + o("handles hash w/ colon", function() { + var data = parseURL("http://www.google.com/test#a:c") + o(data.pathname).equals("/test") + o(data.search).equals("") + o(data.hash).equals("#a:c") + }) + o("handles search w/ slash", function() { + var data = parseURL("http://www.google.com/test?a/c") + o(data.pathname).equals("/test") + o(data.search).equals("?a/c") + o(data.hash).equals("") + }) + o("handles search w/ slash", function() { + var data = parseURL("http://www.google.com/test?a:c") + o(data.pathname).equals("/test") + o(data.search).equals("?a:c") + o(data.hash).equals("") + }) + o("handles pathname w/ colon", function() { + var data = parseURL("http://www.google.com/a:b") + o(data.pathname).equals("/a:b") + }) + }) +}) \ No newline at end of file diff --git a/test-utils/tests/test-pushStateMock.js b/test-utils/tests/test-pushStateMock.js new file mode 100644 index 00000000..ff811487 --- /dev/null +++ b/test-utils/tests/test-pushStateMock.js @@ -0,0 +1,520 @@ +"use strict" + +var o = require("../../ospec/ospec") +var pushStateMock = require("../../test-utils/pushStateMock") + +o.spec("pushStateMock", function() { + + var $window + o.beforeEach(function() { + $window = pushStateMock() + }) + + o.spec("initial state", function() { + o("has url on page load", function() { + o($window.location.href).equals("http://localhost/") + }) + }) + + o.spec("set href", function() { + o("changes url on location.href change", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a") + }) + o("changes url on relative location.href change", function() { + var old = $window.location.href + $window.location.href = "a" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a") + o($window.location.pathname).equals("/a") + }) + o("changes url on dotdot location.href change", function() { + $window.location.href = "a" + var old = $window.location.href + $window.location.href = ".." + + o(old).equals("http://localhost/a") + o($window.location.href).equals("http://localhost/") + o($window.location.pathname).equals("/") + }) + o("changes url on deep dotdot location.href change", function() { + $window.location.href = "a/b/c" + var old = $window.location.href + $window.location.href = ".." + + o(old).equals("http://localhost/a/b/c") + o($window.location.href).equals("http://localhost/a") + o($window.location.pathname).equals("/a") + }) + o("does not change url on dotdot location.href change from root", function() { + var old = $window.location.href + $window.location.href = ".." + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/") + o($window.location.pathname).equals("/") + }) + o("changes url on dot relative location.href change", function() { + var old = $window.location.href + $window.location.href = "a" + $window.location.href = "./b" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/b") + o($window.location.pathname).equals("/b") + }) + o("does not change url on dot location.href change", function() { + var old = $window.location.href + $window.location.href = "a" + $window.location.href = "." + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a") + o($window.location.pathname).equals("/a") + }) + o("changes url on hash-only location.href change", function() { + var old = $window.location.href + $window.location.href = "#a" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/#a") + o($window.location.hash).equals("#a") + }) + o("changes url on search-only location.href change", function() { + var old = $window.location.href + $window.location.href = "?a" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/?a") + o($window.location.search).equals("?a") + }) + o("changes hash on location.href change", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a#b" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a#b") + o($window.location.hash).equals("#b") + }) + o("changes search on location.href change", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a?b" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a?b") + o($window.location.search).equals("?b") + }) + o("changes search and hash on location.href change", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a?b#c" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a?b#c") + o($window.location.search).equals("?b") + o($window.location.hash).equals("#c") + }) + o("handles search with search and hash", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a?b?c#d" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a?b?c#d") + o($window.location.search).equals("?b?c") + o($window.location.hash).equals("#d") + }) + o("handles hash with search and hash", function() { + var old = $window.location.href + $window.location.href = "http://localhost/a#b?c#d" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a#b?c#d") + o($window.location.search).equals("") + o($window.location.hash).equals("#b?c#d") + }) + }) + o.spec("set search", function() { + o("changes url on location.search change", function() { + var old = $window.location.href + $window.location.search = "?b" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/?b") + o($window.location.search).equals("?b") + }) + }) + o.spec("set hash", function() { + o("changes url on location.hash change", function() { + var old = $window.location.href + $window.location.hash = "#b" + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/#b") + o($window.location.hash).equals("#b") + }) + }) + o.spec("pushState", function() { + o("changes url on pushstate", function() { + var old = $window.location.href + $window.history.pushState(null, null, "http://localhost/a") + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/a") + }) + o("changes search on pushstate", function() { + var old = $window.location.href + $window.history.pushState(null, null, "http://localhost/?a") + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/?a") + o($window.location.search).equals("?a") + }) + o("changes search on relative pushstate", function() { + var old = $window.location.href + $window.history.pushState(null, null, "?a") + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/?a") + o($window.location.search).equals("?a") + }) + o("changes hash on pushstate", function() { + var old = $window.location.href + $window.history.pushState(null, null, "http://localhost/#a") + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/#a") + o($window.location.hash).equals("#a") + }) + o("changes hash on relative pushstate", function() { + var old = $window.location.href + $window.history.pushState(null, null, "#a") + + o(old).equals("http://localhost/") + o($window.location.href).equals("http://localhost/#a") + o($window.location.hash).equals("#a") + }) + }) + o.spec("onpopstate", function() { + o("history.back() without history does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.back() after pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + o($window.onpopstate.args[0].type).equals("popstate") + }) + o("history.back() after relative pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("history.back() after search pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/?a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("history.back() after relative search pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "?a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("history.back() after hash pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/#a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("history.back() after relative hash pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "#a") + $window.history.back() + + o($window.onpopstate.callCount).equals(1) + }) + o("history.back() after replacestate does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.replaceState(null, null, "http://localhost/a") + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative replacestate does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.replaceState(null, null, "a") + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative search replacestate does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.replaceState(null, null, "?a") + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.back() after relative hash replacestate does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.replaceState(null, null, "#a") + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.forward() after pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.forward() after search pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/?a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative search pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "?a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.forward() after hash pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "http://localhost/#a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.forward() after relative hash pushstate triggers onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.pushState(null, null, "#a") + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(2) + }) + o("history.back() without history does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("history.forward() without history does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.forward() + + o($window.onpopstate.callCount).equals(0) + }) + o("history navigation without history does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.back() + $window.history.forward() + + o($window.onpopstate.callCount).equals(0) + }) + o("reverse history navigation without history does not trigger onpopstate", function() { + $window.onpopstate = o.spy() + $window.history.forward() + $window.history.back() + + o($window.onpopstate.callCount).equals(0) + }) + o("onpopstate has correct url during call", function(done) { + $window.location.href = "a" + $window.onpopstate = function() { + o($window.location.href).equals("http://localhost/a") + done() + } + $window.history.pushState(null, null, "b") + $window.history.back() + }) + }) + o.spec("onhashchance", function() { + o("onhashchange triggers on location.href change", function() { + $window.onhashchange = o.spy() + $window.location.href = "http://localhost/#a" + + o($window.onhashchange.callCount).equals(1) + o($window.onhashchange.args[0].type).equals("hashchange") + }) + o("onhashchange triggers on relative location.href change", function() { + $window.onhashchange = o.spy() + $window.location.href = "#a" + + o($window.onhashchange.callCount).equals(1) + }) + o("onhashchange triggers on location.hash change", function() { + $window.onhashchange = o.spy() + $window.location.hash = "#a" + + o($window.onhashchange.callCount).equals(1) + }) + o("onhashchange does not trigger on page change", function() { + $window.onhashchange = o.spy() + $window.location.href = "http://localhost/a" + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange does not trigger on page change with different hash", function() { + $window.location.href = "http://localhost/#a" + $window.onhashchange = o.spy() + $window.location.href = "http://localhost/a#b" + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange does not trigger on page change with same hash", function() { + $window.location.href = "http://localhost/#b" + $window.onhashchange = o.spy() + $window.location.href = "http://localhost/a#b" + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange triggers on history.back()", function() { + $window.location.href = "#a" + $window.onhashchange = o.spy() + $window.history.back() + + o($window.onhashchange.callCount).equals(1) + }) + o("onhashchange triggers on history.forward()", function() { + $window.location.href = "#a" + $window.onhashchange = o.spy() + $window.history.back() + $window.history.forward() + + o($window.onhashchange.callCount).equals(2) + }) + o("onhashchange does not trigger on history.back() that causes page change with different hash", function() { + $window.location.href = "#a" + $window.location.href = "a#b" + $window.onhashchange = o.spy() + $window.history.back() + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange does not trigger on history.back() that causes page change with same hash", function() { + $window.location.href = "#a" + $window.location.href = "a#a" + $window.onhashchange = o.spy() + $window.history.back() + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange does not trigger on history.forward() that causes page change with different hash", function() { + $window.location.href = "#a" + $window.location.href = "a#b" + $window.onhashchange = o.spy() + $window.history.back() + $window.history.forward() + + o($window.onhashchange.callCount).equals(0) + }) + o("onhashchange does not trigger on history.forward() that causes page change with same hash", function() { + $window.location.href = "#a" + $window.location.href = "a#b" + $window.onhashchange = o.spy() + $window.history.back() + $window.history.forward() + + o($window.onhashchange.callCount).equals(0) + }) + }) + o.spec("onunload", function() { + o("onunload triggers on location.href change", function() { + $window.onunload = o.spy() + $window.location.href = "http://localhost/a" + + o($window.onunload.callCount).equals(1) + o($window.onunload.args[0].type).equals("unload") + }) + o("onunload triggers on relative location.href change", function() { + $window.onunload = o.spy() + $window.location.href = "a" + + o($window.onunload.callCount).equals(1) + }) + o("onunload triggers on search change via location.href", function() { + $window.onunload = o.spy() + $window.location.href = "http://localhost/?a" + + o($window.onunload.callCount).equals(1) + }) + o("onunload triggers on relative search change via location.href", function() { + $window.onunload = o.spy() + $window.location.href = "?a" + + o($window.onunload.callCount).equals(1) + }) + o("onunload does not trigger on hash change via location.href", function() { + $window.onunload = o.spy() + $window.location.href = "http://localhost/#a" + + o($window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on relative hash change via location.href", function() { + $window.onunload = o.spy() + $window.location.href = "#a" + + o($window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on hash-only history.back()", function() { + $window.location.href = "#a" + $window.onunload = o.spy() + $window.history.back() + + o($window.onunload.callCount).equals(0) + }) + o("onunload does not trigger on hash-only history.forward()", function() { + $window.location.href = "#a" + $window.history.back() + $window.onunload = o.spy() + $window.history.forward() + + o($window.onunload.callCount).equals(0) + }) + o("onunload has correct url during call via location.href change", function(done) { + $window.onunload = function() { + o($window.location.href).equals("http://localhost/") + done() + } + $window.location.href = "a" + }) + o("onunload has correct url during call via location.search change", function(done) { + $window.onunload = function() { + o($window.location.href).equals("http://localhost/") + done() + } + $window.location.search = "?a" + }) + }) +}) \ No newline at end of file