/** * 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); } } } }; }]);