/**
* famous-angular - Integrate Famo.us into AngularJS apps and build Famo.us apps using AngularJS tools
* @version v0.0.12
* @link https://github.com/Famous/famous-angular
* @license
*/
'use strict';
// Put angular bootstrap on hold
window.name = "NG_DEFER_BOOTSTRAP!" + window.name;
//TODO: Ensure that this list stays up-to-date with
// the filesystem (maybe write a bash script
// working around `ls -R1 app/scripts/famous` ?)
var requirements = [
"famous/core/Engine",
"famous/core/EventEmitter",
"famous/core/EventHandler",
"famous/core/Modifier",
"famous/core/RenderNode",
"famous/core/Surface",
"famous/core/Transform",
"famous/core/View",
"famous/core/ViewSequence",
"famous/events/EventArbiter",
"famous/events/EventFilter",
"famous/events/EventMapper",
"famous/inputs/FastClick",
"famous/inputs/GenericSync",
"famous/inputs/MouseSync",
"famous/inputs/PinchSync",
"famous/inputs/RotateSync",
"famous/inputs/TouchSync",
"famous/surfaces/ImageSurface",
"famous/surfaces/InputSurface",
"famous/transitions/Easing",
"famous/transitions/SpringTransition",
"famous/transitions/Transitionable",
"famous/transitions/TransitionableTransform",
"famous/utilities/KeyCodes",
"famous/utilities/Timer",
"famous/views/GridLayout",
"famous/views/RenderController",
"famous/views/Scroller",
"famous/views/Scrollview"
]
//declare the module before the async callback so that
//it will be accessible to other synchronously loaded angular
//components
var ngFameApp = angular.module('famous.angular', []);
require(requirements, function(/*args*/) {
//capture 'arguments' in a variable that will exist in
//child scopes
var required = arguments;
/**
* @ngdoc provider
* @name $famousProvider
* @module famous.angular
* @description
* This provider is loaded as an AMD module and will keep a reference on the complete Famo.us library.
* We use this provider to avoid needing to deal with AMD on any other angular files.
*
* @usage
* You probably won't have to configure this provider
*
* ```js
* angular.module('mySuperApp', ['famous.angular']).config(
* function($famousProvider) {
*
* // Register your modules
* $famousProvider.registerModule('moduleKey', module);
*
* };
* });
* ```
*
*/
ngFameApp.provider('$famous', function() {
// hash for storing modules
var _modules = {};
/**
* @ngdoc method
* @name $famousProvider#registerModule
* @module famous.angular
* @description
* Register the modules that will be available in the $famous service
*
* @param {String} key the key that will be used to register the module
* @param {Misc} module the data that will be returned by the service
*/
this.registerModule = function(key, module) {
//TODO warning if the key is already registered ?
_modules[key] = module;
};
/**
* @ngdoc method
* @name $famousProvider#find
* @module famous.angular
* @description given a selector, retrieves
* the isolate on a template-declared scene graph element. This is useful
* for manipulating Famo.us objects directly after they've been declared in the DOM.
* As in normal Angular, this DOM look-up should be performed in the postLink function
* of a directive.
* @returns {Array} an array of the isolate objects of the selected elements.
*
* @param {String} selector - the selector for the elements to look up
* @usage
* View:
* ```html
*
* ```
* Controller:
* ```javascript
* var scrollViewReference = $famous.find('#myScrollView')[0].renderNode;
* //Now scrollViewReference is pointing to the Famo.us Scrollview object
* //that we created in the view.
* ```
*/
_modules.find = function(selector){
var elems = angular.element(window.document.querySelector(selector));
var scopes = function(elems) {
var _s = [];
angular.forEach(elems, function(elem, i) {
_s[i] = angular.element(elem).scope();
});
return _s;
}(elems);
var isolates = function(scopes) {
var _s = [];
angular.forEach(scopes, function(scope, i) {
_s[i] = scope.isolate[scope.$id];
});
return _s;
}(scopes);
return isolates;
}
this.$get = function() {
/**
* @ngdoc service
* @name $famous
* @module famous.angular
* @description
* This service gives you access to the complete Famo.us library.
*
* @usage
* Use this service to access the registered Famo.us modules as an object.
*
* ```js
* angular.module('mySuperApp', ['famous.angular']).controller(
* function($scope, $famous) {
*
* // Access any registered module
* var EventHandler = $famous['famous/core/EventHandler'];
* $scope.eventHandler = new EventHandler();
*
* };
* });
* ```
*
*/
return _modules;
};
});
ngFameApp.config(['$famousProvider', function($famousProvider) {
for(var i = 0; i < requirements.length; i++) {
$famousProvider.registerModule(requirements[i], required[i]);
}
// console.log('registered modules', famousProvider.$get());
}]);
angular.element(document).ready(function() {
if(angular.resumeBootstrap) angular.resumeBootstrap();
});
});
/**
* @ngdoc service
* @name $famousDecorator
* @module famous.angular
* @description
* Manages the creation and handling of isolate scopes.
*
* Isolate scopes are like a namespacing inside plain Angular child scopes,
* with the purpose of storing properties available only to one particular
* scope.
* The scopes are still able to communicate with the parent via events
* ($emit, $broadcast), yet still have their own $scope properties that will
* not conflict with the parent or other siblings.
*
*/
angular.module('famous.angular')
.factory('$famousDecorator', function () {
//TODO: add repeated logic to these roles
var _roles = {
child: {
},
parent: {
}
}
return {
//TODO: patch into _roles and assign the
// appropriate role to the given scope
addRole: function(role, scope){
},
/**
* @ngdoc method
* @name $famousDecorator#ensureIsolate
* @module famous.angular
* @description
* Checks the passed in scope for an existing isolate property. If
* scope.isolate does not already exist, create it.
*
* If the scope is being used in conjunction with an ng-repeat, assign
* the default ng-repeat index onto the scope.
*
* @returns {Object} the isolated scope object from scope.isolate
*
* @param {String} scope - the scope to ensure that the isolate property
* exists on
*
* @usage
*
* ```js
* var isolate = $famousDecorator.ensureIsolate($scope);
* ```
*/
ensureIsolate: function(scope){
scope.isolate = scope.isolate || {};
scope.isolate[scope.$id] = scope.isolate[scope.$id] || {};
//assign the scope $id to the isolate
var isolate = scope.isolate[scope.$id];
isolate.id = scope.$id;
//assign default ng-repeat index if it exists
//and index isn't already assigned
var i = scope.$eval("$index");
if(i && i !== '$index' && !isolate.index) isolate.index = i;
return isolate;
}
};
});
/**
* @ngdoc directive
* @name faAnimation
* @module famous.angular
* @restrict EA
* @description
* This directive is used to animate an element in conjunction with an {@link api/directive/animate animate} directive
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faAnimation', ['$famous', '$famousDecorator', function ($famous, famousDecorator) {
return {
restrict: 'EA',
scope: true,
compile: function(tElement, tAttrs, transclude){
var Transform = $famous['famous/core/Transform'];
var Transitionable = $famous['famous/transitions/Transitionable'];
var Easing = $famous['famous/transitions/Easing'];
return {
pre: function(scope, element, attrs){
var isolate = famousDecorator.ensureIsolate(scope);
},
post: function(scope, element, attrs){
var isolate = famousDecorator.ensureIsolate(scope);
setTimeout(function(){
isolate.timeline = scope.$eval(attrs.timeline);
isolate._trans = new Transitionable(0);
isolate.play = function(callback){
var transition = {
duration: scope.$eval(attrs.duration),
curve: scope.$eval(attrs.curve) || 'linear'
};
isolate._trans.set(1, transition, function(){
if(callback)
callback();
if(attrs.loop){
//Famo.us silently breaks its transitionable if this runs in
//the same execution context. Maybe a suppressed SO error somewhere?
setTimeout(function(){isolate.replay(callback)}, 0);
}
});
//TODO: handle $animate with a callback
}
isolate.reset = function(){
isolate._trans.set(0);
}
isolate.replay = function(callback){
isolate.reset();
isolate.play(callback);
}
//disengage is a function that
//can unassign the event listener
var _disengage = undefined;
if(attrs.event){
if(_disengage)
_disengage();
_disengage = scope.$on(attrs.event, function(evt, data){
var callback = data && data.callback ? data.callback : undefined;
isolate.replay(callback)
})
}
var id = attrs.id;
if(isolate.timeline === undefined){
isolate.timeline = isolate._trans.get.bind(isolate._trans);
if(attrs.autoplay)
isolate.play();
}
if(!isolate.timeline instanceof Function)
throw 'timeline must be a reference to a function or duration must be provided';
/**
* @ngdoc directive
* @name animate
* @module famous.angular
* @restrict E
* @description
* This directive is used to specify the animation of an element in a {@link api/directive/faAnimation faAnimation} directive
*
* @usage
* ```html
*
*
*
* ```
*/
var animates = element.find('animate');
var declarations = {};
for(var i = 0; i < animates.length; i++){
(function(){
var animate = animates[i];
//DOM selector string that points to our mod of interest
if(animate.attributes['targetmodselector']){
//dig out the reference to our modifier
//TODO: support passing a direct reference to a modifier
// instead of performing a DOM lookup
var modElements = angular.element(element[0].parentNode)[0].querySelectorAll(animate.attributes['targetmodselector'].value);
angular.forEach(modElements, function(modElement){
var modScope = angular.element(modElement).scope();
var modifier = modScope.isolate[modScope.$id].modifier;
var getTransform = modScope.isolate[modScope.$id].getTransform;
//TODO: won't need to special-case curve type 'linear'
// once/if it exists in Easing.js
var curve =
animate.attributes['curve'] &&
animate.attributes['curve'].value !== 'linear'
? Easing[animate.attributes['curve'].value]
: function(j) {return j;}; //linear
//assign the modifier functions
if(animate.attributes['field']){
var field = animate.attributes['field'].value;
var lowerBound =
animate.attributes['timelinelowerbound']
? parseFloat(animate.attributes['timelinelowerbound'].value)
: 0;
var upperBound =
animate.attributes['timelineupperbound']
? parseFloat(animate.attributes['timelineupperbound'].value)
: 1;
if(!animate.attributes['startvalue'])
throw 'you must provide a start value for the animation'
var startValue = scope.$eval(animate.attributes['startvalue'].value);
if(!animate.attributes['endvalue'])
throw 'you must provide an end value for the animation'
var endValue = scope.$eval(animate.attributes['endValue'].value);
//Keep arrays of all declarations so that transformFunctions
//can handle all of the appropriate segments
var modDecs =
declarations[modScope.$id] =
declarations[modScope.$id] || {};
var segments = modDecs[field] = modDecs[field] || [];
segments.push({
field: field,
lowerBound: lowerBound,
upperBound: upperBound,
startValue: startValue,
endValue: endValue,
curve: curve
});
//Keep modDecs[field] sorted
segments.sort(function(a, b){
return a.lowerBound - b.lowerBound;
});
//Check domain overlap:
//after sorting by lowerBounds, if any segment's lower bound
//is lower than the lower bound of any item before it, domains are
//overlapping
for(var j = 1; j < segments.length; j++){
var lower = segments[j].lowerBound;
for(var k = 0; k < j; k++){
if(lower < segments[k].upperBound){
throw "Animate segments have overlapping \
domains for the same field (" + field + "). \
At any point in the timeline, only one \
can affect a given field on the same modifier."
}
}
}
//Domain: timeline function bounded [0,1]
//Subdomains (between pipes): specified subdomains of timeline segments
//Range: output value, determined by interpolating startValue and
// endValue through the easing curves.
// | | | |
// | | | |
// | | | |
// | | | |
// | (ease) | | (ease) |
// | -/|-----------------------|-\ |
// | -/ | | -\ |
// | -/ | | -\ |
// | -/ | | -\ |
// ----|-/ | | -\|-------
// | | | |
//_____|__________|_______________________|__________|_______
// |x(0,0) |x(0,1) |x(1,0) |x(1,1)
//TODO: in order to support nested fa-animation directives,
// this function needs to be exposed somehow. (pass a reference into the directive;
// and then assign this function to that reference?)
//TODO: if needed: make this more efficient. This is a hot-running
// function and we should be able to optimize.
var transformFunction = function(){
var x = isolate.timeline() || 0;
var relevantIndex = 0;
var relevantSegment = segments[relevantIndex];
for(var j = 0; j < segments.length; j++){
//this is the relevant segment if x is in the subdomain
if(x >= segments[j].lowerBound && x <= segments[j].upperBound){
relevantSegment = segments[j];
break;
}
//this is the relevant segment if it is the last one
if(j === segments.length - 1){
relevantSegment = segments[j];
break;
}
//this is the relevant segment if x is greater than its upper
//bound but less than the next segment's lower bound
if(x >= segments[j].upperBound && x < segments[j + 1].lowerBound){
relevantSegment = segments[j];
break;
}
}
if(x <= relevantSegment.lowerBound)
return relevantSegment.startValue;
if(x >= relevantSegment.upperBound)
return relevantSegment.endValue;
//normalize our domain to [0, 1]
var subDomain = (relevantSegment.upperBound - relevantSegment.lowerBound)
var normalizedX = (x - relevantSegment.lowerBound) / subDomain;
//Support interpolating multiple values, e.g. for a Scale array [x,y,z]
if(Array.isArray(relevantSegment.startValue)){
var ret = [];
for(var j = 0; j < relevantSegment.startValue.length; j++){
ret.push(
relevantSegment.startValue[j] + relevantSegment.curve(normalizedX)
*
(relevantSegment.endValue[j] - relevantSegment.startValue[j])
);
}
return ret;
}else{
return relevantSegment.startValue
+ relevantSegment.curve(normalizedX)
* (relevantSegment.endValue
- relevantSegment.startValue);
}
};
var transformComponents = modDecs.transformComponents = modDecs.transformComponents || [];
if(field === 'opacity'){
modifier.opacityFrom(function(){
return transformFunction();
});
}else if (field === 'origin'){
modifier.originFrom(function(){
return transformFunction();
});
}else if (field === 'size'){
modifier.sizeFrom(function(){
return transformFunction();
});
}else{ //transform field
transformComponents.push({
field: field,
fn: transformFunction
})
modifier.transformFrom(function(){
var mult = getTransform && getTransform() ? [getTransform()] : [];
for(var j = 0; j < transformComponents.length; j++){
((function(){
var transVal = transformComponents[j].fn();
var f = transformComponents[j].field;
if(Array.isArray(transVal))
mult.push(Transform[f].apply(this, transVal));
else
mult.push(Transform[f](transVal));
})());
}
//Transform.multiply fails on arrays of <=1 matricies
if(mult.length === 1)
return mult[0]
else
return Transform.multiply.apply(this, mult);
});
}
}
});
}
})();
}
}, 1)//end setTimeout
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faApp
* @module famous.angular
* @restrict EA
* @description
* This directive is the container and entry point to Famo.us/Angular. Behind the scenes,
* it creates a Famous context and then adds child elements
* to that context as they get compiled. Inside of this directive,
* normal HTML content will not get rendered to the screen unless
* it is inside of a {@link api/directive/faSurface fa-surface} directive.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faApp', ["$famous", "$famousDecorator", function ($famous, $famousDecorator) {
return {
template: '',
transclude: true,
scope: true,
restrict: 'EA',
compile: function(tElement, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var View = $famous['famous/core/View'];
var Engine = $famous['famous/core/Engine'];
var Transform = $famous['famous/core/Transform']
element.append('');
isolate.context = Engine.createContext(element[0].querySelector('.famous-angular-container'));
function AppView(){
View.apply(this, arguments);
}
AppView.prototype = Object.create(View.prototype);
AppView.prototype.constructor = AppView;
var getOrValue = function(x) {
return x.get ? x.get() : x;
};
var getTransform = function(data) {
var transforms = [];
if (data.mod().translate && data.mod().translate.length) {
var values = data.mod().translate.map(getOrValue)
transforms.push(Transform.translate.apply(this, values));
}
if (scope["faRotateZ"])
transforms.push(Transform.rotateZ(scope["faRotateZ"]));
if (scope["faSkew"])
transforms.push(Transform.skew(0, 0, scope["faSkew"]));
return Transform.multiply.apply(this, transforms);
};
isolate.view = new AppView();
isolate.context.add(isolate.view);
//HACK: Since Famo.us Engine doesn't yet
//support unregistering contexts, this will keep
//the context from getting updated by the engine
scope.$on('$destroy', function(){
isolate.context.update = angular.noop;
})
//TODO: What if the actual scope hierarchy
//were angular $watched instead of using eventing?
//Could write a function that traverses angular's scopes
//and returns a hash-like
//representation of render-node-containing $scopes
//(via their isolate objects.) Then, tweak the scene
//graph as needed when it sees changes.
//This would make e.g. reflowing elements in a scrollview
//more elegant than the current approach, but would
//require a bit of replumbing. Would need to investigate
//the overhead of $watching a potentially complex scene graph, too
scope.$on('registerChild', function(evt, data){
isolate.view.add(data.renderNode);
evt.stopPropagation();
})
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
transclude(scope, function(clone) {
angular.element(element[0].querySelectorAll('div div')[0]).append(clone);
});
isolate.readyToRender = true;
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faClick
* @module famous.angular
* @restrict A
* @param {expression} faClick {@link https://docs.angularjs.org/guide/expression Expression} to evaluate upon
* click. ({@link https://docs.angularjs.org/guide/expression#-event- Event object is available as `$event`})
* @description
* This directive allows you to specify custom behavior when an element is clicked.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faClick', ["$parse", "$famousDecorator",function ($parse, $famousDecorator) {
return {
restrict: 'A',
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
if (attrs.faClick) {
var renderNode = (isolate.renderNode._eventInput || isolate.renderNode)
renderNode.on("click", function(data) {
var fn = $parse(attrs.faClick);
fn(scope, {$event:data});
if(!scope.$$phase)
scope.$apply();
});
}
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faGridLayout
* @module famous.angular
* @restrict EA
* @description
* This directive will create a Famo.us GridLayout containing the
* specified child elements. The provided `options` object
* will pass directly through to the Famo.us GridLayout's
* constructor. See [https://famo.us/docs/0.1.1/views/GridLayout/]
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faGridLayout', ["$famous", "$famousDecorator", function ($famous, $famousDecorator) {
return {
template: '',
restrict: 'E',
transclude: true,
scope: true,
compile: function(tElem, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var GridLayout = $famous["famous/views/GridLayout"];
var ViewSequence = $famous['famous/core/ViewSequence'];
var _children = [];
var options = scope.$eval(attrs.faOptions) || {};
isolate.renderNode = new GridLayout(options);
var updateGridLayout = function(){
_children.sort(function(a, b){
return a.index - b.index;
});
isolate.renderNode.sequenceFrom(function(_children) {
var _ch = [];
angular.forEach(_children, function(c, i) {
_ch[i] = c.renderNode;
})
return _ch;
}(_children));
}
scope.$on('registerChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
_children.push(data);
updateGridLayout();
evt.stopPropagation();
};
});
scope.$on('unregisterChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
_children = function(_children) {
var _ch = [];
angular.forEach(_children, function(c) {
if(c.id !== data.id) {
_ch.push(c);
}
});
return _ch;
}(_children);
updateGridLayout();
evt.stopPropagation();
}
})
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
transclude(scope, function(clone) {
element.find('div').append(clone);
});
scope.$emit('registerChild', isolate);
}
};
}
};
}]);
/**
* @ngdoc directive
* @name faImageSurface
* @module famous.angular
* @restrict EA
* @param {String} faImageUrl - String url pointing to the image that should be loaded into the Famo.us ImageSurface
* @description
* This directive creates a Famo.us ImageSurface and loads
* the specified ImageUrl.
* @usage
* ```html
*
*
* ```
*/
angular.module('famous.angular')
.directive('faImageSurface', ["$famous", "$famousDecorator", function ($famous, $famousDecorator) {
return {
scope: true,
template: '',
restrict: 'EA',
compile: function(tElem, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var ImageSurface = $famous['famous/surfaces/ImageSurface'];
var Transform = $famous['famous/core/Transform']
var EventHandler = $famous['famous/core/EventHandler'];
//update properties
//TODO: is this going to be a bottleneck?
scope.$watch(
function(){
return isolate.getProperties()
},
function(){
if(isolate.renderNode)
isolate.renderNode.setProperties(isolate.getProperties());
},
true
)
isolate.getProperties = function(){
return {
backgroundColor: scope.$eval(attrs.faBackgroundColor),
color: scope.$eval(attrs.faColor)
};
};
var getOrValue = function(x) {
return x.get ? x.get() : x;
};
isolate.renderNode = new ImageSurface({
size: scope.$eval(attrs.faSize),
class: scope.$eval(attrs.class),
properties: isolate.getProperties()
});
//TODO: support ng-class
if(attrs.class)
isolate.renderNode.setClasses(attrs['class'].split(' '));
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var updateContent = function(){
isolate.renderNode.setContent(attrs.faImageUrl)
};
updateContent();
attrs.$observe('faImageUrl', updateContent);
scope.$emit('registerChild', isolate);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faIndex
* @module famous.angular
* @restrict A
* @description
* This directive is used to specify the rendering order of elements
* inside of a ViewSequence-based component, such as @link api/directive/faScrollView faScrollView}
* or @link api/directive/faGridLayout faGridLayout}. As a special case, when elements are added to
* these controls using ng-repeat, they are automatically assigned the
* $index property exposed by ng-repeat. When adding elements manually
* (e.g. to a faScrollView but not using ng-repeat) or in a case where custom
* order is desired, then the index value must be assigned/overridden using the faIndex directive.
* @usage
* ```html
*
* Surface 1
* Surface 2
*
* ```
*/
angular.module('famous.angular')
.directive('faIndex', ["$parse", "$famousDecorator", function ($parse, $famousDecorator) {
return {
restrict: 'A',
scope: false,
priority: 16,
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
isolate.index = scope.$eval(attrs.faIndex);
scope.$watch(function(){
return scope.$eval(attrs.faIndex)
}, function(){
isolate.index = scope.$eval(attrs.faIndex)
});
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faModifier
* @module famous.angular
* @restrict EA
* @param {Array|Function} faRotate - Array of numbers or function returning an array of numbers to which this Modifier's rotate should be bound.
* @param {Number|Function} faRotateX - Number or function returning a number to which this Modifier's rotateX should be bound
* @param {Number|Function} faRotateY - Number or function returning a number to which this Modifier's rotateY should be bound
* @param {Number|Function} faRotateZ - Number or function returning a number to which this Modifier's rotateZ should be bound
* @param {Array|Function} faScale - Array of numbers or function returning an array of numbers to which this Modifier's scale should be bound
* @param {Array|Function} faSkew - Array of numbers or function returning an array of numbers to which this Modifier's skew should be bound
* @param {Array|Function} faAboutOrigin - Array of arguments (or a function returning an array of arguments) to pass to Transform.aboutOrigin
* @param {Number|Function} faPerspective - Number or array returning a number to which this modifier's perspective (focusZ) should be bound.
* @param {Transform} faTransform - Manually created Famo.us Transform object (an array) that can be passed to the modifier. *Will override all other transform attributes.*
* @param {Number|Function|Transitionable} faOpacity - Number or function returning a number to which this Modifier's opacity should be bound
* @param {Array|Function|Transitionable} faSize - Array of numbers (e.g. [100, 500] for the x- and y-sizes) or function returning an array of numbers to which this Modifier's size should be bound
* @param {Array|Function|Transitionable} faOrigin - Array of numbers (e.g. [.5, 0] for the x- and y-origins) or function returning an array of numbers to which this Modifier's origin should be bound
* @param {Array|Function|Transitionable} faAlign - Array of numbers (e.g. [.5, 0] for the x- and y-aligns) or function returning an array of numbers to which this Modifier's align should be bound
* @param {Array.String} faTransformOrder - Optional array of strings to specify which transforms to apply and in which order. (e.g. `fa-transform-order="['rotateZ', 'translate', 'scale']"`) Default behavior is to evaluate all supported transforms and apply them in alphabetical order.
* @description
* This directive creates a Famo.us Modifier that will affect all children render nodes. Its properties can be bound
* to values (e.g. `fa-translate="[15, 20, 1]"`, Famo.us Transitionable objects, or to functions that return numbers.
* @usage
* ```html
*
*
* I'm translucent, skewed, rotated, and translated
*
* ```
*/
angular.module('famous.angular')
.directive('faModifier', ["$famous", "$famousDecorator", "$parse", function ($famous, $famousDecorator, $parse) {
return {
template: '',
transclude: true,
restrict: 'EA',
priority: 2,
scope: true,
compile: function(tElement, tAttrs, transclude){
return {
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var RenderNode = $famous['famous/core/RenderNode']
var Modifier = $famous['famous/core/Modifier']
var Transform = $famous['famous/core/Transform']
var get = function(x) {
if (x instanceof Function) return x();
return x.get ? x.get() : x;
};
//TODO: make a stand-alone window-level utility
// object to store stuff like this
/* Copied from angular.js */
var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
var MOZ_HACK_REGEXP = /^moz([A-Z])/;
function camelCase(name) {
return name.
replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
}).
replace(MOZ_HACK_REGEXP, 'Moz$1');
}
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
function directiveNormalize(name) {
return camelCase(name.replace(PREFIX_REGEXP, ''));
}
/* end copy from angular.js */
var _transformFields = [
"aboutOrigin",
"perspective",
"rotate",
"rotateAxis",
"rotateX",
"rotateY",
"rotateZ",
"scale",
"skew",
"translate"
];
attrs.$observe('faTransformOrder', function(){
var candidate = scope.$eval(attrs.faTransformOrder);
if(candidate !== undefined) _transformFields = candidate;
});
var _parsedTransforms = {};
angular.forEach(_transformFields, function(field){
var attrName = directiveNormalize('fa-' + field);
attrs.$observe(attrName, function(){
_parsedTransforms[field] = $parse(attrs[attrName]);
})
})
var _transformFn = angular.noop;
attrs.$observe('faTransform', function(){
_transformFn = $parse(attrs.faTransform);
});
isolate.getTransform = function() {
//if faTransform is provided, return it
//instead of looping through the other transforms.
var override = _transformFn(scope);
if(override !== undefined){
if(override instanceof Function) return override();
else if(override instanceof Object && override.get !== undefined) return override.get();
else return override;
}
var transforms = [];
angular.forEach(_transformFields, function(field){
var candidate = _parsedTransforms[field] ? _parsedTransforms[field](scope) : undefined;
if(candidate !== undefined){
if(candidate instanceof Function) transforms.push(candidate())
else if(candidate instanceof Array) transforms.push(Transform[field].apply(this, candidate))
else transforms.push(Transform[field].call(this, candidate));
}
});
if(!transforms.length) return undefined;
else if (transforms.length === 1) return transforms[0]
else return Transform.multiply.apply(this, transforms);
};
var _alignFn = angular.noop;
attrs.$observe('faAlign', function(){
_alignFn = $parse(attrs.faAlign);
});
isolate.getAlign = function(){
var ret = _alignFn(scope);
if(ret instanceof Function) return ret();
else if(ret instanceof Object && ret.get !== undefined) return ret.get();
else return ret;
}
var _opacityFn = angular.noop;
attrs.$observe('faOpacity', function(){
_opacityFn = $parse(attrs.faOpacity);
});
isolate.getOpacity = function(){
var ret = _opacityFn(scope);
if(ret === undefined) return 1;
else if(ret instanceof Function) return ret();
else if(ret instanceof Object && ret.get !== undefined) return ret.get();
else return ret;
}
var _sizeFn = angular.noop;
attrs.$observe('faSize', function(){
_sizeFn = $parse(attrs.faSize);
});
isolate.getSize = function(){
var ret = _sizeFn(scope);
if(ret instanceof Function) return ret();
else if(ret instanceof Object && ret.get !== undefined) return ret.get();
else return ret;
}
var _originFn = angular.noop;
attrs.$observe('faOrigin', function(){
_originFn = $parse(attrs.faOrigin);
});
isolate.getOrigin = function(){
var ret = _originFn(scope);
if(ret instanceof Function) return ret();
else if(ret instanceof Object && ret.get !== undefined) return ret.get();
else return ret;
}
isolate.modifier = new Modifier({
transform: isolate.getTransform,
size: isolate.getSize,
opacity: isolate.getOpacity,
origin: isolate.getOrigin,
align: isolate.getAlign
});
isolate.renderNode = new RenderNode().add(isolate.modifier)
scope.$on('$destroy', function() {
isolate.modifier.setOpacity(0);
scope.$emit('unregisterChild', {id: scope.$id});
});
scope.$on('registerChild', function(evt, data){
if(evt.targetScope.$id !== evt.currentScope.$id){
isolate.renderNode.add(data.renderNode);
evt.stopPropagation();
}
})
transclude(scope, function(clone) {
element.find('div').append(clone);
});
scope.$emit('registerChild', isolate);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faPipeFrom
* @module famous.angular
* @restrict A
* @priority 16
* @param {Object} EventHandler - target handler object
* @description
* This directive remove an handler object from set of downstream handlers. Undoes work of "pipe"
* from a faPipeTo directive.
*
* @usage
* ```html
*
*
*
* ```
*/
//UNTESTED as of 2014-05-13
angular.module('famous.angular')
.directive('faPipeFrom', ['$famous', '$famousDecorator', function ($famous, $famousDecorator) {
return {
restrict: 'A',
scope: false,
priority: 16,
compile: function() {
var Engine = $famous['famous/core/Engine'];
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
scope.$watch(
function(){
return scope.$eval(attrs.faPipeFrom);
},
function(newTarget, oldTarget){
var source = isolate.renderNode || Engine;
if(oldTarget instanceof Array){
for(var i = 0; i < oldTarget.length; i++){
oldTarget[i].unpipe(source);
}
}else if(oldTarget !== undefined){
oldTarget.unpipe(source);
}
if(newTarget instanceof Array){
for(var i = 0; i < newTarget.length; i++){
newTarget[i].pipe(source);
}
}else if(newTarget !== undefined){
newTarget.pipe(source);
}
}
);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faPipeTo
* @module famous.angular
* @restrict A
* @param {Object} EventHandler - Event handler target object
* @description
* This directive add an event handler object to set of downstream handlers.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faPipeTo', ['$famous', '$famousDecorator', function ($famous, $famousDecorator) {
return {
restrict: 'A',
scope: false,
priority: 16,
compile: function() {
var Engine = $famous['famous/core/Engine'];
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
scope.$watch(
function(){
return scope.$eval(attrs.faPipeTo);
},
function(newPipe, oldPipe){
var target = isolate.renderNode || Engine;
if(oldPipe instanceof Array){
for(var i = 0; i < oldPipe.length; i++){
target.unpipe(oldPipe[i]);
}
}else if(oldPipe !== undefined){
target.unpipe(oldPipe);
}
if(newPipe instanceof Array){
for(var i = 0; i < newPipe.length; i++){
target.pipe(newPipe[i]);
}
}else if(newPipe !== undefined){
target.pipe(newPipe);
}
}
);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faRenderNode
* @module famous.angular
* @restrict EA
* @description
* A directive to insert a {@link https://famo.us/docs/0.1.1/core/RenderNode/ Famo.us RenderNode} that is
* a wrapper for inserting a renderable component (like a Modifer or Surface) into the render tree.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faRenderNode', ["$famous", "$famousDecorator", function ($famous, $famousDecorator) {
return {
template: '',
transclude: true,
scope: true,
restrict: 'EA',
compile: function(tElement, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var Engine = $famous['famous/core/Engine'];
var getOrValue = function(x) {
return x.get ? x.get() : x;
};
isolate.children = [];
attrs.$observe('faPipeTo', function(val){
var pipeTo = scope.$eval(val);
if(pipeTo)
Engine.pipe(pipeTo);
})
isolate.renderNode = scope.$eval(attrs.faNode);
scope.$on('$destroy', function() {
scope.$emit('unregisterChild', {id: scope.$id});
});
scope.$on('registerChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
isolate.renderNode.add(data.renderNode);
isolate.children.push(data);
evt.stopPropagation();
}
})
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
transclude(scope, function(clone) {
element.find('div').append(clone);
});
scope.$emit('registerChild', isolate);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faScrollView
* @module famous.angular
* @restrict E
* @description
* This directive allows you to specify a {@link https://famo.us/docs/0.1.1/views/Scrollview/ famo.us Scrollview}
* that will lay out a collection of renderables sequentially in the specified direction
* and will allow you to scroll through them with mousewheel or touch events.
*
* @usage
* ```html
*
*
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faScrollView', ['$famous', '$famousDecorator', '$timeout', function ($famous, $famousDecorator, $timeout) {
return {
template: '',
restrict: 'E',
transclude: true,
scope: true,
compile: function(tElem, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var ScrollView = $famous["famous/views/Scrollview"];
var ViewSequence = $famous['famous/core/ViewSequence'];
var Surface = $famous['famous/core/Surface'];
var _children = [];
var options = scope.$eval(attrs.faOptions) || {};
isolate.renderNode = new ScrollView(options);
var updateScrollview = function(init){
//$timeout hack used here because the
//updateScrollview function will get called
//before the $index values get re-bound
//through ng-repeat. The result is that
//the items get sorted here, then the indexes
//get re-bound, and thus the results are incorrectly
//ordered.
$timeout(function(){
_children.sort(function(a, b){
return a.index - b.index;
});
var options = {
array: function(_children) {
var _ch = [];
angular.forEach(_children, function(c, i) {
_ch[i] = c.renderNode;
})
return _ch;
}(_children)
};
//set the first page on the scrollview if
//specified
if(init)
options.index = scope.$eval(attrs.faStartIndex);
var viewSeq = new ViewSequence(options);
isolate.renderNode.sequenceFrom(viewSeq);
})
}
scope.$on('registerChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
_children.push(data);
updateScrollview(true);
evt.stopPropagation();
};
});
scope.$on('unregisterChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
_children = function(_children) {
var _ch = [];
angular.forEach(_children, function(c) {
if(c.id !== data.id) {
_ch.push(c);
}
});
return _ch;
}(_children);
updateScrollview();
evt.stopPropagation();
}
})
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
transclude(scope, function(clone) {
element.find('div').append(clone);
});
scope.$emit('registerChild', isolate);
}
};
}
};
}]);
/**
* @ngdoc directive
* @name faSurface
* @module famous.angular
* @restrict EA
* @description
* This directive is used to create general Famo.us surfaces, which are the
* leaf nodes of the scene graph. The content inside
* surfaces is what gets rendered to the screen.
* This is where you can create form elements, attach
* images, or output raw text content with one-way databinding {{}}.
* You can include entire complex HTML snippets inside a faSurface, including
* ngIncludes or custom (vanilla Angular) directives.
*
* @usage
* ```html
*
* Here's some data-bound content {{myScopeVariable}}
*
* ```
*/
angular.module('famous.angular')
.directive('faSurface', ['$famous', '$famousDecorator', '$interpolate', '$controller', '$compile', function ($famous, $famousDecorator, $interpolate, $controller, $compile) {
return {
scope: true,
transclude: true,
template: '',
restrict: 'EA',
compile: function(tElem, tAttrs, transclude){
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var Surface = $famous['famous/core/Surface'];
var Transform = $famous['famous/core/Transform']
var EventHandler = $famous['famous/core/EventHandler'];
//update properties
//TODO: is this going to be a bottleneck?
scope.$watch(
function(){
return isolate.getProperties()
},
function(){
if(isolate.renderNode)
isolate.renderNode.setProperties(isolate.getProperties());
},
true
)
isolate.getProperties = function(){
return {
backgroundColor: scope.$eval(attrs.faBackgroundColor),
color: scope.$eval(attrs.faColor)
};
};
isolate.renderNode = new Surface({
size: scope.$eval(attrs.faSize),
class: scope.$eval(attrs.class),
properties: isolate.getProperties()
});
//TODO: support ng-class
if(attrs.class)
isolate.renderNode.setClasses(attrs['class'].split(' '));
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
var updateContent = function(){
// var compiledEl = isolate.compiledEl = isolate.compiledEl || $compile(element.find('div.fa-surface').contents())(scope)
// isolate.renderNode.setContent(isolate.compiledEl.context);
//TODO check if $compile is needed ?
isolate.renderNode.setContent(element[0].querySelector('div.fa-surface'));
};
updateContent();
//boilerplate
transclude(scope, function(clone) {
angular.element(element[0].querySelectorAll('div.fa-surface')).append(clone);
});
scope.$emit('registerChild', isolate);
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faTap
* @module famous.angular
* @restrict A
* @param {expression} faTap Expression to evaluate upon tap. (Event object is available as `$event`)
* @description
* This directive allows you to specify custom behavior when an element is tapped.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faTap', ['$parse', '$famousDecorator', function ($parse, $famousDecorator) {
return {
restrict: 'A',
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
if (attrs.faTap) {
var renderNode = (isolate.renderNode._eventInput || isolate.renderNode)
var _dragging = false;
renderNode.on("touchmove", function(data) {
_dragging = true;
return data;
});
renderNode.on("touchend", function(data) {
if (!_dragging){
var fn = $parse(attrs.faTap);
fn(scope, {$event:data});
if(!scope.$$phase)
scope.$apply();
}
_dragging = false
});
}
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faTouchend
* @module famous.angular
* @restrict A
* @param {expression} faTouchend Expression to evaluate upon touchend. (Event object is available as `$event`)
* @description
* This directive allows you to specify custom behavior after an element that {@link https://developer.mozilla.org/en-US/docs/Web/Reference/Events/touchend has been touched}.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faTouchend', ['$parse', '$famousDecorator', function ($parse, $famousDecorator) {
return {
restrict: 'A',
scope: false,
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
if (attrs.faTouchEnd) {
var renderNode = (isolate.renderNode._eventInput || isolate.renderNode)
renderNode.on("touchend", function(data) {
var fn = $parse(attrs.faTouchMove);
fn(scope, {$event:data});
if(!scope.$$phase)
scope.$apply();
});
}
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faTouchmove
* @module famous.angular
* @restrict A
* @param {expression} faTouchmove Expression to evaluate upon touchmove. (Event object is available as `$event`)
* @description
* This directive allows you to specify custom behavior when an element is {@link https://developer.mozilla.org/en-US/docs/Web/Reference/Events/touchmove moved along a touch surface}.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faTouchmove', ['$parse', '$famousDecorator', function ($parse, $famousDecorator) {
return {
restrict: 'A',
scope: false,
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
if (attrs.faTouchMove) {
var renderNode = (isolate.renderNode._eventInput || isolate.renderNode)
renderNode.on("touchmove", function(data) {
var fn = $parse(attrs.faTouchMove);
fn(scope, {$event:data});
if(!scope.$$phase)
scope.$apply();
});
}
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faTouchstart
* @module famous.angular
* @restrict A
* @param {expression} faTouchstart Expression to evaluate upon touchstart. (Event object is available as `$event`)
* @description
* This directive allows you to specify custom behavior when an element is {@link https://developer.mozilla.org/en-US/docs/Web/Reference/Events/touchstart touched upon a touch surface}.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faTouchstart', ['$parse', '$famousDecorator', function ($parse, $famousDecorator) {
return {
restrict: 'A',
scope: false,
compile: function() {
return {
post: function(scope, element, attrs) {
var isolate = $famousDecorator.ensureIsolate(scope);
if (attrs.faTouchStart) {
var renderNode = (isolate.renderNode._eventInput || isolate.renderNode)
renderNode.on("touchstart", function(data) {
var fn = $parse(attrs.faTouchStart);
fn(scope, {$event:data});
if(!scope.$$phase)
scope.$apply();
});
}
}
}
}
};
}]);
/**
* @ngdoc directive
* @name faView
* @module famous.angular
* @restrict EA
* @description
* This directive is used to wrap child elements into a View render node. This is especially useful for grouping.
* Use an `` surrounded by a `` in order to affect the View's position, scale, etc.
*
* @usage
* ```html
*
*
*
* ```
*/
angular.module('famous.angular')
.directive('faView', ["$famous", "$famousDecorator", function ($famous, $famousDecorator) {
return {
template: '',
transclude: true,
scope: true,
restrict: 'EA',
compile: function(tElement, tAttrs, transclude){
var View = $famous['famous/core/View'];
return {
pre: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
isolate.children = [];
var getOrValue = function(x) {
return x.get ? x.get() : x;
};
isolate.renderNode = new View({
size: scope.$eval(attrs.faSize) || [undefined, undefined]
});
scope.$on('$destroy', function() {
scope.$emit('unregisterChild', {id: scope.$id});
});
scope.$on('registerChild', function(evt, data){
if(evt.targetScope.$id != scope.$id){
isolate.renderNode.add(data.renderNode);
isolate.children.push(data);
evt.stopPropagation();
}
})
},
post: function(scope, element, attrs){
var isolate = $famousDecorator.ensureIsolate(scope);
transclude(scope, function(clone) {
element.find('div').append(clone);
});
scope.$emit('registerChild', isolate);
}
}
}
};
}]);