├── .gitignore ├── .jshintrc ├── .travis.yml ├── History.md ├── Makefile ├── README.md ├── component.json ├── dist └── ripple.js ├── lib ├── bindings │ ├── attribute.js │ ├── child.js │ ├── directive.js │ ├── index.js │ └── text.js ├── index.js ├── proto.js └── static.js ├── package.json └── test ├── .jshintrc ├── karma.conf.js ├── runner.html ├── specs ├── attribute-interpolation.js ├── composing.js ├── destroy.js ├── directives.js ├── interpolation.js ├── lifecycle.js ├── model.js ├── mounting.js ├── owners.js ├── text-interpolation.js ├── view.js └── watching.js └── utils ├── mocha.css └── mocha.js /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | node_modules -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqeqeq": true, 3 | "camelcase": true, 4 | "indent": 2, 5 | "newcap": true, 6 | "eqnull": true, 7 | "browser": true, 8 | "asi": true, 9 | "multistr": true, 10 | "undef": true, 11 | "unused": true, 12 | "trailing": true, 13 | "sub": true, 14 | "node": true, 15 | "laxbreak": true, 16 | "globals": { 17 | "console": true 18 | } 19 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | script: 7 | - make ci 8 | 9 | notifications: 10 | email: 11 | - antshort+travis@gmail.com -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.5.3 / 2014-07-09 3 | ================== 4 | 5 | * Optionally use `new` when creating views 6 | * Fixed issue with expressions failing and not returning null 7 | 8 | 0.5.2 / 2014-06-16 9 | ================== 10 | 11 | * Added `initialize`. If this method exists, it will be called after the `created` event. 12 | 13 | 0.5.1 / 2014-06-15 14 | ================== 15 | 16 | * Fixed issue with text bindings not rendering elements if there was whitespace around it 17 | 18 | 0.5.0 / 2014-06-14 19 | ================== 20 | 21 | * removed the `scope` option. This cleaned up a lot of code, but it means the each plugin won't work in it's current state. Instead, we'll prefer to pass in views instead of just building sub-views from templates. It makes the library smaller and the code more maintainable. 22 | * Removed the need to use `.parse`. 23 | * Views now have the signature `new View(attrs, options)` instead of using the `data` property. 24 | * New `View.attr` method for defining attributes. This lets us create getters/setters and means plugins can do cool things with the attributes (like making them required, setting defaults or enforcing a type). 25 | * Once attributes are defined using `attr` you can access them like `view.name = 'foo'` instead of doing `view.set('name', 'foo')`. Although the get and set methods still exist. 26 | * Removed a bunch of the files and make it simpler. Specifically removed the `model`. 27 | * Views now have a unique ID 28 | * Consistent code formatting 29 | * Owner can only be set once and can't be changed. If this restriction doesn't work in practice we can revisit. 30 | * Text bindings will render objects that have a .el property. This means you can set other views as attributes on a view and it will render it. 31 | * Removed `create` and the ability to create child views. This was only used for the each plugin. Instead, create views and add their plugins manually. Less magic. 32 | * Interpolator can't be accessed now, meaning you can't change delimiters. This is an edge case and probably doesn't need to be used, it now means we don't need a different interpolator for every view created. 33 | * Bumped component to v1. 34 | * Directives now remove the attributes from the template before rendering 35 | 36 | 0.4.0 / 2014-04-29 37 | ================== 38 | 39 | * Allow watching for all changes with `view.watch(callback)` 40 | * Using an updated/simplified path observer - `0.2.0` 41 | * Added `view.create` method for creating child views with the same bindings 42 | * Moved `render` into the view so it can be modified by plugins. eg. virtual dom 43 | 44 | 0.3.5 / 2014-04-23 45 | ================== 46 | 47 | * Added make targets for releases 48 | 49 | 0.3.4 / 2014-04-23 50 | ================== 51 | 52 | * Fixed before/after helper methods 53 | * Updated examples README.md 54 | * Updated clock example 55 | 56 | 0.3.3 / 2014-04-19 57 | ================== 58 | 59 | * Merge pull request #11 from olivoil/master 60 | * Continue walking DOM nodes after child binding 61 | * Merge pull request #6 from Nami-Doc/patch-1 62 | * Fix small typo 63 | * Added docs on composing views 64 | * Updated docs 65 | 66 | 0.3.2 / 2014-04-16 67 | ================== 68 | 69 | * Using raf-queue which is a simpler version of fastdom 70 | * Made requirable by browserify 71 | * Added docs and examples 72 | 73 | 0.3.0 / 2014-04-13 74 | ================== 75 | 76 | * Allow custom templates per view 77 | 78 | 0.2.3 / 2014-04-13 79 | ================== 80 | 81 | * Passing el and view through to directives to reduce use of confusing `this` 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COMPONENT = ./node_modules/.bin/component 2 | KARMA = ./node_modules/karma/bin/karma 3 | JSHINT = ./node_modules/.bin/jshint 4 | MOCHA = ./node_modules/.bin/mocha-phantomjs 5 | BUMP = ./node_modules/.bin/bump 6 | MINIFY = ./node_modules/.bin/minify 7 | BFC = ./node_modules/.bin/bfc 8 | 9 | build: components $(find lib/*.js) 10 | @${COMPONENT} build --dev 11 | 12 | prod: 13 | @${COMPONENT} build 14 | 15 | components: node_modules component.json 16 | @${COMPONENT} install --dev 17 | 18 | clean: 19 | rm -fr build components dist 20 | 21 | node_modules: 22 | npm install 23 | 24 | minify: build 25 | ${MINIFY} build/build.js build/build.min.js 26 | 27 | karma: build 28 | ${KARMA} start test/karma.conf.js --no-auto-watch --single-run 29 | 30 | lint: node_modules 31 | ${JSHINT} lib/*.js 32 | 33 | test: lint build 34 | ${MOCHA} /test/runner.html 35 | 36 | standalone: 37 | @${COMPONENT} build --standalone ripple 38 | @-rm -r dist 39 | @-mkdir dist 40 | @${BFC} build/build.js > dist/ripple.js 41 | 42 | ci: test 43 | 44 | patch: 45 | ${BUMP} patch 46 | 47 | minor: 48 | ${BUMP} minor 49 | 50 | release: test 51 | VERSION=`node -p "require('./component.json').version"` && \ 52 | git changelog --tag $$VERSION && \ 53 | git release $$VERSION 54 | 55 | .PHONY: clean test karma patch release prod 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ripple.js 2 | 3 | [![Build Status](https://travis-ci.org/ripplejs/ripple.svg?branch=master)](https://travis-ci.org/ripplejs/ripple) 4 | 5 | A tiny foundation for building reactive views with plugins. It aims to have a similar API to [Reactive](https://github.com/component/reactive), but allow composition of views, like [React](http://facebook.github.io/react/). 6 | The major difference for other view libraries is that there are no globals used at all. Each view has its own set of bindings and plugins. This 7 | makes composition of views really easy. 8 | 9 | ```js 10 | var Person = ripple('
{{name}}
') 11 | .attr('name', { required: true, type: 'string' }); 12 | 13 | var person = new Person({ 14 | name: 'Tom' 15 | }); 16 | 17 | person.appendTo(document.body); 18 | person.name = "Barry"; // DOM updates automatically 19 | ``` 20 | 21 | ## Install 22 | 23 | ```js 24 | component install ripplejs/ripple 25 | ``` 26 | 27 | ## Browser Support 28 | 29 | Supports real browsers and IE9+. 30 | 31 | ## Documentation 32 | 33 | [Documentation is on the wiki](https://github.com/ripplejs/ripple/wiki). 34 | 35 | ## Examples 36 | 37 | * [Clock](http://jsfiddle.net/chrisbuttery/QnHPj/3/) 38 | * [Counter](http://jsfiddle.net/anthonyshort/ybq9Q/light/) 39 | * [Like Button](http://jsfiddle.net/anthonyshort/ZA2gQ/6/light/) 40 | * [Markdown Editor](http://jsfiddle.net/anthonyshort/QGK3r/light/) 41 | * [Iteration](http://jsfiddle.net/chrisbuttery/4j5ZD/1/light/) 42 | 43 | See more examples at [ripplejs/examples](https://github.com/ripplejs/examples) 44 | 45 | ## Plugins 46 | 47 | * [shortcuts](https://github.com/ripplejs/shortcuts) - add custom key binding combos 48 | * [events](https://github.com/ripplejs/events) - add event listeners to the DOM and call methods on the view 49 | * [each](https://github.com/ripplejs/each) - Basic iteration using the `each` directive. 50 | * [bind-methods](https://github.com/ripplejs/bind-methods) - Bind all methods on the prototype to the view 51 | * [markdown](https://github.com/ripplejs/markdown) - Adds a directive to render markdown using Marked. 52 | * [extend](https://github.com/ripplejs/extend) - Makes adding methods to the view prototype a little cleaner 53 | * [intervals](https://github.com/ripplejs/intervals) - Easily add and remove intervals 54 | * [computed](https://github.com/ripplejs/computed) - Add computed properties. 55 | * [refs](https://github.com/ripplejs/refs) - Easily reference elements within the template 56 | * [dispatch](https://github.com/ripplejs/dispatch) - Dispatch custom DOM events up the tree 57 | 58 | [View and add them on the wiki](https://github.com/ripplejs/ripple/wiki/Plugins) 59 | 60 | 61 | ## License 62 | 63 | MIT 64 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ripple", 3 | "version": "0.5.3", 4 | "main": "lib/index.js", 5 | "scripts": [ 6 | "lib/index.js", 7 | "lib/proto.js", 8 | "lib/static.js", 9 | "lib/bindings/index.js", 10 | "lib/bindings/directive.js", 11 | "lib/bindings/text.js", 12 | "lib/bindings/attribute.js", 13 | "lib/bindings/child.js" 14 | ], 15 | "dependencies": { 16 | "anthonyshort/attributes": "*", 17 | "anthonyshort/dom-walk": "0.1.0", 18 | "anthonyshort/is-boolean-attribute": "*", 19 | "anthonyshort/raf-queue": "0.2.0", 20 | "component/domify": "*", 21 | "component/each": "*", 22 | "component/emitter": "*", 23 | "component/type": "*", 24 | "ripplejs/interpolate": "0.4.5", 25 | "ripplejs/path-observer": "0.2.0", 26 | "yields/uniq": "*" 27 | }, 28 | "development": { 29 | "component/assert": "*" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/ripple.js: -------------------------------------------------------------------------------- 1 | 2 | ;(function(){ 3 | 4 | /** 5 | * Require the module at `name`. 6 | * 7 | * @param {String} name 8 | * @return {Object} exports 9 | * @api public 10 | */ 11 | 12 | function _require(name) { 13 | var module = _require.modules[name]; 14 | if (!module) throw new Error('failed to require "' + name + '"'); 15 | 16 | if (!('exports' in module) && typeof module.definition === 'function') { 17 | module.client = module.component = true; 18 | module.definition.call(this, module.exports = {}, module); 19 | delete module.definition; 20 | } 21 | 22 | return module.exports; 23 | } 24 | 25 | /** 26 | * Registered modules. 27 | */ 28 | 29 | _require.modules = {}; 30 | 31 | /** 32 | * Register module at `name` with callback `definition`. 33 | * 34 | * @param {String} name 35 | * @param {Function} definition 36 | * @api private 37 | */ 38 | 39 | _require.register = function (name, definition) { 40 | _require.modules[name] = { 41 | definition: definition 42 | }; 43 | }; 44 | 45 | /** 46 | * Define a module's exports immediately with `exports`. 47 | * 48 | * @param {String} name 49 | * @param {Generic} exports 50 | * @api private 51 | */ 52 | 53 | _require.define = function (name, exports) { 54 | _require.modules[name] = { 55 | exports: exports 56 | }; 57 | }; 58 | _require.register("anthonyshort~attributes@0.0.1", function (exports, module) { 59 | module.exports = function(el) { 60 | var attrs = el.attributes, 61 | ret = {}, 62 | attr, 63 | i; 64 | 65 | for (i = attrs.length - 1; i >= 0; i--) { 66 | attr = attrs.item(i); 67 | ret[attr.nodeName] = attr.nodeValue; 68 | } 69 | 70 | return ret; 71 | }; 72 | }); 73 | 74 | _require.register("anthonyshort~is-boolean-attribute@0.0.1", function (exports, module) { 75 | 76 | /** 77 | * https://github.com/kangax/html-minifier/issues/63#issuecomment-18634279 78 | */ 79 | 80 | var attrs = [ 81 | "allowfullscreen", 82 | "async", 83 | "autofocus", 84 | "checked", 85 | "compact", 86 | "declare", 87 | "default", 88 | "defer", 89 | "disabled", 90 | "formnovalidate", 91 | "hidden", 92 | "inert", 93 | "ismap", 94 | "itemscope", 95 | "multiple", 96 | "multiple", 97 | "muted", 98 | "nohref", 99 | "noresize", 100 | "noshade", 101 | "novalidate", 102 | "nowrap", 103 | "open", 104 | "readonly", 105 | "required", 106 | "reversed", 107 | "seamless", 108 | "selected", 109 | "sortable", 110 | "truespeed", 111 | "typemustmatch", 112 | "contenteditable", 113 | "spellcheck" 114 | ]; 115 | 116 | module.exports = function(attr){ 117 | return attrs.indexOf(attr) > -1; 118 | }; 119 | }); 120 | 121 | _require.register("component~domify@1.2.2", function (exports, module) { 122 | 123 | /** 124 | * Expose `parse`. 125 | */ 126 | 127 | module.exports = parse; 128 | 129 | /** 130 | * Wrap map from jquery. 131 | */ 132 | 133 | var map = { 134 | legend: [1, '
', '
'], 135 | tr: [2, '', '
'], 136 | col: [2, '', '
'], 137 | _default: [0, '', ''] 138 | }; 139 | 140 | map.td = 141 | map.th = [3, '', '
']; 142 | 143 | map.option = 144 | map.optgroup = [1, '']; 145 | 146 | map.thead = 147 | map.tbody = 148 | map.colgroup = 149 | map.caption = 150 | map.tfoot = [1, '', '
']; 151 | 152 | map.text = 153 | map.circle = 154 | map.ellipse = 155 | map.line = 156 | map.path = 157 | map.polygon = 158 | map.polyline = 159 | map.rect = [1, '','']; 160 | 161 | /** 162 | * Parse `html` and return the children. 163 | * 164 | * @param {String} html 165 | * @return {Array} 166 | * @api private 167 | */ 168 | 169 | function parse(html) { 170 | if ('string' != typeof html) throw new TypeError('String expected'); 171 | 172 | // tag name 173 | var m = /<([\w:]+)/.exec(html); 174 | if (!m) return document.createTextNode(html); 175 | 176 | html = html.replace(/^\s+|\s+$/g, ''); // Remove leading/trailing whitespace 177 | 178 | var tag = m[1]; 179 | 180 | // body support 181 | if (tag == 'body') { 182 | var el = document.createElement('html'); 183 | el.innerHTML = html; 184 | return el.removeChild(el.lastChild); 185 | } 186 | 187 | // wrap map 188 | var wrap = map[tag] || map._default; 189 | var depth = wrap[0]; 190 | var prefix = wrap[1]; 191 | var suffix = wrap[2]; 192 | var el = document.createElement('div'); 193 | el.innerHTML = prefix + html + suffix; 194 | while (depth--) el = el.lastChild; 195 | 196 | // one element 197 | if (el.firstChild == el.lastChild) { 198 | return el.removeChild(el.firstChild); 199 | } 200 | 201 | // several elements 202 | var fragment = document.createDocumentFragment(); 203 | while (el.firstChild) { 204 | fragment.appendChild(el.removeChild(el.firstChild)); 205 | } 206 | 207 | return fragment; 208 | } 209 | 210 | }); 211 | 212 | _require.register("component~type@1.0.0", function (exports, module) { 213 | 214 | /** 215 | * toString ref. 216 | */ 217 | 218 | var toString = Object.prototype.toString; 219 | 220 | /** 221 | * Return the type of `val`. 222 | * 223 | * @param {Mixed} val 224 | * @return {String} 225 | * @api public 226 | */ 227 | 228 | module.exports = function(val){ 229 | switch (toString.call(val)) { 230 | case '[object Function]': return 'function'; 231 | case '[object Date]': return 'date'; 232 | case '[object RegExp]': return 'regexp'; 233 | case '[object Arguments]': return 'arguments'; 234 | case '[object Array]': return 'array'; 235 | case '[object String]': return 'string'; 236 | } 237 | 238 | if (val === null) return 'null'; 239 | if (val === undefined) return 'undefined'; 240 | if (val && val.nodeType === 1) return 'element'; 241 | if (val === Object(val)) return 'object'; 242 | 243 | return typeof val; 244 | }; 245 | 246 | }); 247 | 248 | _require.register("component~props@1.1.2", function (exports, module) { 249 | /** 250 | * Global Names 251 | */ 252 | 253 | var globals = /\b(this|Array|Date|Object|Math|JSON)\b/g; 254 | 255 | /** 256 | * Return immediate identifiers parsed from `str`. 257 | * 258 | * @param {String} str 259 | * @param {String|Function} map function or prefix 260 | * @return {Array} 261 | * @api public 262 | */ 263 | 264 | module.exports = function(str, fn){ 265 | var p = unique(props(str)); 266 | if (fn && 'string' == typeof fn) fn = prefixed(fn); 267 | if (fn) return map(str, p, fn); 268 | return p; 269 | }; 270 | 271 | /** 272 | * Return immediate identifiers in `str`. 273 | * 274 | * @param {String} str 275 | * @return {Array} 276 | * @api private 277 | */ 278 | 279 | function props(str) { 280 | return str 281 | .replace(/\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\//g, '') 282 | .replace(globals, '') 283 | .match(/[$a-zA-Z_]\w*/g) 284 | || []; 285 | } 286 | 287 | /** 288 | * Return `str` with `props` mapped with `fn`. 289 | * 290 | * @param {String} str 291 | * @param {Array} props 292 | * @param {Function} fn 293 | * @return {String} 294 | * @api private 295 | */ 296 | 297 | function map(str, props, fn) { 298 | var re = /\.\w+|\w+ *\(|"[^"]*"|'[^']*'|\/([^/]+)\/|[a-zA-Z_]\w*/g; 299 | return str.replace(re, function(_){ 300 | if ('(' == _[_.length - 1]) return fn(_); 301 | if (!~props.indexOf(_)) return _; 302 | return fn(_); 303 | }); 304 | } 305 | 306 | /** 307 | * Return unique array. 308 | * 309 | * @param {Array} arr 310 | * @return {Array} 311 | * @api private 312 | */ 313 | 314 | function unique(arr) { 315 | var ret = []; 316 | 317 | for (var i = 0; i < arr.length; i++) { 318 | if (~ret.indexOf(arr[i])) continue; 319 | ret.push(arr[i]); 320 | } 321 | 322 | return ret; 323 | } 324 | 325 | /** 326 | * Map with prefix `str`. 327 | */ 328 | 329 | function prefixed(str) { 330 | return function(_){ 331 | return str + _; 332 | }; 333 | } 334 | 335 | }); 336 | 337 | _require.register("component~to-function@2.0.5", function (exports, module) { 338 | 339 | /** 340 | * Module Dependencies 341 | */ 342 | 343 | var expr; 344 | try { 345 | expr = _require("component~props@1.1.2"); 346 | } catch(e) { 347 | expr = _require("component~props@1.1.2"); 348 | } 349 | 350 | /** 351 | * Expose `toFunction()`. 352 | */ 353 | 354 | module.exports = toFunction; 355 | 356 | /** 357 | * Convert `obj` to a `Function`. 358 | * 359 | * @param {Mixed} obj 360 | * @return {Function} 361 | * @api private 362 | */ 363 | 364 | function toFunction(obj) { 365 | switch ({}.toString.call(obj)) { 366 | case '[object Object]': 367 | return objectToFunction(obj); 368 | case '[object Function]': 369 | return obj; 370 | case '[object String]': 371 | return stringToFunction(obj); 372 | case '[object RegExp]': 373 | return regexpToFunction(obj); 374 | default: 375 | return defaultToFunction(obj); 376 | } 377 | } 378 | 379 | /** 380 | * Default to strict equality. 381 | * 382 | * @param {Mixed} val 383 | * @return {Function} 384 | * @api private 385 | */ 386 | 387 | function defaultToFunction(val) { 388 | return function(obj){ 389 | return val === obj; 390 | }; 391 | } 392 | 393 | /** 394 | * Convert `re` to a function. 395 | * 396 | * @param {RegExp} re 397 | * @return {Function} 398 | * @api private 399 | */ 400 | 401 | function regexpToFunction(re) { 402 | return function(obj){ 403 | return re.test(obj); 404 | }; 405 | } 406 | 407 | /** 408 | * Convert property `str` to a function. 409 | * 410 | * @param {String} str 411 | * @return {Function} 412 | * @api private 413 | */ 414 | 415 | function stringToFunction(str) { 416 | // immediate such as "> 20" 417 | if (/^ *\W+/.test(str)) return new Function('_', 'return _ ' + str); 418 | 419 | // properties such as "name.first" or "age > 18" or "age > 18 && age < 36" 420 | return new Function('_', 'return ' + get(str)); 421 | } 422 | 423 | /** 424 | * Convert `object` to a function. 425 | * 426 | * @param {Object} object 427 | * @return {Function} 428 | * @api private 429 | */ 430 | 431 | function objectToFunction(obj) { 432 | var match = {}; 433 | for (var key in obj) { 434 | match[key] = typeof obj[key] === 'string' 435 | ? defaultToFunction(obj[key]) 436 | : toFunction(obj[key]); 437 | } 438 | return function(val){ 439 | if (typeof val !== 'object') return false; 440 | for (var key in match) { 441 | if (!(key in val)) return false; 442 | if (!match[key](val[key])) return false; 443 | } 444 | return true; 445 | }; 446 | } 447 | 448 | /** 449 | * Built the getter function. Supports getter style functions 450 | * 451 | * @param {String} str 452 | * @return {String} 453 | * @api private 454 | */ 455 | 456 | function get(str) { 457 | var props = expr(str); 458 | if (!props.length) return '_.' + str; 459 | 460 | var val, i, prop; 461 | for (i = 0; i < props.length; i++) { 462 | prop = props[i]; 463 | val = '_.' + prop; 464 | val = "('function' == typeof " + val + " ? " + val + "() : " + val + ")"; 465 | 466 | // mimic negative lookbehind to avoid problems with nested properties 467 | str = stripNested(prop, str, val); 468 | } 469 | 470 | return str; 471 | } 472 | 473 | /** 474 | * Mimic negative lookbehind to avoid problems with nested properties. 475 | * 476 | * See: http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript 477 | * 478 | * @param {String} prop 479 | * @param {String} str 480 | * @param {String} val 481 | * @return {String} 482 | * @api private 483 | */ 484 | 485 | function stripNested (prop, str, val) { 486 | return str.replace(new RegExp('(\\.)?' + prop, 'g'), function($0, $1) { 487 | return $1 ? $0 : val; 488 | }); 489 | } 490 | 491 | }); 492 | 493 | _require.register("component~each@0.2.4", function (exports, module) { 494 | 495 | /** 496 | * Module dependencies. 497 | */ 498 | 499 | var type = _require("component~type@1.0.0"); 500 | var toFunction = _require("component~to-function@2.0.5"); 501 | 502 | /** 503 | * HOP reference. 504 | */ 505 | 506 | var has = Object.prototype.hasOwnProperty; 507 | 508 | /** 509 | * Iterate the given `obj` and invoke `fn(val, i)` 510 | * in optional context `ctx`. 511 | * 512 | * @param {String|Array|Object} obj 513 | * @param {Function} fn 514 | * @param {Object} [ctx] 515 | * @api public 516 | */ 517 | 518 | module.exports = function(obj, fn, ctx){ 519 | fn = toFunction(fn); 520 | ctx = ctx || this; 521 | switch (type(obj)) { 522 | case 'array': 523 | return array(obj, fn, ctx); 524 | case 'object': 525 | if ('number' == typeof obj.length) return array(obj, fn, ctx); 526 | return object(obj, fn, ctx); 527 | case 'string': 528 | return string(obj, fn, ctx); 529 | } 530 | }; 531 | 532 | /** 533 | * Iterate string chars. 534 | * 535 | * @param {String} obj 536 | * @param {Function} fn 537 | * @param {Object} ctx 538 | * @api private 539 | */ 540 | 541 | function string(obj, fn, ctx) { 542 | for (var i = 0; i < obj.length; ++i) { 543 | fn.call(ctx, obj.charAt(i), i); 544 | } 545 | } 546 | 547 | /** 548 | * Iterate object keys. 549 | * 550 | * @param {Object} obj 551 | * @param {Function} fn 552 | * @param {Object} ctx 553 | * @api private 554 | */ 555 | 556 | function object(obj, fn, ctx) { 557 | for (var key in obj) { 558 | if (has.call(obj, key)) { 559 | fn.call(ctx, key, obj[key]); 560 | } 561 | } 562 | } 563 | 564 | /** 565 | * Iterate array-ish. 566 | * 567 | * @param {Array|Object} obj 568 | * @param {Function} fn 569 | * @param {Object} ctx 570 | * @api private 571 | */ 572 | 573 | function array(obj, fn, ctx) { 574 | for (var i = 0; i < obj.length; ++i) { 575 | fn.call(ctx, obj[i], i); 576 | } 577 | } 578 | 579 | }); 580 | 581 | _require.register("component~emitter@1.1.2", function (exports, module) { 582 | 583 | /** 584 | * Expose `Emitter`. 585 | */ 586 | 587 | module.exports = Emitter; 588 | 589 | /** 590 | * Initialize a new `Emitter`. 591 | * 592 | * @api public 593 | */ 594 | 595 | function Emitter(obj) { 596 | if (obj) return mixin(obj); 597 | }; 598 | 599 | /** 600 | * Mixin the emitter properties. 601 | * 602 | * @param {Object} obj 603 | * @return {Object} 604 | * @api private 605 | */ 606 | 607 | function mixin(obj) { 608 | for (var key in Emitter.prototype) { 609 | obj[key] = Emitter.prototype[key]; 610 | } 611 | return obj; 612 | } 613 | 614 | /** 615 | * Listen on the given `event` with `fn`. 616 | * 617 | * @param {String} event 618 | * @param {Function} fn 619 | * @return {Emitter} 620 | * @api public 621 | */ 622 | 623 | Emitter.prototype.on = 624 | Emitter.prototype.addEventListener = function(event, fn){ 625 | this._callbacks = this._callbacks || {}; 626 | (this._callbacks[event] = this._callbacks[event] || []) 627 | .push(fn); 628 | return this; 629 | }; 630 | 631 | /** 632 | * Adds an `event` listener that will be invoked a single 633 | * time then automatically removed. 634 | * 635 | * @param {String} event 636 | * @param {Function} fn 637 | * @return {Emitter} 638 | * @api public 639 | */ 640 | 641 | Emitter.prototype.once = function(event, fn){ 642 | var self = this; 643 | this._callbacks = this._callbacks || {}; 644 | 645 | function on() { 646 | self.off(event, on); 647 | fn.apply(this, arguments); 648 | } 649 | 650 | on.fn = fn; 651 | this.on(event, on); 652 | return this; 653 | }; 654 | 655 | /** 656 | * Remove the given callback for `event` or all 657 | * registered callbacks. 658 | * 659 | * @param {String} event 660 | * @param {Function} fn 661 | * @return {Emitter} 662 | * @api public 663 | */ 664 | 665 | Emitter.prototype.off = 666 | Emitter.prototype.removeListener = 667 | Emitter.prototype.removeAllListeners = 668 | Emitter.prototype.removeEventListener = function(event, fn){ 669 | this._callbacks = this._callbacks || {}; 670 | 671 | // all 672 | if (0 == arguments.length) { 673 | this._callbacks = {}; 674 | return this; 675 | } 676 | 677 | // specific event 678 | var callbacks = this._callbacks[event]; 679 | if (!callbacks) return this; 680 | 681 | // remove all handlers 682 | if (1 == arguments.length) { 683 | delete this._callbacks[event]; 684 | return this; 685 | } 686 | 687 | // remove specific handler 688 | var cb; 689 | for (var i = 0; i < callbacks.length; i++) { 690 | cb = callbacks[i]; 691 | if (cb === fn || cb.fn === fn) { 692 | callbacks.splice(i, 1); 693 | break; 694 | } 695 | } 696 | return this; 697 | }; 698 | 699 | /** 700 | * Emit `event` with the given args. 701 | * 702 | * @param {String} event 703 | * @param {Mixed} ... 704 | * @return {Emitter} 705 | */ 706 | 707 | Emitter.prototype.emit = function(event){ 708 | this._callbacks = this._callbacks || {}; 709 | var args = [].slice.call(arguments, 1) 710 | , callbacks = this._callbacks[event]; 711 | 712 | if (callbacks) { 713 | callbacks = callbacks.slice(0); 714 | for (var i = 0, len = callbacks.length; i < len; ++i) { 715 | callbacks[i].apply(this, args); 716 | } 717 | } 718 | 719 | return this; 720 | }; 721 | 722 | /** 723 | * Return array of callbacks for `event`. 724 | * 725 | * @param {String} event 726 | * @return {Array} 727 | * @api public 728 | */ 729 | 730 | Emitter.prototype.listeners = function(event){ 731 | this._callbacks = this._callbacks || {}; 732 | return this._callbacks[event] || []; 733 | }; 734 | 735 | /** 736 | * Check if this emitter has `event` handlers. 737 | * 738 | * @param {String} event 739 | * @return {Boolean} 740 | * @api public 741 | */ 742 | 743 | Emitter.prototype.hasListeners = function(event){ 744 | return !! this.listeners(event).length; 745 | }; 746 | 747 | }); 748 | 749 | _require.register("timoxley~to-array@0.2.1", function (exports, module) { 750 | /** 751 | * Convert an array-like object into an `Array`. 752 | * If `collection` is already an `Array`, then will return a clone of `collection`. 753 | * 754 | * @param {Array | Mixed} collection An `Array` or array-like object to convert e.g. `arguments` or `NodeList` 755 | * @return {Array} Naive conversion of `collection` to a new `Array`. 756 | * @api public 757 | */ 758 | 759 | module.exports = function toArray(collection) { 760 | if (typeof collection === 'undefined') return [] 761 | if (collection === null) return [null] 762 | if (collection === window) return [window] 763 | if (typeof collection === 'string') return [collection] 764 | if (isArray(collection)) return collection 765 | if (typeof collection.length != 'number') return [collection] 766 | if (typeof collection === 'function' && collection instanceof Function) return [collection] 767 | 768 | var arr = [] 769 | for (var i = 0; i < collection.length; i++) { 770 | if (Object.prototype.hasOwnProperty.call(collection, i) || i in collection) { 771 | arr.push(collection[i]) 772 | } 773 | } 774 | if (!arr.length) return [] 775 | return arr 776 | } 777 | 778 | function isArray(arr) { 779 | return Object.prototype.toString.call(arr) === "[object Array]"; 780 | } 781 | 782 | }); 783 | 784 | _require.register("jaycetde~dom-contains@master", function (exports, module) { 785 | 'use strict'; 786 | 787 | var containsFn 788 | , node = window.Node 789 | ; 790 | 791 | if (node && node.prototype) { 792 | if (node.prototype.contains) { 793 | containsFn = node.prototype.contains; 794 | } else if (node.prototype.compareDocumentPosition) { 795 | containsFn = function (arg) { 796 | return !!(node.prototype.compareDocumentPosition.call(this, arg) & 16); 797 | }; 798 | } 799 | } 800 | 801 | if (!containsFn) { 802 | containsFn = function (arg) { 803 | if (arg) { 804 | while ((arg = arg.parentNode)) { 805 | if (arg === this) { 806 | return true; 807 | } 808 | } 809 | } 810 | return false; 811 | }; 812 | } 813 | 814 | module.exports = function (a, b) { 815 | var adown = a.nodeType === 9 ? a.documentElement : a 816 | , bup = b && b.parentNode 817 | ; 818 | 819 | return a === bup || !!(bup && bup.nodeType === 1 && containsFn.call(adown, bup)); 820 | }; 821 | 822 | }); 823 | 824 | _require.register("anthonyshort~dom-walk@0.1.0", function (exports, module) { 825 | var array = _require("timoxley~to-array@0.2.1"); 826 | var contains = _require("jaycetde~dom-contains@master"); 827 | 828 | function walk(el, process, done, root) { 829 | root = root || el; 830 | var end = done || function(){}; 831 | var nodes = array(el.childNodes); 832 | 833 | function next(){ 834 | if(nodes.length === 0) return end(); 835 | var nextNode = nodes.shift(); 836 | if(!contains(root, nextNode)) return next(); 837 | walk(nextNode, process, next, root); 838 | } 839 | 840 | process(el, next); 841 | } 842 | 843 | module.exports = walk; 844 | }); 845 | 846 | _require.register("component~raf@1.1.3", function (exports, module) { 847 | /** 848 | * Expose `requestAnimationFrame()`. 849 | */ 850 | 851 | exports = module.exports = window.requestAnimationFrame 852 | || window.webkitRequestAnimationFrame 853 | || window.mozRequestAnimationFrame 854 | || window.oRequestAnimationFrame 855 | || window.msRequestAnimationFrame 856 | || fallback; 857 | 858 | /** 859 | * Fallback implementation. 860 | */ 861 | 862 | var prev = new Date().getTime(); 863 | function fallback(fn) { 864 | var curr = new Date().getTime(); 865 | var ms = Math.max(0, 16 - (curr - prev)); 866 | var req = setTimeout(fn, ms); 867 | prev = curr; 868 | return req; 869 | } 870 | 871 | /** 872 | * Cancel. 873 | */ 874 | 875 | var cancel = window.cancelAnimationFrame 876 | || window.webkitCancelAnimationFrame 877 | || window.mozCancelAnimationFrame 878 | || window.oCancelAnimationFrame 879 | || window.msCancelAnimationFrame 880 | || window.clearTimeout; 881 | 882 | exports.cancel = function(id){ 883 | cancel.call(window, id); 884 | }; 885 | 886 | }); 887 | 888 | _require.register("anthonyshort~raf-queue@0.2.0", function (exports, module) { 889 | var raf = _require("component~raf@1.1.3"); 890 | var queue = []; 891 | var requestId; 892 | var id = 0; 893 | 894 | /** 895 | * Add a job to the queue passing in 896 | * an optional context to call the function in 897 | * 898 | * @param {Function} fn 899 | * @param {Object} cxt 900 | */ 901 | 902 | function frame (fn, cxt) { 903 | var frameId = id++; 904 | var length = queue.push({ 905 | id: frameId, 906 | fn: fn, 907 | cxt: cxt 908 | }); 909 | if(!requestId) requestId = raf(flush); 910 | return frameId; 911 | }; 912 | 913 | /** 914 | * Remove a job from the queue using the 915 | * frameId returned when it was added 916 | * 917 | * @param {Number} id 918 | */ 919 | 920 | frame.cancel = function (id) { 921 | for (var i = queue.length - 1; i >= 0; i--) { 922 | if(queue[i].id === id) { 923 | queue.splice(i, 1); 924 | break; 925 | } 926 | } 927 | }; 928 | 929 | /** 930 | * Add a function to the queue, but only once 931 | * 932 | * @param {Function} fn 933 | * @param {Object} cxt 934 | */ 935 | 936 | frame.once = function (fn, cxt) { 937 | for (var i = queue.length - 1; i >= 0; i--) { 938 | if(queue[i].fn === fn) return; 939 | } 940 | frame(fn, cxt); 941 | }; 942 | 943 | /** 944 | * Get the current queue length 945 | */ 946 | 947 | frame.queued = function () { 948 | return queue.length; 949 | }; 950 | 951 | /** 952 | * Clear the queue and remove all pending jobs 953 | */ 954 | 955 | frame.clear = function () { 956 | queue = []; 957 | if(requestId) raf.cancel(requestId); 958 | requestId = null; 959 | }; 960 | 961 | /** 962 | * Fire a function after all of the jobs in the 963 | * current queue have fired. This is usually used 964 | * in testing. 965 | */ 966 | 967 | frame.defer = function (fn) { 968 | raf(raf.bind(null, fn)); 969 | }; 970 | 971 | /** 972 | * Flushes the queue and runs each job 973 | */ 974 | 975 | function flush () { 976 | while(queue.length) { 977 | var job = queue.shift(); 978 | job.fn.call(job.cxt); 979 | } 980 | requestId = null; 981 | } 982 | 983 | module.exports = frame; 984 | }); 985 | 986 | _require.register("component~indexof@0.0.1", function (exports, module) { 987 | 988 | var indexOf = [].indexOf; 989 | 990 | module.exports = function(arr, obj){ 991 | if (indexOf) return arr.indexOf(obj); 992 | for (var i = 0; i < arr.length; ++i) { 993 | if (arr[i] === obj) return i; 994 | } 995 | return -1; 996 | }; 997 | }); 998 | 999 | _require.register("yields~uniq@master", function (exports, module) { 1000 | 1001 | /** 1002 | * dependencies 1003 | */ 1004 | 1005 | try { 1006 | var indexOf = _require("component~indexof@0.0.1"); 1007 | } catch(e){ 1008 | var indexOf = _require("indexof-component"); 1009 | } 1010 | 1011 | /** 1012 | * Create duplicate free array 1013 | * from the provided `arr`. 1014 | * 1015 | * @param {Array} arr 1016 | * @param {Array} select 1017 | * @return {Array} 1018 | */ 1019 | 1020 | module.exports = function (arr, select) { 1021 | var len = arr.length, ret = [], v; 1022 | select = select ? (select instanceof Array ? select : [select]) : false; 1023 | 1024 | for (var i = 0; i < len; i++) { 1025 | v = arr[i]; 1026 | if (select && !~indexOf(select, v)) { 1027 | ret.push(v); 1028 | } else if (!~indexOf(ret, v)) { 1029 | ret.push(v); 1030 | } 1031 | } 1032 | return ret; 1033 | }; 1034 | 1035 | }); 1036 | 1037 | _require.register("guille~ms.js@0.6.1", function (exports, module) { 1038 | /** 1039 | * Helpers. 1040 | */ 1041 | 1042 | var s = 1000; 1043 | var m = s * 60; 1044 | var h = m * 60; 1045 | var d = h * 24; 1046 | var y = d * 365.25; 1047 | 1048 | /** 1049 | * Parse or format the given `val`. 1050 | * 1051 | * Options: 1052 | * 1053 | * - `long` verbose formatting [false] 1054 | * 1055 | * @param {String|Number} val 1056 | * @param {Object} options 1057 | * @return {String|Number} 1058 | * @api public 1059 | */ 1060 | 1061 | module.exports = function(val, options){ 1062 | options = options || {}; 1063 | if ('string' == typeof val) return parse(val); 1064 | return options.long 1065 | ? long(val) 1066 | : short(val); 1067 | }; 1068 | 1069 | /** 1070 | * Parse the given `str` and return milliseconds. 1071 | * 1072 | * @param {String} str 1073 | * @return {Number} 1074 | * @api private 1075 | */ 1076 | 1077 | function parse(str) { 1078 | var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); 1079 | if (!match) return; 1080 | var n = parseFloat(match[1]); 1081 | var type = (match[2] || 'ms').toLowerCase(); 1082 | switch (type) { 1083 | case 'years': 1084 | case 'year': 1085 | case 'y': 1086 | return n * y; 1087 | case 'days': 1088 | case 'day': 1089 | case 'd': 1090 | return n * d; 1091 | case 'hours': 1092 | case 'hour': 1093 | case 'h': 1094 | return n * h; 1095 | case 'minutes': 1096 | case 'minute': 1097 | case 'm': 1098 | return n * m; 1099 | case 'seconds': 1100 | case 'second': 1101 | case 's': 1102 | return n * s; 1103 | case 'ms': 1104 | return n; 1105 | } 1106 | } 1107 | 1108 | /** 1109 | * Short format for `ms`. 1110 | * 1111 | * @param {Number} ms 1112 | * @return {String} 1113 | * @api private 1114 | */ 1115 | 1116 | function short(ms) { 1117 | if (ms >= d) return Math.round(ms / d) + 'd'; 1118 | if (ms >= h) return Math.round(ms / h) + 'h'; 1119 | if (ms >= m) return Math.round(ms / m) + 'm'; 1120 | if (ms >= s) return Math.round(ms / s) + 's'; 1121 | return ms + 'ms'; 1122 | } 1123 | 1124 | /** 1125 | * Long format for `ms`. 1126 | * 1127 | * @param {Number} ms 1128 | * @return {String} 1129 | * @api private 1130 | */ 1131 | 1132 | function long(ms) { 1133 | return plural(ms, d, 'day') 1134 | || plural(ms, h, 'hour') 1135 | || plural(ms, m, 'minute') 1136 | || plural(ms, s, 'second') 1137 | || ms + ' ms'; 1138 | } 1139 | 1140 | /** 1141 | * Pluralization helper. 1142 | */ 1143 | 1144 | function plural(ms, n, name) { 1145 | if (ms < n) return; 1146 | if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; 1147 | return Math.ceil(ms / n) + ' ' + name + 's'; 1148 | } 1149 | 1150 | }); 1151 | 1152 | _require.register("visionmedia~debug@1.0.4", function (exports, module) { 1153 | 1154 | /** 1155 | * This is the web browser implementation of `debug()`. 1156 | * 1157 | * Expose `debug()` as the module. 1158 | */ 1159 | 1160 | exports = module.exports = _require("visionmedia~debug@1.0.4/debug.js"); 1161 | exports.log = log; 1162 | exports.formatArgs = formatArgs; 1163 | exports.save = save; 1164 | exports.load = load; 1165 | exports.useColors = useColors; 1166 | 1167 | /** 1168 | * Colors. 1169 | */ 1170 | 1171 | exports.colors = [ 1172 | 'lightseagreen', 1173 | 'forestgreen', 1174 | 'goldenrod', 1175 | 'dodgerblue', 1176 | 'darkorchid', 1177 | 'crimson' 1178 | ]; 1179 | 1180 | /** 1181 | * Currently only WebKit-based Web Inspectors, Firefox >= v31, 1182 | * and the Firebug extension (any Firefox version) are known 1183 | * to support "%c" CSS customizations. 1184 | * 1185 | * TODO: add a `localStorage` variable to explicitly enable/disable colors 1186 | */ 1187 | 1188 | function useColors() { 1189 | // is webkit? http://stackoverflow.com/a/16459606/376773 1190 | return ('WebkitAppearance' in document.documentElement.style) || 1191 | // is firebug? http://stackoverflow.com/a/398120/376773 1192 | (window.console && (console.firebug || (console.exception && console.table))) || 1193 | // is firefox >= v31? 1194 | // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages 1195 | (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31); 1196 | } 1197 | 1198 | /** 1199 | * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. 1200 | */ 1201 | 1202 | exports.formatters.j = function(v) { 1203 | return JSON.stringify(v); 1204 | }; 1205 | 1206 | 1207 | /** 1208 | * Colorize log arguments if enabled. 1209 | * 1210 | * @api public 1211 | */ 1212 | 1213 | function formatArgs() { 1214 | var args = arguments; 1215 | var useColors = this.useColors; 1216 | 1217 | args[0] = (useColors ? '%c' : '') 1218 | + this.namespace 1219 | + (useColors ? ' %c' : ' ') 1220 | + args[0] 1221 | + (useColors ? '%c ' : ' ') 1222 | + '+' + exports.humanize(this.diff); 1223 | 1224 | if (!useColors) return args; 1225 | 1226 | var c = 'color: ' + this.color; 1227 | args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1)); 1228 | 1229 | // the final "%c" is somewhat tricky, because there could be other 1230 | // arguments passed either before or after the %c, so we need to 1231 | // figure out the correct index to insert the CSS into 1232 | var index = 0; 1233 | var lastC = 0; 1234 | args[0].replace(/%[a-z%]/g, function(match) { 1235 | if ('%%' === match) return; 1236 | index++; 1237 | if ('%c' === match) { 1238 | // we only are interested in the *last* %c 1239 | // (the user may have provided their own) 1240 | lastC = index; 1241 | } 1242 | }); 1243 | 1244 | args.splice(lastC, 0, c); 1245 | return args; 1246 | } 1247 | 1248 | /** 1249 | * Invokes `console.log()` when available. 1250 | * No-op when `console.log` is not a "function". 1251 | * 1252 | * @api public 1253 | */ 1254 | 1255 | function log() { 1256 | // This hackery is required for IE8, 1257 | // where the `console.log` function doesn't have 'apply' 1258 | return 'object' == typeof console 1259 | && 'function' == typeof console.log 1260 | && Function.prototype.apply.call(console.log, console, arguments); 1261 | } 1262 | 1263 | /** 1264 | * Save `namespaces`. 1265 | * 1266 | * @param {String} namespaces 1267 | * @api private 1268 | */ 1269 | 1270 | function save(namespaces) { 1271 | try { 1272 | if (null == namespaces) { 1273 | localStorage.removeItem('debug'); 1274 | } else { 1275 | localStorage.debug = namespaces; 1276 | } 1277 | } catch(e) {} 1278 | } 1279 | 1280 | /** 1281 | * Load `namespaces`. 1282 | * 1283 | * @return {String} returns the previously persisted debug modes 1284 | * @api private 1285 | */ 1286 | 1287 | function load() { 1288 | var r; 1289 | try { 1290 | r = localStorage.debug; 1291 | } catch(e) {} 1292 | return r; 1293 | } 1294 | 1295 | /** 1296 | * Enable namespaces listed in `localStorage.debug` initially. 1297 | */ 1298 | 1299 | exports.enable(load()); 1300 | 1301 | }); 1302 | 1303 | _require.register("visionmedia~debug@1.0.4/debug.js", function (exports, module) { 1304 | 1305 | /** 1306 | * This is the common logic for both the Node.js and web browser 1307 | * implementations of `debug()`. 1308 | * 1309 | * Expose `debug()` as the module. 1310 | */ 1311 | 1312 | exports = module.exports = debug; 1313 | exports.coerce = coerce; 1314 | exports.disable = disable; 1315 | exports.enable = enable; 1316 | exports.enabled = enabled; 1317 | exports.humanize = _require("guille~ms.js@0.6.1"); 1318 | 1319 | /** 1320 | * The currently active debug mode names, and names to skip. 1321 | */ 1322 | 1323 | exports.names = []; 1324 | exports.skips = []; 1325 | 1326 | /** 1327 | * Map of special "%n" handling functions, for the debug "format" argument. 1328 | * 1329 | * Valid key names are a single, lowercased letter, i.e. "n". 1330 | */ 1331 | 1332 | exports.formatters = {}; 1333 | 1334 | /** 1335 | * Previously assigned color. 1336 | */ 1337 | 1338 | var prevColor = 0; 1339 | 1340 | /** 1341 | * Previous log timestamp. 1342 | */ 1343 | 1344 | var prevTime; 1345 | 1346 | /** 1347 | * Select a color. 1348 | * 1349 | * @return {Number} 1350 | * @api private 1351 | */ 1352 | 1353 | function selectColor() { 1354 | return exports.colors[prevColor++ % exports.colors.length]; 1355 | } 1356 | 1357 | /** 1358 | * Create a debugger with the given `namespace`. 1359 | * 1360 | * @param {String} namespace 1361 | * @return {Function} 1362 | * @api public 1363 | */ 1364 | 1365 | function debug(namespace) { 1366 | 1367 | // define the `disabled` version 1368 | function disabled() { 1369 | } 1370 | disabled.enabled = false; 1371 | 1372 | // define the `enabled` version 1373 | function enabled() { 1374 | 1375 | var self = enabled; 1376 | 1377 | // set `diff` timestamp 1378 | var curr = +new Date(); 1379 | var ms = curr - (prevTime || curr); 1380 | self.diff = ms; 1381 | self.prev = prevTime; 1382 | self.curr = curr; 1383 | prevTime = curr; 1384 | 1385 | // add the `color` if not set 1386 | if (null == self.useColors) self.useColors = exports.useColors(); 1387 | if (null == self.color && self.useColors) self.color = selectColor(); 1388 | 1389 | var args = Array.prototype.slice.call(arguments); 1390 | 1391 | args[0] = exports.coerce(args[0]); 1392 | 1393 | if ('string' !== typeof args[0]) { 1394 | // anything else let's inspect with %o 1395 | args = ['%o'].concat(args); 1396 | } 1397 | 1398 | // apply any `formatters` transformations 1399 | var index = 0; 1400 | args[0] = args[0].replace(/%([a-z%])/g, function(match, format) { 1401 | // if we encounter an escaped % then don't increase the array index 1402 | if (match === '%%') return match; 1403 | index++; 1404 | var formatter = exports.formatters[format]; 1405 | if ('function' === typeof formatter) { 1406 | var val = args[index]; 1407 | match = formatter.call(self, val); 1408 | 1409 | // now we need to remove `args[index]` since it's inlined in the `format` 1410 | args.splice(index, 1); 1411 | index--; 1412 | } 1413 | return match; 1414 | }); 1415 | 1416 | if ('function' === typeof exports.formatArgs) { 1417 | args = exports.formatArgs.apply(self, args); 1418 | } 1419 | var logFn = enabled.log || exports.log || console.log.bind(console); 1420 | logFn.apply(self, args); 1421 | } 1422 | enabled.enabled = true; 1423 | 1424 | var fn = exports.enabled(namespace) ? enabled : disabled; 1425 | 1426 | fn.namespace = namespace; 1427 | 1428 | return fn; 1429 | } 1430 | 1431 | /** 1432 | * Enables a debug mode by namespaces. This can include modes 1433 | * separated by a colon and wildcards. 1434 | * 1435 | * @param {String} namespaces 1436 | * @api public 1437 | */ 1438 | 1439 | function enable(namespaces) { 1440 | exports.save(namespaces); 1441 | 1442 | var split = (namespaces || '').split(/[\s,]+/); 1443 | var len = split.length; 1444 | 1445 | for (var i = 0; i < len; i++) { 1446 | if (!split[i]) continue; // ignore empty strings 1447 | namespaces = split[i].replace(/\*/g, '.*?'); 1448 | if (namespaces[0] === '-') { 1449 | exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); 1450 | } else { 1451 | exports.names.push(new RegExp('^' + namespaces + '$')); 1452 | } 1453 | } 1454 | } 1455 | 1456 | /** 1457 | * Disable debug output. 1458 | * 1459 | * @api public 1460 | */ 1461 | 1462 | function disable() { 1463 | exports.enable(''); 1464 | } 1465 | 1466 | /** 1467 | * Returns true if the given mode name is enabled, false otherwise. 1468 | * 1469 | * @param {String} name 1470 | * @return {Boolean} 1471 | * @api public 1472 | */ 1473 | 1474 | function enabled(name) { 1475 | var i, len; 1476 | for (i = 0, len = exports.skips.length; i < len; i++) { 1477 | if (exports.skips[i].test(name)) { 1478 | return false; 1479 | } 1480 | } 1481 | for (i = 0, len = exports.names.length; i < len; i++) { 1482 | if (exports.names[i].test(name)) { 1483 | return true; 1484 | } 1485 | } 1486 | return false; 1487 | } 1488 | 1489 | /** 1490 | * Coerce `val`. 1491 | * 1492 | * @param {Mixed} val 1493 | * @return {Mixed} 1494 | * @api private 1495 | */ 1496 | 1497 | function coerce(val) { 1498 | if (val instanceof Error) return val.stack || val.message; 1499 | return val; 1500 | } 1501 | 1502 | }); 1503 | 1504 | _require.register("ripplejs~expression@0.2.0", function (exports, module) { 1505 | var props = _require("component~props@1.1.2"); 1506 | var unique = _require("yields~uniq@master"); 1507 | var cache = {}; 1508 | 1509 | function Expression(str) { 1510 | this.str = str; 1511 | this.props = unique(props(str)); 1512 | this.fn = compile(str, this.props); 1513 | } 1514 | 1515 | Expression.prototype.exec = function(scope, context){ 1516 | scope = scope || {}; 1517 | var args = scope ? values(scope, this.props) : []; 1518 | return this.fn.apply(context, args); 1519 | }; 1520 | 1521 | Expression.prototype.toString = function(){ 1522 | return this.str; 1523 | }; 1524 | 1525 | function values(obj, keys) { 1526 | return keys.map(function(key){ 1527 | return obj[key]; 1528 | }); 1529 | } 1530 | 1531 | function compile(str, props){ 1532 | if(cache[str]) return cache[str]; 1533 | var args = props.slice(); 1534 | args.push('return ' + str); 1535 | var fn = Function.apply(null, args); 1536 | cache[str] = fn; 1537 | return fn; 1538 | } 1539 | 1540 | module.exports = Expression; 1541 | }); 1542 | 1543 | _require.register("component~format-parser@0.0.2", function (exports, module) { 1544 | 1545 | /** 1546 | * Parse the given format `str`. 1547 | * 1548 | * @param {String} str 1549 | * @return {Array} 1550 | * @api public 1551 | */ 1552 | 1553 | module.exports = function(str){ 1554 | return str.split(/ *\| */).map(function(call){ 1555 | var parts = call.split(':'); 1556 | var name = parts.shift(); 1557 | var args = parseArgs(parts.join(':')); 1558 | 1559 | return { 1560 | name: name, 1561 | args: args 1562 | }; 1563 | }); 1564 | }; 1565 | 1566 | /** 1567 | * Parse args `str`. 1568 | * 1569 | * @param {String} str 1570 | * @return {Array} 1571 | * @api private 1572 | */ 1573 | 1574 | function parseArgs(str) { 1575 | var args = []; 1576 | var re = /"([^"]*)"|'([^']*)'|([^ \t,]+)/g; 1577 | var m; 1578 | 1579 | while (m = re.exec(str)) { 1580 | args.push(m[2] || m[1] || m[0]); 1581 | } 1582 | 1583 | return args; 1584 | } 1585 | 1586 | }); 1587 | 1588 | _require.register("ripplejs~interpolate@0.4.5", function (exports, module) { 1589 | var Expression = _require("ripplejs~expression@0.2.0"); 1590 | var parse = _require("component~format-parser@0.0.2"); 1591 | var unique = _require("yields~uniq@master"); 1592 | var debug = _require("visionmedia~debug@1.0.4")('ripplejs/interpolate'); 1593 | 1594 | /** 1595 | * Run a value through all filters 1596 | * 1597 | * @param {Mixed} val Any value returned from an expression 1598 | * @param {Array} types The filters eg. currency | float | floor 1599 | * @param {Object} fns Mapping of filter names, eg. currency, to functions 1600 | * @return {Mixed} 1601 | */ 1602 | 1603 | function filter(val, types, fns) { 1604 | fns = fns || {}; 1605 | var filters = parse(types.join('|')); 1606 | filters.forEach(function(f){ 1607 | var name = f.name.trim(); 1608 | var fn = fns[name]; 1609 | var args = f.args.slice(); 1610 | args.unshift(val); 1611 | if(!fn) throw new Error('Missing filter named "' + name + '"'); 1612 | val = fn.apply(null, args); 1613 | }); 1614 | return val; 1615 | } 1616 | 1617 | /** 1618 | * Create a new interpolator 1619 | */ 1620 | 1621 | function Interpolate() { 1622 | this.match = /\{\{([^}]+)\}\}/g; 1623 | this.filters = {}; 1624 | } 1625 | 1626 | /** 1627 | * Hook for plugins 1628 | * 1629 | * @param {Function} fn 1630 | * 1631 | * @return {Interpolate} 1632 | */ 1633 | 1634 | Interpolate.prototype.use = function(fn) { 1635 | fn(this); 1636 | return this; 1637 | }; 1638 | 1639 | /** 1640 | * Set the delimiters 1641 | * 1642 | * @param {Regex} match 1643 | * 1644 | * @return {Interpolate} 1645 | */ 1646 | 1647 | Interpolate.prototype.delimiters = function(match) { 1648 | this.match = match; 1649 | return this; 1650 | }; 1651 | 1652 | /** 1653 | * Check if a string matches the delimiters 1654 | * 1655 | * @param {String} input 1656 | * 1657 | * @return {Array} 1658 | */ 1659 | 1660 | Interpolate.prototype.matches = function(input) { 1661 | var test = new RegExp(this.match.source); 1662 | var matches = test.exec(input); 1663 | if(!matches) return []; 1664 | return matches; 1665 | }; 1666 | 1667 | /** 1668 | * Add a new filter 1669 | * 1670 | * @param {String} name 1671 | * @param {Function} fn 1672 | * 1673 | * @return {Interpolate} 1674 | */ 1675 | 1676 | Interpolate.prototype.filter = function(name, fn){ 1677 | this.filters[name] = fn; 1678 | return this; 1679 | }; 1680 | 1681 | /** 1682 | * Interpolate a string using the contents 1683 | * inside of the delimiters 1684 | * 1685 | * @param {String} input 1686 | * @param {Object} options 1687 | * @return {String} 1688 | */ 1689 | 1690 | Interpolate.prototype.exec = function(input, options){ 1691 | options = options || {}; 1692 | var parts = input.split('|'); 1693 | var expr = parts.shift(); 1694 | var fn = new Expression(expr); 1695 | var val; 1696 | 1697 | try { 1698 | val = fn.exec(options.scope, options.context); 1699 | } 1700 | catch (e) { 1701 | debug(e.message); 1702 | } 1703 | 1704 | if(parts.length) { 1705 | val = filter(val, parts, options.filters || this.filters); 1706 | } 1707 | return val; 1708 | }; 1709 | 1710 | /** 1711 | * Check if a string has interpolation 1712 | * 1713 | * @param {String} input 1714 | * 1715 | * @return {Boolean} 1716 | */ 1717 | 1718 | Interpolate.prototype.has = function(input) { 1719 | return input.search(this.match) > -1; 1720 | }; 1721 | 1722 | /** 1723 | * Interpolate as a string and replace each 1724 | * match with the interpolated value 1725 | * 1726 | * @return {String} 1727 | */ 1728 | 1729 | Interpolate.prototype.replace = function(input, options){ 1730 | var self = this; 1731 | return input.replace(this.match, function(_, match){ 1732 | var val = self.exec(match, options); 1733 | return (val == null) ? '' : val; 1734 | }); 1735 | }; 1736 | 1737 | /** 1738 | * Get the interpolated value from a string 1739 | */ 1740 | 1741 | Interpolate.prototype.value = function(input, options){ 1742 | var matches = this.matches(input); 1743 | if( matches.length === 0 ) return input; 1744 | if( matches[0].trim().length !== input.trim().length ) return this.replace(input, options); 1745 | return this.exec(matches[1], options); 1746 | }; 1747 | 1748 | /** 1749 | * Get all the interpolated values from a string 1750 | * 1751 | * @return {Array} Array of values 1752 | */ 1753 | 1754 | Interpolate.prototype.values = function(input, options){ 1755 | var self = this; 1756 | return this.map(input, function(match){ 1757 | return self.value(match, options); 1758 | }); 1759 | }; 1760 | 1761 | /** 1762 | * Find all the properties used in all expressions in a string 1763 | * @param {String} str 1764 | * @return {Array} 1765 | */ 1766 | 1767 | Interpolate.prototype.props = function(str) { 1768 | var arr = []; 1769 | this.each(str, function(match, expr, filters){ 1770 | var fn = new Expression(expr); 1771 | arr = arr.concat(fn.props); 1772 | }); 1773 | return unique(arr); 1774 | }; 1775 | 1776 | /** 1777 | * Loop through each matched expression in a string 1778 | * 1779 | * @param {String} str 1780 | * 1781 | * @return {void} 1782 | */ 1783 | 1784 | Interpolate.prototype.each = function(str, callback) { 1785 | var m; 1786 | var index = 0; 1787 | var re = this.match; 1788 | while (m = re.exec(str)) { 1789 | var parts = m[1].split('|'); 1790 | var expr = parts.shift(); 1791 | var filters = parts.join('|'); 1792 | callback(m[0], expr, filters, index); 1793 | index++; 1794 | } 1795 | }; 1796 | 1797 | /** 1798 | * Map the string 1799 | * 1800 | * @param {String} str 1801 | * @param {Function} callback 1802 | * 1803 | * @return {Array} 1804 | */ 1805 | 1806 | Interpolate.prototype.map = function(str, callback) { 1807 | var ret = []; 1808 | this.each(str, function(){ 1809 | ret.push(callback.apply(null, arguments)); 1810 | }); 1811 | return ret; 1812 | }; 1813 | 1814 | /** 1815 | * Export the constructor 1816 | * 1817 | * @type {Function} 1818 | */ 1819 | 1820 | module.exports = Interpolate; 1821 | }); 1822 | 1823 | _require.register("ripplejs~keypath@0.0.1", function (exports, module) { 1824 | exports.get = function(obj, path) { 1825 | var parts = path.split('.'); 1826 | var value = obj; 1827 | while(parts.length) { 1828 | var part = parts.shift(); 1829 | value = value[part]; 1830 | if(value === undefined) parts.length = 0; 1831 | } 1832 | return value; 1833 | }; 1834 | 1835 | exports.set = function(obj, path, value) { 1836 | var parts = path.split('.'); 1837 | var target = obj; 1838 | var last = parts.pop(); 1839 | while(parts.length) { 1840 | part = parts.shift(); 1841 | if(!target[part]) target[part] = {}; 1842 | target = target[part]; 1843 | } 1844 | target[last] = value; 1845 | }; 1846 | }); 1847 | 1848 | _require.register("ripplejs~path-observer@0.2.0", function (exports, module) { 1849 | var emitter = _require("component~emitter@1.1.2"); 1850 | var keypath = _require("ripplejs~keypath@0.0.1"); 1851 | var type = _require("component~type@1.0.0"); 1852 | var raf = _require("anthonyshort~raf-queue@0.2.0"); 1853 | 1854 | module.exports = function(obj) { 1855 | 1856 | /** 1857 | * Stores each observer created for each 1858 | * path so they're singletons. This allows us to 1859 | * fire change events on all related paths. 1860 | * 1861 | * @type {Object} 1862 | */ 1863 | var cache = {}; 1864 | 1865 | /** 1866 | * Takes a path and announces whenever 1867 | * the value at that path changes. 1868 | * 1869 | * @param {String} path The keypath to the value 'foo.bar.baz' 1870 | */ 1871 | function PathObserver(path) { 1872 | if(!(this instanceof PathObserver)) return new PathObserver(path); 1873 | if(cache[path]) return cache[path]; 1874 | this.path = path; 1875 | Object.defineProperty(this, 'value', { 1876 | get: function() { 1877 | return keypath.get(obj, this.path); 1878 | }, 1879 | set: function(val) { 1880 | keypath.set(obj, this.path, val); 1881 | } 1882 | }); 1883 | cache[path] = this; 1884 | } 1885 | 1886 | /** 1887 | * Remove all path observers 1888 | */ 1889 | PathObserver.dispose = function() { 1890 | for(var path in cache) { 1891 | cache[path].dispose(); 1892 | } 1893 | this.off(); 1894 | }; 1895 | 1896 | /** 1897 | * Emit a change event next tick 1898 | */ 1899 | PathObserver.change = function() { 1900 | raf.once(this.notify, this); 1901 | }; 1902 | 1903 | /** 1904 | * Notify observers of a change 1905 | */ 1906 | PathObserver.notify = function() { 1907 | this.emit('change'); 1908 | }; 1909 | 1910 | /** 1911 | * Mixin 1912 | */ 1913 | emitter(PathObserver); 1914 | emitter(PathObserver.prototype); 1915 | 1916 | /** 1917 | * Get the value of the path. 1918 | * 1919 | * @return {Mixed} 1920 | */ 1921 | PathObserver.prototype.get = function() { 1922 | return this.value; 1923 | }; 1924 | 1925 | /** 1926 | * Set the value of the keypath 1927 | * 1928 | * @return {PathObserver} 1929 | */ 1930 | PathObserver.prototype.set = function(val) { 1931 | var current = this.value; 1932 | 1933 | if (type(val) === 'object') { 1934 | var changes = 0; 1935 | for (var key in val) { 1936 | var path = new PathObserver(this.path + '.' + key); 1937 | path.once('change', function(){ 1938 | changes += 1; 1939 | }); 1940 | path.set(val[key]); 1941 | } 1942 | if (changes > 0) { 1943 | this.emit('change', this.value, current); 1944 | } 1945 | return; 1946 | } 1947 | 1948 | // no change 1949 | if(current === val) return this; 1950 | 1951 | this.value = val; 1952 | this.emit('change', this.value, current); 1953 | PathObserver.change(); 1954 | return this; 1955 | }; 1956 | 1957 | /** 1958 | * Bind to changes on this path 1959 | * 1960 | * @param {Function} fn 1961 | * 1962 | * @return {Function} 1963 | */ 1964 | PathObserver.prototype.change = function(fn){ 1965 | var self = this; 1966 | self.on('change', fn); 1967 | return function(){ 1968 | self.off('change', fn); 1969 | }; 1970 | }; 1971 | 1972 | /** 1973 | * Clean up and remove all event bindings 1974 | */ 1975 | PathObserver.prototype.dispose = function(){ 1976 | this.off('change'); 1977 | delete cache[this.path]; 1978 | }; 1979 | 1980 | return PathObserver; 1981 | }; 1982 | }); 1983 | 1984 | _require.register("ripple", function (exports, module) { 1985 | var emitter = _require("component~emitter@1.1.2"); 1986 | var observer = _require("ripplejs~path-observer@0.2.0"); 1987 | var proto = _require("ripple/lib/proto.js"); 1988 | var statics = _require("ripple/lib/static.js"); 1989 | var id = 0; 1990 | 1991 | /** 1992 | * Allow for a selector or an element to be passed in 1993 | * as the template for the view 1994 | */ 1995 | 1996 | function getTemplate(template) { 1997 | if (template.indexOf('#') === 0 || template.indexOf('.') === 0) { 1998 | template = document.querySelector(template); 1999 | } 2000 | if (typeof template.innerHTML === 'string') { 2001 | template = template.innerHTML; 2002 | } 2003 | return template; 2004 | } 2005 | 2006 | /** 2007 | * Create a new view from a template string 2008 | * 2009 | * @param {String} template 2010 | * 2011 | * @return {View} 2012 | */ 2013 | 2014 | module.exports = function(template) { 2015 | if (!template) throw new Error('template is required'); 2016 | template = getTemplate(template); 2017 | 2018 | function View (attrs, options) { 2019 | if (!(this instanceof View)) return new View(attrs, options); 2020 | attrs = attrs || {}; 2021 | options = options || {}; 2022 | View.emit('construct', this, attrs, options); 2023 | this.options = options; 2024 | this.id = id++; 2025 | this.root = this; 2026 | this.attrs = attrs; 2027 | this.observer = observer(attrs); 2028 | this.template = options.template || template; 2029 | if (options.owner) { 2030 | this.owner = options.owner; 2031 | this.root = this.owner.root; 2032 | this.owner.on('destroying', this.destroy.bind(this)); 2033 | } 2034 | View.emit('created', this); 2035 | if (this.initialize) this.initialize(); 2036 | this.el = this.render(); 2037 | View.emit('ready', this); 2038 | } 2039 | 2040 | // mixins 2041 | 2042 | emitter(View); 2043 | emitter(View.prototype); 2044 | 2045 | // statics 2046 | 2047 | View.attrs = {}; 2048 | View.components = {}; 2049 | View.directives = {}; 2050 | View.filters = {}; 2051 | for (var staticKey in statics) View[staticKey] = statics[staticKey]; 2052 | 2053 | // prototype 2054 | 2055 | View.prototype.view = View; 2056 | for (var protoKey in proto) View.prototype[protoKey] = proto[protoKey]; 2057 | 2058 | return View; 2059 | }; 2060 | 2061 | }); 2062 | 2063 | _require.register("ripple/lib/proto.js", function (exports, module) { 2064 | var render = _require("ripple/lib/bindings/index.js"); 2065 | var Interpolator = _require("ripplejs~interpolate@0.4.5"); 2066 | 2067 | /** 2068 | * Run expressions 2069 | * 2070 | * @type {Interpolator} 2071 | */ 2072 | 2073 | var interpolator = new Interpolator(); 2074 | 2075 | /** 2076 | * Get a node using element the element itself 2077 | * or a CSS selector 2078 | * 2079 | * @param {Element|String} node 2080 | * 2081 | * @return {Element} 2082 | */ 2083 | 2084 | function getNode(node) { 2085 | if (typeof node === 'string') { 2086 | node = document.querySelector(node); 2087 | if (node === null) throw new Error('node does not exist'); 2088 | } 2089 | return node; 2090 | } 2091 | 2092 | /** 2093 | * Set the state off the view. This will trigger 2094 | * refreshes to the UI. If we were previously 2095 | * watching the parent scope for changes to this 2096 | * property, we will remove all of those watchers 2097 | * and then bind them to our model instead. 2098 | * 2099 | * @param {Object} obj 2100 | */ 2101 | 2102 | exports.set = function(path, value) { 2103 | if (typeof path !== 'string') { 2104 | for(var name in path) this.set(name, path[name]); 2105 | return this; 2106 | } 2107 | this.observer(path).set(value); 2108 | return this; 2109 | }; 2110 | 2111 | /** 2112 | * Get some data 2113 | * 2114 | * @param {String} path 2115 | */ 2116 | 2117 | exports.get = function(path) { 2118 | return this.observer(path).get(); 2119 | }; 2120 | 2121 | /** 2122 | * Get all the properties used in a string 2123 | * 2124 | * @param {String} str 2125 | * 2126 | * @return {Array} 2127 | */ 2128 | 2129 | exports.props = function(str) { 2130 | return interpolator.props(str); 2131 | }; 2132 | 2133 | /** 2134 | * Remove the element from the DOM 2135 | */ 2136 | 2137 | exports.destroy = function() { 2138 | this.emit('destroying'); 2139 | this.view.emit('destroying', this); 2140 | this.remove(); 2141 | this.observer.dispose(); 2142 | this.off(); 2143 | }; 2144 | 2145 | /** 2146 | * Is the view mounted in the DOM 2147 | * 2148 | * @return {Boolean} 2149 | */ 2150 | 2151 | exports.isMounted = function() { 2152 | return this.el != null && this.el.parentNode != null; 2153 | }; 2154 | 2155 | /** 2156 | * Render the view to an element. This should 2157 | * only ever render the element once. 2158 | */ 2159 | 2160 | exports.render = function() { 2161 | return render({ 2162 | view: this, 2163 | template: this.template, 2164 | directives: this.view.directives, 2165 | components: this.view.components 2166 | }); 2167 | }; 2168 | 2169 | /** 2170 | * Mount the view onto a node 2171 | * 2172 | * @param {Element|String} node An element or CSS selector 2173 | * 2174 | * @return {View} 2175 | */ 2176 | 2177 | exports.appendTo = function(node) { 2178 | getNode(node).appendChild(this.el); 2179 | this.emit('mounted'); 2180 | this.view.emit('mounted', this); 2181 | return this; 2182 | }; 2183 | 2184 | /** 2185 | * Replace an element in the DOM with this view 2186 | * 2187 | * @param {Element|String} node An element or CSS selector 2188 | * 2189 | * @return {View} 2190 | */ 2191 | 2192 | exports.replace = function(node) { 2193 | var target = getNode(node); 2194 | target.parentNode.replaceChild(this.el, target); 2195 | this.emit('mounted'); 2196 | this.view.emit('mounted', this); 2197 | return this; 2198 | }; 2199 | 2200 | /** 2201 | * Insert the view before a node 2202 | * 2203 | * @param {Element|String} node 2204 | * 2205 | * @return {View} 2206 | */ 2207 | 2208 | exports.before = function(node) { 2209 | var target = getNode(node); 2210 | target.parentNode.insertBefore(this.el, target); 2211 | this.emit('mounted'); 2212 | this.view.emit('mounted', this); 2213 | return this; 2214 | }; 2215 | 2216 | /** 2217 | * Insert the view after a node 2218 | * 2219 | * @param {Element|String} node 2220 | * 2221 | * @return {View} 2222 | */ 2223 | 2224 | exports.after = function(node) { 2225 | var target = getNode(node); 2226 | target.parentNode.insertBefore(this.el, target.nextSibling); 2227 | this.emit('mounted'); 2228 | this.view.emit('mounted', this); 2229 | return this; 2230 | }; 2231 | 2232 | /** 2233 | * Remove the view from the DOM 2234 | * 2235 | * @return {View} 2236 | */ 2237 | 2238 | exports.remove = function() { 2239 | if (this.isMounted() === false) return this; 2240 | this.el.parentNode.removeChild(this.el); 2241 | this.emit('unmounted'); 2242 | this.view.emit('unmounted', this); 2243 | return this; 2244 | }; 2245 | 2246 | /** 2247 | * Interpolate a string 2248 | * 2249 | * @param {String} str 2250 | */ 2251 | 2252 | exports.interpolate = function(str) { 2253 | var self = this; 2254 | var data = {}; 2255 | var props = this.props(str); 2256 | props.forEach(function(prop){ 2257 | data[prop] = self.get(prop); 2258 | }); 2259 | return interpolator.value(str, { 2260 | context: this, 2261 | scope: data, 2262 | filters: this.view.filters 2263 | }); 2264 | }; 2265 | 2266 | /** 2267 | * Watch a property for changes 2268 | * 2269 | * @param {Strign} prop 2270 | * @param {Function} callback 2271 | */ 2272 | 2273 | exports.watch = function(prop, callback) { 2274 | var self = this; 2275 | if (Array.isArray(prop)) { 2276 | return prop.forEach(function(name){ 2277 | self.watch(name, callback); 2278 | }); 2279 | } 2280 | if (typeof prop === 'function') { 2281 | this.observer.on('change', prop); 2282 | } 2283 | else { 2284 | this.observer(prop).on('change', callback); 2285 | } 2286 | return this; 2287 | }; 2288 | 2289 | /** 2290 | * Stop watching a property 2291 | * 2292 | * @param {Strign} prop 2293 | * @param {Function} callback 2294 | */ 2295 | 2296 | exports.unwatch = function(prop, callback) { 2297 | var self = this; 2298 | if (Array.isArray(prop)) { 2299 | return prop.forEach(function(name){ 2300 | self.unwatch(name, callback); 2301 | }); 2302 | } 2303 | if (typeof prop === 'function') { 2304 | this.observer.off('change', prop); 2305 | } 2306 | else { 2307 | this.observer(prop).off('change', callback); 2308 | } 2309 | return this; 2310 | }; 2311 | }); 2312 | 2313 | _require.register("ripple/lib/static.js", function (exports, module) { 2314 | var type = _require("component~type@1.0.0"); 2315 | 2316 | /** 2317 | * Add an attribute. This allows attributes to be created 2318 | * and set with attributes. It also creates getters and 2319 | * setters for the attributes on the view. 2320 | * 2321 | * @param {String} name 2322 | * @param {Object} options 2323 | * 2324 | * @return {View} 2325 | */ 2326 | 2327 | exports.attr = function(name, options) { 2328 | options = options || {}; 2329 | this.attrs[name] = options; 2330 | this.on('construct', function(view, attrs){ 2331 | if (attrs[name] == null) { 2332 | attrs[name] = options.default; 2333 | } 2334 | if (options.required && attrs[name] == null) { 2335 | throw new Error(name + ' is a required attribute'); 2336 | } 2337 | if (options.type && attrs[name] != null && type(attrs[name]) !== options.type) { 2338 | throw new Error(name + ' should be type "' + options.type + '"'); 2339 | } 2340 | }); 2341 | Object.defineProperty(this.prototype, name, { 2342 | set: function(value) { 2343 | this.set(name, value); 2344 | }, 2345 | get: function() { 2346 | return this.get(name); 2347 | } 2348 | }); 2349 | return this; 2350 | }; 2351 | 2352 | /** 2353 | * Add a directive 2354 | * 2355 | * @param {String|Regex} match 2356 | * @param {Function} fn 2357 | * 2358 | * @return {View} 2359 | */ 2360 | 2361 | exports.directive = function(name, fn) { 2362 | if (typeof name !== 'string') { 2363 | for(var key in name) { 2364 | this.directive(key, name[key]); 2365 | } 2366 | return; 2367 | } 2368 | this.directives[name] = fn; 2369 | return this; 2370 | }; 2371 | 2372 | /** 2373 | * Add a component 2374 | * 2375 | * @param {String} match 2376 | * @param {Function} fn 2377 | * 2378 | * @return {View} 2379 | */ 2380 | 2381 | exports.compose = function(name, fn) { 2382 | if (typeof name !== 'string') { 2383 | for(var key in name) { 2384 | this.compose(key, name[key]); 2385 | } 2386 | return; 2387 | } 2388 | this.components[name.toLowerCase()] = fn; 2389 | return this; 2390 | }; 2391 | 2392 | /** 2393 | * Add interpolation filter 2394 | * 2395 | * @param {String} name 2396 | * @param {Function} fn 2397 | * 2398 | * @return {View} 2399 | */ 2400 | 2401 | exports.filter = function(name, fn) { 2402 | if (typeof name !== 'string') { 2403 | for(var key in name) { 2404 | this.filter(key, name[key]); 2405 | } 2406 | return; 2407 | } 2408 | this.filters[name] = fn; 2409 | return this; 2410 | }; 2411 | 2412 | /** 2413 | * Use a plugin 2414 | * 2415 | * @return {View} 2416 | */ 2417 | 2418 | exports.use = function(fn, options) { 2419 | fn(this, options); 2420 | return this; 2421 | }; 2422 | 2423 | }); 2424 | 2425 | _require.register("ripple/lib/bindings/index.js", function (exports, module) { 2426 | var walk = _require("anthonyshort~dom-walk@0.1.0"); 2427 | var each = _require("component~each@0.2.4"); 2428 | var attrs = _require("anthonyshort~attributes@0.0.1"); 2429 | var domify = _require("component~domify@1.2.2"); 2430 | var TextBinding = _require("ripple/lib/bindings/text.js"); 2431 | var AttrBinding = _require("ripple/lib/bindings/attribute.js"); 2432 | var ChildBinding = _require("ripple/lib/bindings/child.js"); 2433 | var Directive = _require("ripple/lib/bindings/directive.js"); 2434 | 2435 | module.exports = function(options) { 2436 | var view = options.view; 2437 | var el = domify(options.template); 2438 | var fragment = document.createDocumentFragment(); 2439 | fragment.appendChild(el); 2440 | 2441 | var activeBindings = []; 2442 | 2443 | // Walk down the newly created view element 2444 | // and bind everything to the model 2445 | walk(el, function(node, next){ 2446 | if(node.nodeType === 3) { 2447 | activeBindings.push(new TextBinding(view, node)); 2448 | } 2449 | else if(node.nodeType === 1) { 2450 | var View = options.components[node.nodeName.toLowerCase()]; 2451 | if(View) { 2452 | activeBindings.push(new ChildBinding(view, node, View)); 2453 | return next(); 2454 | } 2455 | each(attrs(node), function(attr){ 2456 | var binding = options.directives[attr]; 2457 | if(binding) { 2458 | activeBindings.push(new Directive(view, node, attr, binding)); 2459 | } 2460 | else { 2461 | activeBindings.push(new AttrBinding(view, node, attr)); 2462 | } 2463 | }); 2464 | } 2465 | next(); 2466 | }); 2467 | 2468 | view.once('destroying', function(){ 2469 | while (activeBindings.length) { 2470 | activeBindings.shift().unbind(); 2471 | } 2472 | }); 2473 | 2474 | view.activeBindings = activeBindings; 2475 | 2476 | return fragment.firstChild; 2477 | }; 2478 | 2479 | }); 2480 | 2481 | _require.register("ripple/lib/bindings/directive.js", function (exports, module) { 2482 | var raf = _require("anthonyshort~raf-queue@0.2.0"); 2483 | 2484 | /** 2485 | * Creates a new directive using a binding object. 2486 | * 2487 | * @param {View} view 2488 | * @param {Element} node 2489 | * @param {String} attr 2490 | * @param {Object} binding 2491 | */ 2492 | 2493 | function Directive(view, node, attr, binding) { 2494 | this.queue = this.queue.bind(this); 2495 | this.view = view; 2496 | if (typeof binding === 'function') { 2497 | this.binding = { update: binding }; 2498 | } 2499 | else { 2500 | this.binding = binding; 2501 | } 2502 | this.text = node.getAttribute(attr); 2503 | this.node = node; 2504 | this.attr = attr; 2505 | this.props = view.props(this.text); 2506 | node.removeAttribute(attr); 2507 | this.bind(); 2508 | } 2509 | 2510 | /** 2511 | * Start watching the view for changes 2512 | */ 2513 | 2514 | Directive.prototype.bind = function(){ 2515 | var view = this.view; 2516 | var queue = this.queue; 2517 | 2518 | if (this.binding.bind) { 2519 | this.binding.bind.call(this, this.node, this.view); 2520 | } 2521 | 2522 | this.props.forEach(function(prop){ 2523 | view.watch(prop, queue); 2524 | }); 2525 | 2526 | this.update(); 2527 | }; 2528 | 2529 | /** 2530 | * Stop watching the view for changes 2531 | */ 2532 | 2533 | Directive.prototype.unbind = function(){ 2534 | var view = this.view; 2535 | var queue = this.queue; 2536 | 2537 | this.props.forEach(function(prop){ 2538 | view.unwatch(prop, queue); 2539 | }); 2540 | 2541 | if (this.job) { 2542 | raf.cancel(this.job); 2543 | } 2544 | 2545 | if (this.binding.unbind) { 2546 | this.binding.unbind.call(this, this.node, this.view); 2547 | } 2548 | }; 2549 | 2550 | /** 2551 | * Update the attribute. 2552 | */ 2553 | 2554 | Directive.prototype.update = function(){ 2555 | var value = this.view.interpolate(this.text); 2556 | this.binding.update.call(this, value, this.node, this.view); 2557 | }; 2558 | 2559 | /** 2560 | * Queue an update 2561 | */ 2562 | 2563 | Directive.prototype.queue = function(){ 2564 | if (this.job) { 2565 | raf.cancel(this.job); 2566 | } 2567 | this.job = raf(this.update, this); 2568 | }; 2569 | 2570 | module.exports = Directive; 2571 | }); 2572 | 2573 | _require.register("ripple/lib/bindings/text.js", function (exports, module) { 2574 | var raf = _require("anthonyshort~raf-queue@0.2.0"); 2575 | 2576 | /** 2577 | * Create a new text binding on a node 2578 | * 2579 | * @param {View} view 2580 | * @param {Element} node 2581 | */ 2582 | 2583 | function TextBinding(view, node) { 2584 | this.update = this.update.bind(this); 2585 | this.view = view; 2586 | this.text = node.data; 2587 | this.node = node; 2588 | this.props = view.props(this.text); 2589 | this.render = this.render.bind(this); 2590 | if (this.props.length) { 2591 | this.bind(); 2592 | } 2593 | } 2594 | 2595 | /** 2596 | * Bind changes in the expression to the view 2597 | */ 2598 | 2599 | TextBinding.prototype.bind = function(){ 2600 | var view = this.view; 2601 | var update = this.update; 2602 | 2603 | this.props.forEach(function(prop){ 2604 | view.watch(prop, update); 2605 | }); 2606 | 2607 | this.render(); 2608 | }; 2609 | 2610 | /** 2611 | * Stop watching the expression for changes 2612 | */ 2613 | 2614 | TextBinding.prototype.unbind = function(){ 2615 | var view = this.view; 2616 | var update = this.update; 2617 | 2618 | this.props.forEach(function(prop){ 2619 | view.unwatch(prop, update); 2620 | }); 2621 | 2622 | if (this.job) { 2623 | raf.cancel(this.job); 2624 | } 2625 | }; 2626 | 2627 | /** 2628 | * Render the expression value to the DOM 2629 | */ 2630 | 2631 | TextBinding.prototype.render = function(){ 2632 | var node = this.node; 2633 | var val = this.view.interpolate(this.text); 2634 | 2635 | if (val == null) { 2636 | this.node.data = ''; 2637 | } 2638 | else if (val instanceof Element) { 2639 | node.parentNode.replaceChild(val, node); 2640 | this.node = val; 2641 | } 2642 | else if (val.el instanceof Element) { 2643 | node.parentNode.replaceChild(val.el, node); 2644 | this.node = val.el; 2645 | } 2646 | else { 2647 | var newNode = document.createTextNode(val); 2648 | node.parentNode.replaceChild(newNode, node); 2649 | this.node = newNode; 2650 | } 2651 | }; 2652 | 2653 | /** 2654 | * Schedule an update to the text element on the next frame. 2655 | * This will only ever trigger one render no matter how 2656 | * many times it is called 2657 | */ 2658 | 2659 | TextBinding.prototype.update = function(){ 2660 | if (this.job) { 2661 | raf.cancel(this.job); 2662 | } 2663 | this.job = raf(this.render, this); 2664 | }; 2665 | 2666 | module.exports = TextBinding; 2667 | 2668 | }); 2669 | 2670 | _require.register("ripple/lib/bindings/attribute.js", function (exports, module) { 2671 | var isBoolean = _require("anthonyshort~is-boolean-attribute@0.0.1"); 2672 | var raf = _require("anthonyshort~raf-queue@0.2.0"); 2673 | 2674 | /** 2675 | * Creates a new attribute text binding for a view. 2676 | * If the view attribute contains interpolation, the 2677 | * attribute will be automatically updated whenever the 2678 | * result of the expression changes. 2679 | * 2680 | * Updating will be called once per tick. So if there 2681 | * are multiple changes to the view in a single tick, 2682 | * this will only touch the DOM once. 2683 | * 2684 | * @param {View} view 2685 | * @param {Element} node 2686 | * @param {String} attr 2687 | */ 2688 | 2689 | function AttrBinding(view, node, attr) { 2690 | this.update = this.update.bind(this); 2691 | this.view = view; 2692 | this.text = node.getAttribute(attr); 2693 | this.node = node; 2694 | this.attr = attr; 2695 | this.props = view.props(this.text); 2696 | this.bind(); 2697 | } 2698 | 2699 | /** 2700 | * Start watching the view for changes 2701 | */ 2702 | 2703 | AttrBinding.prototype.bind = function(){ 2704 | if(!this.props.length) return; 2705 | var view = this.view; 2706 | var update = this.update; 2707 | 2708 | this.props.forEach(function(prop){ 2709 | view.watch(prop, update); 2710 | }); 2711 | 2712 | this.render(); 2713 | }; 2714 | 2715 | /** 2716 | * Stop watching the view for changes 2717 | */ 2718 | 2719 | AttrBinding.prototype.unbind = function(){ 2720 | if(!this.props.length) return; 2721 | var view = this.view; 2722 | var update = this.update; 2723 | 2724 | this.props.forEach(function(prop){ 2725 | view.unwatch(prop, update); 2726 | }); 2727 | 2728 | if (this.job) { 2729 | raf.cancel(this.job); 2730 | } 2731 | }; 2732 | 2733 | /** 2734 | * Update the attribute 2735 | */ 2736 | 2737 | AttrBinding.prototype.render = function(){ 2738 | var val = this.view.interpolate(this.text); 2739 | if (val == null) val = ''; 2740 | if (isBoolean(this.attr) && !val) { 2741 | this.node.removeAttribute(this.attr); 2742 | } 2743 | else { 2744 | this.node.setAttribute(this.attr, val); 2745 | } 2746 | }; 2747 | 2748 | /** 2749 | * Update the attribute. 2750 | */ 2751 | 2752 | AttrBinding.prototype.update = function(){ 2753 | if (this.job) { 2754 | raf.cancel(this.job); 2755 | } 2756 | this.job = raf(this.render, this); 2757 | }; 2758 | 2759 | module.exports = AttrBinding; 2760 | }); 2761 | 2762 | _require.register("ripple/lib/bindings/child.js", function (exports, module) { 2763 | var attrs = _require("anthonyshort~attributes@0.0.1"); 2764 | var each = _require("component~each@0.2.4"); 2765 | var unique = _require("yields~uniq@master"); 2766 | var raf = _require("anthonyshort~raf-queue@0.2.0"); 2767 | 2768 | /** 2769 | * Creates a new sub-view at a node and binds 2770 | * it to the parent 2771 | * 2772 | * @param {View} view 2773 | * @param {Element} node 2774 | * @param {Function} View 2775 | */ 2776 | 2777 | function ChildBinding(view, node, View) { 2778 | this.update = this.update.bind(this); 2779 | this.view = view; 2780 | this.attrs = attrs(node); 2781 | this.props = this.getProps(); 2782 | var data = this.values(); 2783 | data.yield = node.innerHTML; 2784 | this.child = new View(data, { 2785 | owner: view 2786 | }); 2787 | this.child.replace(node); 2788 | this.child.on('destroyed', this.unbind.bind(this)); 2789 | this.node = this.child.el; 2790 | this.bind(); 2791 | } 2792 | 2793 | /** 2794 | * Get all of the properties used in all of the attributes 2795 | * 2796 | * @return {Array} 2797 | */ 2798 | 2799 | ChildBinding.prototype.getProps = function(){ 2800 | var ret = []; 2801 | var view = this.view; 2802 | each(this.attrs, function(name, value){ 2803 | ret = ret.concat(view.props(value)); 2804 | }); 2805 | return unique(ret); 2806 | }; 2807 | 2808 | /** 2809 | * Bind to changes on the view. Whenever a property 2810 | * changes we'll update the child with the new values. 2811 | */ 2812 | 2813 | ChildBinding.prototype.bind = function(){ 2814 | var self = this; 2815 | var view = this.view; 2816 | 2817 | this.props.forEach(function(prop){ 2818 | view.watch(prop, self.update); 2819 | }); 2820 | 2821 | this.send(); 2822 | }; 2823 | 2824 | /** 2825 | * Get all the data from the node 2826 | * 2827 | * @return {Object} 2828 | */ 2829 | 2830 | ChildBinding.prototype.values = function(){ 2831 | var view = this.view; 2832 | var ret = {}; 2833 | each(this.attrs, function(name, value){ 2834 | ret[name] = view.interpolate(value); 2835 | }); 2836 | return ret; 2837 | }; 2838 | 2839 | /** 2840 | * Send the data to the child 2841 | */ 2842 | 2843 | ChildBinding.prototype.send = function(){ 2844 | this.child.set(this.values()); 2845 | }; 2846 | 2847 | /** 2848 | * Unbind this view from the parent 2849 | */ 2850 | 2851 | ChildBinding.prototype.unbind = function(){ 2852 | var view = this.view; 2853 | var update = this.update; 2854 | 2855 | this.props.forEach(function(prop){ 2856 | view.unwatch(prop, update); 2857 | }); 2858 | 2859 | if (this.job) { 2860 | raf.cancel(this.job); 2861 | } 2862 | }; 2863 | 2864 | /** 2865 | * Update the child view will updated values from 2866 | * the parent. This will batch changes together 2867 | * and only fire once per tick. 2868 | */ 2869 | 2870 | ChildBinding.prototype.update = function(){ 2871 | if (this.job) { 2872 | raf.cancel(this.job); 2873 | } 2874 | this.job = raf(this.send, this); 2875 | }; 2876 | 2877 | module.exports = ChildBinding; 2878 | 2879 | }); 2880 | 2881 | if (typeof exports == "object") { 2882 | module.exports = _require("ripple"); 2883 | } else if (typeof define == "function" && define.amd) { 2884 | define([], function(){ return _require("ripple"); }); 2885 | } else { 2886 | this["ripple"] = _require("ripple"); 2887 | } 2888 | })() 2889 | -------------------------------------------------------------------------------- /lib/bindings/attribute.js: -------------------------------------------------------------------------------- 1 | var isBoolean = require('is-boolean-attribute'); 2 | var raf = require('raf-queue'); 3 | 4 | /** 5 | * Creates a new attribute text binding for a view. 6 | * If the view attribute contains interpolation, the 7 | * attribute will be automatically updated whenever the 8 | * result of the expression changes. 9 | * 10 | * Updating will be called once per tick. So if there 11 | * are multiple changes to the view in a single tick, 12 | * this will only touch the DOM once. 13 | * 14 | * @param {View} view 15 | * @param {Element} node 16 | * @param {String} attr 17 | */ 18 | 19 | function AttrBinding(view, node, attr) { 20 | this.update = this.update.bind(this); 21 | this.view = view; 22 | this.text = node.getAttribute(attr); 23 | this.node = node; 24 | this.attr = attr; 25 | this.props = view.props(this.text); 26 | this.bind(); 27 | } 28 | 29 | /** 30 | * Start watching the view for changes 31 | */ 32 | 33 | AttrBinding.prototype.bind = function(){ 34 | if(!this.props.length) return; 35 | var view = this.view; 36 | var update = this.update; 37 | 38 | this.props.forEach(function(prop){ 39 | view.watch(prop, update); 40 | }); 41 | 42 | this.render(); 43 | }; 44 | 45 | /** 46 | * Stop watching the view for changes 47 | */ 48 | 49 | AttrBinding.prototype.unbind = function(){ 50 | if(!this.props.length) return; 51 | var view = this.view; 52 | var update = this.update; 53 | 54 | this.props.forEach(function(prop){ 55 | view.unwatch(prop, update); 56 | }); 57 | 58 | if (this.job) { 59 | raf.cancel(this.job); 60 | } 61 | }; 62 | 63 | /** 64 | * Update the attribute 65 | */ 66 | 67 | AttrBinding.prototype.render = function(){ 68 | var val = this.view.interpolate(this.text); 69 | if (val == null) val = ''; 70 | if (isBoolean(this.attr) && !val) { 71 | this.node.removeAttribute(this.attr); 72 | } 73 | else { 74 | this.node.setAttribute(this.attr, val); 75 | } 76 | }; 77 | 78 | /** 79 | * Update the attribute. 80 | */ 81 | 82 | AttrBinding.prototype.update = function(){ 83 | if (this.job) { 84 | raf.cancel(this.job); 85 | } 86 | this.job = raf(this.render, this); 87 | }; 88 | 89 | module.exports = AttrBinding; -------------------------------------------------------------------------------- /lib/bindings/child.js: -------------------------------------------------------------------------------- 1 | var attrs = require('attributes'); 2 | var each = require('each'); 3 | var unique = require('uniq'); 4 | var raf = require('raf-queue'); 5 | 6 | /** 7 | * Creates a new sub-view at a node and binds 8 | * it to the parent 9 | * 10 | * @param {View} view 11 | * @param {Element} node 12 | * @param {Function} View 13 | */ 14 | 15 | function ChildBinding(view, node, View) { 16 | this.update = this.update.bind(this); 17 | this.view = view; 18 | this.attrs = attrs(node); 19 | this.props = this.getProps(); 20 | var data = this.values(); 21 | data.yield = node.innerHTML; 22 | this.child = new View(data, { 23 | owner: view 24 | }); 25 | this.child.replace(node); 26 | this.child.on('destroyed', this.unbind.bind(this)); 27 | this.node = this.child.el; 28 | this.bind(); 29 | } 30 | 31 | /** 32 | * Get all of the properties used in all of the attributes 33 | * 34 | * @return {Array} 35 | */ 36 | 37 | ChildBinding.prototype.getProps = function(){ 38 | var ret = []; 39 | var view = this.view; 40 | each(this.attrs, function(name, value){ 41 | ret = ret.concat(view.props(value)); 42 | }); 43 | return unique(ret); 44 | }; 45 | 46 | /** 47 | * Bind to changes on the view. Whenever a property 48 | * changes we'll update the child with the new values. 49 | */ 50 | 51 | ChildBinding.prototype.bind = function(){ 52 | var self = this; 53 | var view = this.view; 54 | 55 | this.props.forEach(function(prop){ 56 | view.watch(prop, self.update); 57 | }); 58 | 59 | this.send(); 60 | }; 61 | 62 | /** 63 | * Get all the data from the node 64 | * 65 | * @return {Object} 66 | */ 67 | 68 | ChildBinding.prototype.values = function(){ 69 | var view = this.view; 70 | var ret = {}; 71 | each(this.attrs, function(name, value){ 72 | ret[name] = view.interpolate(value); 73 | }); 74 | return ret; 75 | }; 76 | 77 | /** 78 | * Send the data to the child 79 | */ 80 | 81 | ChildBinding.prototype.send = function(){ 82 | this.child.set(this.values()); 83 | }; 84 | 85 | /** 86 | * Unbind this view from the parent 87 | */ 88 | 89 | ChildBinding.prototype.unbind = function(){ 90 | var view = this.view; 91 | var update = this.update; 92 | 93 | this.props.forEach(function(prop){ 94 | view.unwatch(prop, update); 95 | }); 96 | 97 | if (this.job) { 98 | raf.cancel(this.job); 99 | } 100 | }; 101 | 102 | /** 103 | * Update the child view will updated values from 104 | * the parent. This will batch changes together 105 | * and only fire once per tick. 106 | */ 107 | 108 | ChildBinding.prototype.update = function(){ 109 | if (this.job) { 110 | raf.cancel(this.job); 111 | } 112 | this.job = raf(this.send, this); 113 | }; 114 | 115 | module.exports = ChildBinding; 116 | -------------------------------------------------------------------------------- /lib/bindings/directive.js: -------------------------------------------------------------------------------- 1 | var raf = require('raf-queue'); 2 | 3 | /** 4 | * Creates a new directive using a binding object. 5 | * 6 | * @param {View} view 7 | * @param {Element} node 8 | * @param {String} attr 9 | * @param {Object} binding 10 | */ 11 | 12 | function Directive(view, node, attr, binding) { 13 | this.queue = this.queue.bind(this); 14 | this.view = view; 15 | if (typeof binding === 'function') { 16 | this.binding = { update: binding }; 17 | } 18 | else { 19 | this.binding = binding; 20 | } 21 | this.text = node.getAttribute(attr); 22 | this.node = node; 23 | this.attr = attr; 24 | this.props = view.props(this.text); 25 | node.removeAttribute(attr); 26 | this.bind(); 27 | } 28 | 29 | /** 30 | * Start watching the view for changes 31 | */ 32 | 33 | Directive.prototype.bind = function(){ 34 | var view = this.view; 35 | var queue = this.queue; 36 | 37 | if (this.binding.bind) { 38 | this.binding.bind.call(this, this.node, this.view); 39 | } 40 | 41 | this.props.forEach(function(prop){ 42 | view.watch(prop, queue); 43 | }); 44 | 45 | this.update(); 46 | }; 47 | 48 | /** 49 | * Stop watching the view for changes 50 | */ 51 | 52 | Directive.prototype.unbind = function(){ 53 | var view = this.view; 54 | var queue = this.queue; 55 | 56 | this.props.forEach(function(prop){ 57 | view.unwatch(prop, queue); 58 | }); 59 | 60 | if (this.job) { 61 | raf.cancel(this.job); 62 | } 63 | 64 | if (this.binding.unbind) { 65 | this.binding.unbind.call(this, this.node, this.view); 66 | } 67 | }; 68 | 69 | /** 70 | * Update the attribute. 71 | */ 72 | 73 | Directive.prototype.update = function(){ 74 | var value = this.view.interpolate(this.text); 75 | this.binding.update.call(this, value, this.node, this.view); 76 | }; 77 | 78 | /** 79 | * Queue an update 80 | */ 81 | 82 | Directive.prototype.queue = function(){ 83 | if (this.job) { 84 | raf.cancel(this.job); 85 | } 86 | this.job = raf(this.update, this); 87 | }; 88 | 89 | module.exports = Directive; -------------------------------------------------------------------------------- /lib/bindings/index.js: -------------------------------------------------------------------------------- 1 | var walk = require('dom-walk'); 2 | var each = require('each'); 3 | var attrs = require('attributes'); 4 | var domify = require('domify'); 5 | var TextBinding = require('./text'); 6 | var AttrBinding = require('./attribute'); 7 | var ChildBinding = require('./child'); 8 | var Directive = require('./directive'); 9 | 10 | module.exports = function(options) { 11 | var view = options.view; 12 | var el = domify(options.template); 13 | var fragment = document.createDocumentFragment(); 14 | fragment.appendChild(el); 15 | 16 | var activeBindings = []; 17 | 18 | // Walk down the newly created view element 19 | // and bind everything to the model 20 | walk(el, function(node, next){ 21 | if(node.nodeType === 3) { 22 | activeBindings.push(new TextBinding(view, node)); 23 | } 24 | else if(node.nodeType === 1) { 25 | var View = options.components[node.nodeName.toLowerCase()]; 26 | if(View) { 27 | activeBindings.push(new ChildBinding(view, node, View)); 28 | return next(); 29 | } 30 | each(attrs(node), function(attr){ 31 | var binding = options.directives[attr]; 32 | if(binding) { 33 | activeBindings.push(new Directive(view, node, attr, binding)); 34 | } 35 | else { 36 | activeBindings.push(new AttrBinding(view, node, attr)); 37 | } 38 | }); 39 | } 40 | next(); 41 | }); 42 | 43 | view.once('destroying', function(){ 44 | while (activeBindings.length) { 45 | activeBindings.shift().unbind(); 46 | } 47 | }); 48 | 49 | view.activeBindings = activeBindings; 50 | 51 | return fragment.firstChild; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/bindings/text.js: -------------------------------------------------------------------------------- 1 | var raf = require('raf-queue'); 2 | 3 | /** 4 | * Create a new text binding on a node 5 | * 6 | * @param {View} view 7 | * @param {Element} node 8 | */ 9 | 10 | function TextBinding(view, node) { 11 | this.update = this.update.bind(this); 12 | this.view = view; 13 | this.text = node.data; 14 | this.node = node; 15 | this.props = view.props(this.text); 16 | this.render = this.render.bind(this); 17 | if (this.props.length) { 18 | this.bind(); 19 | } 20 | } 21 | 22 | /** 23 | * Bind changes in the expression to the view 24 | */ 25 | 26 | TextBinding.prototype.bind = function(){ 27 | var view = this.view; 28 | var update = this.update; 29 | 30 | this.props.forEach(function(prop){ 31 | view.watch(prop, update); 32 | }); 33 | 34 | this.render(); 35 | }; 36 | 37 | /** 38 | * Stop watching the expression for changes 39 | */ 40 | 41 | TextBinding.prototype.unbind = function(){ 42 | var view = this.view; 43 | var update = this.update; 44 | 45 | this.props.forEach(function(prop){ 46 | view.unwatch(prop, update); 47 | }); 48 | 49 | if (this.job) { 50 | raf.cancel(this.job); 51 | } 52 | }; 53 | 54 | /** 55 | * Render the expression value to the DOM 56 | */ 57 | 58 | TextBinding.prototype.render = function(){ 59 | var node = this.node; 60 | var val = this.view.interpolate(this.text); 61 | 62 | if (val == null) { 63 | this.node.data = ''; 64 | } 65 | else if (val instanceof Element) { 66 | node.parentNode.replaceChild(val, node); 67 | this.node = val; 68 | } 69 | else if (val.el instanceof Element) { 70 | node.parentNode.replaceChild(val.el, node); 71 | this.node = val.el; 72 | } 73 | else { 74 | var newNode = document.createTextNode(val); 75 | node.parentNode.replaceChild(newNode, node); 76 | this.node = newNode; 77 | } 78 | }; 79 | 80 | /** 81 | * Schedule an update to the text element on the next frame. 82 | * This will only ever trigger one render no matter how 83 | * many times it is called 84 | */ 85 | 86 | TextBinding.prototype.update = function(){ 87 | if (this.job) { 88 | raf.cancel(this.job); 89 | } 90 | this.job = raf(this.render, this); 91 | }; 92 | 93 | module.exports = TextBinding; 94 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var emitter = require('emitter'); 2 | var observer = require('path-observer'); 3 | var proto = require('./proto'); 4 | var statics = require('./static'); 5 | var id = 0; 6 | 7 | /** 8 | * Allow for a selector or an element to be passed in 9 | * as the template for the view 10 | */ 11 | 12 | function getTemplate(template) { 13 | if (template.indexOf('#') === 0 || template.indexOf('.') === 0) { 14 | template = document.querySelector(template); 15 | } 16 | if (typeof template.innerHTML === 'string') { 17 | template = template.innerHTML; 18 | } 19 | return template; 20 | } 21 | 22 | /** 23 | * Create a new view from a template string 24 | * 25 | * @param {String} template 26 | * 27 | * @return {View} 28 | */ 29 | 30 | module.exports = function(template) { 31 | if (!template) throw new Error('template is required'); 32 | template = getTemplate(template); 33 | 34 | function View (attrs, options) { 35 | if (!(this instanceof View)) return new View(attrs, options); 36 | attrs = attrs || {}; 37 | options = options || {}; 38 | View.emit('construct', this, attrs, options); 39 | this.options = options; 40 | this.id = id++; 41 | this.root = this; 42 | this.attrs = attrs; 43 | this.observer = observer(attrs); 44 | this.template = options.template || template; 45 | if (options.owner) { 46 | this.owner = options.owner; 47 | this.root = this.owner.root; 48 | this.owner.on('destroying', this.destroy.bind(this)); 49 | } 50 | View.emit('created', this); 51 | if (this.initialize) this.initialize(); 52 | this.el = this.render(); 53 | View.emit('ready', this); 54 | } 55 | 56 | // mixins 57 | 58 | emitter(View); 59 | emitter(View.prototype); 60 | 61 | // statics 62 | 63 | View.attrs = {}; 64 | View.components = {}; 65 | View.directives = {}; 66 | View.filters = {}; 67 | for (var staticKey in statics) View[staticKey] = statics[staticKey]; 68 | 69 | // prototype 70 | 71 | View.prototype.view = View; 72 | for (var protoKey in proto) View.prototype[protoKey] = proto[protoKey]; 73 | 74 | return View; 75 | }; 76 | -------------------------------------------------------------------------------- /lib/proto.js: -------------------------------------------------------------------------------- 1 | var render = require('./bindings'); 2 | var Interpolator = require('interpolate'); 3 | 4 | /** 5 | * Run expressions 6 | * 7 | * @type {Interpolator} 8 | */ 9 | 10 | var interpolator = new Interpolator(); 11 | 12 | /** 13 | * Get a node using element the element itself 14 | * or a CSS selector 15 | * 16 | * @param {Element|String} node 17 | * 18 | * @return {Element} 19 | */ 20 | 21 | function getNode(node) { 22 | if (typeof node === 'string') { 23 | node = document.querySelector(node); 24 | if (node === null) throw new Error('node does not exist'); 25 | } 26 | return node; 27 | } 28 | 29 | /** 30 | * Set the state off the view. This will trigger 31 | * refreshes to the UI. If we were previously 32 | * watching the parent scope for changes to this 33 | * property, we will remove all of those watchers 34 | * and then bind them to our model instead. 35 | * 36 | * @param {Object} obj 37 | */ 38 | 39 | exports.set = function(path, value) { 40 | if (typeof path !== 'string') { 41 | for(var name in path) this.set(name, path[name]); 42 | return this; 43 | } 44 | this.observer(path).set(value); 45 | return this; 46 | }; 47 | 48 | /** 49 | * Get some data 50 | * 51 | * @param {String} path 52 | */ 53 | 54 | exports.get = function(path) { 55 | return this.observer(path).get(); 56 | }; 57 | 58 | /** 59 | * Get all the properties used in a string 60 | * 61 | * @param {String} str 62 | * 63 | * @return {Array} 64 | */ 65 | 66 | exports.props = function(str) { 67 | return interpolator.props(str); 68 | }; 69 | 70 | /** 71 | * Remove the element from the DOM 72 | */ 73 | 74 | exports.destroy = function() { 75 | this.emit('destroying'); 76 | this.view.emit('destroying', this); 77 | this.remove(); 78 | this.observer.dispose(); 79 | this.off(); 80 | }; 81 | 82 | /** 83 | * Is the view mounted in the DOM 84 | * 85 | * @return {Boolean} 86 | */ 87 | 88 | exports.isMounted = function() { 89 | return this.el != null && this.el.parentNode != null; 90 | }; 91 | 92 | /** 93 | * Render the view to an element. This should 94 | * only ever render the element once. 95 | */ 96 | 97 | exports.render = function() { 98 | return render({ 99 | view: this, 100 | template: this.template, 101 | directives: this.view.directives, 102 | components: this.view.components 103 | }); 104 | }; 105 | 106 | /** 107 | * Mount the view onto a node 108 | * 109 | * @param {Element|String} node An element or CSS selector 110 | * 111 | * @return {View} 112 | */ 113 | 114 | exports.appendTo = function(node) { 115 | getNode(node).appendChild(this.el); 116 | this.emit('mounted'); 117 | this.view.emit('mounted', this); 118 | return this; 119 | }; 120 | 121 | /** 122 | * Replace an element in the DOM with this view 123 | * 124 | * @param {Element|String} node An element or CSS selector 125 | * 126 | * @return {View} 127 | */ 128 | 129 | exports.replace = function(node) { 130 | var target = getNode(node); 131 | target.parentNode.replaceChild(this.el, target); 132 | this.emit('mounted'); 133 | this.view.emit('mounted', this); 134 | return this; 135 | }; 136 | 137 | /** 138 | * Insert the view before a node 139 | * 140 | * @param {Element|String} node 141 | * 142 | * @return {View} 143 | */ 144 | 145 | exports.before = function(node) { 146 | var target = getNode(node); 147 | target.parentNode.insertBefore(this.el, target); 148 | this.emit('mounted'); 149 | this.view.emit('mounted', this); 150 | return this; 151 | }; 152 | 153 | /** 154 | * Insert the view after a node 155 | * 156 | * @param {Element|String} node 157 | * 158 | * @return {View} 159 | */ 160 | 161 | exports.after = function(node) { 162 | var target = getNode(node); 163 | target.parentNode.insertBefore(this.el, target.nextSibling); 164 | this.emit('mounted'); 165 | this.view.emit('mounted', this); 166 | return this; 167 | }; 168 | 169 | /** 170 | * Remove the view from the DOM 171 | * 172 | * @return {View} 173 | */ 174 | 175 | exports.remove = function() { 176 | if (this.isMounted() === false) return this; 177 | this.el.parentNode.removeChild(this.el); 178 | this.emit('unmounted'); 179 | this.view.emit('unmounted', this); 180 | return this; 181 | }; 182 | 183 | /** 184 | * Interpolate a string 185 | * 186 | * @param {String} str 187 | */ 188 | 189 | exports.interpolate = function(str) { 190 | var self = this; 191 | var data = {}; 192 | var props = this.props(str); 193 | props.forEach(function(prop){ 194 | data[prop] = self.get(prop); 195 | }); 196 | return interpolator.value(str, { 197 | context: this, 198 | scope: data, 199 | filters: this.view.filters 200 | }); 201 | }; 202 | 203 | /** 204 | * Watch a property for changes 205 | * 206 | * @param {Strign} prop 207 | * @param {Function} callback 208 | */ 209 | 210 | exports.watch = function(prop, callback) { 211 | var self = this; 212 | if (Array.isArray(prop)) { 213 | return prop.forEach(function(name){ 214 | self.watch(name, callback); 215 | }); 216 | } 217 | if (typeof prop === 'function') { 218 | this.observer.on('change', prop); 219 | } 220 | else { 221 | this.observer(prop).on('change', callback); 222 | } 223 | return this; 224 | }; 225 | 226 | /** 227 | * Stop watching a property 228 | * 229 | * @param {Strign} prop 230 | * @param {Function} callback 231 | */ 232 | 233 | exports.unwatch = function(prop, callback) { 234 | var self = this; 235 | if (Array.isArray(prop)) { 236 | return prop.forEach(function(name){ 237 | self.unwatch(name, callback); 238 | }); 239 | } 240 | if (typeof prop === 'function') { 241 | this.observer.off('change', prop); 242 | } 243 | else { 244 | this.observer(prop).off('change', callback); 245 | } 246 | return this; 247 | }; -------------------------------------------------------------------------------- /lib/static.js: -------------------------------------------------------------------------------- 1 | var type = require('type'); 2 | 3 | /** 4 | * Add an attribute. This allows attributes to be created 5 | * and set with attributes. It also creates getters and 6 | * setters for the attributes on the view. 7 | * 8 | * @param {String} name 9 | * @param {Object} options 10 | * 11 | * @return {View} 12 | */ 13 | 14 | exports.attr = function(name, options) { 15 | options = options || {}; 16 | this.attrs[name] = options; 17 | this.on('construct', function(view, attrs){ 18 | if (attrs[name] == null) { 19 | attrs[name] = options.default; 20 | } 21 | if (options.required && attrs[name] == null) { 22 | throw new Error(name + ' is a required attribute'); 23 | } 24 | if (options.type && attrs[name] != null && type(attrs[name]) !== options.type) { 25 | throw new Error(name + ' should be type "' + options.type + '"'); 26 | } 27 | }); 28 | Object.defineProperty(this.prototype, name, { 29 | set: function(value) { 30 | this.set(name, value); 31 | }, 32 | get: function() { 33 | return this.get(name); 34 | } 35 | }); 36 | return this; 37 | }; 38 | 39 | /** 40 | * Add a directive 41 | * 42 | * @param {String|Regex} match 43 | * @param {Function} fn 44 | * 45 | * @return {View} 46 | */ 47 | 48 | exports.directive = function(name, fn) { 49 | if (typeof name !== 'string') { 50 | for(var key in name) { 51 | this.directive(key, name[key]); 52 | } 53 | return; 54 | } 55 | this.directives[name] = fn; 56 | return this; 57 | }; 58 | 59 | /** 60 | * Add a component 61 | * 62 | * @param {String} match 63 | * @param {Function} fn 64 | * 65 | * @return {View} 66 | */ 67 | 68 | exports.compose = function(name, fn) { 69 | if (typeof name !== 'string') { 70 | for(var key in name) { 71 | this.compose(key, name[key]); 72 | } 73 | return; 74 | } 75 | this.components[name.toLowerCase()] = fn; 76 | return this; 77 | }; 78 | 79 | /** 80 | * Add interpolation filter 81 | * 82 | * @param {String} name 83 | * @param {Function} fn 84 | * 85 | * @return {View} 86 | */ 87 | 88 | exports.filter = function(name, fn) { 89 | if (typeof name !== 'string') { 90 | for(var key in name) { 91 | this.filter(key, name[key]); 92 | } 93 | return; 94 | } 95 | this.filters[name] = fn; 96 | return this; 97 | }; 98 | 99 | /** 100 | * Use a plugin 101 | * 102 | * @return {View} 103 | */ 104 | 105 | exports.use = function(fn, options) { 106 | fn(this, options); 107 | return this; 108 | }; 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ripplejs", 3 | "version": "0.5.3", 4 | "main": "dist/ripple.js", 5 | "description": "Minimal reactive views for building user interfaces", 6 | "devDependencies": { 7 | "karma": "~0.12.1", 8 | "karma-mocha": "~0.1.3", 9 | "karma-coverage": "~0.2.1", 10 | "karma-script-launcher": "~0.1.0", 11 | "karma-phantomjs-launcher": "~0.1.2", 12 | "karma-chrome-launcher": "~0.1.2", 13 | "karma-firefox-launcher": "~0.1.3", 14 | "karma-safari-launcher": "~0.1.1", 15 | "mocha-phantomjs": "~3.3.2", 16 | "jshint": "~2.4.4", 17 | "component": "1.*", 18 | "bump": "git://github.com/ianstormtaylor/bump", 19 | "minify": "~0.2.6", 20 | "bfc": "*" 21 | } 22 | } -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqeqeq": true, 3 | "browser": true, 4 | "asi": true, 5 | "undef": true, 6 | "unused": true, 7 | "trailing": true, 8 | "sub": true, 9 | "node": true, 10 | "laxbreak": true, 11 | "globals": { 12 | "console": true, 13 | "it": true, 14 | "describe": true, 15 | "before": true, 16 | "after": true 17 | } 18 | } -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '../', 4 | autoWatch: false, 5 | port: 9876, 6 | colors: true, 7 | captureTimeout: 60000, 8 | singleRun: true, 9 | logLevel: config.LOG_INFO, 10 | frameworks: ['mocha'], 11 | reporters: ['progress'], 12 | files: [ 13 | 'build/build.js', 14 | 'test/specs/**/*.js' 15 | ], 16 | browsers: [ 17 | 'Chrome', 18 | 'Firefox', 19 | 'Safari' 20 | ] 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/specs/attribute-interpolation.js: -------------------------------------------------------------------------------- 1 | describe('attribute interpolation', function () { 2 | var assert = require('assert'); 3 | var ripple = require('ripple'); 4 | var frame = require('raf-queue'); 5 | var View, view, el; 6 | 7 | beforeEach(function () { 8 | View = ripple(''); 9 | view = new View({ 10 | foo: 'bar', 11 | hidden: true 12 | }); 13 | el = view.el; 14 | view.appendTo(document.body); 15 | }); 16 | 17 | afterEach(function () { 18 | view.destroy(); 19 | }); 20 | 21 | it('should interpolate attributes', function(done){ 22 | frame.defer(function(){ 23 | assert(el.id === 'bar'); 24 | done(); 25 | }); 26 | }) 27 | 28 | it('should render initial values immediately', function () { 29 | assert(el.id === 'bar'); 30 | }); 31 | 32 | it('should not render undefined', function () { 33 | var View = ripple('
'); 34 | var view = new View(); 35 | assert(view.el.id === ""); 36 | }); 37 | 38 | it('should update interpolated attributes', function(done){ 39 | view.set('foo', 'baz'); 40 | frame.defer(function(){ 41 | assert(el.id === 'baz'); 42 | done(); 43 | }); 44 | }) 45 | 46 | it('should toggle boolean attributes', function(done){ 47 | frame.defer(function(){ 48 | assert(view.el.hasAttribute('hidden')); 49 | view.set('hidden', false); 50 | frame.defer(function(){ 51 | assert(view.el.hasAttribute('hidden') === false); 52 | done(); 53 | }); 54 | }); 55 | }) 56 | 57 | }); -------------------------------------------------------------------------------- /test/specs/composing.js: -------------------------------------------------------------------------------- 1 | describe('composing views', function () { 2 | 3 | var assert = require('assert'); 4 | var ripple = require('ripple'); 5 | var frame = require('raf-queue'); 6 | var child, view; 7 | 8 | beforeEach(function () { 9 | Child = ripple('
'); 10 | Parent = ripple(''); 11 | Parent.compose('child', Child); 12 | view = new Parent({ 13 | color: 'red' 14 | }); 15 | view.appendTo(document.body); 16 | }); 17 | 18 | afterEach(function () { 19 | view.remove(); 20 | }); 21 | 22 | it('should not traverse composed view elements', function () { 23 | Child = ripple('
'); 24 | Parent = ripple('
{{foo}}
'); 25 | Parent.compose('child', Child); 26 | var parent = new Parent(); 27 | parent.appendTo(document.body); 28 | parent.remove(); 29 | }); 30 | 31 | it('should pass data to the component', function () { 32 | assert(view.el.id === "test", view.el.id); 33 | }); 34 | 35 | it('should pass data as an expression to the component', function () { 36 | assert(view.el.getAttribute('color') === "red"); 37 | }); 38 | 39 | it('should update data passed to the component', function (done) { 40 | view.set('color', 'blue'); 41 | frame.defer(function(){ 42 | assert(view.el.getAttribute('color') === "blue"); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should use custom content', function (done) { 48 | var Child = ripple('
{{yield}}
'); 49 | var Parent = ripple('foo'); 50 | Parent.compose('child', Child); 51 | var view = new Parent(); 52 | view.appendTo(document.body); 53 | frame.defer(function(){ 54 | assert(view.el.outerHTML === '
foo
'); 55 | view.remove(); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should allow a component as the root element', function (done) { 61 | Child = ripple('
child
'); 62 | Parent = ripple(''); 63 | Parent.compose('child', Child); 64 | view = new Parent(); 65 | view.appendTo(document.body); 66 | frame.defer(function(){ 67 | assert(view.el.outerHTML === '
child
'); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should keep parsing the template', function (done) { 73 | var Child = ripple('
Child
'); 74 | var Other = ripple('
Other
'); 75 | var Parent = ripple('
'); 76 | Parent.compose('child', Child); 77 | Parent.compose('other', Other); 78 | Parent.directive('test', function(value){ 79 | assert(value === "bar"); 80 | done(); 81 | }); 82 | var view = new Parent(); 83 | view.appendTo(document.body); 84 | frame.defer(function(){ 85 | view.remove(); 86 | }); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /test/specs/destroy.js: -------------------------------------------------------------------------------- 1 | describe('destroying', function () { 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var frame = require('raf-queue'); 5 | var View; 6 | 7 | beforeEach(function () { 8 | View = ripple('
{{text}}
'); 9 | }); 10 | 11 | it('should remove all event listeners', function (done) { 12 | var view = new View(); 13 | view.on('foo', function(){ 14 | done(false); 15 | }); 16 | view.destroy(); 17 | view.emit('foo'); 18 | done(); 19 | }); 20 | 21 | it('should remove all change listeners', function (done) { 22 | var view = new View({ 23 | foo: 'bar' 24 | }); 25 | view.watch('foo', function(){ 26 | done(false); 27 | }); 28 | view.destroy(); 29 | view.set('foo', 'baz'); 30 | done(); 31 | }); 32 | 33 | it('should unmount when destroyed', function (done) { 34 | View.on('unmounted', function(){ 35 | done(); 36 | }); 37 | view = new View(); 38 | view.appendTo(document.body); 39 | view.destroy(); 40 | }); 41 | 42 | it('should unbind all bindings', function () { 43 | view = new View(); 44 | view.appendTo(document.body); 45 | assert(view.activeBindings.length !== 0); 46 | view.destroy(); 47 | assert(view.activeBindings.length === 0); 48 | }); 49 | 50 | it('should not run text changes after it has been destroyed', function (done) { 51 | view = new View(); 52 | var el = view.el; 53 | view.appendTo(document.body); 54 | view.set('text', 'foo'); 55 | view.destroy(); 56 | frame.defer(function(){ 57 | assert(el.innerHTML === ''); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should not run attribute changes after it has been destroyed', function (done) { 63 | var View = ripple('
'); 64 | view = new View(); 65 | var el = view.el; 66 | view.appendTo(document.body); 67 | view.set('text', 'foo'); 68 | view.destroy(); 69 | frame.defer(function(){ 70 | assert(el.id === ''); 71 | done(); 72 | }); 73 | }); 74 | 75 | }); -------------------------------------------------------------------------------- /test/specs/directives.js: -------------------------------------------------------------------------------- 1 | describe('directives', function () { 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | 5 | it('should match directives with a string', function(done){ 6 | var View = ripple('
'); 7 | View.directive('data-test', { 8 | update: function(value, el, view){ 9 | assert(value === 'foo'); 10 | assert(view instanceof View); 11 | done(); 12 | } 13 | }); 14 | var view = new View(); 15 | view.appendTo('body'); 16 | view.destroy(); 17 | }); 18 | 19 | it('should use just an update method', function(done){ 20 | var View = ripple('
'); 21 | View.directive('data-test', function(value){ 22 | assert(value === 'foo'); 23 | done(); 24 | }); 25 | var view = new View(); 26 | }); 27 | 28 | it('should pass in the element and the view', function(done){ 29 | var View = ripple('
'); 30 | View.directive('data-test', function(value, el, view) { 31 | assert(value === 'foo'); 32 | assert(el instanceof Element); 33 | assert(view instanceof View); 34 | done(); 35 | }); 36 | var view = new View(); 37 | }); 38 | 39 | it('should update with interpolated values', function(done){ 40 | var View = ripple('
'); 41 | View.directive('data-test', { 42 | update: function(value) { 43 | assert(value === 'bar'); 44 | done(); 45 | } 46 | }); 47 | var view = new View({ 48 | foo: 'bar' 49 | }); 50 | }); 51 | 52 | it('should call the binding in the context of the directive', function (done) { 53 | var View = ripple('
'); 54 | View.directive('data-test', function(value){ 55 | assert(this.constructor.name === 'Directive'); 56 | done(); 57 | }); 58 | var view = new View(); 59 | }); 60 | 61 | }); -------------------------------------------------------------------------------- /test/specs/interpolation.js: -------------------------------------------------------------------------------- 1 | describe('interpolation', function(){ 2 | var assert = require('assert'); 3 | var ripple = require('ripple'); 4 | var frame = require('raf-queue'); 5 | var View, view; 6 | 7 | beforeEach(function () { 8 | View = ripple('
'); 9 | View.filter('caps', function(val){ 10 | return val.toUpperCase(); 11 | }); 12 | view = new View(); 13 | }); 14 | 15 | it('should add filters', function () { 16 | view.set('foo', 'bar'); 17 | assert( view.interpolate('{{foo | caps}}') === "BAR"); 18 | }); 19 | 20 | it('should add filters as objects', function () { 21 | var View = ripple('
'); 22 | View.filter({ 23 | caps: function(val){ 24 | return val.toUpperCase(); 25 | }, 26 | lower: function(val){ 27 | return val.toLowerCase(); 28 | } 29 | }); 30 | view = new View(); 31 | view.set('foo', 'bar'); 32 | assert( view.interpolate('{{foo | caps | lower}}') === "bar"); 33 | }); 34 | 35 | it('should return the raw value for simple expressions', function(){ 36 | view.set('names', ['Fred']); 37 | var val = view.interpolate('{{names}}'); 38 | assert(Array.isArray(val)); 39 | assert(val[0] === 'Fred'); 40 | }); 41 | 42 | it('should interpolate properties with a $', function () { 43 | view.set('$value', 'Fred'); 44 | var val = view.interpolate('{{$value}}'); 45 | assert(val === 'Fred'); 46 | }); 47 | 48 | it('should not interpolate properties named this', function () { 49 | view.set('this', 'Fred'); 50 | var val = view.interpolate('{{this}}'); 51 | assert(val === view); 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /test/specs/lifecycle.js: -------------------------------------------------------------------------------- 1 | describe('lifecycle events', function () { 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | 5 | beforeEach(function () { 6 | View = ripple('
'); 7 | }); 8 | 9 | it('should fire a construct event', function (done) { 10 | View.on('construct', function(){ 11 | done(); 12 | }); 13 | new View(); 14 | }); 15 | 16 | it('should fire a created event', function (done) { 17 | View.on('created', function(){ 18 | done(); 19 | }); 20 | new View(); 21 | }); 22 | 23 | it('should fire a ready event', function (done) { 24 | View.on('ready', function(){ 25 | done(); 26 | }); 27 | new View(); 28 | }); 29 | 30 | it('should fire a mounted event', function (done) { 31 | View.on('mounted', function(){ 32 | done(); 33 | }); 34 | new View() 35 | .appendTo(document.body) 36 | .remove(); 37 | }); 38 | 39 | it('should fire an unmounted event', function (done) { 40 | View.on('unmounted', function(){ 41 | done(); 42 | }); 43 | new View() 44 | .appendTo(document.body) 45 | .remove(); 46 | }); 47 | 48 | it('should fire a destroy event', function (done) { 49 | View.on('destroying', function(){ 50 | done(); 51 | }); 52 | new View() 53 | .destroy() 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/specs/model.js: -------------------------------------------------------------------------------- 1 | describe('model', function(){ 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var View, view; 5 | 6 | beforeEach(function(){ 7 | View = ripple('
'); 8 | }); 9 | 10 | it('should set properties in the constructor', function(){ 11 | view = new View({ 12 | foo: 'bar' 13 | }); 14 | assert( view.get('foo') === 'bar' ); 15 | assert( view.attrs.foo === 'bar' ); 16 | }) 17 | 18 | it('should work with no properties', function(){ 19 | view = new View(); 20 | view.set('foo', 'bar'); 21 | assert( view.get('foo') === 'bar' ); 22 | assert( view.attrs.foo === 'bar' ); 23 | }) 24 | 25 | it('should set key and value', function(){ 26 | view = new View(); 27 | view.set('foo', 'bar'); 28 | assert( view.get('foo') === 'bar' ); 29 | }); 30 | 31 | it('should set key and value with an object', function(){ 32 | view = new View(); 33 | view.set({ 'foo' : 'bar' }); 34 | assert( view.get('foo') === 'bar' ); 35 | assert( view.attrs.foo === 'bar' ); 36 | }); 37 | 38 | it('should set and object with a falsy 2nd param', function(){ 39 | view = new View(); 40 | view.set({ 'foo' : 'bar' }, undefined); 41 | assert( view.get('foo') === 'bar' ); 42 | }); 43 | 44 | it('should emit change events', function(){ 45 | var match = false; 46 | view = new View(); 47 | view.watch('foo', function(){ 48 | match = true; 49 | }); 50 | view.set('foo', 'bar'); 51 | assert(match === true); 52 | }); 53 | 54 | it('should set properties in constructor', function(){ 55 | var obj = new View({ 56 | 'foo':'bar' 57 | }); 58 | assert( obj.get('foo') === 'bar' ); 59 | }); 60 | 61 | it('should set nested properties', function(){ 62 | view = new View(); 63 | view.set('foo.bar', 'baz'); 64 | assert( view.get('foo').bar === 'baz' ); 65 | }); 66 | 67 | it('should get nested properties', function(){ 68 | view = new View(); 69 | view.set('foo', { 70 | bar: 'baz' 71 | }); 72 | assert( view.get('foo.bar') === 'baz' ); 73 | }); 74 | 75 | it('should return undefined for missing nested properties', function(){ 76 | view = new View(); 77 | view.set('razz.tazz', 'bar'); 78 | assert( view.get('foo') === undefined ); 79 | assert( view.get('foo.bar') === undefined ); 80 | assert( view.get('razz.tazz.jazz') === undefined ); 81 | }) 82 | 83 | 84 | }); -------------------------------------------------------------------------------- /test/specs/mounting.js: -------------------------------------------------------------------------------- 1 | describe('mounting', function () { 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var View; 5 | 6 | beforeEach(function () { 7 | View = ripple('
'); 8 | }); 9 | 10 | it('should mount to an element', function(done){ 11 | View.on('mounted', function(){ 12 | assert(document.body.contains(view.el)); 13 | done(); 14 | }); 15 | view = new View(); 16 | view.appendTo(document.body); 17 | view.remove(); 18 | }) 19 | 20 | it('should mount using a selector', function (done) { 21 | View.on('mounted', function(){ 22 | assert(document.body.contains(view.el)); 23 | done(); 24 | }); 25 | view = new View(); 26 | view.appendTo('body'); 27 | view.remove(); 28 | }); 29 | 30 | it('should unmount', function(){ 31 | view = new View(); 32 | view.appendTo(document.body); 33 | var el = view.el; 34 | view.remove(); 35 | assert(document.body.contains(el) === false); 36 | }) 37 | 38 | it('should not unmount when mounting another element', function () { 39 | var test = document.createElement('div'); 40 | document.body.appendChild(test); 41 | var count = 0; 42 | View.on('unmounted', function(){ 43 | count++; 44 | }); 45 | view = new View(); 46 | view.appendTo('body'); 47 | view.appendTo(test); 48 | assert(count === 0); 49 | view.remove(); 50 | }); 51 | 52 | it('should replace an element', function(){ 53 | var test = document.createElement('div'); 54 | document.body.appendChild(test); 55 | view = new View(); 56 | view.replace(test); 57 | assert( test.parentNode == null ); 58 | view.remove(); 59 | }); 60 | 61 | it('should insert before an element', function(){ 62 | var test = document.createElement('div'); 63 | document.body.appendChild(test); 64 | view = new View(); 65 | view.before(test); 66 | assert( test.previousSibling === view.el ); 67 | view.remove(); 68 | }); 69 | 70 | it('should insert after an element', function(){ 71 | var test = document.createElement('div'); 72 | test.classList.add('parentEl'); 73 | document.body.appendChild(test); 74 | view = new View(); 75 | view.after(".parentEl"); 76 | assert( test.nextSibling === view.el ); 77 | view.remove(); 78 | }); 79 | 80 | it('should not unmount if not mounted', function () { 81 | var count = 0; 82 | View.on('unmounted', function(){ 83 | count += 1; 84 | }); 85 | view = new View(); 86 | view 87 | .appendTo('body') 88 | .remove() 89 | .remove(); 90 | assert(count === 1); 91 | }); 92 | }); -------------------------------------------------------------------------------- /test/specs/owners.js: -------------------------------------------------------------------------------- 1 | describe('owners', function () { 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var View, parent, grandchild, child; 5 | 6 | beforeEach(function () { 7 | var Parent = ripple('
Parent
'); 8 | var Child = ripple('
Child
'); 9 | var GrandChild = ripple('
GrandChild
'); 10 | parent = new Parent(); 11 | child = new Child(null, { 12 | owner: parent 13 | }); 14 | grandchild = new GrandChild(null, { 15 | owner: child 16 | }); 17 | }); 18 | 19 | it('should be able to have an owner', function () { 20 | assert(child.owner === parent, 'child owner should be the parent'); 21 | assert(grandchild.owner == child, 'grandchild owner should be the child'); 22 | }); 23 | 24 | it('should set the root', function () { 25 | assert(grandchild.root == parent); 26 | assert(child.root == parent); 27 | }); 28 | 29 | it('should remove children when destroyed', function (done) { 30 | grandchild.on('destroying', function(){ 31 | done(); 32 | }); 33 | parent.destroy(); 34 | }); 35 | 36 | }); -------------------------------------------------------------------------------- /test/specs/text-interpolation.js: -------------------------------------------------------------------------------- 1 | describe('text interpolation', function () { 2 | var assert = require('assert'); 3 | var ripple = require('ripple'); 4 | var frame = require('raf-queue'); 5 | var View, view, el; 6 | 7 | beforeEach(function () { 8 | View = ripple('
{{text}}
'); 9 | view = new View({ 10 | text: 'Ted' 11 | }); 12 | view.appendTo('body'); 13 | }); 14 | 15 | afterEach(function(){ 16 | view.remove(); 17 | }); 18 | 19 | it('should interpolate text nodes', function(done){ 20 | frame.defer(function(){ 21 | assert(view.el.innerHTML === 'Ted'); 22 | done(); 23 | }); 24 | }) 25 | 26 | it('should render initial props immediately', function () { 27 | assert(view.el.innerHTML === 'Ted'); 28 | }); 29 | 30 | it('should not render null or undefined', function () { 31 | var View = ripple('
{{foo}}
'); 32 | var view = new View(); 33 | assert(view.el.innerHTML === ""); 34 | }); 35 | 36 | it('should remove the binding when the view is destroyed', function(done){ 37 | var el = view.el; 38 | frame.defer(function(){ 39 | view.destroy(); 40 | view.set('text', 'Barney'); 41 | frame.defer(function(){ 42 | assert(el.innerHTML === "Ted"); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | it('should batch text node interpolation', function(done){ 49 | var count = 0; 50 | var view = new View(); 51 | var previous = view.interpolate; 52 | 53 | view.interpolate = function(){ 54 | count++; 55 | return previous.apply(this, arguments); 56 | }; 57 | 58 | view.set('text', 'one'); 59 | view.set('text', 'two'); 60 | view.set('text', 'three'); 61 | 62 | frame.defer(function(){ 63 | assert(count === 1); 64 | assert(view.el.innerHTML === 'three'); 65 | done(); 66 | }); 67 | }) 68 | 69 | it('should update interpolated text nodes', function(done){ 70 | view.set('text', 'Fred'); 71 | frame.defer(function(){ 72 | assert(view.el.innerHTML === 'Fred'); 73 | done(); 74 | }); 75 | }) 76 | 77 | it('should handle elements as values', function(done){ 78 | var test = document.createElement('div'); 79 | view.set('text', test); 80 | frame.defer(function(){ 81 | assert(view.el.firstChild === test); 82 | done(); 83 | }); 84 | }) 85 | 86 | it('should update elements as values', function(done){ 87 | var test = document.createElement('div'); 88 | var test2 = document.createElement('ul'); 89 | view.set('text', test); 90 | frame.defer(function(){ 91 | view.set('text', test2); 92 | frame.defer(function(){ 93 | assert(view.el.firstChild === test2); 94 | done(); 95 | }); 96 | }); 97 | }) 98 | 99 | it('should handle when the value is no longer an element', function(done){ 100 | var test = document.createElement('div'); 101 | view.set('text', test); 102 | frame.defer(function(){ 103 | view.set('text', 'bar'); 104 | frame.defer(function(){ 105 | assert(view.el.innerHTML === 'bar'); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should update from an non-string value', function(done){ 112 | view.set('text', null); 113 | frame.defer(function(){ 114 | view.set('text', 'bar'); 115 | frame.defer(function(){ 116 | assert(view.el.innerHTML === 'bar'); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | 122 | }); -------------------------------------------------------------------------------- /test/specs/view.js: -------------------------------------------------------------------------------- 1 | describe('View', function(){ 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var View; 5 | 6 | it('should create a function that returns an View', function(){ 7 | View = ripple('
'); 8 | var view = new View(); 9 | assert(view); 10 | }); 11 | 12 | it('should have a unique id', function () { 13 | var view = new View(); 14 | var view2 = new View(); 15 | assert(view.id); 16 | assert(view.id !== view2.id); 17 | }); 18 | 19 | it('should call an initialize method if it exists', function (done) { 20 | View = ripple('
'); 21 | View.prototype.initialize = function() { 22 | done(); 23 | }; 24 | new View(); 25 | }); 26 | 27 | it('should create a view with a selector', function () { 28 | var test = document.createElement('div'); 29 | test.id = 'foo'; 30 | document.body.appendChild(test); 31 | View = ripple('#foo'); 32 | var view = new View(); 33 | assert(view.template = '
'); 34 | }); 35 | 36 | it('should construct with properties', function(){ 37 | var view = new View({ 38 | foo: 'bar' 39 | }); 40 | assert(view.get('foo') === 'bar'); 41 | }) 42 | 43 | it('should set values', function () { 44 | var view = new View({ 45 | foo: 'bar' 46 | }); 47 | view.set('foo', 'baz'); 48 | assert( view.get('foo') === 'baz' ); 49 | }); 50 | 51 | it('should be able to set default properties', function () { 52 | var View = ripple('
') 53 | .attr('first', { default: 'Fred' }) 54 | .attr('last', { default: 'Flintstone' }); 55 | var view = new View(); 56 | view.set('first', 'Wilma'); 57 | assert(view.first === 'Wilma', 'First name should be Wilma'); 58 | assert(view.last === 'Flintstone', 'Last name should be Flintstone'); 59 | }); 60 | 61 | it('should add required attributes', function (done) { 62 | var View = ripple('
') 63 | .attr('first', { required: true }); 64 | try { 65 | new View(); 66 | done(false); 67 | } 68 | catch (e) { 69 | assert(e); 70 | done(); 71 | } 72 | }); 73 | 74 | it('should add required attributes with defaults', function (done) { 75 | var View = ripple('
') 76 | .attr('first', { required: true, default: 'foo' }); 77 | var view = new View(); 78 | assert(view.first === 'foo'); 79 | done(); 80 | }); 81 | 82 | it('should have typed attributes', function (done) { 83 | var View = ripple('
') 84 | .attr('first', { type: 'string' }); 85 | try { 86 | new View({ 'first': 10 }); 87 | done(false); 88 | } 89 | catch (e) { 90 | assert(e); 91 | done(); 92 | } 93 | }); 94 | 95 | it('should have typed required attributes', function (done) { 96 | var View = ripple('
') 97 | .attr('first', { required: true, type: 'string' }); 98 | try { 99 | new View(); 100 | done(false); 101 | } 102 | catch (e) { 103 | assert(e); 104 | done(); 105 | } 106 | }); 107 | 108 | it('should have typed attributes with defaults', function (done) { 109 | var View = ripple('
') 110 | .attr('first', { default: 'foo', type: 'string' }); 111 | var view = new View(); 112 | assert(view.first === 'foo'); 113 | done(); 114 | }); 115 | 116 | it('should have different bindings for each view', function () { 117 | var i = 0; 118 | var One = ripple('
'); 119 | One.directive('foo', function(val){ 120 | i++; 121 | }); 122 | var Two = ripple('
'); 123 | var one = new One(); 124 | var two = new Two(); 125 | assert(i === 1); 126 | }); 127 | 128 | it('should have the same bindings for each instance', function () { 129 | var one = new View(); 130 | var two = new View(); 131 | assert(two.bindings === one.bindings); 132 | }); 133 | 134 | it('should allow a custom template when created', function () { 135 | var view = new View(null, { 136 | template: '' 137 | }); 138 | assert(view.el.outerHTML === ''); 139 | }); 140 | 141 | }) -------------------------------------------------------------------------------- /test/specs/watching.js: -------------------------------------------------------------------------------- 1 | describe('watching', function(){ 2 | var ripple = require('ripple'); 3 | var assert = require('assert'); 4 | var View = ripple('
'); 5 | 6 | it('should watch for changes', function(done){ 7 | var view = new View(); 8 | view.set('foo', 'bar'); 9 | view.watch('foo', function(){ 10 | done(); 11 | }) 12 | view.set('foo', 'baz'); 13 | }) 14 | 15 | it('should unwatch all changes to a property', function(done){ 16 | var view = new View(); 17 | view.set('foo', 'bar'); 18 | view.watch('foo', function(){ 19 | done(false); 20 | }) 21 | view.unwatch('foo'); 22 | view.set('foo', 'baz'); 23 | done(); 24 | }) 25 | 26 | it('should unwatch changes with a property and a function', function(done){ 27 | var view = new View(); 28 | view.set('foo', 'bar'); 29 | function change(){ 30 | done(false); 31 | } 32 | view.watch('foo', change); 33 | view.unwatch('foo', change); 34 | view.set('foo', 'baz'); 35 | done(); 36 | }) 37 | 38 | it('should use the change method for binding to changes', function(done){ 39 | view = new View(); 40 | view.watch('one', function(change){ 41 | assert(change === 1); 42 | done(); 43 | }); 44 | view.set('one', 1); 45 | }) 46 | 47 | if('should watch all changes', function(done){ 48 | view = new View(); 49 | view.watch(function(){ 50 | done(); 51 | }); 52 | view.set('one', 1); 53 | }); 54 | 55 | if('should unwatch all changes', function(done){ 56 | view = new View(); 57 | view.watch(function change(){ 58 | done(false); 59 | }); 60 | view.unwatch(change); 61 | view.set('one', 1); 62 | done(); 63 | }); 64 | 65 | it('should bind to changes of multiple properties', function(){ 66 | var called = 0; 67 | view = new View(); 68 | view.watch(['one', 'two'], function(attr, value){ 69 | called += 1; 70 | }); 71 | view.set('one', 1); 72 | assert(called === 1); 73 | }) 74 | 75 | it('should unbind to changes of multiple properties', function(){ 76 | var called = 0; 77 | view = new View(); 78 | function change(){ 79 | called += 1; 80 | } 81 | view.watch(['one', 'two'], change); 82 | view.unwatch(['one', 'two'], change); 83 | view.set('one', 1); 84 | view.set('two', 1); 85 | assert(called === 0); 86 | }) 87 | 88 | describe('nested properties', function(){ 89 | var view; 90 | 91 | beforeEach(function(){ 92 | view = new View({ 93 | foo: { 94 | bar: 'baz' 95 | } 96 | }); 97 | }); 98 | 99 | it('should emit events for the bottom edge', function(done){ 100 | view.watch('foo.bar', function(){ 101 | done(); 102 | }); 103 | view.set('foo.bar', 'zab'); 104 | }) 105 | 106 | it('should not emit events in the middle', function(){ 107 | var called = false; 108 | view.watch('foo', function(val){ 109 | called = true; 110 | }); 111 | view.set('foo.bar', 'zab'); 112 | assert(called === false); 113 | }) 114 | 115 | it('should emit when setting an object in the middle', function () { 116 | var called = false; 117 | view.watch('foo', function(val){ 118 | called = true; 119 | }); 120 | view.set('foo', { 121 | bar: 'zab' 122 | }); 123 | assert(called === true); 124 | }); 125 | 126 | it('should not emit events if the value has not changed', function(){ 127 | var called = 0; 128 | view.set('foo.bar', 'zab'); 129 | view.watch('foo', function(val){ 130 | called++; 131 | }); 132 | view.watch('foo.bar', function(val){ 133 | called++; 134 | }); 135 | view.set('foo', { 136 | bar: 'zab' 137 | }); 138 | assert(called === 0); 139 | }) 140 | 141 | }) 142 | 143 | }) -------------------------------------------------------------------------------- /test/utils/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | /** 140 | * (1): approximate for browsers not supporting calc 141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 142 | * ^^ seriously 143 | */ 144 | #mocha .test pre { 145 | display: block; 146 | float: left; 147 | clear: left; 148 | font: 12px/1.5 monaco, monospace; 149 | margin: 5px; 150 | padding: 15px; 151 | border: 1px solid #eee; 152 | max-width: 85%; /*(1)*/ 153 | max-width: calc(100% - 42px); /*(2)*/ 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-border-radius: 3px; 157 | -webkit-box-shadow: 0 1px 3px #eee; 158 | -moz-border-radius: 3px; 159 | -moz-box-shadow: 0 1px 3px #eee; 160 | border-radius: 3px; 161 | } 162 | 163 | #mocha .test h2 { 164 | position: relative; 165 | } 166 | 167 | #mocha .test a.replay { 168 | position: absolute; 169 | top: 3px; 170 | right: 0; 171 | text-decoration: none; 172 | vertical-align: middle; 173 | display: block; 174 | width: 15px; 175 | height: 15px; 176 | line-height: 15px; 177 | text-align: center; 178 | background: #eee; 179 | font-size: 15px; 180 | -moz-border-radius: 15px; 181 | border-radius: 15px; 182 | -webkit-transition: opacity 200ms; 183 | -moz-transition: opacity 200ms; 184 | transition: opacity 200ms; 185 | opacity: 0.3; 186 | color: #888; 187 | } 188 | 189 | #mocha .test:hover a.replay { 190 | opacity: 1; 191 | } 192 | 193 | #mocha-report.pass .test.fail { 194 | display: none; 195 | } 196 | 197 | #mocha-report.fail .test.pass { 198 | display: none; 199 | } 200 | 201 | #mocha-report.pending .test.pass, 202 | #mocha-report.pending .test.fail { 203 | display: none; 204 | } 205 | #mocha-report.pending .test.pass.pending { 206 | display: block; 207 | } 208 | 209 | #mocha-error { 210 | color: #c00; 211 | font-size: 1.5em; 212 | font-weight: 100; 213 | letter-spacing: 1px; 214 | } 215 | 216 | #mocha-stats { 217 | position: fixed; 218 | top: 15px; 219 | right: 10px; 220 | font-size: 12px; 221 | margin: 0; 222 | color: #888; 223 | z-index: 1; 224 | } 225 | 226 | #mocha-stats .progress { 227 | float: right; 228 | padding-top: 0; 229 | } 230 | 231 | #mocha-stats em { 232 | color: black; 233 | } 234 | 235 | #mocha-stats a { 236 | text-decoration: none; 237 | color: inherit; 238 | } 239 | 240 | #mocha-stats a:hover { 241 | border-bottom: 1px solid #eee; 242 | } 243 | 244 | #mocha-stats li { 245 | display: inline-block; 246 | margin: 0 5px; 247 | list-style: none; 248 | padding-top: 11px; 249 | } 250 | 251 | #mocha-stats canvas { 252 | width: 40px; 253 | height: 40px; 254 | } 255 | 256 | #mocha code .comment { color: #ddd; } 257 | #mocha code .init { color: #2f6fad; } 258 | #mocha code .string { color: #5890ad; } 259 | #mocha code .keyword { color: #8a6343; } 260 | #mocha code .number { color: #2f6fad; } 261 | 262 | @media screen and (max-device-width: 480px) { 263 | #mocha { 264 | margin: 60px 0px; 265 | } 266 | 267 | #mocha #stats { 268 | position: absolute; 269 | } 270 | } 271 | --------------------------------------------------------------------------------