Commit 66dd8d2d authored by Addy Osmani's avatar Addy Osmani

Merge pull request #210 from cujojs/cujo-pull

Cujo todo implementation
parents 21c9a3f9 6c7fbb92
# Cujojs TodoMVC
[Cujojs](http://cujojs.com) is an *architectural framework* for building highly modular, scalable, maintainable applications in Javascript. It provides architectural plumbing, such as modules (AMD and CommonJS), declarative application composition, declarative connections, and aspect oriented programming.
It is not a typical MV\* framework, although it does provide MV\* building blocks, such as templating and data binding.
## Highlights:
Some things we feel are interesting about cujojs's TodoMVC as compared to other implementations:
* Application composition is separate from application logic
* Code is *highly* modular and organized into components, each consisting of
one or more of the following:
* Composition specification (a.k.a. "wire spec")
* JavaScript controller module
* helper modules
* localization bundle (strings.js)
* HTML template (template.html)
* CSS file, typically unthemed (structure.css)
* HTML templates are clean and simple, editable by mere mortals.
* OOCSS is used for visual state changes
* zero direct style manipulation
* drastically simplifies HTML templates
* JavaScript environment is shimmed, rather than abstracted
* code to modern standards, not to abstraction layers
* All strings are easily internationalized
* Application code has no explicit dependencies on:
* DOMReady - the application lifecycle, even DOMReady, is managed
transparently. Things that can happen before DOMReady, do.
Things that can't, don't.
* DOM Query engine
* DOM Event library
## Credit
TodoMVC Template Created by [Sindre Sorhus](http://sindresorhus.com)
\ No newline at end of file
TODO before release
---
* implement filters (using routing or another method)
* build javascript using cram.js
* use curl's preloads feature rather than .next() in run.js
* use a theme.css file
define(function () {
"use strict";
var updateRemainingCount, textProp;
updateRemainingCount = normalizeTextProp;
return {
/**
* Create a new todo
* @injected
* @param todo {Object} data used to create new todo
* @param todo.text {String} text of the todo
*/
createTodo: function(todo) {},
/**
* Remove an existing todo
* @injected
* @param todo {Object} existing todo, or object with same identifier, to remove
*/
removeTodo: function(todo) {},
/**
* Update an existing todo
* @injected
* @param todo {Object} updated todo
*/
updateTodo: function(todo) {},
/**
* Start inline editing a todo
* @param node {Node} Dom node of the todo
*/
beginEditTodo: function(node) {
this.querySelector('.edit', node).select();
},
/**
* Finish editing a todo
* @param todo {Object} todo to finish editing and save changes
*/
endEditTodo: function(todo) {
// As per application spec, todos edited to have empty
// text should be removed.
if (/\S/.test(todo.text)) {
this.updateTodo(todo);
} else {
this.removeTodo(todo);
}
},
/**
* Remove all completed todos
*/
removeCompleted: function() {
var todos = this.todos;
todos.forEach(function(todo) {
if(todo.complete) todos.remove(todo);
});
},
/**
* Check/uncheck all todos
*/
toggleAll: function() {
var todos, complete;
todos = this.todos;
complete = this.masterCheckbox.checked;
todos.forEach(function(todo) {
todo.complete = complete;
todos.update(todo);
});
},
/**
* Update the remaining and completed counts, and update
* the check/uncheck all checkbox if all todos have become
* checked or unchecked.
*/
updateCount: function() {
var total, checked;
total = checked = 0;
this.todos.forEach(function(todo) {
total++;
if(todo.complete) checked++;
});
this.masterCheckbox.checked = total > 0 && checked === total;
this.updateTotalCount(total);
this.updateCompletedCount(checked);
this.updateRemainingCount(total - checked);
},
updateTotalCount: function(total) {},
updateCompletedCount: function(completed) {
this.countNode.innerHTML = completed;
},
updateRemainingCount: function (remaining) {
updateRemainingCount(this.remainingNodes, remaining);
}
};
/**
* Self-optimizing function to set the text of a node
*/
function normalizeTextProp () {
// sniff for proper textContent property
textProp = 'textContent' in document.documentElement ? 'textContent' : 'innerText';
// resume normally
(updateRemainingCount = setTextProp).apply(this, arguments);
}
function setTextProp (nodes, value) {
for (var i = 0; i < nodes.length; i++) {
nodes[i][textProp] = '' + value;
}
}
});
\ No newline at end of file
define({
// TODO: Deal with singular vs. plural
itemsLeft: {
zero: '\\o/ no todo items!!!',
one: 'only one item left!',
many: 'ugh, <strong></strong> items left'
},
filter: {
all: 'All',
active: 'Active',
completed: 'Completed'
},
clearCompleted: 'Clear completed'
});
\ No newline at end of file
.remaining-count-zero,
.remaining-count-one {
display: none;
}
.remaining-zero .remaining-count-zero {
display: inline;
}
.remaining-one .remaining-count-one {
display: inline;
}
.remaining-zero .remaining-count-many,
.remaining-one .remaining-count-many {
display: none;
}
#clear-completed {
opacity: 1;
/* TODO: this is theme/skin. Move to a theme file */
-webkit-transition: all .1s ease;
}
.completed-zero #clear-completed {
opacity: 0;
}
#footer {
display: none;
}
.todos-one #footer,
.todos-many #footer {
display: block;
}
/* TODO: Reinstate once we add routing */
#filters {
display: none;
}
\ No newline at end of file
<footer id="footer">
<!-- This should be `0 items left` by default -->
<span id="todo-count">
<span class="remaining-count-zero">${itemsLeft.zero}</span>
<span class="remaining-count-one">${itemsLeft.one}</span>
<span class="remaining-count-many">${itemsLeft.many}</span>
</span>
<!-- Remove this if you don't implement routing -->
<ul id="filters">
<li>
<a class="selected" href="#/">${filter.all}</a>
</li>
<li>
<a href="#/active">${filter.active}</a>
</li>
<li>
<a href="#/completed">${filter.completed}</a>
</li>
</ul>
<button id="clear-completed">${clearCompleted} (<span class="count">1</span>)</button>
</footer>
define(function() {
return function(todo) {
todo.text = todo.text && todo.text.trim() || '';
todo.complete = !!todo.complete;
return todo;
}
});
\ No newline at end of file
define(function() {
/**
* Since we're using a datastore (localStorage) that doesn't generate ids and such
* for us, this transform generates a GUID id and a dateCreated. It can be
* injected into a pipeline for creating new todos.
*/
return function generateMetadata(item) {
item.id = guidLike();
item.dateCreated = new Date().getTime();
return item;
};
// GUID-like generation, not actually a GUID, tho, from:
// http://stackoverflow.com/questions/7940616/what-makes-this-pseudo-guid-generator-better-than-math-random
function s4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
function guidLike() {
return (s4()+s4()+"-"+s4()+"-"+s4()+"-"+s4()+"-"+s4()+s4()+s4());
}
});
\ No newline at end of file
define({
title: 'todos',
todo: {
placeholder: 'What needs to be done?'
}
});
\ No newline at end of file
<header id="header">
<h1>${title}</h1>
<form>
<input id="new-todo" name="text" placeholder="${todo.placeholder}" autofocus>
</form>
</header>
define(function() {
/**
* Validate a todo
*/
return function validateTodo(todo) {
var valid, result;
// Must be a valid object, and have a text property that is non-empty
valid = todo && 'text' in todo && todo.text.trim();
result = { valid: !!valid };
if(!valid) result.errors = [{ property: 'text', message: 'missing' }];
return result;
}
});
\ No newline at end of file
define({
edit: 'Double-click to edit a todo',
templateBy: 'Template by',
createdBy: 'Created by',
partOf: 'Part of'
});
\ No newline at end of file
<footer id="info">
<p>${edit}</p>
<p>${templateBy} <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>${createdBy} <a href="http://cujojs.com">cujojs</a></p>
<p>${partOf} <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
define(function() {
/**
* Custom data linking handler for setting the "completed" class.
* The intent here is just to show that you can implement custom
* handlers for data/dom linking to do anything that isn't provided
* by default.
*/
return function(node, data, info) {
// Simple-minded implementation just to show custom data linking handler
node.className = data[info.prop] ? 'completed' : '';
};
});
\ No newline at end of file
define({
markAll: 'Mark all as complete'
});
\ No newline at end of file
#toggle-all {
display: none;
}
.todos-one #toggle-all, .todos-many #toggle-all {
display: block;
}
\ No newline at end of file
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">${markAll}</label>
<ul id="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label></label>
<button class="destroy"></button>
</div>
<input class="edit" value="">
</li>
</ul>
</section>
define({
// The root node where all the views will be inserted
root: { $ref: 'dom!todoapp' },
// Render and insert the create view
createView: {
render: {
template: { module: 'text!create/template.html' },
replace: { module: 'i18n!create/strings' }
},
insert: { first: 'root' }
},
// Hook up the form to auto-reset whenever a new todo is added
createForm: {
element: { $ref: 'dom.first!form', at: 'createView' },
connect: { 'todos.onAdd': 'reset' }
},
// Render and insert the list of todos, linking it to the
// data and mapping data fields to the DOM
listView: {
render: {
template: { module: 'text!list/template.html' },
replace: { module: 'i18n!list/strings' },
css: { module: 'css!list/structure.css' }
},
insert: { after: 'createView' },
bind: {
to: { $ref: 'todos' },
comparator: 'dateCreated',
bindings: {
text: 'label, .edit',
complete: [
'.toggle',
{ attr: 'classList', handler: { module: 'list/setCompletedClass' } }
]
}
}
},
// Render and insert the "controls" view--this has the todo count,
// filters, and clear completed button.
controlsView: {
render: {
template: { module: 'text!controls/template.html' },
replace: { module: 'i18n!controls/strings' },
css: { module: 'css!controls/structure.css' }
},
insert: { after: 'listView' }
},
// Render and insert the footer. This is mainly static text, but
// is still fully internationalized.
footerView: {
render: {
template: { module: 'text!footer/template.html' },
replace: { module: 'i18n!footer/strings' }
},
insert: { after: 'root' }
},
// Create a localStorage adapter that will use the storage
// key 'todos-cujo' for storing todos. This is also linked,
// creating a two-way linkage between the listView and the
// data storage.
todoStore: {
create: {
module: 'cola/adapter/LocalStorage',
args: 'todos-cujo'
},
bind: {
to: { $ref: 'todos' }
}
},
todos: {
create: {
module: 'cola/Hub',
args: {
strategyOptions: {
validator: { module: 'create/validateTodo' }
}
}
},
before: {
add: 'cleanTodo | generateMetadata',
update: 'cleanTodo'
}
},
// The main controller, which is acting more like a mediator in this
// application by reacting to events in multiple views.
// Typically, cujo-based apps will have several (or many) smaller
// view controllers. Since this is a relatively simple application,
// a single controller fits well.
todoController: {
prototype: { create: 'controller' },
properties: {
todos: { $ref: 'todos' },
createTodo: { compose: 'parseForm | todos.add' },
removeTodo: { compose: 'todos.remove' },
updateTodo: { compose: 'todos.update' },
querySelector: { $ref: 'dom.first!' },
masterCheckbox: { $ref: 'dom.first!#toggle-all', at: 'listView' },
countNode: { $ref: 'dom.first!.count', at: 'controlsView' },
remainingNodes: { $ref: 'dom.all!#todo-count strong', at: 'controlsView' }
},
on: {
createView: {
'submit:form': 'createTodo'
},
listView: {
'click:.destroy': 'removeTodo',
'change:.toggle': 'updateTodo',
'click:#toggle-all': 'toggleAll',
'dblclick:.view': 'todos.edit',
'change,focusout:.edit': 'todos.submit' // also need way to submit on [enter]
},
controlsView: {
'click:#clear-completed': 'removeCompleted'
}
},
connect: {
updateTotalCount: 'setTodosTotalState',
updateRemainingCount: 'setTodosRemainingState',
updateCompletedCount: 'setTodosCompletedState',
'todos.onChange': 'updateCount',
'todos.onEdit': 'todos.findNode | toggleEditingState.add | beginEditTodo',
'todos.onSubmit': 'todos.findNode | toggleEditingState.remove | todos.findItem | endEditTodo'
}
},
parseForm: { module: 'cola/dom/formToObject' },
cleanTodo: { module: 'create/cleanTodo' },
generateMetadata: { module: 'create/generateMetadata' },
toggleEditingState: {
create: {
module: 'wire/dom/transform/toggleClasses',
args: {
classes: 'editing'
}
}
},
setTodosTotalState: {
create: {
module: 'wire/dom/transform/cardinality',
args: { node: { $ref: 'root' }, prefix: 'todos' }
}
},
setTodosRemainingState: {
create: {
module: 'wire/dom/transform/cardinality',
args: { node: { $ref: 'root' }, prefix: 'remaining' }
}
},
setTodosCompletedState: {
create: {
module: 'wire/dom/transform/cardinality',
args: { node: { $ref: 'root' }, prefix: 'completed' }
}
},
plugins: [
// { module: 'wire/debug', trace: true },
{ module: 'wire/dom' },
{ module: 'wire/dom/render' },
{ module: 'wire/on' },
{ module: 'wire/aop' },
{ module: 'wire/connect' },
{ module: 'cola' }
]
});
\ No newline at end of file
(function( curl ) {
var config = {
baseUrl: 'app',
paths: {
curl: '../lib/curl/src/curl'
},
pluginPath: 'curl/plugin',
packages: [
{ name: 'wire', location: '../lib/wire', main: 'wire' },
{ name: 'when', location: '../lib/when', main: 'when' },
{ name: 'aop', location: '../lib/aop', main: 'aop' },
{ name: 'cola', location: '../lib/cola', main: 'cola' },
{ name: 'poly', location: '../lib/poly', main: 'poly' }
]
};
curl(config, ['poly/string', 'poly/array']).next(['wire!main']);
})( curl );
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>cujo • TodoMVC</title>
<link rel="stylesheet" href="../../../assets/base.css">
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
<script src="lib/curl/src/curl.js"></script>
<script src="app/run.js"></script>
</head>
<body>
<section id="todoapp"></section>
<!-- Scripts here. Don't remove this ↓ -->
<script src="../../../assets/base.js"></script>
</body>
</html>
\ No newline at end of file
[submodule "test/util"]
path = test/util
url = https://github.com/dojo/util.git
Open Source Initiative OSI - The MIT License
http://www.opensource.org/licenses/mit-license.php
Copyright (c) 2011 Brian Cavalier
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Please Note: this project has moved from briancavalier/aop to cujojs/aop.
Any existing forks have been automatically moved to cujojs/aop. However,
you'll need to update your clone and submodule remotes manually.
Update the url in your .git/config, and also .gitmodules for submodules:
```
git://github.com/cujojs/aop.git
https://cujojs@github.com/cujojs/aop.git
```
Helpful link for updating submodules:
[Git Submodules: Adding, Using, Removing, Updating](http://chrisjean.com/2009/04/20/git-submodules-adding-using-removing-and-updating/)
----
[Aspect Oriented Programming](http://en.wikipedia.org/wiki/Aspect-oriented_programming "Aspect-oriented programming - Wikipedia, the free encyclopedia") for Javascript.
## Changelog
### v0.5.3
* First official release as part of [cujojs](http://github.com/cujojs)
* Minor doc and package.json tweaks
### v0.5.2
* Revert to larger, more builder-friendly module boilerplate. No functional change.
### v0.5.1
* Minor corrections and updates to `package.json`
### v0.5.0
* Rewritten Advisor that allows entire aspects to be unwoven (removed) easily.
# Beers to:
* [AspectJ](http://www.eclipse.org/aspectj/) and [Spring Framework AOP](http://static.springsource.org/spring/docs/3.0.x/reference/aop.html) for inspiration and great docs
* Implementation ideas from @phiggins42's [uber.js AOP](https://github.com/phiggins42/uber.js/blob/master/lib/aop.js)
* API ideas from [jquery-aop](http://code.google.com/p/jquery-aop/)
\ No newline at end of file
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* aop
* Aspect Oriented Programming for Javascript
*
* aop is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
* @version 0.5.3
*/
(function (define) {
define(function () {
var ap, prepend, append, slice, isArray, freeze;
freeze = Object.freeze || function (o) { return o; };
ap = Array.prototype;
prepend = ap.unshift;
append = ap.push;
slice = ap.slice;
isArray = Array.isArray || function(it) {
return Object.prototype.toString.call(it) == '[object Array]';
};
/**
* Helper to convert arguments to an array
* @param a {Arguments} arguments
* @return {Array}
*/
function argsToArray(a) {
return slice.call(a);
}
function forEach(array, func) {
for (var i = 0, len = array.length; i < len; ++i) {
func(array[i]);
}
}
function forEachReverse(array, func) {
for (var i = array.length - 1; i >= 0; --i) {
func(array[i]);
}
}
var iterators = {
// Before uses reverse iteration
before: forEachReverse
};
// All other advice types use forward iteration
// Around is a special case that uses recursion rather than
// iteration. See Advisor._callAroundAdvice
iterators.on
= iterators.afterReturning
= iterators.afterThrowing
= iterators.after
= forEach;
function proceedAlreadyCalled() { throw new Error("proceed() may only be called once"); }
function Advisor(target, func) {
var orig, advisor, advised;
this.target = target;
this.func = func;
this.aspects = [];
orig = this.orig = target[func];
advisor = this;
advised = this.advised = function() {
var context, args, result, afterType, exception;
context = this;
function callOrig(args) {
var result = orig.apply(context, args);
advisor._callSimpleAdvice('on', context, args);
return result;
}
function callAfter(afterType, args) {
advisor._callSimpleAdvice(afterType, context, args);
}
args = argsToArray(arguments);
afterType = 'afterReturning';
advisor._callSimpleAdvice('before', context, args);
try {
result = advisor._callAroundAdvice(context, func, args, callOrig);
} catch(e) {
result = exception = e;
// Switch to afterThrowing
afterType = 'afterThrowing';
}
args = [result];
callAfter(afterType, args);
callAfter('after', args);
if(exception) {
throw exception;
}
return result;
};
advised._advisor = this;
}
Advisor.prototype = {
/**
* Invoke all advice functions in the supplied context, with the supplied args
*
* @param adviceType
* @param context
* @param args
*/
_callSimpleAdvice: function(adviceType, context, args) {
// before advice runs LIFO, from most-recently added to least-recently added.
// All other advice is FIFO
var iterator = iterators[adviceType];
iterator(this.aspects, function(aspect) {
var advice = aspect[adviceType];
advice && advice.apply(context, args);
});
},
/**
* Invoke all around advice and then the original method
*
* @param context
* @param method
* @param args
* @param orig
*/
_callAroundAdvice: function (context, method, args, orig) {
var len, aspects;
aspects = this.aspects;
len = aspects.length;
/**
* Call the next function in the around chain, which will either be another around
* advice, or the orig method.
* @param i {Number} index of the around advice
* @param args {Array} arguments with with to call the next around advice
*/
function callNext(i, args) {
var aspect;
// Skip to next aspect that has around advice
while (i >= 0 && (aspect = aspects[i]) && typeof aspect.around !== 'function') --i;
// If we exhausted all aspects, finally call the original
// Otherwise, if we found another around, call it
return (i < 0) ? orig.call(context, args) : callAround(aspect.around, i, args);
}
function callAround(around, i, args) {
var proceed, joinpoint;
/**
* Create proceed function that calls the next around advice, or the original. Overwrites itself so that it can only be called once.
* @param [args] {Array} optional arguments to use instead of the original arguments
*/
proceed = function (args) {
proceed = proceedAlreadyCalled;
return callNext(i - 1, args);
};
// Joinpoint is immutable
joinpoint = freeze({
target: context,
method: method,
args: args,
proceed: function (/* newArgs */) {
// if new arguments were provided, use them
return proceed(arguments.length > 0 ? argsToArray(arguments) : args);
}
});
// Call supplied around advice function
return around.call(context, joinpoint);
}
return callNext(len - 1, args);
},
/**
* Adds the supplied aspect to the advised target method
*
* @param aspect
*/
add: function(aspect) {
var aspects, advisor;
advisor = this;
aspects = advisor.aspects;
aspects.push(aspect);
return {
remove: function () {
for (var i = aspects.length; i >= 0; --i) {
if (aspects[i] === aspect) {
aspects.splice(i, 1);
break;
}
}
// If there are no aspects left, restore the original method
if (!aspects.length) {
advisor.remove();
}
}
};
},
/**
* Removes the Advisor and thus, all aspects from the advised target method, and
* restores the original target method, copying back all properties that may have
* been added or updated on the advised function.
*/
remove: function () {
delete this.advised._advisor;
this.target[this.func] = this.orig;
}
};
// Returns the advisor for the target object-function pair. A new advisor
// will be created if one does not already exist.
Advisor.get = function(target, func) {
if(!(func in target)) return;
var advisor, advised;
advised = target[func];
if(typeof advised !== 'function') throw new Error('Advice can only be applied to functions: ' + func);
advisor = advised._advisor;
if(!advisor) {
advisor = new Advisor(target, func);
target[func] = advisor.advised;
}
return advisor;
};
function addAspectToMethod(target, method, aspect) {
var advisor = Advisor.get(target, method);
return advisor && advisor.add(aspect);
}
function addAspectToAll(target, methodArray, aspect) {
var removers, added, f, i;
removers = [];
i = 0;
while((f = methodArray[i++])) {
added = addAspectToMethod(target, f, aspect);
added && removers.push(added);
}
return {
remove: function() {
for (var i = removers.length - 1; i >= 0; --i) {
removers[i].remove();
}
}
};
}
function addAspect(target, pointcut, aspect) {
// pointcut can be: string, Array of strings, RegExp, Function(targetObject): Array of strings
// advice can be: object, Function(targetObject, targetMethodName)
var pointcutType, remove;
target = findTarget(target);
if (isArray(pointcut)) {
remove = addAspectToAll(target, pointcut, aspect);
} else {
pointcutType = typeof pointcut;
if (pointcutType === 'string') {
if (typeof target[pointcut] === 'function') {
remove = addAspectToMethod(target, pointcut, aspect);
}
} else if (pointcutType === 'function') {
remove = addAspectToAll(target, pointcut(target), aspect);
} else {
// Assume the pointcut is a RegExp
for (var p in target) {
// TODO: Decide whether hasOwnProperty is correct here
// Only apply to own properties that are functions, and match the pointcut regexp
if (typeof target[p] === 'function' && pointcut.test(p)) {
// if(object.hasOwnProperty(p) && typeof object[p] === 'function' && pointcut.test(p)) {
remove = addAspectToMethod(target, p, aspect);
}
}
}
}
return remove;
}
function findTarget(target) {
return target.prototype || target;
}
// Create an API function for the specified advice type
function adviceApi(type) {
return function(target, func, adviceFunc) {
var aspect = {};
aspect[type] = adviceFunc;
return addAspect(target, func, aspect);
};
}
// Public API
return {
// General add aspect
// Returns a function that will remove the newly-added aspect
add: addAspect,
// Add a single, specific type of advice
// returns a function that will remove the newly-added advice
before: adviceApi('before'),
around: adviceApi('around'),
on: adviceApi('on'),
afterReturning: adviceApi('afterReturning'),
afterThrowing: adviceApi('afterThrowing'),
after: adviceApi('after')
};
});
})(typeof define == 'function'
? define
: function (factory) {
typeof module != 'undefined'
? (module.exports = factory())
: (this.aop = factory());
}
// Boilerplate for AMD, Node, and browser global
);
{
"name": "aop",
"version": "0.5.3",
"description": "AOP for JS with before, on, afterReturning, afterThrowing, after advice, and pointcut support",
"keywords": ["aop"],
"licenses": [
{
"type": "MIT",
"url": "http://www.opensource.org/licenses/mit-license.php"
}
],
"repositories": [
{
"type": "git",
"url": "https://github.com/cujojs/aop"
}
],
"bugs": "https://github.com/cujojs/aop/issues",
"maintainers": [
{
"name": "Brian Cavalier",
"web": "http://hovercraftstudios.com"
},
{
"name": "Brian Cavalier",
"web": "http://hovercraftstudios.com"
}
],
"main": "./aop",
"directories": {
"test": "test"
}
}
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>after Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = "foo"; // const
// Test fixture
function Fixture(shouldThrow) {
this.val = 0;
this.shouldThrow = shouldThrow;
}
Fixture.prototype = {
method: function(a) {
this.val++;
if(this.shouldThrow) {
throw new Error('testing after advice with throw');
}
return this.val;
}
};
doh.register('after', [
function testAfterReturn1() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.after(target, 'method', function afterAdvice(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(this.val, a);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
},
function testAfterThrow1() {
var target = new Fixture(true);
// Starting value
doh.assertEqual(0, target.val);
aop.after(target, 'method', function afterAdvice(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the exception that was thrown
doh.assertTrue(a instanceof Error);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
});
try {
target.method(arg);
doh.assertTrue(false);
} catch(e) {}
// after method call, val should have changed
doh.assertEqual(1, target.val);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>afterReturning Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = "foo"; // const
// Test fixture
function Fixture() {
this.val = 0;
}
Fixture.prototype = {
method: function(a) {
this.val++;
return this.val;
}
};
doh.register('afterReturning', [
function testAfterReturning1() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.afterReturning(target, 'method', function afterReturning1(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(this.val, a);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
},
function testAfterReturning2() {
var target = new Fixture();
var count = 0;
// Add 3 advices and test their invocation order,
// args, and return value
// Starting value
doh.assertEqual(0, target.val);
aop.afterReturning(target, 'method', function afterReturning0(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(this.val, a);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
// after* advice is stacked left to right such that advice added
// later is called later, so count should not have
// been incremented yet.
doh.assertEqual(0, count);
// Increment count so it can be verified in next advice
count++;
});
aop.afterReturning(target, 'method', function afterReturning1(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(this.val, a);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
doh.assertEqual(1, count);
// Increment count so it can be verified in next advice
count++;
});
aop.afterReturning(target, 'method', function afterReturning2(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(this.val, a);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
doh.assertEqual(2, count);
});
var ret = target.method(arg);
// original method should only have been called once, so
// val should only be 1.
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>afterThrowing Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = "foo"; // const
// Test fixture
function Fixture() {
this.val = 0;
}
Fixture.prototype = {
method: function(a) {
this.val++;
throw new Error('testing afterThrowing');
}
};
doh.register('afterThrowing', [
function testAfterReturning1() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.afterThrowing(target, 'method', function afterReturning1(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the exception that was thrown
doh.assertTrue(a instanceof Error);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
});
try {
target.method(arg);
doh.assertTrue(false);
} catch(e) {}
// after method call, val should have changed
doh.assertEqual(1, target.val);
},
function testAfterReturning2() {
var target = new Fixture();
var count = 0;
// Add 3 advices and test their invocation order,
// args, and return value
// Starting value
doh.assertEqual(0, target.val);
aop.afterThrowing(target, 'method', function afterReturning0(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the exception that was thrown
doh.assertTrue(a instanceof Error);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
// after* advice is stacked left to right such that advice added
// later is called later, so count should not have
// been incremented yet.
doh.assertEqual(0, count);
// Increment count so it can be verified in next advice
count++;
});
aop.afterThrowing(target, 'method', function afterReturning1(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the exception that was thrown
doh.assertTrue(a instanceof Error);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
doh.assertEqual(1, count);
// Increment count so it can be verified in next advice
count++;
});
aop.afterThrowing(target, 'method', function afterReturning2(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the exception that was thrown
doh.assertTrue(a instanceof Error);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
doh.assertEqual(2, count);
});
try {
target.method(arg);
doh.assertTrue(false);
} catch(e) {}
// after method call, val should have changed
doh.assertEqual(1, target.val);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>All Unit Tests</title>
<meta http-equiv="REFRESH" content="0;url=util/doh/runner.html?testUrl=../../all">
</head>
<body>
<div id="testBody"></div>
</body>
</html>
\ No newline at end of file
// DOH seems to faily consistently on the first test suite, so I'm putting
// in this fake suite so it will fail and all the real tests results will
// be meaningful.
doh.registerUrl('_fake', '../../_fake-doh.html');
// Real tests
// Basic advice
doh.registerUrl('before', '../../before.html');
doh.registerUrl('around', '../../around.html');
doh.registerUrl('on', '../../on.html');
doh.registerUrl('afterReturning', '../../afterReturning.html');
doh.registerUrl('afterThrowing', '../../afterThrowing.html');
doh.registerUrl('after', '../../after.html');
// Pointcuts
doh.registerUrl('pointcut', '../../pointcut.html');
doh.registerUrl('prototype', '../../prototype.html');
// Remove
doh.registerUrl('remove', '../../remove.html');
// Go
doh.run();
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>around Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = 1; // const
// Test fixture
function Fixture(shouldThrow) {
this.val = 0;
this.shouldThrow = shouldThrow;
}
Fixture.prototype = {
method: function(add) {
this.val += add;
if(this.shouldThrow) {
throw new Error('testing around advice with throw');
}
return this.val;
}
};
doh.register('around', [
function testAround() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.around(target, 'method', function aroundAdvice(joinpoint) {
// this should be the advised object
doh.assertEqual(target, this);
doh.assertEqual(target, joinpoint.target);
// arg should be the return value from the orig method
doh.assertEqual(1, joinpoint.args.length);
doh.assertEqual(arg, joinpoint.args[0]);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(0, this.val);
var ret = joinpoint.proceed();
doh.assertEqual(1, ret);
doh.assertEqual(1, this.val);
return ret;
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(1, ret);
},
function testAroundMultipleProceedShouldFail() {
var target = new Fixture();
aop.around(target, 'method', function aroundAdvice(joinpoint) {
// Calling joinpoint.proceed() multiple times should fail
var ret, success;
ret = joinpoint.proceed();
success = false;
try {
ret = joinpoint.proceed();
} catch(e) {
success = true;
}
doh.assertTrue(success);
return ret;
});
target.method(arg);
},
function testMultipleAround() {
var target = new Fixture();
var count = 0;
// Around advice should "stack" in layers from inner to outer
// Inner
aop.around(target, 'method', function aroundAdviceInner(joinpoint) {
// Verify the outer around has been called
doh.assertEqual(1, count);
// This will proceed to the original method
joinpoint.proceed();
// Verify no more arounds have been called
doh.assertEqual(1, count);
// Indicate this inner around has been called
count++;
});
// Outer
aop.around(target, 'method', function aroundAdviceOuter(joinpoint) {
// Verify this is the first around advice to be called
doh.assertEqual(0, count);
count++;
// This will proceed to the inner around
joinpoint.proceed();
// Verify that the inner around around has been called
doh.assertEqual(2, count);
});
target.method(arg);
},
function testAroundModifyArgs() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.around(target, 'method', function aroundAdvice(joinpoint) {
// arg should be the return value from the orig method
doh.assertEqual(1, joinpoint.args.length);
doh.assertEqual(arg, joinpoint.args[0]);
// after function should be called (duh) after
// the original, so val will have changed.
doh.assertEqual(0, this.val);
// Modify the original args and pass them through to
// the original func
var modifiedArgs = [10];
var ret = joinpoint.proceed(modifiedArgs);
doh.assertEqual(10, ret);
doh.assertEqual(10, this.val);
return ret;
});
var ret = target.method(arg);
// after method call, val should have changed based on the modified args
doh.assertEqual(10, target.val);
// Make sure the return value is preserved, also based on the modified args
doh.assertEqual(10, ret);
},
function testAroundModifyReturnVal() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.around(target, 'method', function aroundAdvice(joinpoint) {
joinpoint.proceed();
return 10;
});
var ret = target.method(arg);
// after method call, val should be 1, since original args were
// not modified.
doh.assertEqual(1, target.val);
// Make sure we got the modified return value
doh.assertEqual(10, ret);
},
function testAroundPreventOriginal() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.around(target, 'method', function aroundAdvice(joinpoint) {
// Intentionally do not proceed to original method
// var ret = joinpoint.proceed();
return this.val;
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(0, target.val);
// Make sure the return value is preserved
doh.assertEqual(0, ret);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>before Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = "foo"; // const
// Test fixture
function Fixture() {
this.val = 0;
}
Fixture.prototype = {
method: function(a) {
return (++this.val);
}
};
doh.register('before', [
function testBefore1() {
var target = new Fixture();
aop.before(target, 'method', function before1(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should not change
doh.assertEqual(arg, a);
// before function should be called (duh) before
// the original, so val should not have changed yet.
doh.assertEqual(0, this.val);
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
},
function testBefore2() {
var target = new Fixture();
var beforeCount = 0;
// Add 3 before advices and test their invocation order,
// args, and return value
aop.before(target, 'method', function before0(a) {
doh.assertEqual(target, this);
doh.assertEqual(arg, a);
// *ALL* before functions should be called (duh) before
// the original, so val should not have changed yet.
doh.assertEqual(0, this.val);
// Before advice is stacked such that advice added
// later is called first, so beforeCount should have
// been incremented.
doh.assertEqual(2, beforeCount);
});
aop.before(target, 'method', function before1(a) {
doh.assertEqual(target, this);
doh.assertEqual(arg, a);
// *ALL* before functions should be called (duh) before
// the original, so val should not have changed yet.
doh.assertEqual(0, this.val);
// Before advice is stacked "right to left", such that
// advice added later is called first, so before2
// should be called earlier than before1, and beforeCount
// should have been incremented.
doh.assertEqual(1, beforeCount);
// Increment beforeCount so it can be verified in before0
beforeCount++;
});
aop.before(target, 'method', function before2(a) {
doh.assertEqual(target, this);
doh.assertEqual(arg, a);
// *ALL* before functions should be called (duh) before
// the original, so val should not have changed yet.
doh.assertEqual(0, this.val);
// before2 should be called first, so beforeCount should
// be zero.
doh.assertEqual(0, beforeCount);
// Increment beforeCount so it can be verified in before1
beforeCount++;
});
var ret = target.method(arg);
// original method should only have been called once, so
// val should only be 1.
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>on Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
var arg = "foo"; // const
// Test fixture
function Fixture(shouldThrow) {
this.val = 0;
this.shouldThrow = shouldThrow;
}
Fixture.prototype = {
method: function(a) {
this.val++;
if(this.shouldThrow) {
throw new Error('testing after advice with throw');
}
return this.val;
}
};
doh.register('on', [
function testOn1() {
var target = new Fixture();
// Starting value
doh.assertEqual(0, target.val);
aop.on(target, 'method', function on(a) {
// this should be the advised object
doh.assertEqual(target, this);
// arg should be the return value from the orig method
doh.assertEqual(arg, a);
// on function should be called after
// the original, so val will have changed.
doh.assertEqual(1, this.val);
});
var ret = target.method(arg);
// after method call, val should have changed
doh.assertEqual(1, target.val);
// Make sure the return value is preserved
doh.assertEqual(ret, target.val);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>Pointcut Unit Tests</title>
<script src="util/doh/runner.js"></script>
<script src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
function makeProto() {
var n, proto, i;
proto = {};
i = 0;
while((n = arguments[i++])) {
(function(n) {
proto[n] = function() { this.calls.push(n); };
})(n);
}
return proto;
}
// Test fixture
function Fixture() {
this.calls = [];
}
Fixture.prototype = makeProto('method1', 'method2', 'methodA', 'methodB', 'blah');
doh.register('pointcut', [
function testStringPointcut1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(target, 'method1', {
before: advice,
after: advice
});
target.method1();
doh.assertEqual(2, called);
doh.assertEqual(['method1'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'blah'], target.calls);
},
function testArrayPointcut1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(target, ['method1', 'methodA'], {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
},
function testRegExpPointcut1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(target, /^method(1|a)$/i, {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
},
function testFunctionPointcut1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
function pointcut(target) {
return ['method1', 'methodA'];
}
aop.add(target, pointcut, {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>Prototype AOP Unit Tests</title>
<script src="util/doh/runner.js"></script>
<script src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
function makeProto() {
var n, proto, i;
proto = {};
i = 0;
while((n = arguments[i++])) {
(function(n) {
proto[n] = function() { this.calls.push(n); };
})(n);
}
return proto;
}
// Test fixture
function Fixture() {
this.calls = [];
}
Fixture.prototype = makeProto('method1', 'method2', 'methodA', 'methodB', 'blah');
doh.register('prototype', [
function testStringPrototype1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(Fixture, 'method1', {
before: advice,
after: advice
});
target.method1();
doh.assertEqual(2, called);
doh.assertEqual(['method1'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'blah'], target.calls);
},
function testArrayPrototype1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(Fixture, ['method1', 'methodA'], {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
},
function testRegExpPrototype1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
aop.add(Fixture, /^method(1|a)$/i, {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
},
function testFunctionPrototype1() {
var target = new Fixture();
var called = 0;
function advice() { called++; }
function pointcut(target) {
return ['method1', 'methodA'];
}
aop.add(Fixture, pointcut, {
before: advice
});
target.method1();
doh.assertEqual(1, called);
doh.assertEqual(['method1'], target.calls);
target.methodA();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA'], target.calls);
target.blah();
doh.assertEqual(2, called);
doh.assertEqual(['method1', 'methodA', 'blah'], target.calls);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>remove aspect Unit Tests</title>
<script type="text/javascript" src="util/doh/runner.js"></script>
<script type="text/javascript" src="../aop.js"></script>
<script type="text/javascript">
(function(global, doh, aop, undef) {
function Fixture() { this.count = 0; }
Fixture.prototype.method = function() {}
function increment(joinpoint) {
this.count++;
if(joinpoint) {
joinpoint.proceed();
}
}
doh.register('remove', [
function testRemove() {
// Just test that the advisor is added, and then
// removed when the final aspect is removed
var fixture, ref;
fixture = new Fixture();
ref = aop.before(fixture, 'method', increment);
doh.assertTrue(!!fixture.method._advisor);
ref.remove();
doh.assertEqual(undef, fixture.method._advisor);
},
function testRemove1() {
var fixture, ref;
fixture = new Fixture();
ref = aop.before(fixture, 'method', increment);
fixture.method();
// after method call, val should have changed
doh.assertEqual(1, fixture.count);
ref.remove();
fixture.method();
// after removing, advice should not be called
// again, so count should not have changed
doh.assertEqual(1, fixture.count);
},
function testRemoveAround() {
var fixture, ref;
fixture = new Fixture();
ref = aop.around(fixture, 'method', increment);
fixture.method();
// after method call, val should have changed
doh.assertEqual(1, fixture.count);
ref.remove();
fixture.method();
// after removing, advice should not be called
// again, so count should not have changed
doh.assertEqual(1, fixture.count);
},
function testRemoveAspect() {
var fixture, ref;
fixture = new Fixture();
ref = aop.add(fixture, 'method',{
before: increment,
on: increment,
around: increment,
afterReturning: increment,
after: increment
});
fixture.method();
// after method call, val should have changed
doh.assertEqual(5, fixture.count);
ref.remove();
fixture.method();
// after removing, advice should not be called
// again, so count should not have changed
doh.assertEqual(5, fixture.count);
},
function testRemoveOneFromMultiple() {
var fixture, ref;
fixture = new Fixture();
// Add 3 aspects, but only remove 1
aop.before(fixture, 'method', increment);
ref = aop.before(fixture, 'method', increment);
aop.before(fixture, 'method', increment);
fixture.method();
// after method call, val should have changed
doh.assertEqual(3, fixture.count);
ref.remove();
fixture.method();
// removed advice should not be called
doh.assertEqual(5, fixture.count);
}
]);
doh.run();
})(window, doh, aop);
</script>
</head>
<body>
</body>
</html>
\ No newline at end of file
[submodule "test/curl"]
path = test/curl
url = https://unscriptable@github.com/cujojs/curl.git
[submodule "test/util"]
path = test/util
url = https://unscriptable@github.com/cujojs/util.git
[submodule "support/when"]
path = support/when
url = https://github.com/cujojs/when.git
(function (define) {
define(function () {
"use strict";
var adapters;
adapters = {};
/**
* Finds an adapter for the given object and the role.
* This is overly simplistic for now. We can replace this
* resolver later.
* @param object {Object}
* @param type {String}
* @description Loops through all Adapters registered with
* AdapterResolver.register, calling each Adapter's canHandle
* method. Adapters added later are found first.
*/
function AdapterResolver (object, type) {
var adaptersOfType, i, Adapter;
adaptersOfType = adapters[type];
if (adaptersOfType) {
i = adaptersOfType.length;
while ((Adapter = adaptersOfType[--i])) {
if (Adapter.canHandle(object)) {
return Adapter;
}
}
}
}
AdapterResolver.register = function registerAdapter (Adapter, type) {
if (!(type in adapters)) adapters[type] = [];
adapters[type].push(Adapter);
};
return AdapterResolver;
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(); }
));
\ No newline at end of file
This diff is collapsed.
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function () {
var undef, missing = {};
/**
* @constructor
* @param identifier {Function}
* @param comparator {Function}
*/
function SortedMap (identifier, comparator) {
// identifier is required, comparator is optional
this.clear();
/**
* Fetches a value item for the given key item or the special object,
* missing, if the value item was not found.
* @private
* @param keyItem
* @returns {Object} the value item that was set for the supplied
* key item or the special object, missing, if it was not found.
*/
this._fetch = function (keyItem) {
var symbol = identifier(keyItem);
return symbol in this._index ? this._index[symbol] : missing;
};
/**
* Performs a binary search to find the bucket position of a
* key item within the key items list. Only used if we have a
* comparator.
* @private
* @param keyItem
* @param exactMatch {Boolean} if true, must be an exact match to the key
* item, not just the correct position for a key item that sorts
* the same.
* @returns {Number|Undefined}
*/
this._pos = function (keyItem, exactMatch) {
var pos, sorted, symbol;
sorted = this._sorted;
symbol = identifier(keyItem);
function getKey (pos) { return sorted[pos] ? sorted[pos][0].key : {}; }
pos = binarySearch(0, sorted.length, keyItem, getKey, comparator);
if (exactMatch) {
if (symbol != identifier(sorted[pos][0].key)) {
pos = -1;
}
}
return pos;
};
this._bucketOffset = function (bucketPos) {
var total, i;
total = 0;
for (i = 0; i < bucketPos; i++) {
total += this._sorted[i].length;
}
return total;
};
if (!comparator) {
this._pos = function (keyItem, exact) {
return exact ? -1 : this._sorted.length;
};
}
/**
* Given a keyItem and its bucket position in the list of key items,
* inserts an value item into the bucket of value items.
* This method can be overridden by other objects that need to
* have objects in the same order as the key values.
* @private
* @param valueItem
* @param keyItem
* @param pos
* @returns {Number} the absolute position of this item amongst
* all items in all buckets.
*/
this._insert = function (keyItem, pos, valueItem) {
var pair, symbol, entry, absPos;
// insert into index
pair = { key: keyItem, value: valueItem };
symbol = identifier(keyItem);
this._index[symbol] = pair;
// insert into sorted table
if (pos >= 0) {
absPos = this._bucketOffset(pos);
entry = this._sorted[pos] && this._sorted[pos][0];
// is this a new row (at end of array)?
if (!entry) {
this._sorted[pos] = [pair];
}
// are there already items of the same sort position here?
else if (comparator(entry.key, keyItem) == 0) {
absPos += this._sorted[pos].push(pair) - 1;
}
// or do we need to insert a new row?
else {
this._sorted.splice(pos, 0, [pair]);
}
}
else {
absPos = -1;
}
return absPos;
};
/**
* Given a key item and its bucket position in the list of key items,
* removes a value item from the bucket of value items.
* This method can be overridden by other objects that need to
* have objects in the same order as the key values.
* @private
* @param keyItem
* @param pos
* @returns {Number} the absolute position of this item amongst
* all items in all buckets.
*/
this._remove = function remove (keyItem, pos) {
var symbol, entries, i, entry, absPos;
symbol = identifier(keyItem);
// delete from index
delete this._index[symbol];
// delete from sorted table
if (pos >= 0) {
absPos = this._bucketOffset(pos);
entries = this._sorted[pos] || [];
i = entries.length;
// find it and remove it
while ((entry = entries[--i])) {
if (symbol == identifier(entry.key)) {
entries.splice(i, 1);
break;
}
}
absPos += i;
// if we removed all pairs at this position
if (entries.length == 0) {
this._sorted.splice(pos, 1);
}
}
else {
absPos = -1;
}
return absPos;
};
this._setComparator = function (newComparator) {
var p, pair, pos;
comparator = newComparator;
this._sorted = [];
for (p in this._index) {
pair = this._index[p];
pos = this._pos(pair.key);
this._insert(pair.key, pos, pair.value);
}
};
}
SortedMap.prototype = {
get: function (keyItem) {
var pair;
pair = this._fetch(keyItem);
return pair == missing ? undef : pair.value;
},
add: function (keyItem, valueItem) {
var pos, absPos;
if (arguments.length < 2) throw new Error('SortedMap.add: must supply keyItem and valueItem args');
// don't insert twice. bail if we already have it
if (this._fetch(keyItem) != missing) return;
// find pos and insert
pos = this._pos(keyItem);
absPos = this._insert(keyItem, pos, valueItem);
return absPos;
},
remove: function (keyItem) {
var valueItem, pos, absPos;
// don't remove if we don't already have it
valueItem = this._fetch(keyItem);
if (valueItem == missing) return;
// find positions and delete
pos = this._pos(keyItem, true);
absPos = this._remove(keyItem, pos);
return absPos;
},
forEach: function (lambda) {
var i, j, len, len2, entries;
for (i = 0, len = this._sorted.length; i < len; i++) {
entries = this._sorted[i];
for (j = 0, len2 = entries.length; j < len2; j++) {
lambda(entries[j].value, entries[j].key);
}
}
},
clear: function() {
// hashmap of object-object pairs
this._index = {};
// 2d array of objects
this._sorted = [];
},
setComparator: function (comparator) {
this._setComparator(comparator);
}
};
return SortedMap;
/**
* Searches through a list of items, looking for the correct slot
* for a new item to be added.
* @param min {Number} points at the first possible slot
* @param max {Number} points at the slot after the last possible slot
* @param item anything comparable via < and >
* @param getter {Function} a function to retrieve a item at a specific
* slot: function (pos) { return items[pos]; }
* @param comparator {Function} function to compare to items. must return
* a number.
* @returns {Number} returns the slot where the item should be placed
* into the list.
*/
function binarySearch (min, max, item, getter, comparator) {
var mid, compare;
if (max <= min) return min;
do {
mid = Math.floor((min + max) / 2);
compare = comparator(item, getter(mid));
if (isNaN(compare)) throw new Error('SortedMap: invalid comparator result ' + compare);
// if we've narrowed down to a choice of just two slots
if (max - min <= 1) {
return compare == 0 ? mid : compare > 0 ? max : min;
}
// don't use mid +/- 1 or we may miss in-between values
if (compare > 0) min = mid;
else if (compare < 0) max = mid;
else return mid;
}
while (true);
}
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
/** MIT License (c) copyright B Cavalier & J Hann */
// TODO: Evaluate whether ArrayAdapter should use SortedMap internally to
// store items in sorted order based on its comparator
(function(define) {
define(function (require) {
"use strict";
var when, methods, undef;
when = require('when');
/**
* Manages a collection of objects taken from the supplied dataArray
* @param dataArray {Array} array of data objects to use as the initial
* population
* @param options.identifier {Function} function that returns a key/id for
* a data item.
* @param options.comparator {Function} comparator function that will
* be propagated to other adapters as needed
*/
function ArrayAdapter(dataArray, options) {
if(!options) options = {};
this._options = options;
// Use the default comparator if none provided.
// The consequences of this are that the default comparator will
// be propagated to downstream adapters *instead of* an upstream
// adapter's comparator
this.comparator = options.comparator || this._defaultComparator;
this.identifier = options.identifier || defaultIdentifier;
if('provide' in options) {
this.provide = options.provide;
}
this._array = dataArray;
this.clear();
var self = this;
when(dataArray, function(array) {
mixin(self, methods);
self._init(array);
});
}
ArrayAdapter.prototype = {
provide: true,
_init: function(dataArray) {
if(dataArray && dataArray.length) {
addAll(this, dataArray);
}
},
/**
* Default comparator that uses an item's position in the array
* to order the items. This is important when an input array is already
* in sorted order, so the user doesn't have to specify a comparator,
* and so the order can be propagated to other adapters.
* @param a
* @param b
* @return {Number} -1 if a is before b in the input array
* 1 if a is after b in the input array
* 0 iff a and b have the same symbol as returned by the configured identifier
*/
_defaultComparator: function(a, b) {
var aIndex, bIndex;
aIndex = this._index(this.identifier(a));
bIndex = this._index(this.identifier(b));
return aIndex - bIndex;
},
comparator: undef,
identifier: undef,
// just stubs for now
getOptions: function () {
return this._options;
},
forEach: function(lambda) { return this._forEach(lambda); },
add: function(item) { return this._add(item); },
remove: function(item) { return this._remove(item); },
update: function(item) { return this._update(item); },
clear: function() { return this._clear(); }
};
methods = {
_forEach: function(lambda) {
var i, data, len;
i = 0;
data = this._data;
len = data.length;
for(; i < len; i++) {
// TODO: Should we catch exceptions here?
lambda(data[i]);
}
},
_add: function(item) {
var key, index;
key = this.identifier(item);
index = this._index;
if(key in index) return null;
index[key] = this._data.push(item) - 1;
return index[key];
},
_remove: function(itemOrId) {
var key, at, index, data;
key = this.identifier(itemOrId);
index = this._index;
if(!(key in index)) return null;
data = this._data;
at = index[key];
data.splice(at, 1);
// Rebuild index
this._index = buildIndex(data, this.identifier);
return at;
},
_update: function (item) {
var key, at, index;
key = this.identifier(item);
index = this._index;
at = index[key];
if (at >= 0) {
this._data[at] = item;
}
else {
index[key] = this._data.push(item) - 1;
}
return at;
},
_clear: function() {
this._data = [];
this._index = {};
}
};
mixin(ArrayAdapter.prototype, methods, makePromiseAware);
/**
*
* @param to
* @param from
* @param [transform]
*/
function mixin(to, from, transform) {
var name, func;
for(name in from) {
if(from.hasOwnProperty(name)) {
func = from[name];
to[name] = transform ? transform(func) : func;
}
}
return to;
}
/**
* Returns a new function that will delay execution of the supplied
* function until this._resultSetPromise has resolved.
*
* @param func {Function} original function
* @return {Promise}
*/
function makePromiseAware(func) {
return function promiseAware() {
var self, args;
self = this;
args = Array.prototype.slice.call(arguments);
return when(this._array, function() {
return func.apply(self, args);
});
}
}
ArrayAdapter.canHandle = function(it) {
return it && (when.isPromise(it) || Object.prototype.toString.call(it) == '[object Array]');
};
function defaultIdentifier(item) {
return typeof item == 'object' ? item.id : item;
}
/**
* Adds all the items, starting at the supplied start index,
* to the supplied adapter.
* @param adapter
* @param items
*/
function addAll(adapter, items) {
for(var i = 0, len = items.length; i < len; i++) {
adapter.add(items[i]);
}
}
function buildIndex(items, keyFunc) {
var index, i, len;
index = {};
for(i = 0, len = items.length; i < len; i++) {
index[keyFunc(items[i])] = i;
}
return index;
}
return ArrayAdapter;
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(require); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function(global, define) {
define(function (require) {
"use strict";
var when, defaultIdentifier, undef;
defaultIdentifier = require('./../identifier/default');
when = require('when');
function LocalStorageAdapter(namespace, options) {
if (!namespace) throw new Error('cola/LocalStorageAdapter: must provide a storage namespace');
this._namespace = namespace;
if (!options) options = {};
if('provide' in options) {
this.provide = options.provide;
}
this._storage = options.localStorage || global.localStorage;
if(!this._storage) throw new Error('cola/LocalStorageAdapter: localStorage not available, must be supplied in options');
this.identifier = options.identifier || defaultIdentifier;
var data = this._storage.getItem(namespace);
this._data = data ? JSON.parse(data) : {};
}
LocalStorageAdapter.prototype = {
provide: true,
identifier: undef,
getOptions: function() {
return {};
},
forEach: function(lambda) {
var data = this._data;
for(var key in data) {
lambda(data[key]);
}
},
add: function(item) {
var id = this.identifier(item);
if(id in this._data) return null;
this._data[id] = item;
this._sync();
return id;
},
remove: function(item) {
var id = this.identifier(item);
if(!(id in this._data)) return null;
delete this._data[id];
this._sync();
return item;
},
update: function(item) {
var id = this.identifier(item);
if(!(id in this._data)) return null;
this._data[id] = item;
this._sync();
return item;
},
clear: function() {
this._storage.removeItem(this._namespace);
},
_sync: function() {
this._storage.setItem(this._namespace, JSON.stringify(this._data));
}
};
return LocalStorageAdapter;
});
})(this.window || global,
typeof define == 'function'
? define
: function(factory) { module.exports = factory(require); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function (require) {
"use strict";
var when = require('when');
/**
* Adapter that handles a plain object or a promise for a plain object
* @constructor
* @param obj {Object|Promise}
* @param options {Object}
*/
function ObjectAdapter(obj, options) {
this._obj = obj;
this._options = options;
}
ObjectAdapter.prototype = {
update: function (item) {
var self = this;
return when(this._obj, function(obj) {
function updateSynchronously(item) {
// don't replace item in case we got a partial object
for (var p in item) {
obj[p] = item[p];
}
}
self.update = updateSynchronously;
return updateSynchronously(item);
});
},
getOptions: function () {
return this._options;
}
};
/**
* Tests whether the given object is a candidate to be handled by
* this adapter. Returns true if the object is of type 'object'.
* @param obj
* @returns {Boolean}
*/
ObjectAdapter.canHandle = function (obj) {
// this seems close enough to ensure that instanceof works.
// a RegExp will pass as a valid prototype, but I am not sure
// this is a bad thing even if it is unusual.
// IMPORTANT: since promises *are* objects, the check for isPromise
// must come first in the OR
return obj && (when.isPromise(obj) || Object.prototype.toString.call(obj) == '[object Object]');
};
return ObjectAdapter;
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
\ No newline at end of file
/** MIT License (c) copyright B Cavalier & J Hann */
(function(define) {
define(function (require) {
"use strict";
var ArrayAdapter, ObjectAdapter, when;
ArrayAdapter = require('./Array');
ObjectAdapter = require('./Object');
when = require('when');
/**
* Manages a collection of objects created by transforming the input Object
* (or Promise for an Object) into a collection using the supplied
* options.transform
* @constructor
* @param object {Object|Promise} Object or Promise for an Object
* @param options.identifier {Function} function that returns a key/id for
* a data item.
* @param options.comparator {Function} comparator function that will
* be propagated to other adapters as needed
* @param options.transform {Function} transform function that will
* transform the input object into a collection
*/
function WidenAdapter(object, options) {
if (!(options && options.transform)) {
throw new Error("options.transform must be provided");
}
this._transform = options.transform;
delete options.transform;
ArrayAdapter.call(this, object, options);
}
WidenAdapter.prototype = new ArrayAdapter();
WidenAdapter.prototype._init = function(object) {
ArrayAdapter.prototype._init.call(this, this._transform(object));
};
/**
* Tests whether the given object is a candidate to be handled by
* this adapter. Returns true if the object is a promise or
* ArrayAdapter.canHandle returns true;
*
* WARNING: Testing for a promise is NOT sufficient, since the promise
* may result to something that this adapter cannot handle.
*
* @param it
* @return {Boolean}
*/
WidenAdapter.canHandle = function(it) {
return when.isPromise(it) || ObjectAdapter.canHandle(it);
};
return WidenAdapter;
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(require); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function(define) {
define(function (require) {
// "use strict";
var when, SortedMap, undef;
when = require('when');
SortedMap = require('./../SortedMap');
/**
* Manages a collection of objects taken a queryable data source, which
* must provide query, add, and remove methods
* @constructor
* @param datasource {Object} queryable data source with query, add, put, remove methods
* @param [options.comparator] {Function} comparator function that will
* be propagated to other adapters as needed. Note that QueryAdapter does not
* use this comparator internally.
*/
function QueryAdapter(datasource, options) {
var identifier, dsQuery, self;
if(!datasource) throw new Error('cola/QueryAdapter: datasource must be provided');
this._datasource = datasource;
if(!options) options = {};
this._options = options;
if('provide' in options) {
this.provide = options.provide;
}
// Always use the datasource's identity as the identifier
identifier = this.identifier =
function(item) {
// TODO: remove dojo-specific behavior
return datasource.getIdentity(item);
};
// If no comparator provided, generate one that uses
// the object identity
this.comparator = this._options.comparator ||
function(a, b) {
var aKey, bKey;
aKey = identifier(a);
bKey = identifier(b);
return aKey == bKey ? 0
: aKey < bKey ? -1
: 1;
};
this._items = new SortedMap(identifier, this.comparator);
// override the store's query
dsQuery = datasource.query;
self = this;
datasource.query = function(query) {
return self._queue(function() {
return when(dsQuery.call(datasource, arguments), function(results) {
self._items = new SortedMap(self.identifier, self.comparator);
self._initResultSet(results);
return results;
});
});
};
}
QueryAdapter.prototype = {
provide: true,
comparator: undef,
identifier: undef,
query: function(query) {
return this._datasource.query.apply(this._datasource, arguments);
},
/**
* Adds op to the internal queue of async tasks to ensure that
* it will run in the order added and not overlap with other async tasks
* @param op {Function} async task (function that returns a promise) to add
* to the internal queue
* @return {Promise} promise that will resolver/reject when op has completed
* @private
*/
_queue: function(op) {
this._inflight = when(this._inflight, function() {
return op();
});
return this._inflight;
},
/**
* Initialized the internal map of items
* @param results {Array} array of result items
* @private
*/
_initResultSet: function (results) {
var map, i, len, item, self;
map = this._items;
map.clear();
self = this;
for(i = 0, len = results.length; i < len; i++) {
item = results[i];
map.add(item, item);
self.add(item);
}
},
getOptions: function() {
return this._options;
},
forEach: function(lambda) {
var self = this;
return this._queue(function() {
return self._items.forEach(lambda);
});
},
add: function(item) {
var items, added, self;
items = this._items;
added = items.add(item, item);
if(added >= 0 && !this._dontCallDatasource) {
self = this;
// This is optimistic, maybe overly so. It notifies listeners
// that the item is added, even though there may be an inflight
// async store.add(). If the add fails, it tries to revert
// by removing the item from the local map, notifying listeners
// that it is removed, and "rethrowing" the failure.
// When we move all data to a central SortedMap, we can handle
// this behavior with a strategy.
return when(this._datasource.add(item),
function(returned) {
if (self._itemWasUpdatedByDatasource(returned)) {
self._execMethodWithoutCallingDatasource('update', returned);
}
},
function(err) {
self._execMethodWithoutCallingDatasource('remove', item);
throw err;
}
);
}
},
// TODO: allow an item or an id to be provided
remove: function(item) {
var removed, items;
items = this._items;
removed = items.remove(item);
if(removed >= 0 && !this._dontCallDatasource) {
// TODO: remove dojo-specific behavior
var id = this._datasource.getIdentity(item);
// Similar to add() above, this should be replaced with a
// central SortedMap and strategy.
return when(this._datasource.remove(id),
null, // If all goes according to plan, great, nothing to do
function(err) {
self._execMethodWithoutCallingDatasource('add', item);
throw err;
}
);
}
},
update: function(item) {
var orig, items, self;
items = this._items;
orig = items.get(item);
if(orig) {
this._replace(orig, item);
if (!this._dontCallDatasource) {
self = this;
// Similar to add() above, this should be replaced with a
// central SortedMap and strategy.
return when(this._datasource.put(item),
function(returned) {
if (self._itemWasUpdatedByDatasource(returned)) {
self._execMethodWithoutCallingDatasource('update', returned);
}
},
function(err) {
self._execMethodWithoutCallingDatasource('update', orig);
throw err;
}
);
}
}
},
_replace: function(oldItem, newItem) {
this._items.remove(oldItem);
this._items.add(newItem, newItem);
},
_itemWasUpdatedByDatasource: function(item) {
return hasProperties(item);
},
_execMethodWithoutCallingDatasource: function(method, item) {
this._dontCallDatasource = true;
try {
return this[method](item);
}
finally {
this._dontCallDatasource = false;
}
},
clear: function() {
this._initResultSet([]);
}
};
QueryAdapter.canHandle = function(it) {
return it && typeof it.query == 'function' && !(it instanceof QueryAdapter);
};
return QueryAdapter;
function hasProperties (o) {
if (!o) return false;
for (var p in o) return true;
}
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(require); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
// TODO:
// 1. Incrementally recompute the join for items added to or removed from the
// primary adapter. This requires precomputing and hanging onto the joinMap
// 2. Recompute the join when items are added to or removed from the supplimental
(function(define) {
define(function (require) {
"use strict";
var when, methodsToReplace;
when = require('when');
methodsToReplace = {
add: 1,
update: 1,
remove: 1,
clear: 1
};
/**
* Decorates the supplied primary adapter so that it will provide
* data that is joined from a secondary source specified in options.joinWith
* using the join strategy specified in options.strategy
*
* @param primary {Object} primary adapter
* @param options.joinWith {Object} secondary adapter
* @param options.strategy {Function} join strategy to use in joining
* data from the primary and secondary adapters
*/
return function makeJoined(primary, options) {
if(!(options && options.joinWith && options.strategy)) {
throw new Error('options.joinWith and options.strategy are required');
}
var forEachOrig, joined, methodName, secondary, joinStrategy, primaryProxy;
secondary = options.joinWith;
joinStrategy = options.strategy;
function replaceMethod(adapter, methodName) {
var orig = adapter[methodName];
adapter[methodName] = function() {
// Force the join to be recomputed when data changes
// This is way too aggressive, but also very safe.
// We can optimize to incrementally recompute if it
// becomes a problem.
joined = null;
return orig.apply(adapter, arguments);
}
}
// Replace the primary adapters cola event methods
for(methodName in methodsToReplace) {
replaceMethod(primary, methodName);
}
// Create a proxy adapter that has a forEach that provides
// access to the primary adapter's *original* data. We must
// send this to the join strategy since we're *replacing* the
// primary adapter's forEach with one that calls the joinStrategy.
// That would lead to an infinite call cycle.
forEachOrig = primary.forEach;
primaryProxy = {
forEach: function() {
return forEachOrig.apply(primary, arguments);
}
};
primary.forEach = function(lambda) {
if(!joined) {
joined = joinStrategy(primaryProxy, secondary);
}
return when(joined, function(joined) {
for(var i = 0, len = joined.length; i < len; i++) {
lambda(joined[i]);
}
});
};
return primary;
};
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(require); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function (require) {
var when;
when = require('when');
/**
* Returns a view of the supplied collection adapter, such that the view
* appears to contain transformed items, and delegates to the supplied
* adapter. If an inverse transform is supplied, either via the
* inverse param, or via transform.inverse, it will be used when items
* are added or removed
* @param adapter {Object} the adapter for which to create a transformed view
* @param transform {Function} the transform to apply to items. It may return
* a promise
* @param [inverse] {Function} inverse transform, can be provided explicitly
* if transform doesn't have an inverse property (transform.inverse). It may
* return a promise
*/
function transformCollection(adapter, transform, inverse) {
if(!transform) throw new Error('No transform supplied');
inverse = inverse || transform.inverse;
return {
comparator: adapter.comparator,
identifier: adapter.identifier,
forEach: function(lambda) {
var inflight;
// Ensure that these happen sequentially, even when
// the transform function is async
function transformedLambda(item) {
inflight = when(inflight, function() {
return when(transform(item), lambda);
});
return inflight;
}
return when(adapter.forEach(transformedLambda), function() {
return inflight;
});
},
add: makeTransformedAndPromiseAware(adapter, 'add', inverse),
remove: makeTransformedAndPromiseAware(adapter, 'remove', inverse),
update: makeTransformedAndPromiseAware(adapter, 'update', inverse),
clear: function() {
// return the original clear result since it may be a promise
return adapter.clear();
},
getOptions: function() {
return adapter.getOptions();
}
}
}
return transformCollection;
/**
* Creates a promise-aware version of the adapter method that supports
* transform functions that may return a promise
* @param adapter {Object} original adapter
* @param method {String} name of adapter method to make promise-aware
* @param transform {Function} transform function to apply to items
* before passing them to the original adapter method
* @return {Function} if transform is provided, returns a new function
* that applies the (possibly async) supplied transform and invokes
* the original adapter method with the transform result. If transform
* is falsey, a no-op function will be returned
*/
function makeTransformedAndPromiseAware(adapter, method, transform) {
return transform
? function(item) {
return when(transform(item), function(transformed) {
return adapter[method](transformed);
})
}
: noop;
}
function noop() {}
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
\ No newline at end of file
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function (require) {
"use strict";
var SortedMap = require('./../SortedMap');
/**
* Decorator that applies transforms to properties flowing in
* and out of an ObjectAdapter (or similar).
* @param adapter {Object}
* @param transforms {Object}
*/
function addPropertyTransforms (adapter, transforms) {
var origGet, origAdd, origUpdate, origForEach, transformedItemMap;
// only override if transforms has properties
if (transforms && hasProperties(transforms)) {
origGet = adapter.get;
origAdd = adapter.add;
origUpdate = adapter.update;
origForEach = adapter.forEach;
transformedItemMap = new SortedMap(adapter.identifier, adapter.comparator);
if (origGet) {
adapter.get = function transformedGet (id) {
return untransformItem(origGet.call(adapter, id), transforms, transformedItemMap);
};
}
if (origAdd) {
adapter.add = function transformedAdd (item) {
return origAdd.call(adapter, transformItem(item, transforms, transformedItemMap));
};
}
if (origUpdate) {
adapter.update = function transformedUpdate (item) {
return origUpdate.call(adapter, transformItem(item, transforms, transformedItemMap));
};
}
if (origForEach) {
adapter.forEach = function transformedForEach (lambda) {
// Note: potential performance improvement if we cache the
// transformed lambdas in a hashmap.
function transformedLambda (item, key) {
var inverted = untransformItem(item, transforms, transformedItemMap);
return lambda(inverted, key);
}
return origForEach.call(adapter, transformedLambda);
};
}
}
return adapter;
}
return addPropertyTransforms;
function identity (val) { return val; }
function hasProperties (obj) {
for (var p in obj) return true;
}
function transformItem (item, transforms, map) {
var transformed, name, transform;
transformed = {};
// direct transforms
for (name in item) {
transform = transforms[name] || identity;
transformed[name] = transform(item[name], name, item);
}
// derived transforms
for (name in transforms) {
if (!(name in item)) {
transformed[name] = transforms[name](null, name, item);
}
}
// remove should be a noop if we don't already have it
map.remove(transformed);
map.add(transformed, item);
return transformed;
}
function untransformItem (transformed, transforms, map) {
var origItem, name, transform;
// get original item
origItem = map.get(transformed);
for (name in origItem) {
transform = transforms[name] && transforms[name].inverse;
if (transform) {
origItem[name] = transform(transformed[name], name, transformed);
}
}
return origItem;
}
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* wire/cola plugin
*
* wire is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*/
(function(define) {
define(['when', './relational/propertiesKey', './comparator/byProperty'],
function(when, propertiesKey, byProperty) {
var defaultComparator, defaultQuerySelector, defaultQuerySelectorAll,
defaultOn, excludeOptions;
defaultComparator = byProperty('id');
defaultQuerySelector = { $ref: 'dom.first!' };
defaultQuerySelectorAll = { $ref: 'dom.all!' };
defaultOn = { $ref: 'on!' };
function initBindOptions(incomingOptions, pluginOptions) {
var options, identifier, comparator;
options = copyOwnProps(incomingOptions, pluginOptions);
if(!options.querySelector) {
options.querySelector = defaultQuerySelector;
}
if(!options.querySelectorAll) {
options.querySelectorAll = defaultQuerySelectorAll;
}
if(!options.on) {
options.on = defaultOn;
}
// TODO: Extend syntax for identifier and comparator
// to allow more fields, and more complex expressions
identifier = options.identifier;
options.identifier = typeof identifier == 'string' || Array.isArray(identifier)
? propertiesKey(identifier)
: identifier;
comparator = options.comparator || defaultComparator;
options.comparator = typeof comparator == 'string'
? byProperty(comparator)
: comparator;
return options;
}
function doBind(facet, options, wire) {
var target = facet.target;
return when(wire(initBindOptions(facet.options, options)),
function(options) {
var to = options.to;
if (!to) throw new Error('wire/cola: "to" must be specified');
to.addSource(target, copyOwnProps(options));
return target;
}
);
}
/**
* We don't want to copy the module property from the plugin options, and
* wire adds the id property, so we need to filter that out too.
* @type {Object}
*/
excludeOptions = {
id: 1,
module: 1
};
return {
wire$plugin: function(ready, destroyed, pluginOptions) {
var options = {};
for(var p in pluginOptions) {
if(!(p in excludeOptions)) {
options[p] = pluginOptions[p];
}
}
function bindFacet(resolver, facet, wire) {
when.chain(doBind(facet, options, wire), resolver);
}
return {
facets: {
bind: {
ready: bindFacet
}
}
};
}
};
/**
* Copies own properties from each src object in the arguments list
* to a new object and returns it. Properties further to the right
* win.
*
* @return {Object} a new object with own properties from all srcs.
*/
function copyOwnProps(/*srcs...*/) {
var i, len, p, src, dst;
dst = {};
for(i = 0, len = arguments.length; i < len; i++) {
src = arguments[i];
if(src) {
for(p in src) {
if(src.hasOwnProperty(p)) {
dst[p] = src[p];
}
}
}
}
return dst;
}
});
})(typeof define == 'function'
// use define for AMD if available
? define
: function(deps, factory) { module.exports = factory.apply(this, deps.map(require)); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function (require) {
"use strict";
var naturalOrder = require('./naturalOrder');
return function(propName, comparator) {
if(!comparator) comparator = naturalOrder;
return function(a, b) {
return comparator(a[propName], b[propName]);
};
};
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
\ No newline at end of file
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*/
(function(define){
define(function() {
"use strict";
return function compose(comparators/*...*/) {
if(!arguments.length) throw new Error('comparator/compose: No comparators provided');
comparators = arguments;
return function(a, b) {
var result, len, i;
i = 0;
len = comparators.length;
do {
result = comparators[i](a, b);
} while(result === 0 && ++i < len);
return result;
};
};
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(); }
);
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function () {
"use strict";
return function(a, b) {
return a == b ? 0
: a < b ? -1
: 1;
};
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(); }
));
\ No newline at end of file
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*/
(function(define){
define(function() {
"use strict";
/**
* Creates a comparator function that compares items in the reverse
* order of the supplied comparator.
*
* @param comparator {Function} original comparator function to reverse
*/
return function reverse(comparator) {
if(typeof comparator != 'function') throw new Error('comparator/reverse: input comparator must be provided');
return function(a, b) {
return comparator(b, a);
};
};
});
})(
typeof define == 'function'
? define
: function(factory) { module.exports = factory(); }
);
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>Array to NodeListAdapter demo</title>
<script src="../test/curl/src/curl.js"></script>
<script type="text/javascript">
curl({
baseUrl: '../',
paths: {
curl: 'test/curl/src/curl'
},
packages: {
cola: { location: '.', main: 'cola' },
when: { location: 'test/when', main: 'when' }
}
});
curl(
[
'cola/mediator/syncCollections',
'cola/ArrayAdapter',
'cola/dom/NodeListAdapter',
'cola/dom/NodeAdapter',
'cola/ObjectAdapter',
'cola/AdapterResolver',
'when'
],
function (syncCollections, ArrayAdapter, NodeListAdapter, NodeAdapter, ObjectAdapter, AdapterResolver, when) {
"use strict";
var itemNode, data, sortDirection;
sortDirection = 1;
window.names = data = [
{ id: 3, first: 'Ian', last: 'Cavalier' },
{ id: 1, first: 'Brian', last: 'Cavalier' },
{ id: 2, first: 'John', last: 'Hann' },
{ id: 4, first: 'Scott', last: 'Andrews' },
{ id: 5, first: 'Ilia', last: 'Gilderman' }
];
function getNode (id) {
return document.getElementById(id);
}
function compareByLast (a, b) {
return a.last < b.last ? -sortDirection : a.last > b.last ? sortDirection : 0;
}
function compareByFirst (a, b) {
return a.first < b.first ? -sortDirection : a.first > b.first ? sortDirection : 0;
}
function compareByLastThenFirst (a, b) {
var byLast = compareByLast(a, b);
return byLast == 0 ? compareByFirst(a, b) : byLast;
}
function symbolizeById (o) { return o && o.id; }
function querySelector (selector, node) {
return node.querySelector(selector);
}
function createAdapter(obj, type, options) {
// FIXME: This is just for initial testing
var Adapter, adapter, propertyTransforms;
Adapter = AdapterResolver(obj, type);
// if (!Adapter) throw new Error('wire/cola: could not find Adapter constructor for ' + type);
adapter = Adapter ? new Adapter(obj, options) : obj;
// if (options.bindings && type == 'object') {
// propertyTransforms = createTransformers(options.bindings);
// if (propertyTransforms) {
// adapter = addPropertyTransforms(adapter, propertyTransforms);
// }
// }
return adapter;
}
function init () {
var source, dest;
// register adapter constructors
AdapterResolver.register(ArrayAdapter, 'collection');
AdapterResolver.register(NodeListAdapter, 'collection');
AdapterResolver.register(NodeAdapter, 'object');
AdapterResolver.register(ObjectAdapter, 'object');
// create adapters
source = new ArrayAdapter(data, {
comparator: compareByLast,
symbolizer: symbolizeById
});
dest = new NodeListAdapter(getNode('test'), {
comparator: compareByLastThenFirst,
symbolizer: symbolizeById,
querySelector: querySelector,
bindings: {
first: { node: '[name=first]', prop: 'value' },
last: { node: '[name=last]', prop: 'value' },
id: { node: 'label', events: 'click' }
}
});
console.log("START");
syncCollections(source, dest, createAdapter, { sync: true });
console.log("BEFORE");
when(source.add({ id: 6, first: 'bob', last: 'smith' }), function() {
dest.forEach(function(node, item) {
console.log(item.first);
});
});
console.log("AFTER");
window.printNames = function(e) {
dest.forEach(function(node, item) {
console.log(node, item);
});
};
window.reverseSort = function () {
sortDirection = -sortDirection;
dest.setComparator(dest.comparator);
};
}
curl('domReady!', init);
}
);
</script>
<style>
#test fieldset {
display: none;
}
#test.cola-list-bound fieldset {
display: block;
}
</style>
</head>
<body>
<form id="test">
<div>
Form header
</div>
<div>
<fieldset data-cola-role="item-template">
<label></label>
<input name="first"/>
<input name="last"/>
</fieldset>
</div>
<button type="button" onclick="reverseSort();">Reverse</button>
</form>
</body>
</html>
\ No newline at end of file
/** MIT License (c) copyright B Cavalier & J Hann */
/**
* Stores a watchable interface of collection. This is the interface
* that all collection/list adapters must implement.
*
* @constructor
*
* @param collection {Object}
*/
function ICollectionAdapter (collection) {
}
ICollectionAdapter.prototype = {
/**
* Compares two data items. Works just like the comparator function
* for Array.prototype.sort.
* @memberOf ICollectionAdapter
*
* @param a {Object}
* @param b {Object}
*
* @returns {Number} -1, 0, 1
*
* @description This comparator is used for two purposes:
* 1. to sort the items in the list (sequence)
* 2. to find an item in the list (identity)
* This property is undefined by default and should be injected.
* If not supplied, the mediator will supply one from another source.
* If the comparator tests for identity, it must indicate this via
* a truthy "identity" property on the function. Example:
* function compareWidgets (w1, w2) {
* if (w1.id == w2.id) return 0;
* else return w1.id - w2.id;
* }
* compareWidgets.identity = true;
* myWidgetCollectionAdapter.comparator = compareWidgets;
*/
comparator: undefined,
/**
* Uniquely names a data item. This isn't a key generator,
* it extracts a unique string representation from an object.
* @memberOf ICollectionAdapter
*
* @param object {Object}
*
* @returns {String}
*/
identifier: undefined,
/**
* Indicates that a new item should be added to the collection.
* @memberOf ICollectionAdapter
*
* @param item {Object}
*
* @description This function will only work if the collection has a
* identity function.
*/
add: function (item) {},
/**
* Indicates that an item in the collection should be removed.
* @memberOf ICollectionAdapter
*
* @param item {Object}
*
* @description This function will only work if the collection has a
* identity function.
*/
remove: function (item) {},
/**
* Indicates that an item in the collection should be updated. If the
* item doesn't already exist in the collection, it should be added.
* @memberOf ICollectionAdapter
*
* @param item {Object}
*
* @description This function will only work if the collection has a
* identity function.
*/
update: function (item) {},
/**
* Iterates over all of the items in the collection and runs the
* lambda functionfor each one.
* @param lambda {Function} function (item) { }
*/
forEach: function (lambda) {},
/**
* Optional method to get the options information that
* were provided to the adapter
* @returns {Object}
*/
getOptions: function () {
}
};
/**
* Tests an object to determine if it has an interface sufficient
* for use with this adapter.
* @memberOf ICollectionAdapter
* @static
*
* @param object {Object}
*
* @returns {Boolean}
*/
ICollectionAdapter.canHandle = function (object) {};
/** MIT License (c) copyright B Cavalier & J Hann */
/**
* Creates a cola adapter for interacting with a single object.
* @constructor
* @param object {Object}
*/
function IObjectAdapter (object) {}
IObjectAdapter.prototype = {
/**
* Gets the options information that
* were provided to the adapter.
* @returns {Object}
*/
getOptions: function () {
},
/**
* Signals that one or more of the properties has changed.
* @param item {Object} the newly updated item
*/
update: function (item) {}
};
/**
* Tests whether the given object is a candidate to be handled by
* this adapter.
* @param obj
* @returns {Boolean}
*/
IObjectAdapter.canHandle = function (obj) {};
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function (require) {
"use strict";
var bindingHandler;
bindingHandler = require('../bindingHandler');
/**
* Creates a cola adapter for interacting with dom nodes. Be sure to
* unwatch any watches to prevent memory leaks in Internet Explorer 6-8.
* @constructor
* @param rootNode {Node}
* @param options {Object}
*/
function NodeAdapter (rootNode, options) {
this._rootNode = rootNode;
// set options
if (!options.bindings) options.bindings = {};
this._options = options;
this._handlers = {};
this._createItemToDomHandlers();
}
NodeAdapter.prototype = {
getOptions: function () {
return this._options;
},
set: function (item) {
this._item = item;
this._itemToDom(item, this._handlers);
},
update: function (item) {
this._item = item;
this._itemToDom(item, item);
},
destroy: function () {
this._handlers.forEach(function (handler) {
if (handler.unlisten) handler.unlisten();
});
},
_itemToDom: function (item, hash) {
var p, handler;
for (p in hash) {
handler = this._handlers[p];
if (handler) handler(item);
}
},
_createItemToDomHandlers: function () {
var bindings, creator;
bindings = this._options.bindings;
creator = bindingHandler(this._rootNode, this._options);
Object.keys(bindings).forEach(function (b) {
this._handlers[b] = creator(bindings[b], b);
}, this);
}
};
/**
* Tests whether the given object is a candidate to be handled by
* this adapter. Returns true if this is a DOMNode (or looks like one).
* @param obj
* @returns {Boolean}
*/
NodeAdapter.canHandle = function (obj) {
// crude test if an object is a node.
return obj && obj.tagName && obj.getAttribute && obj.setAttribute;
};
return NodeAdapter;
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
\ No newline at end of file
This diff is collapsed.
(function (define) {
define(function (require, exports) {
// TODO: use has() to select code to use node.classList / DOMSettableTokenList
var splitClassNameRx = /\s+/;
/**
* Returns the list of class names on a node as an array.
* @param node {HTMLElement}
* @returns {Array}
*/
function getClassList (node) {
return node.className.split(splitClassNameRx);
}
/**
* Adds a list of class names on a node and optionally removes some.
* @param node {HTMLElement}
* @param list {Array|Object} a list of class names to add.
* @param [list.add] {Array} a list of class names to add.
* @param [list.remove] {Array} a list of class names to remove.
* @returns {Array} the resulting class names on the node
*
* @description The list param may be supplied with any of the following:
* simple array:
* setClassList(node, ['foo-box', 'bar-box']) (all added)
* simple array w/ remove property:
* list = ['foo-box', 'bar-box'];
* list.remove = ['baz-box'];
* setClassList(node, list);
* object with add and remove array properties:
* list = {
* add: ['foo-box', 'bar-box'],
* remove: ['baz-box']
* };
* setClassList(node, list);
*/
function setClassList (node, list) {
var adds, removes;
if (list) {
// figure out what to add and remove
adds = list.add || list || [];
removes = list.remove || [];
node.className = spliceClassNames(node.className, removes, adds);
}
return getClassList(node);
}
function getClassSet (node) {
var set, classNames, className;
set = {};
classNames = node.className.split(splitClassNameRx);
while ((className = classNames.pop())) set[className] = true;
return set;
}
/**
*
* @param node
* @param classSet {Object}
* @description
* Example bindings:
* stepsCompleted: {
* node: 'viewNode',
* prop: 'classList',
* enumSet: ['one', 'two', 'three']
* },
* permissions: {
* node: 'myview',
* prop: 'classList',
* enumSet: {
* modify: 'can-edit-data',
* create: 'can-add-data',
* remove: 'can-delete-data'
* }
* }
*/
function setClassSet (node, classSet) {
var removes, adds, p, newList;
removes = [];
adds = [];
for (p in classSet) {
if (p) {
if (classSet[p]) {
adds.push(p);
}
else {
removes.push(p);
}
}
}
return node.className = spliceClassNames(node.className, removes, adds);
}
// class parsing
var openRx, closeRx, innerRx, innerSpacesRx, outerSpacesRx;
openRx = '(?:\\b\\s+|^\\s*)(';
closeRx = ')(?:\\b(?!-))|(?:\\s*)$';
innerRx = '|';
innerSpacesRx = /\b\s+\b/;
outerSpacesRx = /^\s+|\s+$/;
/**
* Adds and removes class names to a string.
* @private
* @param className {String} current className
* @param removes {Array} class names to remove
* @param adds {Array} class names to add
* @returns {String} modified className
*/
function spliceClassNames (className, removes, adds) {
var rx, leftovers;
// create regex to find all removes *and adds* since we're going to
// remove them all to prevent duplicates.
removes = trim(removes.concat(adds).join(' '));
adds = trim(adds.join(' '));
rx = new RegExp(openRx
+ removes.replace(innerSpacesRx, innerRx)
+ closeRx, 'g');
// remove and add
return trim(className.replace(rx, function (m) {
// if nothing matched, we're at the end
return !m && adds ? ' ' + adds : '';
}));
}
function trim (str) {
// don't worry about high-unicode spaces. they should never be here.
return str.replace(outerSpacesRx, '');
}
return {
getClassList: getClassList,
setClassList: setClassList,
getClassSet: getClassSet,
setClassSet: setClassSet
};
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));
This diff is collapsed.
(function (define) {
define(function () {
"use strict";
var formNodeRx = /^form$/i;
/**
* Simple function to find a form element.
* @param rootNode {HTMLElement} form node to search under
* @param nodeName {String} form element to find
* @return {HTMLElement}
*/
return function formElementFinder (rootNode, nodeName) {
// use form.elements if this is a form
if (rootNode.elements && rootNode.elements.length) {
return rootNode.elements[nodeName];
}
};
});
}(
typeof define == 'function' && define.amd
? define
: function (factory) { module.exports = factory(); }
));
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment