angular-scalyr
Version:
Angular Directives by Scalyr
1,028 lines (946 loc) • 42.3 kB
JavaScript
/**
* @license scalyr v1.0.3
* (c) 2013 Scalyr, Inc. http://scalyr.com
* License: MIT
*/
'use strict';
// You may just depend on the 'sly' module to pull in all of the
// dependencies.
angular.module('sly', ['slyEvaluate', 'slyRepeat']);
/**
* @fileoverview
* Defines core functions used throughout the Scalyr javascript
* code base. This file is included on every page.
*
* @author Steven Czerwinski <czerwin@scalyr.com>
*/
/**
* @param {Object} value The value to check
* @returns {Boolean} True if value is an Array
*/
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is a Boolean
*/
function isBoolean(value) {
return typeof value == 'boolean';
}
/**
* @param {Object} value The value to check
* @returns {Boolean} True if value is a Date object
*/
function isDate(value) {
return Object.prototype.toString.call(value) === '[object Date]';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is undefined
*/
function isDefined(value) {
return typeof value != 'undefined';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is a Function
*/
function isFunction(value) {
return typeof value == 'function';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is null
*/
function isNull(value) {
return value === null;
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is a Number
*/
function isNumber(value) {
return typeof value == 'number';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is an Object, not including null
*/
function isObject(value) {
return value !== null && typeof value == 'object';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is a string
*/
function isString(value) {
return typeof value == 'string';
}
/**
* @param {*} value The value to check
* @returns {Boolean} True if value is undefined
*/
function isUndefined(value) {
return typeof value == 'undefined';
}
/**
* Converts a String or Boolean value to a Boolean.
*
* @param {String|Boolean} value The value to convert
* @returns {Boolean} Returns true for any String that is not
* null, empty String, or 'false'. If value is a Boolean,
* returns value
*/
function convertToBoolean(value) {
if (isBoolean(value))
return value;
return value !== null && value !== '' && value !== 'false';
}
/**
* Determines if obj has a property named prop.
*
* @param {Object} obj The object to check
* @returns {Boolean} Returns true if obj has a property named
* prop. Only considers the object's own properties
*/
function hasProperty(obj, prop) {
return obj.hasOwnProperty(prop);
}
/**
* @param {*} value The value to check
* @returns {Boolean} Returns true if value is a String
* and has zero length, or if null or undefined
*/
function isStringEmpty(value) {
return isNull(value) || isUndefined(value) ||
(isString(value) && (value.length == 0));
}
/**
* @param {*} value The value to check
* @returns {Boolean} Returns true if value is a String
* and has non-zero length
*/
function isStringNonempty(value) {
return isString(value) && (value.length > 0);
}
/**
* Returns input with the first letter capitalized.
* The input may not be zero length.
*
* @param {String} input The String to capitalize.
* @returns {String} Returns input with the first letter
* capitalized.
*/
function upperCaseFirstLetter(input) {
return input.charAt(0).toUpperCase() + input.slice(1);
}
/**
* Returns true if obj1 and obj2 are equal. This should
* only be used for Arrays, Objects, and value types. This is a deep
* comparison, comparing each property and recursive property to
* be equal (not just ===).
*
* Two Objects or values are considered equivalent if at least one of the following is true:
* - Both objects or values pass `===` comparison.
* - Both objects or values are of the same type and all of their properties pass areEqual
* comparison.
* - Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal).
*
* Note, during property comparision, properties with function values are ignores as are property
* names beginning with '$'.
*
* See angular.equal for more details.
*
* @param {Object|Array|value} obj1 The first object
* @param {Object|Array|value} obj2 The second object
* @returns {Boolean} True if the two objects are equal using a deep
* comparison.
*/
function areEqual(obj1, obj2) {
return angular.equals(obj1, obj2);
}
/**
* @param {Number} a The first Number
* @param {Number} b The second Number
* @returns {Number} The minimum of a and b
*/
function min(a, b) {
return a < b ? a : b;
}
/**
* @param {Number} a The first Number
* @param {Number} b The second Number
* @returns {Number} The maximum of a and b
*/
function max(a, b) {
return a > b ? a : b;
}
/**
* Returns true if the specified String begins with prefix.
*
* @param {*} input The input to check
@ @param {String} prefix The prefix
* @returns {Boolean} True if input is a string that begins with prefix
*/
function beginsWith(input, prefix) {
return isString(input) && input.lastIndexOf(prefix, 0) == 0;
}
/**
* Returns true if the specified String ends with prefix.
*
* @param {*} input The input to check
@ @param {String} postfix The postfix
* @returns {Boolean} True if input is a string that ends with postfix
*/
function endsWith(input, postfix) {
return isString(input) && input.indexOf(postfix, input.length - postfix.length) !== -1;
}
/**
* Returns a deep copy of source, where source can be an Object or an Array. If a destination is
* provided, all of its elements (for Array) or properties (for Objects) are deleted and then all
* elements/properties from the source are copied to it. If source is not an Object or Array,
* source is returned.
*
* See angular.copy for more details.
* @param {Object|Array} source The source
* @param {Object|Array} destination Optional object to copy the elements to
* @returns {Object|Array} The deep copy of source
*/
function copy(source, destination) {
return angular.copy(source, destination);
}
/**
* Removes property from obj.
*
* @param {Object} obj The object
* @param {String} property The property name to delete
*/
function removeProperty(obj, property) {
delete obj[property];
}
/**
* Removes all properties in the array from obj.
*
* @param {Object} obj The object
* @param {Array} properties The properties to remove
*/
function removeProperties(obj, properties) {
for (var i = 0; i < properties.length; ++i)
delete obj[properties[i]];
}
/**
* Invokes the iterator function once for each item in obj collection, which can be either
* an Object or an Array. The iterator function is invoked with iterator(value, key),
* where value is the value of an object property or an array element and key is the
* object property key or array element index. Specifying a context for the function is
* optional. If specified, it becomes 'this' when iterator function is invoked.
*
* See angular.forEach for more details.
*
* @param {Object|Array} The Object or Array over which to iterate
* @param {Function} iterator The iterator function to invoke
* @param {Object} context The value to set for 'this' when invoking the
* iterator function. This is optional
*/
function forEach(obj, iterator, context) {
return angular.forEach(obj, iterator, context);
}
/**
* Used to define a Scalyr javascript library and optionally declare
* dependencies on other libraries. All javascript code not defined in
* this file should be defined as part of a library.
*
* The first argument is the name to call the library. The second argument
* is either a Constructor object for the library or an array where the last
* element is the Constructor for the library and the first to N-1 are string
* names of the libraries this one depends on. If you do declare dependencies,
* the libraries are passed in the Constructor create method in the same order
* as the strings are defined.
*
* Example:
* defineScalyrJsLibrary('myUtils', function() {
* var fooFunction = function(a, b) {
* return a + b;
* };
* return {
* foo: fooFunction
* };
* });
*
* defineScalyrJsLibrary('anotherUtils', [ 'myUtils', function(myUtils) {
* var barFunction = function(a, b) {
* return myUtils.foo(a, b);
* };
* return {
* bar: barFunction
* };
* });
*
* @param {String} libraryName The name for the library
* @param {Constructor|Array} libraryExporter The exporter for the
* library. See above for details
*/
function defineScalyrJsLibrary(libraryName, libraryExporter) {
var moduleDependencies = [];
if (libraryExporter instanceof Array) {
for (var i = 0; i < libraryExporter.length - 1; ++i)
moduleDependencies.push(libraryExporter[i]);
}
return angular.module(libraryName, moduleDependencies)
.factory(libraryName, libraryExporter);
}
/**
* Similar to defineScalyrJsLibary but instead of declaring
* a purely javascript library, this declares an Angular module
* library. The moduleName should be a string used to identify
* this module. The dependencies is an array with the string
* names of Angular modules, Scalyr Angular modules, or Scalyr
* javascript libraries to depend on. The returned object
* can be used to define directives, etc similar to angular.module.
*
* Example:
* defineScalyrAngularModule('slyMyModule', [ 'myTextUtils'])
* .filter('camelCase', function(myTextUtils) {
* return function(input) {
* return myTextUtils.camelCase(input);
* };
* });
*
* @param {String} moduleName The name of the module
* @param {Array} dependencies The names of modules to depend on
*/
function defineScalyrAngularModule(moduleName, dependencies) {
return angular.module(moduleName, dependencies);
}
/**
* @fileoverview
* Module: slyEvaluate
*
* Defines several directives related to preventing evaluating watchers
* on scopes under certain conditions. Here's a list of the directives
* and brief descriptions. See down below for more details.
*
* slyEvaluateOnlyWhen: A directive that prevents updating / evaluating
* all bindings for the current element and its children unless
* the expression has changed values. If new children are added, they
* are always evaluated at least once. It currently assumes the
* expression evaluates to an object and detects changes only by
* a change in object reference.
*
* slyAlwaysEvaluate: Can only be used in conjunction with the
* slyEvaluateOnlyWhen directive. This directive will ensure that
* any expression that is being watched will always be evaluated
* if it contains the specified string (i.e., it will ignore whether
* or not the slyEvaluateOnlyWhen expression has changed.) This
* is useful when you wish to check some expressions all the time.
*
* slyPreventEvaluationWhenHidden: Will only evaluate the bindings
* for the current element and its children if the current element
* is not hidden (detected by the element having the ng-hide CSS class.)
*
* slyShow: Will hide the element if the expression evaluates to false.
* Uses ng-hide to hide the element. This is almost exactly the same
* as ngShow, but it has the advantage that it works better with
* slyPreventEvaluationWhenHidden by guaranteeing it will always evaluate
* its show expression to determine if it should or should not be hidden.
*/
defineScalyrAngularModule('slyEvaluate', ['gatedScope'])
/**
* Directive for preventing all bound expressions in the current element and its children
* from being evaluated unless the specified expression evaluates to a different object.
* Currently, the value assigned to the 'slyEvaluateOnlyWhen' must evaluate to an object.
* Also, reference equality is used to determine if the expression has changed.
* TODO: Make this more versatile, similar to $watch. For now, this is all we need.
*/
.directive('slyEvaluateOnlyWhen', ['$parse', function ($parse) {
return {
// We create a new scope just because it helps segment the gated watchers
// from the parent scope. Unclear if this is that important for perf.
scope: true,
restrict: 'A',
compile: function compile(tElement, tAttrs) {
return {
// We need a separate pre-link function because we want to modify the scope before any of the
// children are passed it.
pre: function preLink(scope, element, attrs) {
var previousValue = null;
var initialized = false;
var expressionToCheck = $parse(attrs['slyEvaluateOnlyWhen']);
var alwaysEvaluateString = null;
if (hasProperty(attrs, 'slyAlwaysEvaluate')) {
alwaysEvaluateString = attrs['slyAlwaysEvaluate'];
if (isStringEmpty(alwaysEvaluateString))
throw new Exception('Empty string is illegal for value of slyAlwaysEvaluate');
}
scope.$addWatcherGate(function evaluteOnlyWhenChecker() {
// We should only return true if expressionToCheck evaluates to a value different
// than previousValue.
var currentValue = expressionToCheck(scope);
if (!initialized) {
initialized = true;
previousValue = currentValue;
return true;
}
var result = previousValue !== currentValue;
previousValue = currentValue;
return result;
}, function shouldGateWatcher(watchExpression) {
// Should return true if the given watcher that's about to be registered should
// be gated.
return isNull(alwaysEvaluateString) ||
!(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0));
}, true /* Evaluate any newly added watchers when they are added */);
},
};
},
};
}])
/**
* Directive for overriding the 'slyEvaluateOnlyWhen' expression for the current element.
* This directive takes a single string value. If this string value is found anywhere in
* an expression that normally would not be evaluated due to the 'slyEvaluateOnlyWhen'
* directive, it is evaluated, regardless of whether or not the value for the expression in
* 'slyEvaluateOnlyWhen' has changed. This is very useful when a certain expression used by
* one of the children of the current element should always be evaluated and is not affected
* by the expression specified in slyEvaluateOnlyWhen.
*/
.directive('slyAlwaysEvaluate', function() {
// This is just a place holder to show that slyAlwaysEvaluate is a legal
// directive. The real work for this directive is done in slyEvaluateOnlyWhen.
return {
restrict: 'A',
link: function(scope, element, attrs) {
},
};
})
/**
* Directive for showing an element, very similar to ngShow. However, this directive
* works better with slyPreventEvaluationWhenHidden because it is ensure it always
* will evaluate the show expression to determine if it should be shown or hidden
* even if slyPreventEvaluationWhenHidden is in effect. This directive also uses
* the ng-hide css class to actually hide the element.
*
* NOTE: We might be able to get better performance if we have this directive directly
* perform a callback on slyPreventEvaluationWhenHidden when it is shown/hidden rather
* than having that directive register a watcher on the css class.
*/
.directive('slyShow', ['$animate', function($animate) {
/**
* @param {*} value The input
* @return {Boolean} True if the value is truthy as determined by angular rules.
*
* Note: This is copied from the Angular source because it is not exposed by Angular
* but we want our directive to behave the same as ngShow. Think about moving this
* to core.js.
*/
function toBoolean(value) {
if (value && value.length !== 0) {
var v = ("" + value);
v = isString(v) ? v.toLowerCase() : v;
value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
} else {
value = false;
}
return value;
}
return {
restrict: 'A',
link: function slyShowLink(scope, element, attr) {
scope.$watch(attr.slyShow, function ngSlyShowAction(value){
$animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
}, false, 'slyShow'); },
};
}])
/**
* Directive for preventing all bound expressions in the current element and its children
* from being evaluated if the current element is hidden as determined by whether or not
* it has the ng-hide class.
*/
.directive('slyPreventEvaluationWhenHidden', function () {
return {
restrict: 'A',
// We create a new scope just because it helps segment the gated watchers
// from the parent scope. Unclear if this is that important for perf.
scope: true,
compile: function compile(tElement, tAttrs) {
return {
// We need a separate pre-link function because we want to modify the scope before any of the
// children are passed it.
pre: function preLink(scope, element, attrs) {
scope.$addWatcherGate(function hiddenChecker() {
// Should only return true if the element is not hidden.
return !element.hasClass('ng-hide');
}, function hiddenDecider(watchExpression, listener, equality, directiveName) {
// Make an exception for slyShow.. do not gate its watcher.
if (isDefined(directiveName) && (directiveName == 'slyShow'))
return false;
return true;
});
},
};
},
};
});
/**
* @fileoverview
* Module: slyRepeat
*
* Contains the slyRepeat directive, which is is a modified version of the
* ngRepeat directive that is meant to be more efficient for creating and
* recreating large lists of bound elements. In particular, it has an
* optimization that will prevent DOM elements from being constantly created
* and destroyed as the contents of the repeated elements change. It does this
* by not destroying DOM elements when they are no longer needed, but instead,
* just hiding them. This might not work for all use cases, but for it does
* for the ones we do wish to heavily optimize. For eample, through profiling,
* we found that destroying DOM elements when flipping through log view pages
* represented a large chunk of CPU time.
*
* Cavaets: The collection expression must evaluate to an array. Animators
* will not work. Track By does not work. Use at your own peril.
*
* @author Steven Czerwinski <czerwin@scalyr.com>
*/
defineScalyrAngularModule('slyRepeat', ['gatedScope'])
.directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) {
/**
* Sets the scope contained in elementScope to gate all its
* watchers based on the isActiveForRepeat proprety.
*
* @param {Object} elementScope The object containing the
* scope and isActiveForRepeat properties.
*/
function gateWatchersForScope(elementScope) {
elementScope.scope.$addWatcherGate(function() {
return elementScope.isActiveForRepeat;
});
}
return {
restrict: 'A',
scope: true,
transclude: 'element',
priority: 1000,
terminal: true,
compile: function(element, attr, linker) {
// Most of the work is done in the post-link function.
return function($scope, $element, $attr) {
// This code is largely based on ngRepeat.
// Parse the expression. It should look like:
// x in some-expression
var expression = $attr.slyRepeat;
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/);
if (!match) {
throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" +
expression + "'.");
}
var iterVar = match[1];
var collectionExpr = match[2];
match = iterVar.match(/^(?:([\$\w]+))$/);
if (!match) {
throw Error("'item' in 'item in collection' should be identifier but got '" +
lhs + "'.");
}
// previousElements will store references to the already existing (DOM) elements
// that were last used for the last rendering of this repeat and were visible.
// We will re-use these elements when executing the next rendering of the repeat when
// the iteration value changes.
var previousElements = [];
// previousElementsBuffer will store references to the already existing (DOM) elements
// that are in the page but were not used for the last rendering of this repeat and were
// therefore marked as inactive and not visible. This happens if the length of the repeat
// iteration goes down over time, since we do not remove the elements. If the repeat length
// was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer.
// We keep this in case the length increases again.
var previousElementBuffer = [];
var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) {
if (!collection)
return;
if (!isArray(collection))
throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr);
var originalPreviousElementsLength = previousElements.length;
// First, reconcile previousElements and collection with respect to the previousElementBuffer.
// Basically, try to grow previousElements to collection.length if we can.
if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) {
var limit = previousElements.length + previousElementBuffer.length;
if (limit > collection.length)
limit = collection.length;
previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length));
}
var currentElements = null;
var currentElementBuffer = [];
var newElements = [];
if (collection.length > previousElements.length) {
// Add in enough elements to account for the larger collection.
for (var i = previousElements.length; i < collection.length; ++i) {
// Need to add in an element for each new item in the collection.
var newElement = {
scope: $scope.$new(),
isActiveForRepeat: true,
};
gateWatchersForScope(newElement);
newElement.scope.$index = i;
newElement.scope.$first = (i == 0);
newElements.push(newElement);
}
currentElements = previousElements.concat(newElements);
currentElementBuffer = previousElementBuffer;
} else if (collection.length < previousElements.length) {
for (var i = collection.length; i < previousElements.length; ++i)
previousElements[i].isActiveForRepeat = false;
currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat(
previousElementBuffer);
currentElements = previousElements;
} else {
currentElements = previousElements;
currentElementBuffer = previousElementBuffer;
}
// We have to fix up the last and middle values in the scope for each element in
// currentElements, since their roles may have changed with the new length.
// We always have to fix the last element.
if (currentElements.length > 0) {
var firstIndexToFix = currentElements.length - 1;
var lastIndexToFix = currentElements.length - 1;
// We also have to fix any new elements that were added.
if (originalPreviousElementsLength < currentElements.length) {
firstIndexToFix = originalPreviousElementsLength;
}
// And we usually have to fix the element before the first element we modified
// in case it used to be last.
if (firstIndexToFix > 0) {
firstIndexToFix = firstIndexToFix - 1;
}
for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) {
currentElements[i].scope.$last = (i == (currentElements.length - 1));
currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1)));
if (!currentElements[i].isActiveForRepeat) {
// If it is not marked as active, make it active. This is also indicates that
// the element is currently hidden, so we have to unhide it.
currentElements[i].isActiveForRepeat = true;
currentElements[i].element.css('display', '');
}
}
}
// Hide all elements that have recently become inactive.
for (var i = 0; i < currentElementBuffer.length; ++i) {
if (currentElementBuffer[i].isActiveForRepeat)
break;
currentElementBuffer[i].element.css('display', 'none');
}
// Assign the new value for the iter variable for each scope.
for (var i = 0; i < currentElements.length; ++i) {
currentElements[i].scope[iterVar] = collection[i];
}
// We have to go back now and clone the DOM element for any new elements we
// added and link them in. We clone the last DOM element we had created already
// for this Repeat.
var prevElement = $element;
if (previousElements.length > 0)
prevElement = previousElements[previousElements.length - 1].element;
for (var i = 0; i < newElements.length; ++i) {
linker(newElements[i].scope, function(clone) {
$animate.enter(clone, null, prevElement);
prevElement = clone;
newElements[i].element = clone;
});
}
previousElements = currentElements;
previousElementBuffer = currentElementBuffer;
});
$scope.$on('$destroy', function() {
deregisterCallback();
});
};
}
};
}]);
/**
* @fileoverview
* Defines an extension to angular.Scope that allows for registering
* 'gating functions' on a scope that will prevent all future watchers
* registered on the scope from being evaluated unless the gating function
* returns true.
*
* By depending on this module, the $rootScope instance and angular.Scope
* class are automatically extended to implement this new capability.
*
* Warning, this implementation depends on protected/private variables
* in the angular.Scope implementation and therefore can break in the
* future due to changes in the angular.Scope implementation. Use at
* your own risk.
*/
defineScalyrAngularModule('gatedScope', [])
.config(['$provide', function($provide) {
// We use a decorator to override methods in $rootScope.
$provide.decorator('$rootScope', ['$delegate', '$exceptionHandler',
function ($rootScope, $exceptionHandler) {
// Make a copy of $rootScope's original methods so that we can access
// them to invoke super methods in the ones we override.
var scopePrototype = {};
for (var key in $rootScope) {
if (isFunction($rootScope[key]))
scopePrototype[key] = $rootScope[key];
}
var Scope = $rootScope.constructor;
// Hold all of our new methods.
var methodsToAdd = {
};
// A constant value that the $digest loop implementation depends on. We
// grab it down below.
var initWatchVal;
/**
* @param {Boolean} isolate Whether or not the new scope should be isolated.
* @param {Scope} parent
* @returns {Scope} A new child scope
*/
methodsToAdd.$new = function(isolate, parent) {
// Because of how scope.$new works, the returned result
// should already have our new methods.
var result = scopePrototype.$new.call(this, isolate, parent);
// We just have to do the work that normally a child class's
// constructor would perform -- initializing our instance vars.
result.$$gatingFunction = this.$$gatingFunction;
result.$$parentGatingFunction = this.$$gatingFunction;
result.$$shouldGateFunction = this.$$shouldGateFunction;
result.$$gatedWatchers = [];
result.$$cleanUpQueue = this.$$cleanUpQueue;
return result;
};
/**
* Digests all of the gated watchers for the specified gating function.
*
* @param {Function} targetGatingFunction The gating function associated
* with the watchers that should be digested
* @returns {Boolean} True if any of the watchers were dirty
*/
methodsToAdd.$digestGated = function gatedScopeDigest(targetGatingFunction) {
// Note, most of this code was stolen from angular's Scope.$digest method.
var watch, value,
watchers,
length,
next, current = this, target = this, last,
dirty = false;
do { // "traverse the scopes" loop
if (watchers = current.$$gatedWatchers) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// Scalyr edit: We do not process a watch function if it is does not
// have the same gating function for which $digestGated was invoked.
if (watch.gatingFunction !== targetGatingFunction)
continue;
// Since we are about to execute the watcher as part of a digestGated
// call, we can remove it from the normal digest queue if it was placed
// there because the watcher was added after the gate function's first
// evaluation.
if (watch && !isNull(watch.cleanUp)) {
watch.cleanUp();
watch.cleanUp = null;
}
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch && (value = watch.get(current)) !== (last = watch.last) &&
!(watch.eq
? areEqual(value, last)
: (typeof value == 'number' && typeof last == 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), current);
// Scalyr edit: Removed the logging code for when the ttl is reached
// here because we don't have access to the ttl in this method.
}
} catch (e) {
$exceptionHandler(e);
}
}
}
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// Scalyr edit: This insanity warning was from angular. We only modified this
// code by checking the $$gatingFunction because it's a good optimization to only go
// down a child of a parent that has the same gating function as what we are processing
// (since if a parent already has a different gating function, there's no way any
// of its children will have the right one).
if (!(next = ((current.$$gatingFunction === targetGatingFunction && current.$$childHead)
|| (current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));
// Mark that this gating function has digested all children.
targetGatingFunction.hasDigested = true;
return dirty;
};
/**
* @inherited $watch
* @param directiveName The fourth parameter is a new optional parameter that allows
* directives aware of this abstraction to pass in their own names to identify
* which directive is registering the watch. This is then passed to the
* shouldGateFunction to help determine if the watcher should be gated by the current
* gatingFunction.
*/
methodsToAdd.$watch = function gatedWatch(watchExpression, listener, objectEquality,
directiveName) {
// Determine if we should gate this watcher.
if (!isNull(this.$$gatingFunction) && (isNull(this.$$shouldGateFunction) ||
this.$$shouldGateFunction(watchExpression, listener, objectEquality, directiveName))) {
// We do a hack here to just switch out the watchers array with our own
// gated list and then invoke the original watch function.
var tmp = this.$$watchers;
this.$$watchers = this.$$gatedWatchers;
// Invoke original watch function.
var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
this.$$watchers = tmp;
this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction;
this.$$gatedWatchers[0].cleanUp = null;
// We know that the last field of the watcher object will be set to initWatchVal, so we
// grab it here.
initWatchVal = this.$$gatedWatchers[0].last;
var watch = this.$$gatedWatchers[0];
// We should make sure the watch expression gets evaluated fully on at least one
// digest cycle even if the gate function is now closed if requested by the gating function's
// value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute
// the watcher we just added and remove itself after the digest cycle completes.
if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) {
var self = this;
watch.cleanUp = scopePrototype.$watch.call(self, function() {
if (!isNull(watch.cleanUp)) {
self.$$cleanUpQueue.unshift(watch.cleanUp);
watch.cleanUp = null;
}
var value;
var last = initWatchVal;
if (watch && (value = watch.get(self)) !== (last = watch.last) &&
!(watch.eq
? areEqual(value, last)
: (typeof value == 'number' && typeof last == 'number'
&& isNaN(value) && isNaN(last)))) {
watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), self);
}
return watch.last;
});
}
return result;
} else {
return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
}
};
/**
* @inherited $digest
*/
methodsToAdd.$digest = function gatedDigest() {
// We have to take care if a scope's digest method was invoked that has a
// gating function in the parent scope. In this case, the watcher for that
// gating function is registered in the parent (the one added in gatedWatch),
// and will not be evaluated here. So, we have to manually see if the gating
// function is true and if so, evaluate any gated watchers for that function on
// this scope. This needs to happen to properly support invoking $digest on a
// scope with a parent scope with a gating function.
// NOTE: It is arguable that we are not correctly handling nested gating functions
// here since we do not know if the parent gating function was nested in other gating
// functions and should be evaluated at all. However, if a caller is invoking
// $digest on a particular scope, we assume the caller is doing that because it
// knows the watchers should be evaluated.
var dirty = false;
if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) {
var ttl = 5;
do {
dirty = this.$digestGated(this.$$parentGatingFunction);
ttl--;
if (dirty && !(ttl--)) {
throw Error(TTL + ' $digest() iterations reached for gated watcher. Aborting!\n' +
'Watchers fired in the last 5 iterations.');
}
} while (dirty);
}
dirty = scopePrototype.$digest.call(this) || dirty;
var cleanUpQueue = this.$$cleanUpQueue;
while (cleanUpQueue.length)
try {
cleanUpQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
return dirty;
}
/**
* Modifies this scope so that all future watchers registered by $watch will
* only be evaluated if gatingFunction returns true. Optionally, you may specify
* a function that will be evaluted on every new call to $watch with the arguments
* passed to it, and that watcher will only be gated if the function returns true.
*
* @param {Function} gatingFunction The gating function which controls whether or not all future
* watchers registered on this scope and its children will be evaluated on a given
* digest cycle. The function will be invoked (with no arguments) on every digest
* and if it returns a truthy result, will cause all gated watchers to be evaluated.
* @param {Function} shouldGateFunction The function that controls whether or not
* a new watcher will be gated using gatingFunction. It is evaluated with the
* arguments to $watch and should return true if the watcher created by those
* arguments should be gated
* @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added
* after the gating function has returned true on a previous digest cycle, the
* the new watcher will be evaluated on the next digest cycle even if the
* gating function is currently return false.
*/
methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction,
shouldEvalNewWatchers) {
var changeCount = 0;
var self = this;
// Set a watcher that sees if our gating function is true, and if so, digests
// all of our associated watchers. Note, this.$watch could already have a
// gating function associated with it, which means this watch won't be executed
// unless all gating functions before us have evaluated to true. We take special
// care of this nested case below.
// We handle nested gating function in a special way. If we are a nested gating
// function (meaning there is already one or more gating functions on this scope and
// our parent scopes), then if those parent gating functions every all evaluate to
// true (which we can tell if the watcher we register here is evaluated), then
// we always evaluate our watcher until our gating function returns true.
var hasNestedGates = !isNull(this.$$gatingFunction);
(function() {
var promotedWatcher = null;
self.$watch(function() {
if (gatingFunction()) {
if (self.$digestGated(gatingFunction))
++changeCount;
} else if (hasNestedGates && isNull(promotedWatcher)) {
promotedWatcher = scopePrototype.$watch.call(self, function() {
if (gatingFunction()) {
promotedWatcher();
promotedWatcher = null;
if (self.$digestGated(gatingFunction))
++changeCount;
}
return changeCount;
});
}
return changeCount;
});
})();
if (isUndefined(shouldGateFunction))
shouldGateFunction = null;
if (isUndefined(shouldEvalNewWatchers))
shouldEvalNewWatchers = false;
this.$$gatingFunction = gatingFunction;
this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers;
this.$$shouldGateFunction = shouldGateFunction;
};
// Extend the original Scope object so that when
// new instances are created, it has the new methods.
angular.extend(Scope.prototype, methodsToAdd);
// Also extend the $rootScope instance since it was created
// before we got a chance to extend Scope.prototype.
angular.extend($rootScope, methodsToAdd);
$rootScope.$$gatingFunction = null;
$rootScope.$$parentGatingFunction = null;
$rootScope.$$shouldGateFunction = null;
$rootScope.$$gatedWatchers = [];
$rootScope.$$cleanUpQueue = [];
return $rootScope;
}]);
}]);