├── LICENSE.txt
├── README.md
├── scalyr.js
├── scripts
├── buildScalyr.js
├── includeFiles
│ ├── header.js
│ └── karma.conf.js
└── startJsTester
└── src
├── js
├── core.js
├── directives
│ ├── slyEvaluate.js
│ └── slyRepeat.js
├── lib
│ └── gatedScope.js
└── thirdparty
│ ├── angular-1.2.1.js
│ └── angular.js
└── tests
├── directives
├── allTests.html
├── slyEvaluateTest.js
└── slyRepeatTest.js
├── lib
├── allTests.html
└── gatedScopeTest.js
├── scalyrUnitTest.js
└── thirdparty
├── angular-mocks.js
├── jasmine-1.3.1
├── MIT.LICENSE
├── jasmine-html.js
├── jasmine.css
└── jasmine.js
├── jasmine
├── MIT.LICENSE
├── jasmine-html.js
├── jasmine.css
└── jasmine.js
├── sinon-1.7.1.js
└── sinon.js
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Scalyr Inc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Angular Directives by Scalyr
2 | ---
3 |
4 | This is the source code for the optimization directives Scalyr created to
5 | improve the responsiveness of its user interface, as discussed in the blog post
6 | [Optimizing AngularJS: 1200ms to 35ms](https://www.scalyr.com/blog/angularjs-1200ms-to-35ms).
7 |
8 | These directives are meant to demonstrate the optimization techniques discussed
9 | in the blog post. The directives were really only meant for internal use, but
10 | since there was sufficient interest from readers, we are publishing the source.
11 | Unfortunately, we cannot make maintaining this library a high priority. We will
12 | push bug fixes and accept pull requests from other developers, but beyond that,
13 | the source is provided as is. We expect this source will act more as a
14 | starting point for other developers rather than as a complete standalone
15 | Javascript library.
16 |
17 | Also note, because some of the optimization techniques rely on non-public
18 | AngularJS variables and methods, these directives may not work for all versions
19 | of AngularJS. The current tests validate them against 1.2.1.
20 |
21 | Furthermore, the directives were built with particular use cases in mind so
22 | they may not have all of the features you would expect. For example, our
23 | repeat directive 'slyRepeat' does not support animations and other features
24 | that 'ngRepeat' does.
25 |
26 | The [scalyr.js](scalyr.js) file contains the Javascript bundle required to use
27 | the directives. More information for each directive can be found in the
28 | src/js/directives directory. Here is a brief description of what is included:
29 |
30 |
31 |
sly
32 |
33 |
34 |
35 |
36 |
Name
Description
37 |
38 |
39 | slyEvaluateOnlyWhen
40 |
41 | An attribute directive that prevents updating / evaluating
42 | all bindings and expressions for the current element and its children
43 | unless the object referenced by the attribute's value changes.
44 | It currently assumes the expression contained in the attribute value
45 | evaluates to an object and detects changes only by a change in object reference.
46 |
47 |
48 | slyAlwaysEvaluate
49 |
50 | An attribute directive that can only be used in conjunction with the
51 | slyEvaluateOnlyWhen directive. This directive will ensure that
52 | any expression that is being watched will always be evaluated
53 | if it contains the string specified in the attribute value (i.e.,
54 | it will ignore whether or not the slyEvaluateOnlyWhen expression has changed.)
55 | This is useful when you wish to check some expressions all the time.
56 | Note, this only works if the directives register a string watch expression
57 | so this may or may not work for some directives depending on their
58 | implementation.
59 |
60 |
61 | slyPreventEvaluationWhenHidden
62 |
63 | An attribute directive that will only
64 | evaluate the bindings and expressions for the current element and its children
65 | if the current element is not hidden (detected by the element having the
66 | 'ng-hide' CSS class.)
67 |
68 |
69 | slyShow
70 |
71 | An attribute directive that Will hide the element if the expression specified
72 | in the atttribute value evaluates to false. Uses the CSS class 'ng-hide' to
73 | hide the element. This is almost exactly the same as ngShow, but it has the
74 | advantage that it works better with slyPreventEvaluationWhenHidden by
75 | guaranteeing it show expression is always evaluated regardless of the effects
76 | of slyPreventEvaluationWhenHidden.
77 |
78 |
79 | slyRepeat
80 |
81 | An attribute directive that is a modified version of the
82 | ngRepeat directive. It is meant to be more efficient for creating and
83 | recreating large lists of elements. In particular, it has an
84 | optimization that will prevent DOM elements from being constantly created
85 | and destroyed as the number of the repeated elements change. It does this
86 | by not destroying DOM elements when they are no longer needed, but instead,
87 | just hiding them. This might not work for all use cases. Cavaets: The
88 | collection expression must evaluate to an array. Animators will not work.
89 | Track By does not work. Use at your own peril.
90 |
91 |
92 |
93 | Please contact contact@scalyr.com for any questions or problems.
94 |
95 | == Contributing ==
96 |
97 | ```
98 | npm install -g karma karma-cli karma-jasmine karma-chrome-launcher
99 | ```
100 |
101 | Then from root directory :
102 | ```
103 | ./scripts/buildScalyr.js
104 | ./scripts/startJsTester
105 | ```
106 |
--------------------------------------------------------------------------------
/scalyr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license scalyr v1.0.3
3 | * (c) 2013 Scalyr, Inc. http://scalyr.com
4 | * License: MIT
5 | */
6 |
7 | 'use strict';
8 |
9 | // You may just depend on the 'sly' module to pull in all of the
10 | // dependencies.
11 | angular.module('sly', ['slyEvaluate', 'slyRepeat']);
12 | /**
13 | * @fileoverview
14 | * Defines core functions used throughout the Scalyr javascript
15 | * code base. This file is included on every page.
16 | *
17 | * @author Steven Czerwinski
18 | */
19 |
20 | /**
21 | * @param {Object} value The value to check
22 | * @returns {Boolean} True if value is an Array
23 | */
24 | function isArray(value) {
25 | return Object.prototype.toString.call(value) === '[object Array]';
26 | }
27 |
28 | /**
29 | * @param {*} value The value to check
30 | * @returns {Boolean} True if value is a Boolean
31 | */
32 | function isBoolean(value) {
33 | return typeof value == 'boolean';
34 | }
35 |
36 | /**
37 | * @param {Object} value The value to check
38 | * @returns {Boolean} True if value is a Date object
39 | */
40 | function isDate(value) {
41 | return Object.prototype.toString.call(value) === '[object Date]';
42 | }
43 |
44 | /**
45 | * @param {*} value The value to check
46 | * @returns {Boolean} True if value is undefined
47 | */
48 | function isDefined(value) {
49 | return typeof value != 'undefined';
50 | }
51 |
52 | /**
53 | * @param {*} value The value to check
54 | * @returns {Boolean} True if value is a Function
55 | */
56 | function isFunction(value) {
57 | return typeof value == 'function';
58 | }
59 |
60 | /**
61 | * @param {*} value The value to check
62 | * @returns {Boolean} True if value is null
63 | */
64 | function isNull(value) {
65 | return value === null;
66 | }
67 |
68 | /**
69 | * @param {*} value The value to check
70 | * @returns {Boolean} True if value is a Number
71 | */
72 | function isNumber(value) {
73 | return typeof value == 'number';
74 | }
75 |
76 | /**
77 | * @param {*} value The value to check
78 | * @returns {Boolean} True if value is an Object, not including null
79 | */
80 | function isObject(value) {
81 | return value !== null && typeof value == 'object';
82 | }
83 |
84 | /**
85 | * @param {*} value The value to check
86 | * @returns {Boolean} True if value is a string
87 | */
88 | function isString(value) {
89 | return typeof value == 'string';
90 | }
91 |
92 | /**
93 | * @param {*} value The value to check
94 | * @returns {Boolean} True if value is undefined
95 | */
96 | function isUndefined(value) {
97 | return typeof value == 'undefined';
98 | }
99 |
100 | /**
101 | * Converts a String or Boolean value to a Boolean.
102 | *
103 | * @param {String|Boolean} value The value to convert
104 | * @returns {Boolean} Returns true for any String that is not
105 | * null, empty String, or 'false'. If value is a Boolean,
106 | * returns value
107 | */
108 | function convertToBoolean(value) {
109 | if (isBoolean(value))
110 | return value;
111 | return value !== null && value !== '' && value !== 'false';
112 | }
113 |
114 | /**
115 | * Determines if obj has a property named prop.
116 | *
117 | * @param {Object} obj The object to check
118 | * @returns {Boolean} Returns true if obj has a property named
119 | * prop. Only considers the object's own properties
120 | */
121 | function hasProperty(obj, prop) {
122 | return obj.hasOwnProperty(prop);
123 | }
124 |
125 | /**
126 | * @param {*} value The value to check
127 | * @returns {Boolean} Returns true if value is a String
128 | * and has zero length, or if null or undefined
129 | */
130 | function isStringEmpty(value) {
131 | return isNull(value) || isUndefined(value) ||
132 | (isString(value) && (value.length == 0));
133 | }
134 |
135 | /**
136 | * @param {*} value The value to check
137 | * @returns {Boolean} Returns true if value is a String
138 | * and has non-zero length
139 | */
140 | function isStringNonempty(value) {
141 | return isString(value) && (value.length > 0);
142 | }
143 |
144 | /**
145 | * Returns input with the first letter capitalized.
146 | * The input may not be zero length.
147 | *
148 | * @param {String} input The String to capitalize.
149 | * @returns {String} Returns input with the first letter
150 | * capitalized.
151 | */
152 | function upperCaseFirstLetter(input) {
153 | return input.charAt(0).toUpperCase() + input.slice(1);
154 | }
155 |
156 | /**
157 | * Returns true if obj1 and obj2 are equal. This should
158 | * only be used for Arrays, Objects, and value types. This is a deep
159 | * comparison, comparing each property and recursive property to
160 | * be equal (not just ===).
161 | *
162 | * Two Objects or values are considered equivalent if at least one of the following is true:
163 | * - Both objects or values pass `===` comparison.
164 | * - Both objects or values are of the same type and all of their properties pass areEqual
165 | * comparison.
166 | * - Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal).
167 | *
168 | * Note, during property comparision, properties with function values are ignores as are property
169 | * names beginning with '$'.
170 | *
171 | * See angular.equal for more details.
172 | *
173 | * @param {Object|Array|value} obj1 The first object
174 | * @param {Object|Array|value} obj2 The second object
175 | * @returns {Boolean} True if the two objects are equal using a deep
176 | * comparison.
177 | */
178 | function areEqual(obj1, obj2) {
179 | return angular.equals(obj1, obj2);
180 | }
181 |
182 | /**
183 | * @param {Number} a The first Number
184 | * @param {Number} b The second Number
185 | * @returns {Number} The minimum of a and b
186 | */
187 | function min(a, b) {
188 | return a < b ? a : b;
189 | }
190 |
191 | /**
192 | * @param {Number} a The first Number
193 | * @param {Number} b The second Number
194 | * @returns {Number} The maximum of a and b
195 | */
196 | function max(a, b) {
197 | return a > b ? a : b;
198 | }
199 |
200 | /**
201 | * Returns true if the specified String begins with prefix.
202 | *
203 | * @param {*} input The input to check
204 | @ @param {String} prefix The prefix
205 | * @returns {Boolean} True if input is a string that begins with prefix
206 | */
207 | function beginsWith(input, prefix) {
208 | return isString(input) && input.lastIndexOf(prefix, 0) == 0;
209 | }
210 |
211 | /**
212 | * Returns true if the specified String ends with prefix.
213 | *
214 | * @param {*} input The input to check
215 | @ @param {String} postfix The postfix
216 | * @returns {Boolean} True if input is a string that ends with postfix
217 | */
218 | function endsWith(input, postfix) {
219 | return isString(input) && input.indexOf(postfix, input.length - postfix.length) !== -1;
220 | }
221 |
222 | /**
223 | * Returns a deep copy of source, where source can be an Object or an Array. If a destination is
224 | * provided, all of its elements (for Array) or properties (for Objects) are deleted and then all
225 | * elements/properties from the source are copied to it. If source is not an Object or Array,
226 | * source is returned.
227 | *
228 | * See angular.copy for more details.
229 | * @param {Object|Array} source The source
230 | * @param {Object|Array} destination Optional object to copy the elements to
231 | * @returns {Object|Array} The deep copy of source
232 | */
233 | function copy(source, destination) {
234 | return angular.copy(source, destination);
235 | }
236 |
237 | /**
238 | * Removes property from obj.
239 | *
240 | * @param {Object} obj The object
241 | * @param {String} property The property name to delete
242 | */
243 | function removeProperty(obj, property) {
244 | delete obj[property];
245 | }
246 |
247 | /**
248 | * Removes all properties in the array from obj.
249 | *
250 | * @param {Object} obj The object
251 | * @param {Array} properties The properties to remove
252 | */
253 | function removeProperties(obj, properties) {
254 | for (var i = 0; i < properties.length; ++i)
255 | delete obj[properties[i]];
256 | }
257 |
258 | /**
259 | * Invokes the iterator function once for each item in obj collection, which can be either
260 | * an Object or an Array. The iterator function is invoked with iterator(value, key),
261 | * where value is the value of an object property or an array element and key is the
262 | * object property key or array element index. Specifying a context for the function is
263 | * optional. If specified, it becomes 'this' when iterator function is invoked.
264 | *
265 | * See angular.forEach for more details.
266 | *
267 | * @param {Object|Array} The Object or Array over which to iterate
268 | * @param {Function} iterator The iterator function to invoke
269 | * @param {Object} context The value to set for 'this' when invoking the
270 | * iterator function. This is optional
271 | */
272 | function forEach(obj, iterator, context) {
273 | return angular.forEach(obj, iterator, context);
274 | }
275 |
276 | /**
277 | * Used to define a Scalyr javascript library and optionally declare
278 | * dependencies on other libraries. All javascript code not defined in
279 | * this file should be defined as part of a library.
280 | *
281 | * The first argument is the name to call the library. The second argument
282 | * is either a Constructor object for the library or an array where the last
283 | * element is the Constructor for the library and the first to N-1 are string
284 | * names of the libraries this one depends on. If you do declare dependencies,
285 | * the libraries are passed in the Constructor create method in the same order
286 | * as the strings are defined.
287 | *
288 | * Example:
289 | * defineScalyrJsLibrary('myUtils', function() {
290 | * var fooFunction = function(a, b) {
291 | * return a + b;
292 | * };
293 | * return {
294 | * foo: fooFunction
295 | * };
296 | * });
297 | *
298 | * defineScalyrJsLibrary('anotherUtils', [ 'myUtils', function(myUtils) {
299 | * var barFunction = function(a, b) {
300 | * return myUtils.foo(a, b);
301 | * };
302 | * return {
303 | * bar: barFunction
304 | * };
305 | * });
306 | *
307 | * @param {String} libraryName The name for the library
308 | * @param {Constructor|Array} libraryExporter The exporter for the
309 | * library. See above for details
310 | */
311 | function defineScalyrJsLibrary(libraryName, libraryExporter) {
312 | var moduleDependencies = [];
313 | if (libraryExporter instanceof Array) {
314 | for (var i = 0; i < libraryExporter.length - 1; ++i)
315 | moduleDependencies.push(libraryExporter[i]);
316 | }
317 |
318 | return angular.module(libraryName, moduleDependencies)
319 | .factory(libraryName, libraryExporter);
320 | }
321 |
322 | /**
323 | * Similar to defineScalyrJsLibary but instead of declaring
324 | * a purely javascript library, this declares an Angular module
325 | * library. The moduleName should be a string used to identify
326 | * this module. The dependencies is an array with the string
327 | * names of Angular modules, Scalyr Angular modules, or Scalyr
328 | * javascript libraries to depend on. The returned object
329 | * can be used to define directives, etc similar to angular.module.
330 | *
331 | * Example:
332 | * defineScalyrAngularModule('slyMyModule', [ 'myTextUtils'])
333 | * .filter('camelCase', function(myTextUtils) {
334 | * return function(input) {
335 | * return myTextUtils.camelCase(input);
336 | * };
337 | * });
338 | *
339 | * @param {String} moduleName The name of the module
340 | * @param {Array} dependencies The names of modules to depend on
341 | */
342 | function defineScalyrAngularModule(moduleName, dependencies) {
343 | return angular.module(moduleName, dependencies);
344 | }
345 | /**
346 | * @fileoverview
347 | * Module: slyEvaluate
348 | *
349 | * Defines several directives related to preventing evaluating watchers
350 | * on scopes under certain conditions. Here's a list of the directives
351 | * and brief descriptions. See down below for more details.
352 | *
353 | * slyEvaluateOnlyWhen: A directive that prevents updating / evaluating
354 | * all bindings for the current element and its children unless
355 | * the expression has changed values. If new children are added, they
356 | * are always evaluated at least once. It currently assumes the
357 | * expression evaluates to an object and detects changes only by
358 | * a change in object reference.
359 | *
360 | * slyAlwaysEvaluate: Can only be used in conjunction with the
361 | * slyEvaluateOnlyWhen directive. This directive will ensure that
362 | * any expression that is being watched will always be evaluated
363 | * if it contains the specified string (i.e., it will ignore whether
364 | * or not the slyEvaluateOnlyWhen expression has changed.) This
365 | * is useful when you wish to check some expressions all the time.
366 | *
367 | * slyPreventEvaluationWhenHidden: Will only evaluate the bindings
368 | * for the current element and its children if the current element
369 | * is not hidden (detected by the element having the ng-hide CSS class.)
370 | *
371 | * slyShow: Will hide the element if the expression evaluates to false.
372 | * Uses ng-hide to hide the element. This is almost exactly the same
373 | * as ngShow, but it has the advantage that it works better with
374 | * slyPreventEvaluationWhenHidden by guaranteeing it will always evaluate
375 | * its show expression to determine if it should or should not be hidden.
376 | */
377 | defineScalyrAngularModule('slyEvaluate', ['gatedScope'])
378 | /**
379 | * Directive for preventing all bound expressions in the current element and its children
380 | * from being evaluated unless the specified expression evaluates to a different object.
381 | * Currently, the value assigned to the 'slyEvaluateOnlyWhen' must evaluate to an object.
382 | * Also, reference equality is used to determine if the expression has changed.
383 | * TODO: Make this more versatile, similar to $watch. For now, this is all we need.
384 | */
385 | .directive('slyEvaluateOnlyWhen', ['$parse', function ($parse) {
386 | return {
387 | // We create a new scope just because it helps segment the gated watchers
388 | // from the parent scope. Unclear if this is that important for perf.
389 | scope: true,
390 | restrict: 'A',
391 | compile: function compile(tElement, tAttrs) {
392 | return {
393 | // We need a separate pre-link function because we want to modify the scope before any of the
394 | // children are passed it.
395 | pre: function preLink(scope, element, attrs) {
396 | var previousValue = null;
397 | var initialized = false;
398 |
399 | var expressionToCheck = $parse(attrs['slyEvaluateOnlyWhen']);
400 | var alwaysEvaluateString = null;
401 | if (hasProperty(attrs, 'slyAlwaysEvaluate')) {
402 | alwaysEvaluateString = attrs['slyAlwaysEvaluate'];
403 | if (isStringEmpty(alwaysEvaluateString))
404 | throw new Exception('Empty string is illegal for value of slyAlwaysEvaluate');
405 | }
406 | scope.$addWatcherGate(function evaluteOnlyWhenChecker() {
407 | // We should only return true if expressionToCheck evaluates to a value different
408 | // than previousValue.
409 | var currentValue = expressionToCheck(scope);
410 | if (!initialized) {
411 | initialized = true;
412 | previousValue = currentValue;
413 | return true;
414 | }
415 | var result = previousValue !== currentValue;
416 | previousValue = currentValue;
417 | return result;
418 | }, function shouldGateWatcher(watchExpression) {
419 | // Should return true if the given watcher that's about to be registered should
420 | // be gated.
421 | return isNull(alwaysEvaluateString) ||
422 | !(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0));
423 | }, true /* Evaluate any newly added watchers when they are added */);
424 | },
425 | };
426 | },
427 | };
428 | }])
429 | /**
430 | * Directive for overriding the 'slyEvaluateOnlyWhen' expression for the current element.
431 | * This directive takes a single string value. If this string value is found anywhere in
432 | * an expression that normally would not be evaluated due to the 'slyEvaluateOnlyWhen'
433 | * directive, it is evaluated, regardless of whether or not the value for the expression in
434 | * 'slyEvaluateOnlyWhen' has changed. This is very useful when a certain expression used by
435 | * one of the children of the current element should always be evaluated and is not affected
436 | * by the expression specified in slyEvaluateOnlyWhen.
437 | */
438 | .directive('slyAlwaysEvaluate', function() {
439 | // This is just a place holder to show that slyAlwaysEvaluate is a legal
440 | // directive. The real work for this directive is done in slyEvaluateOnlyWhen.
441 | return {
442 | restrict: 'A',
443 | link: function(scope, element, attrs) {
444 | },
445 | };
446 | })
447 | /**
448 | * Directive for showing an element, very similar to ngShow. However, this directive
449 | * works better with slyPreventEvaluationWhenHidden because it is ensure it always
450 | * will evaluate the show expression to determine if it should be shown or hidden
451 | * even if slyPreventEvaluationWhenHidden is in effect. This directive also uses
452 | * the ng-hide css class to actually hide the element.
453 | *
454 | * NOTE: We might be able to get better performance if we have this directive directly
455 | * perform a callback on slyPreventEvaluationWhenHidden when it is shown/hidden rather
456 | * than having that directive register a watcher on the css class.
457 | */
458 | .directive('slyShow', ['$animate', function($animate) {
459 | /**
460 | * @param {*} value The input
461 | * @return {Boolean} True if the value is truthy as determined by angular rules.
462 | *
463 | * Note: This is copied from the Angular source because it is not exposed by Angular
464 | * but we want our directive to behave the same as ngShow. Think about moving this
465 | * to core.js.
466 | */
467 | function toBoolean(value) {
468 | if (value && value.length !== 0) {
469 | var v = ("" + value);
470 | v = isString(v) ? v.toLowerCase() : v;
471 | value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
472 | } else {
473 | value = false;
474 | }
475 | return value;
476 | }
477 |
478 | return {
479 | restrict: 'A',
480 | link: function slyShowLink(scope, element, attr) {
481 | scope.$watch(attr.slyShow, function ngSlyShowAction(value){
482 | $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
483 | }, false, 'slyShow'); },
484 | };
485 | }])
486 | /**
487 | * Directive for preventing all bound expressions in the current element and its children
488 | * from being evaluated if the current element is hidden as determined by whether or not
489 | * it has the ng-hide class.
490 | */
491 | .directive('slyPreventEvaluationWhenHidden', function () {
492 | return {
493 | restrict: 'A',
494 | // We create a new scope just because it helps segment the gated watchers
495 | // from the parent scope. Unclear if this is that important for perf.
496 | scope: true,
497 | compile: function compile(tElement, tAttrs) {
498 | return {
499 | // We need a separate pre-link function because we want to modify the scope before any of the
500 | // children are passed it.
501 | pre: function preLink(scope, element, attrs) {
502 | scope.$addWatcherGate(function hiddenChecker() {
503 | // Should only return true if the element is not hidden.
504 | return !element.hasClass('ng-hide');
505 | }, function hiddenDecider(watchExpression, listener, equality, directiveName) {
506 | // Make an exception for slyShow.. do not gate its watcher.
507 | if (isDefined(directiveName) && (directiveName == 'slyShow'))
508 | return false;
509 | return true;
510 | });
511 | },
512 | };
513 | },
514 | };
515 | });
516 |
517 | /**
518 | * @fileoverview
519 | * Module: slyRepeat
520 | *
521 | * Contains the slyRepeat directive, which is is a modified version of the
522 | * ngRepeat directive that is meant to be more efficient for creating and
523 | * recreating large lists of bound elements. In particular, it has an
524 | * optimization that will prevent DOM elements from being constantly created
525 | * and destroyed as the contents of the repeated elements change. It does this
526 | * by not destroying DOM elements when they are no longer needed, but instead,
527 | * just hiding them. This might not work for all use cases, but for it does
528 | * for the ones we do wish to heavily optimize. For eample, through profiling,
529 | * we found that destroying DOM elements when flipping through log view pages
530 | * represented a large chunk of CPU time.
531 | *
532 | * Cavaets: The collection expression must evaluate to an array. Animators
533 | * will not work. Track By does not work. Use at your own peril.
534 | *
535 | * @author Steven Czerwinski
536 | */
537 | defineScalyrAngularModule('slyRepeat', ['gatedScope'])
538 | .directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) {
539 |
540 | /**
541 | * Sets the scope contained in elementScope to gate all its
542 | * watchers based on the isActiveForRepeat proprety.
543 | *
544 | * @param {Object} elementScope The object containing the
545 | * scope and isActiveForRepeat properties.
546 | */
547 | function gateWatchersForScope(elementScope) {
548 | elementScope.scope.$addWatcherGate(function() {
549 | return elementScope.isActiveForRepeat;
550 | });
551 | }
552 |
553 | return {
554 | restrict: 'A',
555 | scope: true,
556 | transclude: 'element',
557 | priority: 1000,
558 | terminal: true,
559 | compile: function(element, attr, linker) {
560 | // Most of the work is done in the post-link function.
561 | return function($scope, $element, $attr) {
562 | // This code is largely based on ngRepeat.
563 |
564 | // Parse the expression. It should look like:
565 | // x in some-expression
566 | var expression = $attr.slyRepeat;
567 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/);
568 | if (!match) {
569 | throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" +
570 | expression + "'.");
571 | }
572 |
573 | var iterVar = match[1];
574 | var collectionExpr = match[2];
575 |
576 | match = iterVar.match(/^(?:([\$\w]+))$/);
577 | if (!match) {
578 | throw Error("'item' in 'item in collection' should be identifier but got '" +
579 | lhs + "'.");
580 | }
581 |
582 | // previousElements will store references to the already existing (DOM) elements
583 | // that were last used for the last rendering of this repeat and were visible.
584 | // We will re-use these elements when executing the next rendering of the repeat when
585 | // the iteration value changes.
586 | var previousElements = [];
587 | // previousElementsBuffer will store references to the already existing (DOM) elements
588 | // that are in the page but were not used for the last rendering of this repeat and were
589 | // therefore marked as inactive and not visible. This happens if the length of the repeat
590 | // iteration goes down over time, since we do not remove the elements. If the repeat length
591 | // was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer.
592 | // We keep this in case the length increases again.
593 | var previousElementBuffer = [];
594 |
595 | var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) {
596 | if (!collection)
597 | return;
598 | if (!isArray(collection))
599 | throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr);
600 | var originalPreviousElementsLength = previousElements.length;
601 | // First, reconcile previousElements and collection with respect to the previousElementBuffer.
602 | // Basically, try to grow previousElements to collection.length if we can.
603 | if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) {
604 | var limit = previousElements.length + previousElementBuffer.length;
605 | if (limit > collection.length)
606 | limit = collection.length;
607 | previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length));
608 | }
609 |
610 | var currentElements = null;
611 | var currentElementBuffer = [];
612 |
613 | var newElements = [];
614 | if (collection.length > previousElements.length) {
615 | // Add in enough elements to account for the larger collection.
616 | for (var i = previousElements.length; i < collection.length; ++i) {
617 | // Need to add in an element for each new item in the collection.
618 | var newElement = {
619 | scope: $scope.$new(),
620 | isActiveForRepeat: true,
621 | };
622 |
623 | gateWatchersForScope(newElement);
624 | newElement.scope.$index = i;
625 | newElement.scope.$first = (i == 0);
626 | newElements.push(newElement);
627 | }
628 | currentElements = previousElements.concat(newElements);
629 | currentElementBuffer = previousElementBuffer;
630 | } else if (collection.length < previousElements.length) {
631 | for (var i = collection.length; i < previousElements.length; ++i)
632 | previousElements[i].isActiveForRepeat = false;
633 |
634 | currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat(
635 | previousElementBuffer);
636 | currentElements = previousElements;
637 | } else {
638 | currentElements = previousElements;
639 | currentElementBuffer = previousElementBuffer;
640 | }
641 |
642 | // We have to fix up the last and middle values in the scope for each element in
643 | // currentElements, since their roles may have changed with the new length.
644 | // We always have to fix the last element.
645 | if (currentElements.length > 0) {
646 | var firstIndexToFix = currentElements.length - 1;
647 | var lastIndexToFix = currentElements.length - 1;
648 | // We also have to fix any new elements that were added.
649 | if (originalPreviousElementsLength < currentElements.length) {
650 | firstIndexToFix = originalPreviousElementsLength;
651 | }
652 | // And we usually have to fix the element before the first element we modified
653 | // in case it used to be last.
654 | if (firstIndexToFix > 0) {
655 | firstIndexToFix = firstIndexToFix - 1;
656 | }
657 | for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) {
658 | currentElements[i].scope.$last = (i == (currentElements.length - 1));
659 | currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1)));
660 | if (!currentElements[i].isActiveForRepeat) {
661 | // If it is not marked as active, make it active. This is also indicates that
662 | // the element is currently hidden, so we have to unhide it.
663 | currentElements[i].isActiveForRepeat = true;
664 | currentElements[i].element.css('display', '');
665 | }
666 | }
667 | }
668 |
669 | // Hide all elements that have recently become inactive.
670 | for (var i = 0; i < currentElementBuffer.length; ++i) {
671 | if (currentElementBuffer[i].isActiveForRepeat)
672 | break;
673 | currentElementBuffer[i].element.css('display', 'none');
674 | }
675 |
676 | // Assign the new value for the iter variable for each scope.
677 | for (var i = 0; i < currentElements.length; ++i) {
678 | currentElements[i].scope[iterVar] = collection[i];
679 | }
680 |
681 | // We have to go back now and clone the DOM element for any new elements we
682 | // added and link them in. We clone the last DOM element we had created already
683 | // for this Repeat.
684 | var prevElement = $element;
685 | if (previousElements.length > 0)
686 | prevElement = previousElements[previousElements.length - 1].element;
687 | for (var i = 0; i < newElements.length; ++i) {
688 | linker(newElements[i].scope, function(clone) {
689 | $animate.enter(clone, null, prevElement);
690 | prevElement = clone;
691 | newElements[i].element = clone;
692 | });
693 | }
694 |
695 | previousElements = currentElements;
696 | previousElementBuffer = currentElementBuffer;
697 | });
698 | $scope.$on('$destroy', function() {
699 | deregisterCallback();
700 | });
701 | };
702 | }
703 | };
704 | }]);
705 | /**
706 | * @fileoverview
707 | * Defines an extension to angular.Scope that allows for registering
708 | * 'gating functions' on a scope that will prevent all future watchers
709 | * registered on the scope from being evaluated unless the gating function
710 | * returns true.
711 | *
712 | * By depending on this module, the $rootScope instance and angular.Scope
713 | * class are automatically extended to implement this new capability.
714 | *
715 | * Warning, this implementation depends on protected/private variables
716 | * in the angular.Scope implementation and therefore can break in the
717 | * future due to changes in the angular.Scope implementation. Use at
718 | * your own risk.
719 | */
720 | defineScalyrAngularModule('gatedScope', [])
721 | .config(['$provide', function($provide) {
722 | // We use a decorator to override methods in $rootScope.
723 | $provide.decorator('$rootScope', ['$delegate', '$exceptionHandler',
724 | function ($rootScope, $exceptionHandler) {
725 |
726 | // Make a copy of $rootScope's original methods so that we can access
727 | // them to invoke super methods in the ones we override.
728 | var scopePrototype = {};
729 | for (var key in $rootScope) {
730 | if (isFunction($rootScope[key]))
731 | scopePrototype[key] = $rootScope[key];
732 | }
733 |
734 | var Scope = $rootScope.constructor;
735 |
736 | // Hold all of our new methods.
737 | var methodsToAdd = {
738 | };
739 |
740 | // A constant value that the $digest loop implementation depends on. We
741 | // grab it down below.
742 | var initWatchVal;
743 |
744 | /**
745 | * @param {Boolean} isolate Whether or not the new scope should be isolated.
746 | * @returns {Scope} A new child scope
747 | */
748 | methodsToAdd.$new = function(isolate) {
749 | // Because of how scope.$new works, the returned result
750 | // should already have our new methods.
751 | var result = scopePrototype.$new.call(this, isolate);
752 |
753 | // We just have to do the work that normally a child class's
754 | // constructor would perform -- initializing our instance vars.
755 | result.$$gatingFunction = this.$$gatingFunction;
756 | result.$$parentGatingFunction = this.$$gatingFunction;
757 | result.$$shouldGateFunction = this.$$shouldGateFunction;
758 | result.$$gatedWatchers = [];
759 | result.$$cleanUpQueue = this.$$cleanUpQueue;
760 |
761 | return result;
762 | };
763 |
764 | /**
765 | * Digests all of the gated watchers for the specified gating function.
766 | *
767 | * @param {Function} targetGatingFunction The gating function associated
768 | * with the watchers that should be digested
769 | * @returns {Boolean} True if any of the watchers were dirty
770 | */
771 | methodsToAdd.$digestGated = function gatedScopeDigest(targetGatingFunction) {
772 | // Note, most of this code was stolen from angular's Scope.$digest method.
773 | var watch, value,
774 | watchers,
775 | length,
776 | next, current = this, target = this, last,
777 | dirty = false;
778 |
779 | do { // "traverse the scopes" loop
780 | if (watchers = current.$$gatedWatchers) {
781 | // process our watches
782 | length = watchers.length;
783 | while (length--) {
784 | try {
785 | watch = watchers[length];
786 | // Scalyr edit: We do not process a watch function if it is does not
787 | // have the same gating function for which $digestGated was invoked.
788 | if (watch.gatingFunction !== targetGatingFunction)
789 | continue;
790 |
791 | // Since we are about to execute the watcher as part of a digestGated
792 | // call, we can remove it from the normal digest queue if it was placed
793 | // there because the watcher was added after the gate function's first
794 | // evaluation.
795 | if (watch && !isNull(watch.cleanUp)) {
796 | watch.cleanUp();
797 | watch.cleanUp = null;
798 | }
799 | // Most common watches are on primitives, in which case we can short
800 | // circuit it with === operator, only when === fails do we use .equals
801 | if (watch && (value = watch.get(current)) !== (last = watch.last) &&
802 | !(watch.eq
803 | ? areEqual(value, last)
804 | : (typeof value == 'number' && typeof last == 'number'
805 | && isNaN(value) && isNaN(last)))) {
806 | dirty = true;
807 | watch.last = watch.eq ? copy(value) : value;
808 | watch.fn(value, ((last === initWatchVal) ? value : last), current);
809 | // Scalyr edit: Removed the logging code for when the ttl is reached
810 | // here because we don't have access to the ttl in this method.
811 | }
812 | } catch (e) {
813 | $exceptionHandler(e);
814 | }
815 | }
816 | }
817 |
818 | // Insanity Warning: scope depth-first traversal
819 | // yes, this code is a bit crazy, but it works and we have tests to prove it!
820 | // Scalyr edit: This insanity warning was from angular. We only modified this
821 | // code by checking the $$gatingFunction because it's a good optimization to only go
822 | // down a child of a parent that has the same gating function as what we are processing
823 | // (since if a parent already has a different gating function, there's no way any
824 | // of its children will have the right one).
825 | if (!(next = ((current.$$gatingFunction === targetGatingFunction && current.$$childHead)
826 | || (current !== target && current.$$nextSibling)))) {
827 | while(current !== target && !(next = current.$$nextSibling)) {
828 | current = current.$parent;
829 | }
830 | }
831 | } while ((current = next));
832 |
833 | // Mark that this gating function has digested all children.
834 | targetGatingFunction.hasDigested = true;
835 | return dirty;
836 | };
837 |
838 | /**
839 | * @inherited $watch
840 | * @param directiveName The fourth parameter is a new optional parameter that allows
841 | * directives aware of this abstraction to pass in their own names to identify
842 | * which directive is registering the watch. This is then passed to the
843 | * shouldGateFunction to help determine if the watcher should be gated by the current
844 | * gatingFunction.
845 | */
846 | methodsToAdd.$watch = function gatedWatch(watchExpression, listener, objectEquality,
847 | directiveName) {
848 | // Determine if we should gate this watcher.
849 | if (!isNull(this.$$gatingFunction) && (isNull(this.$$shouldGateFunction) ||
850 | this.$$shouldGateFunction(watchExpression, listener, objectEquality, directiveName))) {
851 | // We do a hack here to just switch out the watchers array with our own
852 | // gated list and then invoke the original watch function.
853 | var tmp = this.$$watchers;
854 | this.$$watchers = this.$$gatedWatchers;
855 | // Invoke original watch function.
856 | var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
857 | this.$$watchers = tmp;
858 | this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction;
859 | this.$$gatedWatchers[0].cleanUp = null;
860 |
861 | // We know that the last field of the watcher object will be set to initWatchVal, so we
862 | // grab it here.
863 | initWatchVal = this.$$gatedWatchers[0].last;
864 | var watch = this.$$gatedWatchers[0];
865 |
866 | // We should make sure the watch expression gets evaluated fully on at least one
867 | // digest cycle even if the gate function is now closed if requested by the gating function's
868 | // value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute
869 | // the watcher we just added and remove itself after the digest cycle completes.
870 | if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) {
871 | var self = this;
872 | watch.cleanUp = scopePrototype.$watch.call(self, function() {
873 | if (!isNull(watch.cleanUp)) {
874 | self.$$cleanUpQueue.unshift(watch.cleanUp);
875 | watch.cleanUp = null;
876 | }
877 | var value;
878 | var last = initWatchVal;
879 |
880 | if (watch && (value = watch.get(self)) !== (last = watch.last) &&
881 | !(watch.eq
882 | ? areEqual(value, last)
883 | : (typeof value == 'number' && typeof last == 'number'
884 | && isNaN(value) && isNaN(last)))) {
885 | watch.last = watch.eq ? copy(value) : value;
886 | watch.fn(value, ((last === initWatchVal) ? value : last), self);
887 | }
888 | return watch.last;
889 | });
890 | }
891 | return result;
892 | } else {
893 | return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
894 | }
895 | };
896 |
897 | /**
898 | * @inherited $digest
899 | */
900 | methodsToAdd.$digest = function gatedDigest() {
901 | // We have to take care if a scope's digest method was invoked that has a
902 | // gating function in the parent scope. In this case, the watcher for that
903 | // gating function is registered in the parent (the one added in gatedWatch),
904 | // and will not be evaluated here. So, we have to manually see if the gating
905 | // function is true and if so, evaluate any gated watchers for that function on
906 | // this scope. This needs to happen to properly support invoking $digest on a
907 | // scope with a parent scope with a gating function.
908 | // NOTE: It is arguable that we are not correctly handling nested gating functions
909 | // here since we do not know if the parent gating function was nested in other gating
910 | // functions and should be evaluated at all. However, if a caller is invoking
911 | // $digest on a particular scope, we assume the caller is doing that because it
912 | // knows the watchers should be evaluated.
913 | var dirty = false;
914 | if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) {
915 | var ttl = 5;
916 | do {
917 | dirty = this.$digestGated(this.$$parentGatingFunction);
918 | ttl--;
919 |
920 | if (dirty && !(ttl--)) {
921 | throw Error(TTL + ' $digest() iterations reached for gated watcher. Aborting!\n' +
922 | 'Watchers fired in the last 5 iterations.');
923 | }
924 | } while (dirty);
925 | }
926 |
927 | dirty = scopePrototype.$digest.call(this) || dirty;
928 |
929 | var cleanUpQueue = this.$$cleanUpQueue;
930 |
931 | while (cleanUpQueue.length)
932 | try {
933 | cleanUpQueue.shift()();
934 | } catch (e) {
935 | $exceptionHandler(e);
936 | }
937 |
938 | return dirty;
939 | }
940 |
941 | /**
942 | * Modifies this scope so that all future watchers registered by $watch will
943 | * only be evaluated if gatingFunction returns true. Optionally, you may specify
944 | * a function that will be evaluted on every new call to $watch with the arguments
945 | * passed to it, and that watcher will only be gated if the function returns true.
946 | *
947 | * @param {Function} gatingFunction The gating function which controls whether or not all future
948 | * watchers registered on this scope and its children will be evaluated on a given
949 | * digest cycle. The function will be invoked (with no arguments) on every digest
950 | * and if it returns a truthy result, will cause all gated watchers to be evaluated.
951 | * @param {Function} shouldGateFunction The function that controls whether or not
952 | * a new watcher will be gated using gatingFunction. It is evaluated with the
953 | * arguments to $watch and should return true if the watcher created by those
954 | * arguments should be gated
955 | * @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added
956 | * after the gating function has returned true on a previous digest cycle, the
957 | * the new watcher will be evaluated on the next digest cycle even if the
958 | * gating function is currently return false.
959 | */
960 | methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction,
961 | shouldEvalNewWatchers) {
962 | var changeCount = 0;
963 | var self = this;
964 |
965 | // Set a watcher that sees if our gating function is true, and if so, digests
966 | // all of our associated watchers. Note, this.$watch could already have a
967 | // gating function associated with it, which means this watch won't be executed
968 | // unless all gating functions before us have evaluated to true. We take special
969 | // care of this nested case below.
970 |
971 | // We handle nested gating function in a special way. If we are a nested gating
972 | // function (meaning there is already one or more gating functions on this scope and
973 | // our parent scopes), then if those parent gating functions every all evaluate to
974 | // true (which we can tell if the watcher we register here is evaluated), then
975 | // we always evaluate our watcher until our gating function returns true.
976 | var hasNestedGates = !isNull(this.$$gatingFunction);
977 |
978 | (function() {
979 | var promotedWatcher = null;
980 |
981 | self.$watch(function() {
982 | if (gatingFunction()) {
983 | if (self.$digestGated(gatingFunction))
984 | ++changeCount;
985 | } else if (hasNestedGates && isNull(promotedWatcher)) {
986 | promotedWatcher = scopePrototype.$watch.call(self, function() {
987 | if (gatingFunction()) {
988 | promotedWatcher();
989 | promotedWatcher = null;
990 | if (self.$digestGated(gatingFunction))
991 | ++changeCount;
992 | }
993 | return changeCount;
994 | });
995 | }
996 | return changeCount;
997 | });
998 | })();
999 |
1000 |
1001 | if (isUndefined(shouldGateFunction))
1002 | shouldGateFunction = null;
1003 | if (isUndefined(shouldEvalNewWatchers))
1004 | shouldEvalNewWatchers = false;
1005 | this.$$gatingFunction = gatingFunction;
1006 | this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers;
1007 | this.$$shouldGateFunction = shouldGateFunction;
1008 | };
1009 |
1010 | // Extend the original Scope object so that when
1011 | // new instances are created, it has the new methods.
1012 | angular.extend(Scope.prototype, methodsToAdd);
1013 |
1014 | // Also extend the $rootScope instance since it was created
1015 | // before we got a chance to extend Scope.prototype.
1016 | angular.extend($rootScope, methodsToAdd);
1017 |
1018 | $rootScope.$$gatingFunction = null;
1019 | $rootScope.$$parentGatingFunction = null;
1020 | $rootScope.$$shouldGateFunction = null;
1021 | $rootScope.$$gatedWatchers = [];
1022 | $rootScope.$$cleanUpQueue = [];
1023 |
1024 | return $rootScope;
1025 | }]);
1026 | }]);
1027 |
--------------------------------------------------------------------------------
/scripts/buildScalyr.js:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Usage: buildScalyr.js
4 | #
5 | # Concatenates the necessary javascript files to produce scalyr.js.
6 |
7 | function die() {
8 | echo "$1";
9 | exit 1;
10 | }
11 |
12 | if [ ! -f "src/js/core.js" ]; then
13 | die "Could not locate core.js. Are you sure you running from the toplevel directory?";
14 | fi
15 |
16 |
17 | sources=(
18 | "scripts/includeFiles/header.js"
19 | "src/js/core.js"
20 | "src/js/directives/slyEvaluate.js"
21 | "src/js/directives/slyRepeat.js"
22 | "src/js/lib/gatedScope.js"
23 | )
24 |
25 | if [ -f "scalyr.js" ]; then
26 | rm scalyr.js || die "Could not remove scalyr.js file";
27 | fi
28 |
29 | for x in ${sources[@]}; do
30 | cat "$x" >> scalyr.js || die "Failed to build scalyr.js file";
31 | done
32 |
33 | exit 0;
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/scripts/includeFiles/header.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license scalyr v1.0.3
3 | * (c) 2013 Scalyr, Inc. http://scalyr.com
4 | * License: MIT
5 | */
6 |
7 | 'use strict';
8 |
9 | // You may just depend on the 'sly' module to pull in all of the
10 | // dependencies.
11 | angular.module('sly', ['slyEvaluate', 'slyRepeat']);
12 |
--------------------------------------------------------------------------------
/scripts/includeFiles/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // This assumes you run karma from the root of the repository.
3 | //
4 |
5 | module.exports = function(config) {
6 | config.set({
7 | // base path, that will be used to resolve files and exclude
8 | basePath: '../../',
9 |
10 | // Jasmine is our unit test framework.
11 | frameworks: ['jasmine'],
12 |
13 | // list of files / patterns to load in the browser
14 | files: [
15 | 'src/js/thirdparty/angular.js',
16 | 'src/tests/thirdparty/angular-mocks.js',
17 | 'src/tests/thirdparty/sinon.js',
18 | 'src/js/core.js',
19 | 'src/js/lib/*.js',
20 | 'src/js/directives/*.js',
21 | 'src/tests/*.js',
22 | 'src/tests/lib/*.js',
23 | 'src/tests/directives/*.js',
24 | { pattern: 'src/tests/directives/*.html', included: false, served: true },
25 | { pattern: 'src/tests/lib/*.html', included: false, served: true },
26 | { pattern: 'src/tests/thirdparty/jasmine/*.js', included: false, served: true },
27 | { pattern: 'src/tests/thirdparty/jasmine/*.css', included: false, served: true },
28 | ],
29 |
30 | // Tell the http server to sever all files from http://localhost:9876/src/
31 | proxies: {
32 | '/src/': 'http://localhost:9876/base/src/',
33 | },
34 |
35 | // list of files to exclude
36 | exclude: [
37 | ],
38 |
39 | // test results reporter to use
40 | // possible values: 'dots', 'progress', 'junit'
41 | reporters: ['progress'],
42 |
43 |
44 | // web server port
45 | port: 9876,
46 |
47 |
48 | // cli runner port
49 | runnerPort: 9100,
50 |
51 | // enable / disable colors in the output (reporters and logs)
52 | colors: true,
53 |
54 | // level of logging
55 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
56 | logLevel: config.LOG_INFO,
57 |
58 | // enable / disable watching file and executing tests whenever any file changes
59 | autoWatch: true,
60 |
61 | // Start these browsers, currently available:
62 | // - Chrome
63 | // - ChromeCanary
64 | // - Firefox
65 | // - Opera
66 | // - Safari (only Mac)
67 | // - PhantomJS
68 | // - IE (only Windows)
69 | browsers: ['Chrome'],
70 |
71 |
72 | // If browser does not capture in given timeout [ms], kill it
73 | captureTimeout: 60000,
74 |
75 | // Continuous Integration mode
76 | // if true, it capture browsers, run tests and exit
77 | singleRun: false,
78 |
79 | plugins: [
80 | 'karma-jasmine',
81 | 'karma-chrome-launcher',
82 | ],
83 | });
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/scripts/startJsTester:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Usage: startJsTester
4 | #
5 | # This starts karma, the javascript unit test framework. This script
6 | # is meant to be run on a development machine. It will launch Chrome
7 | # and execute all javascript unit tests, reporting the results to
8 | # stdout. By default, the script will never terminate, but instead watch
9 | # for changes to any of the monitored javascript files and HTML resources
10 | # (defined in a configuration file), rerunning all unit tests with each new
11 | # change and reporting the results to stdout.
12 | #
13 | # The unit test framework also runs a light weight http server on
14 | # port 9876 allowing you to access local files over http, enabling
15 | # direct testing of some of the html files. This is better than just
16 | # using the browser to access the files via file:// because the browser's
17 | # default security policies prevents javascript loaded via file:// to
18 | # load other javascript files.
19 | #
20 | # To use this script, you must have installed node.js and karma.
21 | # To install karma, do the following:
22 | # 1. Download the node.js pkg file from http://nodejs.org and follow instructions.
23 | # 2. sudo npm install -g karma karma-jasmine karma-chrome-launcher
24 |
25 | # The karma configuration file, which details configures which js files
26 | # are included, how to run http server and what files to export, etc,
27 | # is in scripts/includeFiles/karma.conf.js
28 |
29 | function die() {
30 | echo "$1";
31 | exit 1;
32 | }
33 |
34 | cd "$(dirname $0)" || die "Failed to cd to script directory";
35 |
36 | karma start includeFiles/karma.conf.js;
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/js/core.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Defines core functions used throughout the Scalyr javascript
4 | * code base. This file is included on every page.
5 | *
6 | * @author Steven Czerwinski
7 | */
8 |
9 | /**
10 | * @param {Object} value The value to check
11 | * @returns {Boolean} True if value is an Array
12 | */
13 | function isArray(value) {
14 | return Object.prototype.toString.call(value) === '[object Array]';
15 | }
16 |
17 | /**
18 | * @param {*} value The value to check
19 | * @returns {Boolean} True if value is a Boolean
20 | */
21 | function isBoolean(value) {
22 | return typeof value == 'boolean';
23 | }
24 |
25 | /**
26 | * @param {Object} value The value to check
27 | * @returns {Boolean} True if value is a Date object
28 | */
29 | function isDate(value) {
30 | return Object.prototype.toString.call(value) === '[object Date]';
31 | }
32 |
33 | /**
34 | * @param {*} value The value to check
35 | * @returns {Boolean} True if value is undefined
36 | */
37 | function isDefined(value) {
38 | return typeof value != 'undefined';
39 | }
40 |
41 | /**
42 | * @param {*} value The value to check
43 | * @returns {Boolean} True if value is a Function
44 | */
45 | function isFunction(value) {
46 | return typeof value == 'function';
47 | }
48 |
49 | /**
50 | * @param {*} value The value to check
51 | * @returns {Boolean} True if value is null
52 | */
53 | function isNull(value) {
54 | return value === null;
55 | }
56 |
57 | /**
58 | * @param {*} value The value to check
59 | * @returns {Boolean} True if value is a Number
60 | */
61 | function isNumber(value) {
62 | return typeof value == 'number';
63 | }
64 |
65 | /**
66 | * @param {*} value The value to check
67 | * @returns {Boolean} True if value is an Object, not including null
68 | */
69 | function isObject(value) {
70 | return value !== null && typeof value == 'object';
71 | }
72 |
73 | /**
74 | * @param {*} value The value to check
75 | * @returns {Boolean} True if value is a string
76 | */
77 | function isString(value) {
78 | return typeof value == 'string';
79 | }
80 |
81 | /**
82 | * @param {*} value The value to check
83 | * @returns {Boolean} True if value is undefined
84 | */
85 | function isUndefined(value) {
86 | return typeof value == 'undefined';
87 | }
88 |
89 | /**
90 | * Converts a String or Boolean value to a Boolean.
91 | *
92 | * @param {String|Boolean} value The value to convert
93 | * @returns {Boolean} Returns true for any String that is not
94 | * null, empty String, or 'false'. If value is a Boolean,
95 | * returns value
96 | */
97 | function convertToBoolean(value) {
98 | if (isBoolean(value))
99 | return value;
100 | return value !== null && value !== '' && value !== 'false';
101 | }
102 |
103 | /**
104 | * Determines if obj has a property named prop.
105 | *
106 | * @param {Object} obj The object to check
107 | * @returns {Boolean} Returns true if obj has a property named
108 | * prop. Only considers the object's own properties
109 | */
110 | function hasProperty(obj, prop) {
111 | return obj.hasOwnProperty(prop);
112 | }
113 |
114 | /**
115 | * @param {*} value The value to check
116 | * @returns {Boolean} Returns true if value is a String
117 | * and has zero length, or if null or undefined
118 | */
119 | function isStringEmpty(value) {
120 | return isNull(value) || isUndefined(value) ||
121 | (isString(value) && (value.length == 0));
122 | }
123 |
124 | /**
125 | * @param {*} value The value to check
126 | * @returns {Boolean} Returns true if value is a String
127 | * and has non-zero length
128 | */
129 | function isStringNonempty(value) {
130 | return isString(value) && (value.length > 0);
131 | }
132 |
133 | /**
134 | * Returns input with the first letter capitalized.
135 | * The input may not be zero length.
136 | *
137 | * @param {String} input The String to capitalize.
138 | * @returns {String} Returns input with the first letter
139 | * capitalized.
140 | */
141 | function upperCaseFirstLetter(input) {
142 | return input.charAt(0).toUpperCase() + input.slice(1);
143 | }
144 |
145 | /**
146 | * Returns true if obj1 and obj2 are equal. This should
147 | * only be used for Arrays, Objects, and value types. This is a deep
148 | * comparison, comparing each property and recursive property to
149 | * be equal (not just ===).
150 | *
151 | * Two Objects or values are considered equivalent if at least one of the following is true:
152 | * - Both objects or values pass `===` comparison.
153 | * - Both objects or values are of the same type and all of their properties pass areEqual
154 | * comparison.
155 | * - Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal).
156 | *
157 | * Note, during property comparision, properties with function values are ignores as are property
158 | * names beginning with '$'.
159 | *
160 | * See angular.equal for more details.
161 | *
162 | * @param {Object|Array|value} obj1 The first object
163 | * @param {Object|Array|value} obj2 The second object
164 | * @returns {Boolean} True if the two objects are equal using a deep
165 | * comparison.
166 | */
167 | function areEqual(obj1, obj2) {
168 | return angular.equals(obj1, obj2);
169 | }
170 |
171 | /**
172 | * @param {Number} a The first Number
173 | * @param {Number} b The second Number
174 | * @returns {Number} The minimum of a and b
175 | */
176 | function min(a, b) {
177 | return a < b ? a : b;
178 | }
179 |
180 | /**
181 | * @param {Number} a The first Number
182 | * @param {Number} b The second Number
183 | * @returns {Number} The maximum of a and b
184 | */
185 | function max(a, b) {
186 | return a > b ? a : b;
187 | }
188 |
189 | /**
190 | * Returns true if the specified String begins with prefix.
191 | *
192 | * @param {*} input The input to check
193 | @ @param {String} prefix The prefix
194 | * @returns {Boolean} True if input is a string that begins with prefix
195 | */
196 | function beginsWith(input, prefix) {
197 | return isString(input) && input.lastIndexOf(prefix, 0) == 0;
198 | }
199 |
200 | /**
201 | * Returns true if the specified String ends with prefix.
202 | *
203 | * @param {*} input The input to check
204 | @ @param {String} postfix The postfix
205 | * @returns {Boolean} True if input is a string that ends with postfix
206 | */
207 | function endsWith(input, postfix) {
208 | return isString(input) && input.indexOf(postfix, input.length - postfix.length) !== -1;
209 | }
210 |
211 | /**
212 | * Returns a deep copy of source, where source can be an Object or an Array. If a destination is
213 | * provided, all of its elements (for Array) or properties (for Objects) are deleted and then all
214 | * elements/properties from the source are copied to it. If source is not an Object or Array,
215 | * source is returned.
216 | *
217 | * See angular.copy for more details.
218 | * @param {Object|Array} source The source
219 | * @param {Object|Array} destination Optional object to copy the elements to
220 | * @returns {Object|Array} The deep copy of source
221 | */
222 | function copy(source, destination) {
223 | return angular.copy(source, destination);
224 | }
225 |
226 | /**
227 | * Removes property from obj.
228 | *
229 | * @param {Object} obj The object
230 | * @param {String} property The property name to delete
231 | */
232 | function removeProperty(obj, property) {
233 | delete obj[property];
234 | }
235 |
236 | /**
237 | * Removes all properties in the array from obj.
238 | *
239 | * @param {Object} obj The object
240 | * @param {Array} properties The properties to remove
241 | */
242 | function removeProperties(obj, properties) {
243 | for (var i = 0; i < properties.length; ++i)
244 | delete obj[properties[i]];
245 | }
246 |
247 | /**
248 | * Invokes the iterator function once for each item in obj collection, which can be either
249 | * an Object or an Array. The iterator function is invoked with iterator(value, key),
250 | * where value is the value of an object property or an array element and key is the
251 | * object property key or array element index. Specifying a context for the function is
252 | * optional. If specified, it becomes 'this' when iterator function is invoked.
253 | *
254 | * See angular.forEach for more details.
255 | *
256 | * @param {Object|Array} The Object or Array over which to iterate
257 | * @param {Function} iterator The iterator function to invoke
258 | * @param {Object} context The value to set for 'this' when invoking the
259 | * iterator function. This is optional
260 | */
261 | function forEach(obj, iterator, context) {
262 | return angular.forEach(obj, iterator, context);
263 | }
264 |
265 | /**
266 | * Used to define a Scalyr javascript library and optionally declare
267 | * dependencies on other libraries. All javascript code not defined in
268 | * this file should be defined as part of a library.
269 | *
270 | * The first argument is the name to call the library. The second argument
271 | * is either a Constructor object for the library or an array where the last
272 | * element is the Constructor for the library and the first to N-1 are string
273 | * names of the libraries this one depends on. If you do declare dependencies,
274 | * the libraries are passed in the Constructor create method in the same order
275 | * as the strings are defined.
276 | *
277 | * Example:
278 | * defineScalyrJsLibrary('myUtils', function() {
279 | * var fooFunction = function(a, b) {
280 | * return a + b;
281 | * };
282 | * return {
283 | * foo: fooFunction
284 | * };
285 | * });
286 | *
287 | * defineScalyrJsLibrary('anotherUtils', [ 'myUtils', function(myUtils) {
288 | * var barFunction = function(a, b) {
289 | * return myUtils.foo(a, b);
290 | * };
291 | * return {
292 | * bar: barFunction
293 | * };
294 | * });
295 | *
296 | * @param {String} libraryName The name for the library
297 | * @param {Constructor|Array} libraryExporter The exporter for the
298 | * library. See above for details
299 | */
300 | function defineScalyrJsLibrary(libraryName, libraryExporter) {
301 | var moduleDependencies = [];
302 | if (libraryExporter instanceof Array) {
303 | for (var i = 0; i < libraryExporter.length - 1; ++i)
304 | moduleDependencies.push(libraryExporter[i]);
305 | }
306 |
307 | return angular.module(libraryName, moduleDependencies)
308 | .factory(libraryName, libraryExporter);
309 | }
310 |
311 | /**
312 | * Similar to defineScalyrJsLibary but instead of declaring
313 | * a purely javascript library, this declares an Angular module
314 | * library. The moduleName should be a string used to identify
315 | * this module. The dependencies is an array with the string
316 | * names of Angular modules, Scalyr Angular modules, or Scalyr
317 | * javascript libraries to depend on. The returned object
318 | * can be used to define directives, etc similar to angular.module.
319 | *
320 | * Example:
321 | * defineScalyrAngularModule('slyMyModule', [ 'myTextUtils'])
322 | * .filter('camelCase', function(myTextUtils) {
323 | * return function(input) {
324 | * return myTextUtils.camelCase(input);
325 | * };
326 | * });
327 | *
328 | * @param {String} moduleName The name of the module
329 | * @param {Array} dependencies The names of modules to depend on
330 | */
331 | function defineScalyrAngularModule(moduleName, dependencies) {
332 | return angular.module(moduleName, dependencies);
333 | }
334 |
--------------------------------------------------------------------------------
/src/js/directives/slyEvaluate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Module: slyEvaluate
4 | *
5 | * Defines several directives related to preventing evaluating watchers
6 | * on scopes under certain conditions. Here's a list of the directives
7 | * and brief descriptions. See down below for more details.
8 | *
9 | * slyEvaluateOnlyWhen: A directive that prevents updating / evaluating
10 | * all bindings for the current element and its children unless
11 | * the expression has changed values. If new children are added, they
12 | * are always evaluated at least once. It currently assumes the
13 | * expression evaluates to an object and detects changes only by
14 | * a change in object reference.
15 | *
16 | * slyAlwaysEvaluate: Can only be used in conjunction with the
17 | * slyEvaluateOnlyWhen directive. This directive will ensure that
18 | * any expression that is being watched will always be evaluated
19 | * if it contains the specified string (i.e., it will ignore whether
20 | * or not the slyEvaluateOnlyWhen expression has changed.) This
21 | * is useful when you wish to check some expressions all the time.
22 | *
23 | * slyPreventEvaluationWhenHidden: Will only evaluate the bindings
24 | * for the current element and its children if the current element
25 | * is not hidden (detected by the element having the ng-hide CSS class.)
26 | *
27 | * slyShow: Will hide the element if the expression evaluates to false.
28 | * Uses ng-hide to hide the element. This is almost exactly the same
29 | * as ngShow, but it has the advantage that it works better with
30 | * slyPreventEvaluationWhenHidden by guaranteeing it will always evaluate
31 | * its show expression to determine if it should or should not be hidden.
32 | */
33 | defineScalyrAngularModule('slyEvaluate', ['gatedScope'])
34 | /**
35 | * Directive for preventing all bound expressions in the current element and its children
36 | * from being evaluated unless the specified expression evaluates to a different object.
37 | * Currently, the value assigned to the 'slyEvaluateOnlyWhen' must evaluate to an object.
38 | * Also, reference equality is used to determine if the expression has changed.
39 | * TODO: Make this more versatile, similar to $watch. For now, this is all we need.
40 | */
41 | .directive('slyEvaluateOnlyWhen', ['$parse', function ($parse) {
42 | return {
43 | // We create a new scope just because it helps segment the gated watchers
44 | // from the parent scope. Unclear if this is that important for perf.
45 | scope: true,
46 | restrict: 'A',
47 | compile: function compile(tElement, tAttrs) {
48 | return {
49 | // We need a separate pre-link function because we want to modify the scope before any of the
50 | // children are passed it.
51 | pre: function preLink(scope, element, attrs) {
52 | var previousValue = null;
53 | var initialized = false;
54 |
55 | var expressionToCheck = $parse(attrs['slyEvaluateOnlyWhen']);
56 | var alwaysEvaluateString = null;
57 | if (hasProperty(attrs, 'slyAlwaysEvaluate')) {
58 | alwaysEvaluateString = attrs['slyAlwaysEvaluate'];
59 | if (isStringEmpty(alwaysEvaluateString))
60 | throw new Exception('Empty string is illegal for value of slyAlwaysEvaluate');
61 | }
62 | scope.$addWatcherGate(function evaluteOnlyWhenChecker() {
63 | // We should only return true if expressionToCheck evaluates to a value different
64 | // than previousValue.
65 | var currentValue = expressionToCheck(scope);
66 | if (!initialized) {
67 | initialized = true;
68 | previousValue = currentValue;
69 | return true;
70 | }
71 | var result = previousValue !== currentValue;
72 | previousValue = currentValue;
73 | return result;
74 | }, function shouldGateWatcher(watchExpression) {
75 | // Should return true if the given watcher that's about to be registered should
76 | // be gated.
77 | return isNull(alwaysEvaluateString) ||
78 | !(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0));
79 | }, true /* Evaluate any newly added watchers when they are added */);
80 | },
81 | };
82 | },
83 | };
84 | }])
85 | /**
86 | * Directive for overriding the 'slyEvaluateOnlyWhen' expression for the current element.
87 | * This directive takes a single string value. If this string value is found anywhere in
88 | * an expression that normally would not be evaluated due to the 'slyEvaluateOnlyWhen'
89 | * directive, it is evaluated, regardless of whether or not the value for the expression in
90 | * 'slyEvaluateOnlyWhen' has changed. This is very useful when a certain expression used by
91 | * one of the children of the current element should always be evaluated and is not affected
92 | * by the expression specified in slyEvaluateOnlyWhen.
93 | */
94 | .directive('slyAlwaysEvaluate', function() {
95 | // This is just a place holder to show that slyAlwaysEvaluate is a legal
96 | // directive. The real work for this directive is done in slyEvaluateOnlyWhen.
97 | return {
98 | restrict: 'A',
99 | link: function(scope, element, attrs) {
100 | },
101 | };
102 | })
103 | /**
104 | * Directive for showing an element, very similar to ngShow. However, this directive
105 | * works better with slyPreventEvaluationWhenHidden because it is ensure it always
106 | * will evaluate the show expression to determine if it should be shown or hidden
107 | * even if slyPreventEvaluationWhenHidden is in effect. This directive also uses
108 | * the ng-hide css class to actually hide the element.
109 | *
110 | * NOTE: We might be able to get better performance if we have this directive directly
111 | * perform a callback on slyPreventEvaluationWhenHidden when it is shown/hidden rather
112 | * than having that directive register a watcher on the css class.
113 | */
114 | .directive('slyShow', ['$animate', function($animate) {
115 | /**
116 | * @param {*} value The input
117 | * @return {Boolean} True if the value is truthy as determined by angular rules.
118 | *
119 | * Note: This is copied from the Angular source because it is not exposed by Angular
120 | * but we want our directive to behave the same as ngShow. Think about moving this
121 | * to core.js.
122 | */
123 | function toBoolean(value) {
124 | if (value && value.length !== 0) {
125 | var v = ("" + value);
126 | v = isString(v) ? v.toLowerCase() : v;
127 | value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
128 | } else {
129 | value = false;
130 | }
131 | return value;
132 | }
133 |
134 | return {
135 | restrict: 'A',
136 | link: function slyShowLink(scope, element, attr) {
137 | scope.$watch(attr.slyShow, function ngSlyShowAction(value){
138 | $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
139 | }, false, 'slyShow'); },
140 | };
141 | }])
142 | /**
143 | * Directive for preventing all bound expressions in the current element and its children
144 | * from being evaluated if the current element is hidden as determined by whether or not
145 | * it has the ng-hide class.
146 | */
147 | .directive('slyPreventEvaluationWhenHidden', function () {
148 | return {
149 | restrict: 'A',
150 | // We create a new scope just because it helps segment the gated watchers
151 | // from the parent scope. Unclear if this is that important for perf.
152 | scope: true,
153 | compile: function compile(tElement, tAttrs) {
154 | return {
155 | // We need a separate pre-link function because we want to modify the scope before any of the
156 | // children are passed it.
157 | pre: function preLink(scope, element, attrs) {
158 | scope.$addWatcherGate(function hiddenChecker() {
159 | // Should only return true if the element is not hidden.
160 | return !element.hasClass('ng-hide');
161 | }, function hiddenDecider(watchExpression, listener, equality, directiveName) {
162 | // Make an exception for slyShow.. do not gate its watcher.
163 | if (isDefined(directiveName) && (directiveName == 'slyShow'))
164 | return false;
165 | return true;
166 | });
167 | },
168 | };
169 | },
170 | };
171 | });
172 |
173 |
--------------------------------------------------------------------------------
/src/js/directives/slyRepeat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Module: slyRepeat
4 | *
5 | * Contains the slyRepeat directive, which is is a modified version of the
6 | * ngRepeat directive that is meant to be more efficient for creating and
7 | * recreating large lists of bound elements. In particular, it has an
8 | * optimization that will prevent DOM elements from being constantly created
9 | * and destroyed as the contents of the repeated elements change. It does this
10 | * by not destroying DOM elements when they are no longer needed, but instead,
11 | * just hiding them. This might not work for all use cases, but for it does
12 | * for the ones we do wish to heavily optimize. For eample, through profiling,
13 | * we found that destroying DOM elements when flipping through log view pages
14 | * represented a large chunk of CPU time.
15 | *
16 | * Cavaets: The collection expression must evaluate to an array. Animators
17 | * will not work. Track By does not work. Use at your own peril.
18 | *
19 | * @author Steven Czerwinski
20 | */
21 | defineScalyrAngularModule('slyRepeat', ['gatedScope'])
22 | .directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) {
23 |
24 | /**
25 | * Sets the scope contained in elementScope to gate all its
26 | * watchers based on the isActiveForRepeat proprety.
27 | *
28 | * @param {Object} elementScope The object containing the
29 | * scope and isActiveForRepeat properties.
30 | */
31 | function gateWatchersForScope(elementScope) {
32 | elementScope.scope.$addWatcherGate(function() {
33 | return elementScope.isActiveForRepeat;
34 | });
35 | }
36 |
37 | return {
38 | restrict: 'A',
39 | scope: true,
40 | transclude: 'element',
41 | priority: 1000,
42 | terminal: true,
43 | compile: function(element, attr, linker) {
44 | // Most of the work is done in the post-link function.
45 | return function($scope, $element, $attr) {
46 | // This code is largely based on ngRepeat.
47 |
48 | // Parse the expression. It should look like:
49 | // x in some-expression
50 | var expression = $attr.slyRepeat;
51 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/);
52 | if (!match) {
53 | throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" +
54 | expression + "'.");
55 | }
56 |
57 | var iterVar = match[1];
58 | var collectionExpr = match[2];
59 |
60 | match = iterVar.match(/^(?:([\$\w]+))$/);
61 | if (!match) {
62 | throw Error("'item' in 'item in collection' should be identifier but got '" +
63 | lhs + "'.");
64 | }
65 |
66 | // previousElements will store references to the already existing (DOM) elements
67 | // that were last used for the last rendering of this repeat and were visible.
68 | // We will re-use these elements when executing the next rendering of the repeat when
69 | // the iteration value changes.
70 | var previousElements = [];
71 | // previousElementsBuffer will store references to the already existing (DOM) elements
72 | // that are in the page but were not used for the last rendering of this repeat and were
73 | // therefore marked as inactive and not visible. This happens if the length of the repeat
74 | // iteration goes down over time, since we do not remove the elements. If the repeat length
75 | // was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer.
76 | // We keep this in case the length increases again.
77 | var previousElementBuffer = [];
78 |
79 | var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) {
80 | if (!collection)
81 | return;
82 | if (!isArray(collection))
83 | throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr);
84 | var originalPreviousElementsLength = previousElements.length;
85 | // First, reconcile previousElements and collection with respect to the previousElementBuffer.
86 | // Basically, try to grow previousElements to collection.length if we can.
87 | if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) {
88 | var limit = previousElements.length + previousElementBuffer.length;
89 | if (limit > collection.length)
90 | limit = collection.length;
91 | previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length));
92 | }
93 |
94 | var currentElements = null;
95 | var currentElementBuffer = [];
96 |
97 | var newElements = [];
98 | if (collection.length > previousElements.length) {
99 | // Add in enough elements to account for the larger collection.
100 | for (var i = previousElements.length; i < collection.length; ++i) {
101 | // Need to add in an element for each new item in the collection.
102 | var newElement = {
103 | scope: $scope.$new(),
104 | isActiveForRepeat: true,
105 | };
106 |
107 | gateWatchersForScope(newElement);
108 | newElement.scope.$index = i;
109 | newElement.scope.$first = (i == 0);
110 | newElements.push(newElement);
111 | }
112 | currentElements = previousElements.concat(newElements);
113 | currentElementBuffer = previousElementBuffer;
114 | } else if (collection.length < previousElements.length) {
115 | for (var i = collection.length; i < previousElements.length; ++i)
116 | previousElements[i].isActiveForRepeat = false;
117 |
118 | currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat(
119 | previousElementBuffer);
120 | currentElements = previousElements;
121 | } else {
122 | currentElements = previousElements;
123 | currentElementBuffer = previousElementBuffer;
124 | }
125 |
126 | // We have to fix up the last and middle values in the scope for each element in
127 | // currentElements, since their roles may have changed with the new length.
128 | // We always have to fix the last element.
129 | if (currentElements.length > 0) {
130 | var firstIndexToFix = currentElements.length - 1;
131 | var lastIndexToFix = currentElements.length - 1;
132 | // We also have to fix any new elements that were added.
133 | if (originalPreviousElementsLength < currentElements.length) {
134 | firstIndexToFix = originalPreviousElementsLength;
135 | }
136 | // And we usually have to fix the element before the first element we modified
137 | // in case it used to be last.
138 | if (firstIndexToFix > 0) {
139 | firstIndexToFix = firstIndexToFix - 1;
140 | }
141 | for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) {
142 | currentElements[i].scope.$last = (i == (currentElements.length - 1));
143 | currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1)));
144 | if (!currentElements[i].isActiveForRepeat) {
145 | // If it is not marked as active, make it active. This is also indicates that
146 | // the element is currently hidden, so we have to unhide it.
147 | currentElements[i].isActiveForRepeat = true;
148 | currentElements[i].element.css('display', '');
149 | }
150 | }
151 | }
152 |
153 | // Hide all elements that have recently become inactive.
154 | for (var i = 0; i < currentElementBuffer.length; ++i) {
155 | if (currentElementBuffer[i].isActiveForRepeat)
156 | break;
157 | currentElementBuffer[i].element.css('display', 'none');
158 | }
159 |
160 | // Assign the new value for the iter variable for each scope.
161 | for (var i = 0; i < currentElements.length; ++i) {
162 | currentElements[i].scope[iterVar] = collection[i];
163 | }
164 |
165 | // We have to go back now and clone the DOM element for any new elements we
166 | // added and link them in. We clone the last DOM element we had created already
167 | // for this Repeat.
168 | var prevElement = $element;
169 | if (previousElements.length > 0)
170 | prevElement = previousElements[previousElements.length - 1].element;
171 | for (var i = 0; i < newElements.length; ++i) {
172 | linker(newElements[i].scope, function(clone) {
173 | $animate.enter(clone, null, prevElement);
174 | prevElement = clone;
175 | newElements[i].element = clone;
176 | });
177 | }
178 |
179 | previousElements = currentElements;
180 | previousElementBuffer = currentElementBuffer;
181 | });
182 | $scope.$on('$destroy', function() {
183 | deregisterCallback();
184 | });
185 | };
186 | }
187 | };
188 | }]);
189 |
--------------------------------------------------------------------------------
/src/js/lib/gatedScope.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Defines an extension to angular.Scope that allows for registering
4 | * 'gating functions' on a scope that will prevent all future watchers
5 | * registered on the scope from being evaluated unless the gating function
6 | * returns true.
7 | *
8 | * By depending on this module, the $rootScope instance and angular.Scope
9 | * class are automatically extended to implement this new capability.
10 | *
11 | * Warning, this implementation depends on protected/private variables
12 | * in the angular.Scope implementation and therefore can break in the
13 | * future due to changes in the angular.Scope implementation. Use at
14 | * your own risk.
15 | */
16 | defineScalyrAngularModule('gatedScope', [])
17 | .config(['$provide', function($provide) {
18 | // We use a decorator to override methods in $rootScope.
19 | $provide.decorator('$rootScope', ['$delegate', '$exceptionHandler',
20 | function ($rootScope, $exceptionHandler) {
21 |
22 | // Make a copy of $rootScope's original methods so that we can access
23 | // them to invoke super methods in the ones we override.
24 | var scopePrototype = {};
25 | for (var key in $rootScope) {
26 | if (isFunction($rootScope[key]))
27 | scopePrototype[key] = $rootScope[key];
28 | }
29 |
30 | var Scope = $rootScope.constructor;
31 |
32 | // Hold all of our new methods.
33 | var methodsToAdd = {
34 | };
35 |
36 | // A constant value that the $digest loop implementation depends on. We
37 | // grab it down below.
38 | var initWatchVal;
39 |
40 | /**
41 | * @param {Boolean} isolate Whether or not the new scope should be isolated.
42 | * @returns {Scope} A new child scope
43 | */
44 | methodsToAdd.$new = function(isolate) {
45 | // Because of how scope.$new works, the returned result
46 | // should already have our new methods.
47 | var result = scopePrototype.$new.call(this, isolate);
48 |
49 | // We just have to do the work that normally a child class's
50 | // constructor would perform -- initializing our instance vars.
51 | result.$$gatingFunction = this.$$gatingFunction;
52 | result.$$parentGatingFunction = this.$$gatingFunction;
53 | result.$$shouldGateFunction = this.$$shouldGateFunction;
54 | result.$$gatedWatchers = [];
55 | result.$$cleanUpQueue = this.$$cleanUpQueue;
56 |
57 | return result;
58 | };
59 |
60 | /**
61 | * Digests all of the gated watchers for the specified gating function.
62 | *
63 | * @param {Function} targetGatingFunction The gating function associated
64 | * with the watchers that should be digested
65 | * @returns {Boolean} True if any of the watchers were dirty
66 | */
67 | methodsToAdd.$digestGated = function gatedScopeDigest(targetGatingFunction) {
68 | // Note, most of this code was stolen from angular's Scope.$digest method.
69 | var watch, value,
70 | watchers,
71 | length,
72 | next, current = this, target = this, last,
73 | dirty = false;
74 |
75 | do { // "traverse the scopes" loop
76 | if (watchers = current.$$gatedWatchers) {
77 | // process our watches
78 | length = watchers.length;
79 | while (length--) {
80 | try {
81 | watch = watchers[length];
82 | // Scalyr edit: We do not process a watch function if it is does not
83 | // have the same gating function for which $digestGated was invoked.
84 | if (watch.gatingFunction !== targetGatingFunction)
85 | continue;
86 |
87 | // Since we are about to execute the watcher as part of a digestGated
88 | // call, we can remove it from the normal digest queue if it was placed
89 | // there because the watcher was added after the gate function's first
90 | // evaluation.
91 | if (watch && !isNull(watch.cleanUp)) {
92 | watch.cleanUp();
93 | watch.cleanUp = null;
94 | }
95 | // Most common watches are on primitives, in which case we can short
96 | // circuit it with === operator, only when === fails do we use .equals
97 | if (watch && (value = watch.get(current)) !== (last = watch.last) &&
98 | !(watch.eq
99 | ? areEqual(value, last)
100 | : (typeof value == 'number' && typeof last == 'number'
101 | && isNaN(value) && isNaN(last)))) {
102 | dirty = true;
103 | watch.last = watch.eq ? copy(value) : value;
104 | watch.fn(value, ((last === initWatchVal) ? value : last), current);
105 | // Scalyr edit: Removed the logging code for when the ttl is reached
106 | // here because we don't have access to the ttl in this method.
107 | }
108 | } catch (e) {
109 | $exceptionHandler(e);
110 | }
111 | }
112 | }
113 |
114 | // Insanity Warning: scope depth-first traversal
115 | // yes, this code is a bit crazy, but it works and we have tests to prove it!
116 | // Scalyr edit: This insanity warning was from angular. We only modified this
117 | // code by checking the $$gatingFunction because it's a good optimization to only go
118 | // down a child of a parent that has the same gating function as what we are processing
119 | // (since if a parent already has a different gating function, there's no way any
120 | // of its children will have the right one).
121 | if (!(next = ((current.$$gatingFunction === targetGatingFunction && current.$$childHead)
122 | || (current !== target && current.$$nextSibling)))) {
123 | while(current !== target && !(next = current.$$nextSibling)) {
124 | current = current.$parent;
125 | }
126 | }
127 | } while ((current = next));
128 |
129 | // Mark that this gating function has digested all children.
130 | targetGatingFunction.hasDigested = true;
131 | return dirty;
132 | };
133 |
134 | /**
135 | * @inherited $watch
136 | * @param directiveName The fourth parameter is a new optional parameter that allows
137 | * directives aware of this abstraction to pass in their own names to identify
138 | * which directive is registering the watch. This is then passed to the
139 | * shouldGateFunction to help determine if the watcher should be gated by the current
140 | * gatingFunction.
141 | */
142 | methodsToAdd.$watch = function gatedWatch(watchExpression, listener, objectEquality,
143 | directiveName) {
144 | // Determine if we should gate this watcher.
145 | if (!isNull(this.$$gatingFunction) && (isNull(this.$$shouldGateFunction) ||
146 | this.$$shouldGateFunction(watchExpression, listener, objectEquality, directiveName))) {
147 | // We do a hack here to just switch out the watchers array with our own
148 | // gated list and then invoke the original watch function.
149 | var tmp = this.$$watchers;
150 | this.$$watchers = this.$$gatedWatchers;
151 | // Invoke original watch function.
152 | var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
153 | this.$$watchers = tmp;
154 | this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction;
155 | this.$$gatedWatchers[0].cleanUp = null;
156 |
157 | // We know that the last field of the watcher object will be set to initWatchVal, so we
158 | // grab it here.
159 | initWatchVal = this.$$gatedWatchers[0].last;
160 | var watch = this.$$gatedWatchers[0];
161 |
162 | // We should make sure the watch expression gets evaluated fully on at least one
163 | // digest cycle even if the gate function is now closed if requested by the gating function's
164 | // value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute
165 | // the watcher we just added and remove itself after the digest cycle completes.
166 | if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) {
167 | var self = this;
168 | watch.cleanUp = scopePrototype.$watch.call(self, function() {
169 | if (!isNull(watch.cleanUp)) {
170 | self.$$cleanUpQueue.unshift(watch.cleanUp);
171 | watch.cleanUp = null;
172 | }
173 | var value;
174 | var last = initWatchVal;
175 |
176 | if (watch && (value = watch.get(self)) !== (last = watch.last) &&
177 | !(watch.eq
178 | ? areEqual(value, last)
179 | : (typeof value == 'number' && typeof last == 'number'
180 | && isNaN(value) && isNaN(last)))) {
181 | watch.last = watch.eq ? copy(value) : value;
182 | watch.fn(value, ((last === initWatchVal) ? value : last), self);
183 | }
184 | return watch.last;
185 | });
186 | }
187 | return result;
188 | } else {
189 | return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality);
190 | }
191 | };
192 |
193 | /**
194 | * @inherited $digest
195 | */
196 | methodsToAdd.$digest = function gatedDigest() {
197 | // We have to take care if a scope's digest method was invoked that has a
198 | // gating function in the parent scope. In this case, the watcher for that
199 | // gating function is registered in the parent (the one added in gatedWatch),
200 | // and will not be evaluated here. So, we have to manually see if the gating
201 | // function is true and if so, evaluate any gated watchers for that function on
202 | // this scope. This needs to happen to properly support invoking $digest on a
203 | // scope with a parent scope with a gating function.
204 | // NOTE: It is arguable that we are not correctly handling nested gating functions
205 | // here since we do not know if the parent gating function was nested in other gating
206 | // functions and should be evaluated at all. However, if a caller is invoking
207 | // $digest on a particular scope, we assume the caller is doing that because it
208 | // knows the watchers should be evaluated.
209 | var dirty = false;
210 | if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) {
211 | var ttl = 5;
212 | do {
213 | dirty = this.$digestGated(this.$$parentGatingFunction);
214 | ttl--;
215 |
216 | if (dirty && !(ttl--)) {
217 | throw Error(TTL + ' $digest() iterations reached for gated watcher. Aborting!\n' +
218 | 'Watchers fired in the last 5 iterations.');
219 | }
220 | } while (dirty);
221 | }
222 |
223 | dirty = scopePrototype.$digest.call(this) || dirty;
224 |
225 | var cleanUpQueue = this.$$cleanUpQueue;
226 |
227 | while (cleanUpQueue.length)
228 | try {
229 | cleanUpQueue.shift()();
230 | } catch (e) {
231 | $exceptionHandler(e);
232 | }
233 |
234 | return dirty;
235 | }
236 |
237 | /**
238 | * Modifies this scope so that all future watchers registered by $watch will
239 | * only be evaluated if gatingFunction returns true. Optionally, you may specify
240 | * a function that will be evaluted on every new call to $watch with the arguments
241 | * passed to it, and that watcher will only be gated if the function returns true.
242 | *
243 | * @param {Function} gatingFunction The gating function which controls whether or not all future
244 | * watchers registered on this scope and its children will be evaluated on a given
245 | * digest cycle. The function will be invoked (with no arguments) on every digest
246 | * and if it returns a truthy result, will cause all gated watchers to be evaluated.
247 | * @param {Function} shouldGateFunction The function that controls whether or not
248 | * a new watcher will be gated using gatingFunction. It is evaluated with the
249 | * arguments to $watch and should return true if the watcher created by those
250 | * arguments should be gated
251 | * @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added
252 | * after the gating function has returned true on a previous digest cycle, the
253 | * the new watcher will be evaluated on the next digest cycle even if the
254 | * gating function is currently return false.
255 | */
256 | methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction,
257 | shouldEvalNewWatchers) {
258 | var changeCount = 0;
259 | var self = this;
260 |
261 | // Set a watcher that sees if our gating function is true, and if so, digests
262 | // all of our associated watchers. Note, this.$watch could already have a
263 | // gating function associated with it, which means this watch won't be executed
264 | // unless all gating functions before us have evaluated to true. We take special
265 | // care of this nested case below.
266 |
267 | // We handle nested gating function in a special way. If we are a nested gating
268 | // function (meaning there is already one or more gating functions on this scope and
269 | // our parent scopes), then if those parent gating functions every all evaluate to
270 | // true (which we can tell if the watcher we register here is evaluated), then
271 | // we always evaluate our watcher until our gating function returns true.
272 | var hasNestedGates = !isNull(this.$$gatingFunction);
273 |
274 | (function() {
275 | var promotedWatcher = null;
276 |
277 | self.$watch(function() {
278 | if (gatingFunction()) {
279 | if (self.$digestGated(gatingFunction))
280 | ++changeCount;
281 | } else if (hasNestedGates && isNull(promotedWatcher)) {
282 | promotedWatcher = scopePrototype.$watch.call(self, function() {
283 | if (gatingFunction()) {
284 | promotedWatcher();
285 | promotedWatcher = null;
286 | if (self.$digestGated(gatingFunction))
287 | ++changeCount;
288 | }
289 | return changeCount;
290 | });
291 | }
292 | return changeCount;
293 | });
294 | })();
295 |
296 |
297 | if (isUndefined(shouldGateFunction))
298 | shouldGateFunction = null;
299 | if (isUndefined(shouldEvalNewWatchers))
300 | shouldEvalNewWatchers = false;
301 | this.$$gatingFunction = gatingFunction;
302 | this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers;
303 | this.$$shouldGateFunction = shouldGateFunction;
304 | };
305 |
306 | // Extend the original Scope object so that when
307 | // new instances are created, it has the new methods.
308 | angular.extend(Scope.prototype, methodsToAdd);
309 |
310 | // Also extend the $rootScope instance since it was created
311 | // before we got a chance to extend Scope.prototype.
312 | angular.extend($rootScope, methodsToAdd);
313 |
314 | $rootScope.$$gatingFunction = null;
315 | $rootScope.$$parentGatingFunction = null;
316 | $rootScope.$$shouldGateFunction = null;
317 | $rootScope.$$gatedWatchers = [];
318 | $rootScope.$$cleanUpQueue = [];
319 |
320 | return $rootScope;
321 | }]);
322 | }]);
323 |
--------------------------------------------------------------------------------
/src/tests/directives/allTests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tests for js/directives directory
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/tests/directives/slyEvaluateTest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Test cases for the slyEvaluate module.
4 | *
5 | * @author Steven Czerwinski
6 | */
7 | describe('slyEvaluate.slyEvaluateOnlyWhen', function() {
8 | // Elements from the test page.
9 | var scope = null;
10 | var page = null;
11 | var span = null;
12 |
13 | // Require the slyEvaluate module.
14 | beforeEach(module('slyEvaluate'));
15 |
16 | // Create the sample DOM elements using sly-evaluate-only-when.
17 | // Specifically, make all updates to page dependent on dataObject
18 | // and have a span containing x.
19 | beforeEach(inject(function($rootScope, $compile) {
20 | scope = $rootScope;
21 | scope.dataObject = {
22 | value: 5,
23 | };
24 | scope.x = 10;
25 |
26 | page = angular.element(
27 | '
' +
28 | '{{x}}' +
29 | '
');
30 |
31 | $compile(page)(scope);
32 | scope.$digest();
33 |
34 | // Set the DOM references.
35 | span = page.find('span');
36 | }));
37 |
38 | it('should initialize all bindings', function() {
39 | // Just check basic functionality.
40 | expect(span.eq(0).text()).toEqual('10');
41 | });
42 |
43 | it('should not update bindings if object has not changed', function() {
44 | // Change x and do a digest. The binding should not have updated
45 | // since object has not changed.
46 | scope.x = 12;
47 | scope.$digest();
48 |
49 | expect(span.eq(0).text()).toEqual('10');
50 | });
51 |
52 | it('should not update bindings even if contents of object has changed', function() {
53 | // Change x and do a digest. The binding should not have updated
54 | // since object has not changed.
55 | scope.x = 12;
56 | scope.dataObject.value = 6;
57 | scope.$digest();
58 |
59 | expect(span.eq(0).text()).toEqual('10');
60 | });
61 |
62 | it('should update bindings after object reference has changed', function() {
63 | // Change both x and dataObject
64 | scope.x = 12;
65 | scope.dataObject = {
66 | value: 5,
67 | };
68 | scope.$digest();
69 |
70 | expect(span.eq(0).text()).toEqual('12');
71 | });
72 |
73 | it('should evaluate a new child no matter what',
74 | inject(function($rootScope, $compile) {
75 | scope = $rootScope;
76 | scope.dataObject = {
77 | value: 5,
78 | };
79 | scope.x = 12;
80 |
81 | page = angular.element('');
82 |
83 | $compile(page)(scope);
84 | scope.$digest();
85 |
86 | // We simulate adding a new child to the div by just creating a new element
87 | // and compiling it in. This is a bit hacky and hopefully won't break.
88 | divScope = page.scope();
89 | span = angular.element('{{x}}');
90 | $compile(span)(divScope);
91 |
92 | scope.$digest();
93 |
94 | expect(span.eq(0).text()).toEqual('12');
95 | }));
96 | });
97 |
98 | describe('slyEvaluate.slyAlwaysEvaluate', function() {
99 | // Elements from the test page.
100 | var scope = null;
101 | var page = null;
102 | var spanX = null;
103 | var spanY = null;
104 |
105 | // Require the slyEvaluate module.
106 | beforeEach(module('slyEvaluate'));
107 |
108 | // Create the sample DOM elements slyAlwaysEvaluate. Specifically,
109 | // gate all changes on dataObject, but put in an exception for 'x'.
110 | // Create spans containing X and Y.
111 | beforeEach(inject(function($rootScope, $compile) {
112 | scope = $rootScope;
113 | scope.dataObject = {
114 | value: 5,
115 | };
116 | scope.x = 10;
117 | scope.y = 'a';
118 |
119 | page = angular.element(
120 | '
' +
121 | '' +
122 | '' +
123 | '
');
124 |
125 | $compile(page)(scope);
126 | scope.$digest();
127 |
128 | // Set the DOM references.
129 | var span = page.find('span');
130 | spanX = span.eq(0);
131 | spanY = span.eq(1);
132 | }));
133 |
134 | it('should initialize bindings correctly', function() {
135 | // Just check basic functionality.
136 | expect(spanX.text()).toEqual('10');
137 | expect(spanY.text()).toEqual('a');
138 | });
139 |
140 | it('should always evaluate expressions containing the exception string', function() {
141 | // If we change both x and y, only x's change should show up after a digest.
142 | scope.x = 15;
143 | scope.y = 'b';
144 | scope.$digest();
145 |
146 | expect(spanX.text()).toEqual('15');
147 | expect(spanY.text()).toEqual('a');
148 | });
149 |
150 | it('should always evaluate all expressions when slyEvaluateOnlyWhen object changes', function() {
151 | // If we change both x and y and the dataObject
152 | scope.x = 15;
153 | scope.y = 'b';
154 | scope.dataObject = {
155 | value: 10,
156 | };
157 | scope.$digest();
158 |
159 | expect(spanX.text()).toEqual('15');
160 | expect(spanY.text()).toEqual('b');
161 | });
162 | });
163 |
164 | describe('slyEvaluate.slyShow', function() {
165 | // Elements from the test page.
166 | var scope = null;
167 | var page = null;
168 | var span = null;
169 |
170 | // Require the slyEvaluate module.
171 | beforeEach(module('slyEvaluate'));
172 |
173 | beforeEach(inject(function($rootScope, $compile) {
174 | scope = $rootScope;
175 | scope.isHidden = false;
176 | scope.x = 1;
177 |
178 | page = angular.element(
179 | '
' +
180 | 'Hi' +
181 | '
');
182 |
183 | $compile(page)(scope);
184 | scope.$digest();
185 |
186 | // Set the DOM references.
187 | span = page.find('span').eq(0);
188 | }));
189 |
190 | it('should initialize showing state correctly', function() {
191 | // Just check basic functionality.
192 | expect(span.hasClass('ng-hide')).toBeFalsy();
193 | });
194 |
195 | it('should hide element when expression becomes false', function() {
196 | scope.isHidden = true;
197 | scope.$digest();
198 | expect(span.hasClass('ng-hide')).toBeTruthy();
199 | });
200 |
201 | it('should show element when expression becomes true', function() {
202 | scope.isHidden = true;
203 | scope.$digest();
204 |
205 | scope.isHidden = false;
206 | scope.$digest();
207 | expect(span.hasClass('ng-hide')).toBeFalsy();
208 | });
209 | });
210 |
211 | describe('slyEvaluate.slyPreventEvaluationWhenHidden', function() {
212 | // Elements from the test page.
213 | var scope = null;
214 | var page = null;
215 | var spanX = null;
216 |
217 | // Require the slyEvaluate module.
218 | beforeEach(module('slyEvaluate'));
219 |
220 | // Create the sample DOM elements slyAlwaysEvaluate. Specifically,
221 | // gate all changes on dataObject, but put in an exception for 'x'.
222 | // Create spans containing X and Y.
223 | beforeEach(inject(function($rootScope, $compile) {
224 | scope = $rootScope;
225 | scope.isHidden = false;
226 | scope.x = 1;
227 |
228 | page = angular.element(
229 | '
' +
230 | '{{x}}' +
231 | '
');
232 |
233 | $compile(page)(scope);
234 | scope.$digest();
235 |
236 | // Set the DOM references.
237 | spanX = page.find('span').eq(0);
238 | }));
239 |
240 | it('should initialize bindings correctly', function() {
241 | // Just check basic functionality.
242 | expect(spanX.text()).toEqual('1');
243 | });
244 |
245 | it('should not update bindings when hidden', function() {
246 | scope.isHidden = true;
247 | scope.$digest();
248 |
249 | // Change x to some new value. It should not be updated in the page.
250 | scope.x = 10;
251 | scope.$digest();
252 |
253 | expect(spanX.text()).toEqual('1');
254 | });
255 |
256 | it('should update bindings when shown', function() {
257 | scope.isHidden = true;
258 | scope.$digest();
259 |
260 | scope.x = 10;
261 | scope.$digest();
262 |
263 | scope.isHidden = false;
264 | scope.$digest();
265 | expect(spanX.text()).toEqual('10');
266 | });
267 | });
268 |
--------------------------------------------------------------------------------
/src/tests/directives/slyRepeatTest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview
3 | * Test cases for the slyRepeat module.
4 | *
5 | * @author Steven Czerwinski
6 | */
7 | describe('slyRepeat.slyRepeat', function() {
8 | // Items will hold references to the DOM elements
9 | // li from the sample HTML page.
10 | var items = null,
11 | scope = null,
12 | page = null;
13 |
14 | // Require the slyRepeat module.
15 | beforeEach(module('slyRepeat'));
16 |
17 | // Create the sample DOM elements with a repeat directive in them.
18 | beforeEach(inject(function($rootScope, $compile) {
19 | scope = $rootScope;
20 | scope.xValues = [ 5, 6, 7];
21 |
22 | // Create a list made of values of xValues, where the first word is the
23 | // value from the array, and then the three boolean fields $first, $middle, $last
24 | // which are automatically created for each element scope.
25 | page = angular.element(
26 | '