├── .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 | [](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 |
--------------------------------------------------------------------------------