<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name ng.directive:ngRepeat
 *
 * @description
 * The `ngRepeat` directive instantiates a template once per item from a collection. Each template
 * instance gets its own scope, where the given loop variable is set to the current collection item,
 * and `$index` is set to the item index or key.
 *
 * Special properties are exposed on the local scope of each template instance, including:
 *
 * | Variable  | Type            | Details                                                                     |
 * |===========|=================|=============================================================================|
 * | `$index`  | {@type number}  | iterator offset of the repeated element (0..length-1)                       |
 * | `$first`  | {@type boolean} | true if the repeated element is first in the iterator.                      |
 * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. |
 * | `$last`   | {@type boolean} | true if the repeated element is last in the iterator.                       |
 *
 * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**,
 * **leave** and **move** effects.
 *
 * @animations
 * enter - when a new item is added to the list or when an item is revealed after a filter
 * leave - when an item is removed from the list or when an item is filtered out
 * move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
 *
 * @element ANY
 * @scope
 * @priority 1000
 * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
 *   formats are currently supported:
 *
 *   * `variable in expression` – where variable is the user defined loop variable and `expression`
 *     is a scope expression giving the collection to enumerate.
 *
 *     For example: `track in cd.tracks`.
 *
 *   * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
 *     and `expression` is the scope expression giving the collection to enumerate.
 *
 *     For example: `(name, age) in {'adam':10, 'amalie':12}`.
 *
 *   * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
 *     which can be used to associate the objects in the collection with the DOM elements. If no tractking function
 *     is specified the ng-repeat associates elements by identity in the collection. It is an error to have
 *     more then one tractking function to  resolve to the same key. (This would mean that two distinct objects are
 *     mapped to the same DOM element, which is not possible.)
 *
 *     For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
 *     will be associated by item identity in the array.
 *
 *     For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
 *     `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
 *     with the corresponding item in the array by identity. Moving the same object in array would move the DOM
 *     element in the same way ian the DOM.
 *
 *     For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this
 *     case the object identity does not matter. Two objects are considered equivalent as long as their `id`
 *     property is same.
 *
 * @example
 * This example initializes the scope to a list of names and
 * then uses `ngRepeat` to display every person:
  <example animations="true">
    <file name="index.html">
      <div ng-init="friends = [
        {name:'John', age:25, gender:'boy'},
        {name:'Jessie', age:30, gender:'girl'},
        {name:'Johanna', age:28, gender:'girl'},
        {name:'Joy', age:15, gender:'girl'},
        {name:'Mary', age:28, gender:'girl'},
        {name:'Peter', age:95, gender:'boy'},
        {name:'Sebastian', age:50, gender:'boy'},
        {name:'Erika', age:27, gender:'girl'},
        {name:'Patrick', age:40, gender:'boy'},
        {name:'Samantha', age:60, gender:'girl'}
      ]">
        I have {{friends.length}} friends. They are:
        <input type="search" ng-model="q" placeholder="filter friends..." />

            [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.

    </file>
    <file name="animations.css">
      .example-repeat-enter,
      .example-repeat-leave,
      .example-repeat-move {
        -webkit-transition:all linear 0.5s;
        -moz-transition:all linear 0.5s;
        -ms-transition:all linear 0.5s;
        -o-transition:all linear 0.5s;
        transition:all linear 0.5s;
      }

      .example-repeat-enter {
        line-height:0;
        opacity:0;
      }
      .example-repeat-enter.example-repeat-enter-active {
        line-height:20px;
        opacity:1;
      }

      .example-repeat-leave {
        opacity:1;
        line-height:20px;
      }
      .example-repeat-leave.example-repeat-leave-active {
        opacity:0;
        line-height:0;
      }

      .example-repeat-move { }
      .example-repeat-move.example-repeat-move-active { }
    </file>
    <file name="scenario.js">
       it('should render initial data set', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);
         expect(r.row(0)).toEqual(["1","John","25"]);
         expect(r.row(1)).toEqual(["2","Jessie","30"]);
         expect(r.row(9)).toEqual(["10","Samantha","60"]);
         expect(binding('friends.length')).toBe("10");
       });

       it('should update repeater when filter predicate changes', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);

         input('q').enter('ma');

         expect(r.count()).toBe(2);
         expect(r.row(0)).toEqual(["1","Mary","28"]);
         expect(r.row(1)).toEqual(["2","Samantha","60"]);
       });
      </file>
    </example>
 */
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
  var NG_REMOVED = '$$NG_REMOVED';
  return {
    transclude: 'element',
    priority: 1000,
    terminal: true,
    compile: function(element, attr, linker) {
      return function($scope, $element, $attr){
        var animate = $animator($scope, $attr);
        var expression = $attr.ngRepeat;
        var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
          trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
          hashFnLocals = {$id: hashKey};

        if (!match) {
          throw ngError(7, "ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
            expression);
        }

        lhs = match[1];
        rhs = match[2];
        trackByExp = match[4];

        if (trackByExp) {
          trackByExpGetter = $parse(trackByExp);
          trackByIdFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        } else {
          trackByIdFn = function(key, value) {
            return hashKey(value);
          }
        }

        match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
        if (!match) {
          throw ngError(8, "ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
                                                                    lhs);
        }
        valueIdentifier = match[3] || match[1];
        keyIdentifier = match[2];

        // Store a list of elements from previous run. This is a hash where key is the item from the
        // iterator, and the value is objects with following properties.
        //   - scope: bound scope
        //   - element: previous element.
        //   - index: position
        var lastBlockMap = {};

        //watch props
        $scope.$watchCollection(rhs, function ngRepeatAction(collection){
          var index, length,
              cursor = $element,     // current position of the node
              nextCursor,
              // Same as lastBlockMap but it has the current state. It will become the
              // lastBlockMap on the next iteration.
              nextBlockMap = {},
              arrayLength,
              childScope,
              key, value, // key/value of iteration
              trackById,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder = [];

          if (isArrayLike(collection)) {
            collectionKeys = collection;
          } else {
            // if object, extract keys, sort them and use to determine order of iteration over obj props
            collectionKeys = [];
            for (key in collection) {
              if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
                collectionKeys.push(key);
              }
            }
            collectionKeys.sort();
          }

          arrayLength = collectionKeys.length;

          // locate existing items
          length = nextBlockOrder.length = collectionKeys.length;
          for(index = 0; index < length; index++) {
           key = (collection === collectionKeys) ? index : collectionKeys[index];
           value = collection[key];
           trackById = trackByIdFn(key, value, index);
           if(lastBlockMap.hasOwnProperty(trackById)) {
             block = lastBlockMap[trackById]
             delete lastBlockMap[trackById];
             nextBlockMap[trackById] = block;
             nextBlockOrder[index] = block;
           } else if (nextBlockMap.hasOwnProperty(trackById)) {
             // restore lastBlockMap
             forEach(nextBlockOrder, function(block) {
               if (block && block.element) lastBlockMap[block.id] = block;
             });
             // This is a duplicate and we need to throw an error
             throw ngError(50, "ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
                                                                                expression,       trackById);
           } else {
             // new never before seen block
             nextBlockOrder[index] = { id: trackById };
             nextBlockMap[trackById] = false;
           }
         }

          // remove existing items
          for (key in lastBlockMap) {
            if (lastBlockMap.hasOwnProperty(key)) {
              block = lastBlockMap[key];
              animate.leave(block.element);
              forEach(block.element, function(element) { element[NG_REMOVED] = true});
              block.scope.$destroy();
            }
          }

          // we are not using forEach for perf reasons (trying to avoid #call)
          for (index = 0, length = collectionKeys.length; index < length; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];

            if (block.element) {
              // if we have already seen this object, then we need to reuse the
              // associated scope/element
              childScope = block.scope;

              nextCursor = cursor[0];
              do {
                nextCursor = nextCursor.nextSibling;
              } while(nextCursor && nextCursor[NG_REMOVED]);

              if (block.element[0] == nextCursor) {
                // do nothing
                cursor = block.element;
              } else {
                // existing item which got moved
                animate.move(block.element, null, cursor);
                cursor = block.element;
              }
            } else {
              // new item which we don't know about
              childScope = $scope.$new();
            }

            childScope[valueIdentifier] = value;
            if (keyIdentifier) childScope[keyIdentifier] = key;
            childScope.$index = index;
            childScope.$first = (index === 0);
            childScope.$last = (index === (arrayLength - 1));
            childScope.$middle = !(childScope.$first || childScope.$last);

            if (!block.element) {
              linker(childScope, function(clone) {
                animate.enter(clone, null, cursor);
                cursor = clone;
                block.scope = childScope;
                block.element = clone;
                nextBlockMap[block.id] = block;
              });
            }
          }
          lastBlockMap = nextBlockMap;
        });
      };
    }
  };
}];

<commit_msg>feat(ngdocs): provide documentation for the new ngRepeat repeater syntax<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name ng.directive:ngRepeat
 *
 * @description
 * The `ngRepeat` directive instantiates a template once per item from a collection. Each template
 * instance gets its own scope, where the given loop variable is set to the current collection item,
 * and `$index` is set to the item index or key.
 *
 * Special properties are exposed on the local scope of each template instance, including:
 *
 * | Variable  | Type            | Details                                                                     |
 * |===========|=================|=============================================================================|
 * | `$index`  | {@type number}  | iterator offset of the repeated element (0..length-1)                       |
 * | `$first`  | {@type boolean} | true if the repeated element is first in the iterator.                      |
 * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. |
 * | `$last`   | {@type boolean} | true if the repeated element is last in the iterator.                       |
 *
 * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**,
 * **leave** and **move** effects.
 *
 *
 * # Special repeat start and end points
 * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
 * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
 * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
 * up to and including the ending HTML tag where **ng-repeat-end** is placed.
 *
 * The example below makes use of this feature:
 *

 *
 *     Header {{ item }}
 *
 *

 *     Body {{ item }}
 *

 *
 *     Footer {{ item }}
 *
 *

 *
 * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to:
 *

 *
 *     Header A
 *
 *

 *     Body A
 *

 *
 *     Footer A
 *
 *
 *     Header B
 *
 *

 *     Body B
 *

 *
 *     Footer B
 *
 *

 *
 * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such
 * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
 *
 * @animations
 * enter - when a new item is added to the list or when an item is revealed after a filter
 * leave - when an item is removed from the list or when an item is filtered out
 * move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
 *
 * @element ANY
 * @scope
 * @priority 1000
 * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
 *   formats are currently supported:
 *
 *   * `variable in expression` – where variable is the user defined loop variable and `expression`
 *     is a scope expression giving the collection to enumerate.
 *
 *     For example: `track in cd.tracks`.
 *
 *   * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
 *     and `expression` is the scope expression giving the collection to enumerate.
 *
 *     For example: `(name, age) in {'adam':10, 'amalie':12}`.
 *
 *   * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
 *     which can be used to associate the objects in the collection with the DOM elements. If no tractking function
 *     is specified the ng-repeat associates elements by identity in the collection. It is an error to have
 *     more then one tractking function to  resolve to the same key. (This would mean that two distinct objects are
 *     mapped to the same DOM element, which is not possible.)
 *
 *     For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
 *     will be associated by item identity in the array.
 *
 *     For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
 *     `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
 *     with the corresponding item in the array by identity. Moving the same object in array would move the DOM
 *     element in the same way ian the DOM.
 *
 *     For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this
 *     case the object identity does not matter. Two objects are considered equivalent as long as their `id`
 *     property is same.
 *
 * @example
 * This example initializes the scope to a list of names and
 * then uses `ngRepeat` to display every person:
  <example animations="true">
    <file name="index.html">
      <div ng-init="friends = [
        {name:'John', age:25, gender:'boy'},
        {name:'Jessie', age:30, gender:'girl'},
        {name:'Johanna', age:28, gender:'girl'},
        {name:'Joy', age:15, gender:'girl'},
        {name:'Mary', age:28, gender:'girl'},
        {name:'Peter', age:95, gender:'boy'},
        {name:'Sebastian', age:50, gender:'boy'},
        {name:'Erika', age:27, gender:'girl'},
        {name:'Patrick', age:40, gender:'boy'},
        {name:'Samantha', age:60, gender:'girl'}
      ]">
        I have {{friends.length}} friends. They are:
        <input type="search" ng-model="q" placeholder="filter friends..." />

            [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.

    </file>
    <file name="animations.css">
      .example-repeat-enter,
      .example-repeat-leave,
      .example-repeat-move {
        -webkit-transition:all linear 0.5s;
        -moz-transition:all linear 0.5s;
        -ms-transition:all linear 0.5s;
        -o-transition:all linear 0.5s;
        transition:all linear 0.5s;
      }

      .example-repeat-enter {
        line-height:0;
        opacity:0;
      }
      .example-repeat-enter.example-repeat-enter-active {
        line-height:20px;
        opacity:1;
      }

      .example-repeat-leave {
        opacity:1;
        line-height:20px;
      }
      .example-repeat-leave.example-repeat-leave-active {
        opacity:0;
        line-height:0;
      }

      .example-repeat-move { }
      .example-repeat-move.example-repeat-move-active { }
    </file>
    <file name="scenario.js">
       it('should render initial data set', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);
         expect(r.row(0)).toEqual(["1","John","25"]);
         expect(r.row(1)).toEqual(["2","Jessie","30"]);
         expect(r.row(9)).toEqual(["10","Samantha","60"]);
         expect(binding('friends.length')).toBe("10");
       });

       it('should update repeater when filter predicate changes', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);

         input('q').enter('ma');

         expect(r.count()).toBe(2);
         expect(r.row(0)).toEqual(["1","Mary","28"]);
         expect(r.row(1)).toEqual(["2","Samantha","60"]);
       });
      </file>
    </example>
 */
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
  var NG_REMOVED = '$$NG_REMOVED';
  return {
    transclude: 'element',
    priority: 1000,
    terminal: true,
    compile: function(element, attr, linker) {
      return function($scope, $element, $attr){
        var animate = $animator($scope, $attr);
        var expression = $attr.ngRepeat;
        var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
          trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
          hashFnLocals = {$id: hashKey};

        if (!match) {
          throw ngError(7, "ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
            expression);
        }

        lhs = match[1];
        rhs = match[2];
        trackByExp = match[4];

        if (trackByExp) {
          trackByExpGetter = $parse(trackByExp);
          trackByIdFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        } else {
          trackByIdFn = function(key, value) {
            return hashKey(value);
          }
        }

        match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
        if (!match) {
          throw ngError(8, "ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
                                                                    lhs);
        }
        valueIdentifier = match[3] || match[1];
        keyIdentifier = match[2];

        // Store a list of elements from previous run. This is a hash where key is the item from the
        // iterator, and the value is objects with following properties.
        //   - scope: bound scope
        //   - element: previous element.
        //   - index: position
        var lastBlockMap = {};

        //watch props
        $scope.$watchCollection(rhs, function ngRepeatAction(collection){
          var index, length,
              cursor = $element,     // current position of the node
              nextCursor,
              // Same as lastBlockMap but it has the current state. It will become the
              // lastBlockMap on the next iteration.
              nextBlockMap = {},
              arrayLength,
              childScope,
              key, value, // key/value of iteration
              trackById,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder = [];

          if (isArrayLike(collection)) {
            collectionKeys = collection;
          } else {
            // if object, extract keys, sort them and use to determine order of iteration over obj props
            collectionKeys = [];
            for (key in collection) {
              if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
                collectionKeys.push(key);
              }
            }
            collectionKeys.sort();
          }

          arrayLength = collectionKeys.length;

          // locate existing items
          length = nextBlockOrder.length = collectionKeys.length;
          for(index = 0; index < length; index++) {
           key = (collection === collectionKeys) ? index : collectionKeys[index];
           value = collection[key];
           trackById = trackByIdFn(key, value, index);
           if(lastBlockMap.hasOwnProperty(trackById)) {
             block = lastBlockMap[trackById]
             delete lastBlockMap[trackById];
             nextBlockMap[trackById] = block;
             nextBlockOrder[index] = block;
           } else if (nextBlockMap.hasOwnProperty(trackById)) {
             // restore lastBlockMap
             forEach(nextBlockOrder, function(block) {
               if (block && block.element) lastBlockMap[block.id] = block;
             });
             // This is a duplicate and we need to throw an error
             throw ngError(50, "ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
                                                                                expression,       trackById);
           } else {
             // new never before seen block
             nextBlockOrder[index] = { id: trackById };
             nextBlockMap[trackById] = false;
           }
         }

          // remove existing items
          for (key in lastBlockMap) {
            if (lastBlockMap.hasOwnProperty(key)) {
              block = lastBlockMap[key];
              animate.leave(block.element);
              forEach(block.element, function(element) { element[NG_REMOVED] = true});
              block.scope.$destroy();
            }
          }

          // we are not using forEach for perf reasons (trying to avoid #call)
          for (index = 0, length = collectionKeys.length; index < length; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];

            if (block.element) {
              // if we have already seen this object, then we need to reuse the
              // associated scope/element
              childScope = block.scope;

              nextCursor = cursor[0];
              do {
                nextCursor = nextCursor.nextSibling;
              } while(nextCursor && nextCursor[NG_REMOVED]);

              if (block.element[0] == nextCursor) {
                // do nothing
                cursor = block.element;
              } else {
                // existing item which got moved
                animate.move(block.element, null, cursor);
                cursor = block.element;
              }
            } else {
              // new item which we don't know about
              childScope = $scope.$new();
            }

            childScope[valueIdentifier] = value;
            if (keyIdentifier) childScope[keyIdentifier] = key;
            childScope.$index = index;
            childScope.$first = (index === 0);
            childScope.$last = (index === (arrayLength - 1));
            childScope.$middle = !(childScope.$first || childScope.$last);

            if (!block.element) {
              linker(childScope, function(clone) {
                animate.enter(clone, null, cursor);
                cursor = clone;
                block.scope = childScope;
                block.element = clone;
                nextBlockMap[block.id] = block;
              });
            }
          }
          lastBlockMap = nextBlockMap;
        });
      };
    }
  };
}];

<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name ng.directive:ngRepeat
 *
 * @description
 * Директива `ngRepeat` создает экземпляры по шаблону для каждого элемента коллекции. Каждый экземпляр шаблона
 * получает собственную область видимости, в которой создаются переменные, имеющиеся в текущем элементе коллекции и
 * в `$index` устанавливается индекс или ключ элемента.
 *
 * Специальные свойства, представленные каждым шаблонным элементом в его локальной области видимости, включают:
 *
 *   * `$index` – `{number}` – сдвиг итератора при повторении элемента (0..length-1)
 *   * `$first` – `{boolean}` – true, если повторяемый элемент является первым элементом в коллекции.
 *   * `$middle` – `{boolean}` – true, если повторяемый элемент не первый и не последний элемент в коллекции.
 *   * `$last` – `{boolean}` – true, если повторяемый элемент является последним элементом коллекции.
 *
 * Кроме того, с помощью атрибута ngAnimate можно задать анимацию для эффектов **enter**,
 * **leave** и **move**.
 *
 * @animations
 * enter - когда новый элемент добавляется в список или показывается после фильтрации
 * leave - когда элемент удаляется из списка или скрывается из результатов фильтрации
 * move - когда сосдний пункт удаляется, изменяя порядок или когда список упорядочивается по другому критерию
 *
 * @element ANY
 * @scope
 * @priority 1000
 * @param {repeat_expression} ngRepeat Выражение, указывающее, как перебирать коллекцию.
 * В настоящее время поддерживаются следующие форматы:
 *
 *   * `variable in expression` – где `variable` это определенная пользователем переменная, которая будет содержать
 *     текущее значение при переборе и `expression` – это выражение в области видимости, которое указывает на
 *     коллекцию для перебора.
 *
 *     Например: `track in cd.tracks`.
 *
 *   * `(key, value) in expression` – где `key` и `value` могут быть идентификаторами, определенными пользователем,
 *     и `expression` это выражение в области видимости, которое возвращает коллекцию для перебора.
 *
 *     Например: `(name, age) in {'adam':10, 'amalie':12}`.
 *
 *   * `variable in expression track by tracking_expression` – Так же можно использовать дополнительную функцию
 *     отслеживания, которая может быть использована для связывания объектов коллекции с DOM-элементами. Если функция
 *     отслеживания специально не указана, то ng-repeat ассоциирует элементы коллекции по идентичности. Нельзя иметь
 *     более одной функции отслеживания с одним и тем же ключем. (Т.е. два различных объекта будут связаны с одним
 *     DOM-элементом, что невозможно).
 *
 *     Например: `item in items` эквивалентно `item in items track by $id(item)`. Это означает, что DOM-элементы
 *     будут ассоциироваться идентично элементам в массиве.
 *
 *     Например: `item in items track by $id(item)`. Созданная функция `$id()` может быть использована для присвоения
 *     уникального свойства `$$hashKey` каждому элементу в массиве. Это свойство затем используется в качестве ключа
 *     для связывания DOM-элемента с соответствующим элементом в массиве по идентичности. Перемещение же элемента в
 *     массиве будет перемещать DOM-элемент по такому же пути.
 *
 *     Например: `item in items track by item.id` это типичный образец, когда элементы загружаются из базы данных
 *     В этом случае идентичность объекта не имеет значения. Два объекта считаются эквивалентными, если их `id`
 *     схожи.
 *
 * @example
 * Пример инициализирует область видимости для списка имен, а затем использует `ngRepeat` для показа каждого человека:
  <example animations="true">
    <file name="index.html">
      <div ng-init="friends = [
        {name:'John', age:25, gender:'boy'},
        {name:'Jessie', age:30, gender:'girl'},
        {name:'Johanna', age:28, gender:'girl'},
        {name:'Joy', age:15, gender:'girl'},
        {name:'Mary', age:28, gender:'girl'},
        {name:'Peter', age:95, gender:'boy'},
        {name:'Sebastian', age:50, gender:'boy'},
        {name:'Erika', age:27, gender:'girl'},
        {name:'Patrick', age:40, gender:'boy'},
        {name:'Samantha', age:60, gender:'girl'}
      ]">
        I have {{friends.length}} friends. They are:
        <input type="search" ng-model="q" placeholder="filter friends..." />

            [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.

    </file>
    <file name="animations.css">
      .example-repeat-enter-setup,
      .example-repeat-leave-setup,
      .example-repeat-move-setup {
        -webkit-transition:all linear 0.5s;
        -moz-transition:all linear 0.5s;
        -ms-transition:all linear 0.5s;
        -o-transition:all linear 0.5s;
        transition:all linear 0.5s;
      }

      .example-repeat-enter-setup {
        line-height:0;
        opacity:0;
      }
      .example-repeat-enter-setup.example-repeat-enter-start {
        line-height:20px;
        opacity:1;
      }

      .example-repeat-leave-setup {
        opacity:1;
        line-height:20px;
      }
      .example-repeat-leave-setup.example-repeat-leave-start {
        opacity:0;
        line-height:0;
      }

      .example-repeat-move-setup { }
      .example-repeat-move-setup.example-repeat-move-start { }
    </file>
    <file name="scenario.js">
       it('should render initial data set', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);
         expect(r.row(0)).toEqual(["1","John","25"]);
         expect(r.row(1)).toEqual(["2","Jessie","30"]);
         expect(r.row(9)).toEqual(["10","Samantha","60"]);
         expect(binding('friends.length')).toBe("10");
       });

       it('should update repeater when filter predicate changes', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);

         input('q').enter('ma');

         expect(r.count()).toBe(2);
         expect(r.row(0)).toEqual(["1","Mary","28"]);
         expect(r.row(1)).toEqual(["2","Samantha","60"]);
       });
      </file>
    </example>
 */
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
  var NG_REMOVED = '$$NG_REMOVED';
  return {
    transclude: 'element',
    priority: 1000,
    terminal: true,
    compile: function(element, attr, linker) {
      return function($scope, $element, $attr){
        var animate = $animator($scope, $attr);
        var expression = $attr.ngRepeat;
        var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
          trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
          hashFnLocals = {$id: hashKey};

        if (!match) {
          throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
            expression + "'.");
        }

        lhs = match[1];
        rhs = match[2];
        trackByExp = match[4];

        if (trackByExp) {
          trackByExpGetter = $parse(trackByExp);
          trackByIdFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        } else {
          trackByIdFn = function(key, value) {
            return hashKey(value);
          }
        }

        match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
        if (!match) {
          throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
              lhs + "'.");
        }
        valueIdentifier = match[3] || match[1];
        keyIdentifier = match[2];

        // Store a list of elements from previous run. This is a hash where key is the item from the
        // iterator, and the value is objects with following properties.
        //   - scope: bound scope
        //   - element: previous element.
        //   - index: position
        var lastBlockMap = {};

        //watch props
        $scope.$watchCollection(rhs, function ngRepeatAction(collection){
          var index, length,
              cursor = $element,     // current position of the node
              nextCursor,
              // Same as lastBlockMap but it has the current state. It will become the
              // lastBlockMap on the next iteration.
              nextBlockMap = {},
              arrayLength,
              childScope,
              key, value, // key/value of iteration
              trackById,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder = [];

          if (isArray(collection)) {
            collectionKeys = collection;
          } else {
            // if object, extract keys, sort them and use to determine order of iteration over obj props
            collectionKeys = [];
            for (key in collection) {
              if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
                collectionKeys.push(key);
              }
            }
            collectionKeys.sort();
          }

          arrayLength = collectionKeys.length;

          // locate existing items
          length = nextBlockOrder.length = collectionKeys.length;
          for(index = 0; index < length; index++) {
           key = (collection === collectionKeys) ? index : collectionKeys[index];
           value = collection[key];
           trackById = trackByIdFn(key, value, index);
           if(lastBlockMap.hasOwnProperty(trackById)) {
             block = lastBlockMap[trackById]
             delete lastBlockMap[trackById];
             nextBlockMap[trackById] = block;
             nextBlockOrder[index] = block;
           } else if (nextBlockMap.hasOwnProperty(trackById)) {
             // restore lastBlockMap
             forEach(nextBlockOrder, function(block) {
               if (block && block.element) lastBlockMap[block.id] = block;
             });
             // This is a duplicate and we need to throw an error
             throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression +
                 ' key: ' + trackById);
           } else {
             // new never before seen block
             nextBlockOrder[index] = { id: trackById };
             nextBlockMap[trackById] = false;
           }
         }

          // remove existing items
          for (key in lastBlockMap) {
            if (lastBlockMap.hasOwnProperty(key)) {
              block = lastBlockMap[key];
              animate.leave(block.element);
              block.element[0][NG_REMOVED] = true;
              block.scope.$destroy();
            }
          }

          // we are not using forEach for perf reasons (trying to avoid #call)
          for (index = 0, length = collectionKeys.length; index < length; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];

            if (block.element) {
              // if we have already seen this object, then we need to reuse the
              // associated scope/element
              childScope = block.scope;

              nextCursor = cursor[0];
              do {
                nextCursor = nextCursor.nextSibling;
              } while(nextCursor && nextCursor[NG_REMOVED]);

              if (block.element[0] == nextCursor) {
                // do nothing
                cursor = block.element;
              } else {
                // existing item which got moved
                animate.move(block.element, null, cursor);
                cursor = block.element;
              }
            } else {
              // new item which we don't know about
              childScope = $scope.$new();
            }

            childScope[valueIdentifier] = value;
            if (keyIdentifier) childScope[keyIdentifier] = key;
            childScope.$index = index;
            childScope.$first = (index === 0);
            childScope.$last = (index === (arrayLength - 1));
            childScope.$middle = !(childScope.$first || childScope.$last);

            if (!block.element) {
              linker(childScope, function(clone) {
                animate.enter(clone, null, cursor);
                cursor = clone;
                block.scope = childScope;
                block.element = clone;
                nextBlockMap[block.id] = block;
              });
            }
          }
          lastBlockMap = nextBlockMap;
        });
      };
    }
  };
}];

<commit_msg>Исправил опечатку<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name ng.directive:ngRepeat
 *
 * @description
 * Директива `ngRepeat` создает экземпляры по шаблону для каждого элемента коллекции. Каждый экземпляр шаблона
 * получает собственную область видимости, в которой создаются переменные, имеющиеся в текущем элементе коллекции и
 * в `$index` устанавливается индекс или ключ элемента.
 *
 * Специальные свойства, представленные каждым шаблонным элементом в его локальной области видимости, включают:
 *
 *   * `$index` – `{number}` – сдвиг итератора при повторении элемента (0..length-1)
 *   * `$first` – `{boolean}` – true, если повторяемый элемент является первым элементом в коллекции.
 *   * `$middle` – `{boolean}` – true, если повторяемый элемент не первый и не последний элемент в коллекции.
 *   * `$last` – `{boolean}` – true, если повторяемый элемент является последним элементом коллекции.
 *
 * Кроме того, с помощью атрибута ngAnimate можно задать анимацию для эффектов **enter**,
 * **leave** и **move**.
 *
 * @animations
 * enter - когда новый элемент добавляется в список или показывается после фильтрации
 * leave - когда элемент удаляется из списка или скрывается из результатов фильтрации
 * move - когда соседний пункт удаляется, изменяя порядок или когда список упорядочивается по другому критерию
 *
 * @element ANY
 * @scope
 * @priority 1000
 * @param {repeat_expression} ngRepeat Выражение, указывающее, как перебирать коллекцию.
 * В настоящее время поддерживаются следующие форматы:
 *
 *   * `variable in expression` – где `variable` это определенная пользователем переменная, которая будет содержать
 *     текущее значение при переборе и `expression` – это выражение в области видимости, которое указывает на
 *     коллекцию для перебора.
 *
 *     Например: `track in cd.tracks`.
 *
 *   * `(key, value) in expression` – где `key` и `value` могут быть идентификаторами, определенными пользователем,
 *     и `expression` это выражение в области видимости, которое возвращает коллекцию для перебора.
 *
 *     Например: `(name, age) in {'adam':10, 'amalie':12}`.
 *
 *   * `variable in expression track by tracking_expression` – Так же можно использовать дополнительную функцию
 *     отслеживания, которая может быть использована для связывания объектов коллекции с DOM-элементами. Если функция
 *     отслеживания специально не указана, то ng-repeat ассоциирует элементы коллекции по идентичности. Нельзя иметь
 *     более одной функции отслеживания с одним и тем же ключем. (Т.е. два различных объекта будут связаны с одним
 *     DOM-элементом, что невозможно).
 *
 *     Например: `item in items` эквивалентно `item in items track by $id(item)`. Это означает, что DOM-элементы
 *     будут ассоциироваться идентично элементам в массиве.
 *
 *     Например: `item in items track by $id(item)`. Созданная функция `$id()` может быть использована для присвоения
 *     уникального свойства `$$hashKey` каждому элементу в массиве. Это свойство затем используется в качестве ключа
 *     для связывания DOM-элемента с соответствующим элементом в массиве по идентичности. Перемещение же элемента в
 *     массиве будет перемещать DOM-элемент по такому же пути.
 *
 *     Например: `item in items track by item.id` это типичный образец, когда элементы загружаются из базы данных
 *     В этом случае идентичность объекта не имеет значения. Два объекта считаются эквивалентными, если их `id`
 *     схожи.
 *
 * @example
 * Пример инициализирует область видимости для списка имен, а затем использует `ngRepeat` для показа каждого человека:
  <example animations="true">
    <file name="index.html">
      <div ng-init="friends = [
        {name:'John', age:25, gender:'boy'},
        {name:'Jessie', age:30, gender:'girl'},
        {name:'Johanna', age:28, gender:'girl'},
        {name:'Joy', age:15, gender:'girl'},
        {name:'Mary', age:28, gender:'girl'},
        {name:'Peter', age:95, gender:'boy'},
        {name:'Sebastian', age:50, gender:'boy'},
        {name:'Erika', age:27, gender:'girl'},
        {name:'Patrick', age:40, gender:'boy'},
        {name:'Samantha', age:60, gender:'girl'}
      ]">
        I have {{friends.length}} friends. They are:
        <input type="search" ng-model="q" placeholder="filter friends..." />

            [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.

    </file>
    <file name="animations.css">
      .example-repeat-enter-setup,
      .example-repeat-leave-setup,
      .example-repeat-move-setup {
        -webkit-transition:all linear 0.5s;
        -moz-transition:all linear 0.5s;
        -ms-transition:all linear 0.5s;
        -o-transition:all linear 0.5s;
        transition:all linear 0.5s;
      }

      .example-repeat-enter-setup {
        line-height:0;
        opacity:0;
      }
      .example-repeat-enter-setup.example-repeat-enter-start {
        line-height:20px;
        opacity:1;
      }

      .example-repeat-leave-setup {
        opacity:1;
        line-height:20px;
      }
      .example-repeat-leave-setup.example-repeat-leave-start {
        opacity:0;
        line-height:0;
      }

      .example-repeat-move-setup { }
      .example-repeat-move-setup.example-repeat-move-start { }
    </file>
    <file name="scenario.js">
       it('should render initial data set', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);
         expect(r.row(0)).toEqual(["1","John","25"]);
         expect(r.row(1)).toEqual(["2","Jessie","30"]);
         expect(r.row(9)).toEqual(["10","Samantha","60"]);
         expect(binding('friends.length')).toBe("10");
       });

       it('should update repeater when filter predicate changes', function() {
         var r = using('.doc-example-live').repeater('ul li');
         expect(r.count()).toBe(10);

         input('q').enter('ma');

         expect(r.count()).toBe(2);
         expect(r.row(0)).toEqual(["1","Mary","28"]);
         expect(r.row(1)).toEqual(["2","Samantha","60"]);
       });
      </file>
    </example>
 */
var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
  var NG_REMOVED = '$$NG_REMOVED';
  return {
    transclude: 'element',
    priority: 1000,
    terminal: true,
    compile: function(element, attr, linker) {
      return function($scope, $element, $attr){
        var animate = $animator($scope, $attr);
        var expression = $attr.ngRepeat;
        var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
          trackByExp, trackByExpGetter, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
          hashFnLocals = {$id: hashKey};

        if (!match) {
          throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
            expression + "'.");
        }

        lhs = match[1];
        rhs = match[2];
        trackByExp = match[4];

        if (trackByExp) {
          trackByExpGetter = $parse(trackByExp);
          trackByIdFn = function(key, value, index) {
            // assign key, value, and $index to the locals so that they can be used in hash functions
            if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
            hashFnLocals[valueIdentifier] = value;
            hashFnLocals.$index = index;
            return trackByExpGetter($scope, hashFnLocals);
          };
        } else {
          trackByIdFn = function(key, value) {
            return hashKey(value);
          }
        }

        match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
        if (!match) {
          throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
              lhs + "'.");
        }
        valueIdentifier = match[3] || match[1];
        keyIdentifier = match[2];

        // Store a list of elements from previous run. This is a hash where key is the item from the
        // iterator, and the value is objects with following properties.
        //   - scope: bound scope
        //   - element: previous element.
        //   - index: position
        var lastBlockMap = {};

        //watch props
        $scope.$watchCollection(rhs, function ngRepeatAction(collection){
          var index, length,
              cursor = $element,     // current position of the node
              nextCursor,
              // Same as lastBlockMap but it has the current state. It will become the
              // lastBlockMap on the next iteration.
              nextBlockMap = {},
              arrayLength,
              childScope,
              key, value, // key/value of iteration
              trackById,
              collectionKeys,
              block,       // last object information {scope, element, id}
              nextBlockOrder = [];

          if (isArray(collection)) {
            collectionKeys = collection;
          } else {
            // if object, extract keys, sort them and use to determine order of iteration over obj props
            collectionKeys = [];
            for (key in collection) {
              if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
                collectionKeys.push(key);
              }
            }
            collectionKeys.sort();
          }

          arrayLength = collectionKeys.length;

          // locate existing items
          length = nextBlockOrder.length = collectionKeys.length;
          for(index = 0; index < length; index++) {
           key = (collection === collectionKeys) ? index : collectionKeys[index];
           value = collection[key];
           trackById = trackByIdFn(key, value, index);
           if(lastBlockMap.hasOwnProperty(trackById)) {
             block = lastBlockMap[trackById]
             delete lastBlockMap[trackById];
             nextBlockMap[trackById] = block;
             nextBlockOrder[index] = block;
           } else if (nextBlockMap.hasOwnProperty(trackById)) {
             // restore lastBlockMap
             forEach(nextBlockOrder, function(block) {
               if (block && block.element) lastBlockMap[block.id] = block;
             });
             // This is a duplicate and we need to throw an error
             throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression +
                 ' key: ' + trackById);
           } else {
             // new never before seen block
             nextBlockOrder[index] = { id: trackById };
             nextBlockMap[trackById] = false;
           }
         }

          // remove existing items
          for (key in lastBlockMap) {
            if (lastBlockMap.hasOwnProperty(key)) {
              block = lastBlockMap[key];
              animate.leave(block.element);
              block.element[0][NG_REMOVED] = true;
              block.scope.$destroy();
            }
          }

          // we are not using forEach for perf reasons (trying to avoid #call)
          for (index = 0, length = collectionKeys.length; index < length; index++) {
            key = (collection === collectionKeys) ? index : collectionKeys[index];
            value = collection[key];
            block = nextBlockOrder[index];

            if (block.element) {
              // if we have already seen this object, then we need to reuse the
              // associated scope/element
              childScope = block.scope;

              nextCursor = cursor[0];
              do {
                nextCursor = nextCursor.nextSibling;
              } while(nextCursor && nextCursor[NG_REMOVED]);

              if (block.element[0] == nextCursor) {
                // do nothing
                cursor = block.element;
              } else {
                // existing item which got moved
                animate.move(block.element, null, cursor);
                cursor = block.element;
              }
            } else {
              // new item which we don't know about
              childScope = $scope.$new();
            }

            childScope[valueIdentifier] = value;
            if (keyIdentifier) childScope[keyIdentifier] = key;
            childScope.$index = index;
            childScope.$first = (index === 0);
            childScope.$last = (index === (arrayLength - 1));
            childScope.$middle = !(childScope.$first || childScope.$last);

            if (!block.element) {
              linker(childScope, function(clone) {
                animate.enter(clone, null, cursor);
                cursor = clone;
                block.scope = childScope;
                block.element = clone;
                nextBlockMap[block.id] = block;
              });
            }
          }
          lastBlockMap = nextBlockMap;
        });
      };
    }
  };
}];

<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name ngSwitch
 * @restrict EA
 *
 * @description
 * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression.
 * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location
 * as specified in the template.
 *
 * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it
 * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element
 * matches the value obtained from the evaluated expression. In other words, you define a container element
 * (where you place the directive), place an expression on the **`on="..."` attribute**
 * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place
 * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on
 * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
 * attribute is displayed.
 *
 *

 * Be aware that the attribute values to match against cannot be expressions. They are interpreted
 * as literal string values to match against.
 * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the
 * value of the expression `$scope.someVal`.
 *

 * @animations
 * | Animation                        | Occurs                              |
 * |----------------------------------|-------------------------------------|
 * | {@link ng.$animate#enter enter}  | after the ngSwitch contents change and the matched child element is placed inside the container |
 * | {@link ng.$animate#leave leave}  | after the ngSwitch contents change and just before the former contents are removed from the DOM |
 *
 * @usage
 *
 * ```
 * <ANY ng-switch="expression">
 *   <ANY ng-switch-when="matchValue1">...</ANY>
 *   <ANY ng-switch-when="matchValue2">...</ANY>
 *   <ANY ng-switch-default>...</ANY>
 * </ANY>
 * ```
 *
 *
 * @scope
 * @priority 1200
 * @param {*} ngSwitch|on expression to match against ng-switch-when.
 * On child elements add:
 *
 * * `ngSwitchWhen`: the case statement to match against. If match then this
 *   case will be displayed. If the same match appears multiple times, all the
 *   elements will be displayed. It is possible to associate mutiple values to
 *   the same `ngSwitchWhen` by defining the optional attribute
 *   `ngSwitchWhenSeparator`. The separator will be used to split the value of
 *   the `ngSwitchWhen` attribute into multiple tokens, and the element will show
 *   if any of the `ngSwitch` evaluates to any of these tokens.
 * * `ngSwitchDefault`: the default case when no other case match. If there
 *   are multiple default cases, all of them will be displayed when no other
 *   case match.
 *
 *
 * @example
  <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch">
    <file name="index.html">

        <select ng-model="selection" ng-options="item for item in items">
        </select>
        selection={{selection}}

Settings Div

Home Span

default

    </file>
    <file name="script.js">
      angular.module('switchExample', ['ngAnimate'])
        .controller('ExampleController', ['$scope', function($scope) {
          $scope.items = ['settings', 'home', 'other'];
          $scope.selection = $scope.items[0];
        }]);
    </file>
    <file name="animations.css">
      .animate-switch-container {
        position:relative;
        background:white;
        border:1px solid black;
        height:40px;
        overflow:hidden;
      }

      .animate-switch {
        padding:10px;
      }

      .animate-switch.ng-animate {
        transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;

        position:absolute;
        top:0;
        left:0;
        right:0;
        bottom:0;
      }

      .animate-switch.ng-leave.ng-leave-active,
      .animate-switch.ng-enter {
        top:-50px;
      }
      .animate-switch.ng-leave,
      .animate-switch.ng-enter.ng-enter-active {
        top:0;
      }
    </file>
    <file name="protractor.js" type="protractor">
      var switchElem = element(by.css('[ng-switch]'));
      var select = element(by.model('selection'));

      it('should start in settings', function() {
        expect(switchElem.getText()).toMatch(/Settings Div/);
      });
      it('should change to home', function() {
        select.all(by.css('option')).get(1).click();
        expect(switchElem.getText()).toMatch(/Home Span/);
      });
      it('should select default', function() {
        select.all(by.css('option')).get(2).click();
        expect(switchElem.getText()).toMatch(/default/);
      });
    </file>
  </example>
 */
var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
  return {
    require: 'ngSwitch',

    // asks for $scope to fool the BC controller module
    controller: ['$scope', function NgSwitchController() {
     this.cases = {};
    }],
    link: function(scope, element, attr, ngSwitchController) {
      var watchExpr = attr.ngSwitch || attr.on,
          selectedTranscludes = [],
          selectedElements = [],
          previousLeaveAnimations = [],
          selectedScopes = [];

      var spliceFactory = function(array, index) {
          return function() { array.splice(index, 1); };
      };

      scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
        var i, ii;
        for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
          $animate.cancel(previousLeaveAnimations[i]);
        }
        previousLeaveAnimations.length = 0;

        for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
          var selected = getBlockNodes(selectedElements[i].clone);
          selectedScopes[i].$destroy();
          var promise = previousLeaveAnimations[i] = $animate.leave(selected);
          promise.then(spliceFactory(previousLeaveAnimations, i));
        }

        selectedElements.length = 0;
        selectedScopes.length = 0;

        if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
          forEach(selectedTranscludes, function(selectedTransclude) {
            selectedTransclude.transclude(function(caseElement, selectedScope) {
              selectedScopes.push(selectedScope);
              var anchor = selectedTransclude.element;
              caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
              var block = { clone: caseElement };

              selectedElements.push(block);
              $animate.enter(caseElement, anchor.parent(), anchor);
            });
          });
        }
      });
    }
  };
}];

var ngSwitchWhenDirective = ngDirective({
  transclude: 'element',
  priority: 1200,
  require: '^ngSwitch',
  multiElement: true,
  link: function(scope, element, attrs, ctrl, $transclude) {

    var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
      // Filter duplicate cases
      function(element, index, array) { return array[index - 1] !== element; }
    );

    forEach(cases, function(whenCase) {
      ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
      ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
    });
  }
});

var ngSwitchDefaultDirective = ngDirective({
  transclude: 'element',
  priority: 1200,
  require: '^ngSwitch',
  multiElement: true,
  link: function(scope, element, attr, ctrl, $transclude) {
    ctrl.cases['?'] = (ctrl.cases['?'] || []);
    ctrl.cases['?'].push({ transclude: $transclude, element: element });
   }
});
<commit_msg>docs(ngSwitch): add separator example<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name ngSwitch
 * @restrict EA
 *
 * @description
 * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression.
 * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location
 * as specified in the template.
 *
 * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it
 * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element
 * matches the value obtained from the evaluated expression. In other words, you define a container element
 * (where you place the directive), place an expression on the **`on="..."` attribute**
 * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place
 * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on
 * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
 * attribute is displayed.
 *
 *

 * Be aware that the attribute values to match against cannot be expressions. They are interpreted
 * as literal string values to match against.
 * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the
 * value of the expression `$scope.someVal`.
 *

 * @animations
 * | Animation                        | Occurs                              |
 * |----------------------------------|-------------------------------------|
 * | {@link ng.$animate#enter enter}  | after the ngSwitch contents change and the matched child element is placed inside the container |
 * | {@link ng.$animate#leave leave}  | after the ngSwitch contents change and just before the former contents are removed from the DOM |
 *
 * @usage
 *
 * ```
 * <ANY ng-switch="expression">
 *   <ANY ng-switch-when="matchValue1">...</ANY>
 *   <ANY ng-switch-when="matchValue2">...</ANY>
 *   <ANY ng-switch-default>...</ANY>
 * </ANY>
 * ```
 *
 *
 * @scope
 * @priority 1200
 * @param {*} ngSwitch|on expression to match against ng-switch-when.
 * On child elements add:
 *
 * * `ngSwitchWhen`: the case statement to match against. If match then this
 *   case will be displayed. If the same match appears multiple times, all the
 *   elements will be displayed. It is possible to associate mutiple values to
 *   the same `ngSwitchWhen` by defining the optional attribute
 *   `ngSwitchWhenSeparator`. The separator will be used to split the value of
 *   the `ngSwitchWhen` attribute into multiple tokens, and the element will show
 *   if any of the `ngSwitch` evaluates to any of these tokens.
 * * `ngSwitchDefault`: the default case when no other case match. If there
 *   are multiple default cases, all of them will be displayed when no other
 *   case match.
 *
 *
 * @example
  <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch">
    <file name="index.html">

        <select ng-model="selection" ng-options="item for item in items">
        </select>
        selection={{selection}}

Settings Div

Home Span

default

    </file>
    <file name="script.js">
      angular.module('switchExample', ['ngAnimate'])
        .controller('ExampleController', ['$scope', function($scope) {
          $scope.items = ['settings', 'home', 'options', 'other'];
          $scope.selection = $scope.items[0];
        }]);
    </file>
    <file name="animations.css">
      .animate-switch-container {
        position:relative;
        background:white;
        border:1px solid black;
        height:40px;
        overflow:hidden;
      }

      .animate-switch {
        padding:10px;
      }

      .animate-switch.ng-animate {
        transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;

        position:absolute;
        top:0;
        left:0;
        right:0;
        bottom:0;
      }

      .animate-switch.ng-leave.ng-leave-active,
      .animate-switch.ng-enter {
        top:-50px;
      }
      .animate-switch.ng-leave,
      .animate-switch.ng-enter.ng-enter-active {
        top:0;
      }
    </file>
    <file name="protractor.js" type="protractor">
      var switchElem = element(by.css('[ng-switch]'));
      var select = element(by.model('selection'));

      it('should start in settings', function() {
        expect(switchElem.getText()).toMatch(/Settings Div/);
      });
      it('should change to home', function() {
        select.all(by.css('option')).get(1).click();
        expect(switchElem.getText()).toMatch(/Home Span/);
      });
      it('should change to settings via "options"', function() {
        select.all(by.css('option')).get(2).click();
        expect(switchElem.getText()).toMatch(/Settings Div/);
      });
      it('should select default', function() {
        select.all(by.css('option')).get(3).click();
        expect(switchElem.getText()).toMatch(/default/);
      });
    </file>
  </example>
 */
var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
  return {
    require: 'ngSwitch',

    // asks for $scope to fool the BC controller module
    controller: ['$scope', function NgSwitchController() {
     this.cases = {};
    }],
    link: function(scope, element, attr, ngSwitchController) {
      var watchExpr = attr.ngSwitch || attr.on,
          selectedTranscludes = [],
          selectedElements = [],
          previousLeaveAnimations = [],
          selectedScopes = [];

      var spliceFactory = function(array, index) {
          return function() { array.splice(index, 1); };
      };

      scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
        var i, ii;
        for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
          $animate.cancel(previousLeaveAnimations[i]);
        }
        previousLeaveAnimations.length = 0;

        for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
          var selected = getBlockNodes(selectedElements[i].clone);
          selectedScopes[i].$destroy();
          var promise = previousLeaveAnimations[i] = $animate.leave(selected);
          promise.then(spliceFactory(previousLeaveAnimations, i));
        }

        selectedElements.length = 0;
        selectedScopes.length = 0;

        if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
          forEach(selectedTranscludes, function(selectedTransclude) {
            selectedTransclude.transclude(function(caseElement, selectedScope) {
              selectedScopes.push(selectedScope);
              var anchor = selectedTransclude.element;
              caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
              var block = { clone: caseElement };

              selectedElements.push(block);
              $animate.enter(caseElement, anchor.parent(), anchor);
            });
          });
        }
      });
    }
  };
}];

var ngSwitchWhenDirective = ngDirective({
  transclude: 'element',
  priority: 1200,
  require: '^ngSwitch',
  multiElement: true,
  link: function(scope, element, attrs, ctrl, $transclude) {

    var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
      // Filter duplicate cases
      function(element, index, array) { return array[index - 1] !== element; }
    );

    forEach(cases, function(whenCase) {
      ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
      ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
    });
  }
});

var ngSwitchDefaultDirective = ngDirective({
  transclude: 'element',
  priority: 1200,
  require: '^ngSwitch',
  multiElement: true,
  link: function(scope, element, attr, ctrl, $transclude) {
    ctrl.cases['?'] = (ctrl.cases['?'] || []);
    ctrl.cases['?'].push({ transclude: $transclude, element: element });
   }
});
<|endoftext|>


<commit_before>'use strict';
/**
 * The AuthorManager controlls all information about the Pad authors
 */

/*
 * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const db = require('./DB');
const CustomError = require('../utils/customError');
const randomString = require('../../static/js/pad_utils').randomString;

exports.getColorPalette = () => [
  '#ffc7c7',
  '#fff1c7',
  '#e3ffc7',
  '#c7ffd5',
  '#c7ffff',
  '#c7d5ff',
  '#e3c7ff',
  '#ffc7f1',
  '#ffa8a8',
  '#ffe699',
  '#cfff9e',
  '#99ffb3',
  '#a3ffff',
  '#99b3ff',
  '#cc99ff',
  '#ff99e5',
  '#e7b1b1',
  '#e9dcAf',
  '#cde9af',
  '#bfedcc',
  '#b1e7e7',
  '#c3cdee',
  '#d2b8ea',
  '#eec3e6',
  '#e9cece',
  '#e7e0ca',
  '#d3e5c7',
  '#bce1c5',
  '#c1e2e2',
  '#c1c9e2',
  '#cfc1e2',
  '#e0bdd9',
  '#baded3',
  '#a0f8eb',
  '#b1e7e0',
  '#c3c8e4',
  '#cec5e2',
  '#b1d5e7',
  '#cda8f0',
  '#f0f0a8',
  '#f2f2a6',
  '#f5a8eb',
  '#c5f9a9',
  '#ececbb',
  '#e7c4bc',
  '#daf0b2',
  '#b0a0fd',
  '#bce2e7',
  '#cce2bb',
  '#ec9afe',
  '#edabbd',
  '#aeaeea',
  '#c4e7b1',
  '#d722bb',
  '#f3a5e7',
  '#ffa8a8',
  '#d8c0c5',
  '#eaaedd',
  '#adc6eb',
  '#bedad1',
  '#dee9af',
  '#e9afc2',
  '#f8d2a0',
  '#b3b3e6',
];

/**
 * Checks if the author exists
 */
exports.doesAuthorExist = async (authorID) => {
  const author = await db.get(`globalAuthor:${authorID}`);

  return author != null;
};

/* exported for backwards compatibility */
exports.doesAuthorExists = exports.doesAuthorExist;

/**
 * Returns the AuthorID for a token.
 * @param {String} token The token
 */
exports.getAuthor4Token = async (token) => {
  const author = await mapAuthorWithDBKey('token2author', token);

  // return only the sub value authorID
  return author ? author.authorID : author;
};

/**
 * Returns the AuthorID for a mapper.
 * @param {String} token The mapper
 * @param {String} name The name of the author (optional)
 */
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
  const author = await mapAuthorWithDBKey('mapper2author', authorMapper);

  if (name) {
    // set the name of this author
    await exports.setAuthorName(author.authorID, name);
  }

  return author;
};

/**
 * Returns the AuthorID for a mapper. We can map using a mapperkey,
 * so far this is token2author and mapper2author
 * @param {String} mapperkey The database key name for this mapper
 * @param {String} mapper The mapper
 */
async function mapAuthorWithDBKey(mapperkey, mapper) {
  // try to map to an author
  const author = await db.get(`${mapperkey}:${mapper}`);

  if (author == null) {
    // there is no author with this mapper, so create one
    const author = await exports.createAuthor(null);

    // create the token2author relation
    await db.set(`${mapperkey}:${mapper}`, author.authorID);

    // return the author
    return author;
  }

  // there is an author with this mapper
  // update the timestamp of this author
  await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());

  // return the author
  return {authorID: author};
}

/**
 * Internal function that creates the database entry for an author
 * @param {String} name The name of the author
 */
exports.createAuthor = (name) => {
  // create the new author name
  const author = `a.${randomString(16)}`;

  // create the globalAuthors db entry
  const authorObj = {
    colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
    name,
    timestamp: Date.now(),
  };

  // set the global author db entry
  // NB: no await, since we're not waiting for the DB set to finish
  db.set(`globalAuthor:${author}`, authorObj);

  return {authorID: author};
};

/**
 * Returns the Author Obj of the author
 * @param {String} author The id of the author
 */
exports.getAuthor = (author) => db.get(`globalAuthor:${author}`);

/**
 * Returns the color Id of the author
 * @param {String} author The id of the author
 */
exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']);

/**
 * Sets the color Id of the author
 * @param {String} author The id of the author
 * @param {String} colorId The color id of the author
 */
exports.setAuthorColorId = (author, colorId) => db.setSub(
    `globalAuthor:${author}`, ['colorId'], colorId);

/**
 * Returns the name of the author
 * @param {String} author The id of the author
 */
exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']);

/**
 * Sets the name of the author
 * @param {String} author The id of the author
 * @param {String} name The name of the author
 */
exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name);

/**
 * Returns an array of all pads this author contributed to
 * @param {String} author The id of the author
 */
exports.listPadsOfAuthor = async (authorID) => {
  /* There are two other places where this array is manipulated:
   * (1) When the author is added to a pad, the author object is also updated
   * (2) When a pad is deleted, each author of that pad is also updated
   */

  // get the globalAuthor
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) {
    // author does not exist
    throw new CustomError('authorID does not exist', 'apierror');
  }

  // everything is fine, return the pad IDs
  const padIDs = Object.keys(author.padIDs || {});

  return {padIDs};
};

/**
 * Adds a new pad to the list of contributions
 * @param {String} author The id of the author
 * @param {String} padID The id of the pad the author contributes to
 */
exports.addPad = async (authorID, padID) => {
  // get the entry
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) return;

  /*
   * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
   * to perform a strict check here
   */
  if (!author.padIDs) {
    // the entry doesn't exist so far, let's create it
    author.padIDs = {};
  }

  // add the entry for this pad
  author.padIDs[padID] = 1; // anything, because value is not used

  // save the new element back
  db.set(`globalAuthor:${authorID}`, author);
};

/**
 * Removes a pad from the list of contributions
 * @param {String} author The id of the author
 * @param {String} padID The id of the pad the author contributes to
 */
exports.removePad = async (authorID, padID) => {
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) return;

  if (author.padIDs != null) {
    // remove pad from author
    delete author.padIDs[padID];
    await db.set(`globalAuthor:${authorID}`, author);
  }
};
<commit_msg>AuthorManager: await db.set in createAuthor<commit_after>'use strict';
/**
 * The AuthorManager controlls all information about the Pad authors
 */

/*
 * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const db = require('./DB');
const CustomError = require('../utils/customError');
const randomString = require('../../static/js/pad_utils').randomString;

exports.getColorPalette = () => [
  '#ffc7c7',
  '#fff1c7',
  '#e3ffc7',
  '#c7ffd5',
  '#c7ffff',
  '#c7d5ff',
  '#e3c7ff',
  '#ffc7f1',
  '#ffa8a8',
  '#ffe699',
  '#cfff9e',
  '#99ffb3',
  '#a3ffff',
  '#99b3ff',
  '#cc99ff',
  '#ff99e5',
  '#e7b1b1',
  '#e9dcAf',
  '#cde9af',
  '#bfedcc',
  '#b1e7e7',
  '#c3cdee',
  '#d2b8ea',
  '#eec3e6',
  '#e9cece',
  '#e7e0ca',
  '#d3e5c7',
  '#bce1c5',
  '#c1e2e2',
  '#c1c9e2',
  '#cfc1e2',
  '#e0bdd9',
  '#baded3',
  '#a0f8eb',
  '#b1e7e0',
  '#c3c8e4',
  '#cec5e2',
  '#b1d5e7',
  '#cda8f0',
  '#f0f0a8',
  '#f2f2a6',
  '#f5a8eb',
  '#c5f9a9',
  '#ececbb',
  '#e7c4bc',
  '#daf0b2',
  '#b0a0fd',
  '#bce2e7',
  '#cce2bb',
  '#ec9afe',
  '#edabbd',
  '#aeaeea',
  '#c4e7b1',
  '#d722bb',
  '#f3a5e7',
  '#ffa8a8',
  '#d8c0c5',
  '#eaaedd',
  '#adc6eb',
  '#bedad1',
  '#dee9af',
  '#e9afc2',
  '#f8d2a0',
  '#b3b3e6',
];

/**
 * Checks if the author exists
 */
exports.doesAuthorExist = async (authorID) => {
  const author = await db.get(`globalAuthor:${authorID}`);

  return author != null;
};

/* exported for backwards compatibility */
exports.doesAuthorExists = exports.doesAuthorExist;

/**
 * Returns the AuthorID for a token.
 * @param {String} token The token
 */
exports.getAuthor4Token = async (token) => {
  const author = await mapAuthorWithDBKey('token2author', token);

  // return only the sub value authorID
  return author ? author.authorID : author;
};

/**
 * Returns the AuthorID for a mapper.
 * @param {String} token The mapper
 * @param {String} name The name of the author (optional)
 */
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
  const author = await mapAuthorWithDBKey('mapper2author', authorMapper);

  if (name) {
    // set the name of this author
    await exports.setAuthorName(author.authorID, name);
  }

  return author;
};

/**
 * Returns the AuthorID for a mapper. We can map using a mapperkey,
 * so far this is token2author and mapper2author
 * @param {String} mapperkey The database key name for this mapper
 * @param {String} mapper The mapper
 */
async function mapAuthorWithDBKey(mapperkey, mapper) {
  // try to map to an author
  const author = await db.get(`${mapperkey}:${mapper}`);

  if (author == null) {
    // there is no author with this mapper, so create one
    const author = await exports.createAuthor(null);

    // create the token2author relation
    await db.set(`${mapperkey}:${mapper}`, author.authorID);

    // return the author
    return author;
  }

  // there is an author with this mapper
  // update the timestamp of this author
  await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now());

  // return the author
  return {authorID: author};
};

/**
 * Internal function that creates the database entry for an author
 * @param {String} name The name of the author
 */
exports.createAuthor = async (name) => {
  // create the new author name
  const author = `a.${randomString(16)}`;

  // create the globalAuthors db entry
  const authorObj = {
    colorId: Math.floor(Math.random() * (exports.getColorPalette().length)),
    name,
    timestamp: Date.now(),
  };

  // set the global author db entry
  await db.set(`globalAuthor:${author}`, authorObj);

  return {authorID: author};
};

/**
 * Returns the Author Obj of the author
 * @param {String} author The id of the author
 */
exports.getAuthor = (author) => db.get(`globalAuthor:${author}`);

/**
 * Returns the color Id of the author
 * @param {String} author The id of the author
 */
exports.getAuthorColorId = (author) => db.getSub(`globalAuthor:${author}`, ['colorId']);

/**
 * Sets the color Id of the author
 * @param {String} author The id of the author
 * @param {String} colorId The color id of the author
 */
exports.setAuthorColorId = (author, colorId) => db.setSub(
    `globalAuthor:${author}`, ['colorId'], colorId);

/**
 * Returns the name of the author
 * @param {String} author The id of the author
 */
exports.getAuthorName = (author) => db.getSub(`globalAuthor:${author}`, ['name']);

/**
 * Sets the name of the author
 * @param {String} author The id of the author
 * @param {String} name The name of the author
 */
exports.setAuthorName = (author, name) => db.setSub(`globalAuthor:${author}`, ['name'], name);

/**
 * Returns an array of all pads this author contributed to
 * @param {String} author The id of the author
 */
exports.listPadsOfAuthor = async (authorID) => {
  /* There are two other places where this array is manipulated:
   * (1) When the author is added to a pad, the author object is also updated
   * (2) When a pad is deleted, each author of that pad is also updated
   */

  // get the globalAuthor
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) {
    // author does not exist
    throw new CustomError('authorID does not exist', 'apierror');
  }

  // everything is fine, return the pad IDs
  const padIDs = Object.keys(author.padIDs || {});

  return {padIDs};
};

/**
 * Adds a new pad to the list of contributions
 * @param {String} author The id of the author
 * @param {String} padID The id of the pad the author contributes to
 */
exports.addPad = async (authorID, padID) => {
  // get the entry
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) return;

  /*
   * ACHTUNG: padIDs can also be undefined, not just null, so it is not possible
   * to perform a strict check here
   */
  if (!author.padIDs) {
    // the entry doesn't exist so far, let's create it
    author.padIDs = {};
  }

  // add the entry for this pad
  author.padIDs[padID] = 1; // anything, because value is not used

  // save the new element back
  db.set(`globalAuthor:${authorID}`, author);
};

/**
 * Removes a pad from the list of contributions
 * @param {String} author The id of the author
 * @param {String} padID The id of the pad the author contributes to
 */
exports.removePad = async (authorID, padID) => {
  const author = await db.get(`globalAuthor:${authorID}`);

  if (author == null) return;

  if (author.padIDs != null) {
    // remove pad from author
    delete author.padIDs[padID];
    await db.set(`globalAuthor:${authorID}`, author);
  }
};
<|endoftext|>


<commit_before>;
/**
 * This is a stylesheet loader which is useful for controlling the dynamic loading of
 * CSS. It provides the following features:
 * - load CSS from a string or a stylesheet URL
 * - returns a promise representing whether the CSS has loaded
 * Inspired by the Filament Group's work on CSS loading: https://github.com/filamentgroup/loadCSS
 */

(function(window) {
    'use strict';

    // Create an array for storing our stylesheet promises.
    var stylesheetsLoaded = [];

    /**
     * Load the supplied stylesheet url at the end of the <body>
     * @param  {string} url         The stylesheet URL to load
     * @param  {string} stylesClass An optional class that we'll attach to the
     *                              rendered <style> element.
     * @param  {string} media       The media to which we wish the stylesheet to apply.
     *                              Defaults to 'all'.
     * @return {window.Promise}     The promise representing whether the stylesheet has loaded.
     */
    function _loadStylesheetFromURL(url, stylesClass, media) {
        // https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js
        var stylesheetPromise = new window.Promise(function(resolve, reject) {

            var doc = window.document;
            var refs = (doc.body || doc.getElementsByTagName('head')[0]).childNodes;
            var ref = refs[ refs.length - 1];

            // Create a new link tag
            var link = document.createElement('link');

            // Use the url argument as source attribute
            link.rel = 'stylesheet'
            link.href = url;

            // Temporarily set media to something inapplicable to ensure
            // it'll fetch without blocking render
            link.media = 'only x';

            if (stylesClass) {
                link.classList.add(stylesClass);
            }

            function loadCB() {
                if (link.addEventListener) {
                    link.removeEventListener( 'load', loadCB );
                }

                link.media = media || 'all';
                resolve(url);
            }

            // Call resolve when it’s loaded
            if (link.addEventListener) {
                link.addEventListener('load', function() {
                    loadCB();
                }, false);
            } else {
                link.attachEvent('load', function() {
                    loadCB();
                });
            }

            // Reject the promise if there’s an error
            if (link.addEventListener) {
                link.addEventListener('error', function() {
                    reject(url);
                }, false);
            } else {
                link.attachEvent('error', function() {
                    reject(url);
                });
            }

            // wait until body is defined before injecting link. This ensures a non-blocking load in IE11.
            function ready(cb) {
                if (doc.body) {
                    return cb();
                }
                setTimeout(function() {
                    ready(cb);
                });
            }

            // Inject link
            // Note: `insertBefore` is used instead of `appendChild`, for safety
            // re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
            ready(function() {
                ref.parentNode.insertBefore(link, ref.nextSibling);
            });
        });

        // Add our script to the stylesheetsLoaded array so other parts of the application
        // can determine whether a given stylesheet is / has been script loaded.
        stylesheetsLoaded.push({
            urlOrStyles: url,
            promise: stylesheetPromise,
        })

        return stylesheetPromise;
    }

    /**
     * Load the supplied styles into the head.
     * @param  {string} styles      The styles we wish to load
     * @param  {string} stylesClass An optional class that we'll attach to the
     *                              rendered <style> element.
     * @param  {string} media       The media to which we wish the stylesheet to apply.
     *                              Defaults to 'all'.
     * @return {window.Promise}     A promise that indicates when the styles are available.
     */
    function _loadStylesheetFromCSS(styles, stylesClass, media) {
        var stylesheetPromise = new window.Promise(function(resolve, reject) {

            var doc = window.document;
            var refs = (doc.body || doc.getElementsByTagName('head')[0]).childNodes;
            var ref = refs[ refs.length - 1];

            var stylesheet = document.createElement('style');

            try {
                stylesheet.type = 'text/css';

                if (stylesheet.styleSheet) {
                    stylesheet.styleSheet.cssText = styles;
                } else {
                    stylesheet.appendChild(document.createTextNode(styles));
                }

                if (stylesClass) {
                    stylesheet.classList.add(stylesClass);
                }

                stylesheet.media = media || 'all';
                ref.parentNode.insertBefore(stylesheet, ref.nextSibling);

                resolve(styles);
            } catch (error) {
                reject(error, styles);
            }
        });

        // Add our script to the stylesheetsLoaded array so other parts of the application
        // can determine whether a given stylesheet is / has been script loaded.
        stylesheetsLoaded.push({
            urlOrStyles: styles,
            promise: stylesheetPromise,
        })

        return stylesheetPromise;
    }

    /**
     * Return the promise of the supplied stylesheet.
     * If the stylesheet has not already been loaded, force the load.
     * @param  {string} urlOrStyles  A script URL
     * @param  {string} stylesClass  An optional class that we'll attach to the
     *                               rendered <style> element.
     * @param  {string} media        The media to which we wish the stylesheet to apply.
     *                               Defaults to 'all'.
     * @return {window.Promise}      The promise representing whether the stylesheet has loaded.
     */
    var ffxnzLoadStylesheet = function(urlOrStyles, stylesClass, media) {
        var promise;

        var stylesheets = stylesheetsLoaded.filter(function(item) {
            return item.urlOrStyles === urlOrStyles;
        });

        if (stylesheets.length) {
            promise = stylesheets[0].promise;
        } else {
            // Our stylesheet hasn't been previously requested for load,
            // so let's determine if it's a URL or a string of CSS and load it.
            if (/\.css$/.test(urlOrStyles)) {
                promise = _loadStylesheetFromURL(urlOrStyles, stylesClass, media);
            } else {
                promise = _loadStylesheetFromCSS(urlOrStyles, stylesClass, media);
            }
        }

        return promise;
    }

    if (typeof module !== 'undefined' && module.exports) {
        // commonjs
        exports = module.exports = ffxnzLoadStylesheet;
    } else {
        // browserland
        window.nz = window.nz || {};
        window.nz.ffx = window.nz.ffx || {};
        window.nz.ffx.loadStylesheet = ffxnzLoadStylesheet;
        window.nz.ffx.stylesheetsLoaded = stylesheetsLoaded;
    }

}(typeof global !== 'undefined' ? global : this))
<commit_msg>Resolve code review comments<commit_after>;
/**
 * This is a stylesheet loader which is useful for controlling the dynamic loading of
 * CSS. It provides the following features:
 * - load CSS from a string or a stylesheet URL
 * - returns a promise representing whether the CSS has loaded
 * Inspired by the Filament Group's work on CSS loading: https://github.com/filamentgroup/loadCSS
 */

(function(window) {
    'use strict';

    // Create an array for storing our stylesheet promises.
    var stylesheetsLoaded = [];

    /**
     * Load the supplied stylesheet url at the end of the <body>
     * @param  {string} url         The stylesheet URL to load
     * @param  {string} stylesClass An optional class that we'll attach to the
     *                              rendered <style> element.
     * @param  {string} media       The media to which we wish the stylesheet to apply.
     *                              Defaults to 'all'.
     * @return {window.Promise}     The promise representing whether the stylesheet has loaded.
     */
    function _loadStylesheetFromURL(url, stylesClass, media) {
        // https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js
        var stylesheetPromise = new window.Promise(function(resolve, reject) {

            var doc = window.document;
            var refs = (doc.body || doc.getElementsByTagName('head')[0]).childNodes;
            var ref = refs[ refs.length - 1];

            // Create a new link tag
            var link = document.createElement('link');

            // Use the url argument as source attribute
            link.rel = 'stylesheet';
            link.href = url;

            // Temporarily set media to something inapplicable to ensure
            // it'll fetch without blocking render
            link.media = 'only x';

            if (stylesClass) {
                link.classList.add(stylesClass);
            }

            function loadCB() {
                if (link.addEventListener) {
                    link.removeEventListener( 'load', loadCB );
                }

                link.media = media || 'all';
                resolve(url);
            }

            // Call resolve when it’s loaded
            if (link.addEventListener) {
                link.addEventListener('load', function() {
                    loadCB();
                }, false);
            } else {
                link.attachEvent('load', function() {
                    loadCB();
                });
            }

            // Reject the promise if there’s an error
            if (link.addEventListener) {
                link.addEventListener('error', function() {
                    reject(url);
                }, false);
            } else {
                link.attachEvent('error', function() {
                    reject(url);
                });
            }

            // wait until body is defined before injecting link. This ensures a non-blocking load in IE11.
            function ready(cb) {
                if (doc.body) {
                    return cb();
                }
                setTimeout(function() {
                    ready(cb);
                });
            }

            // Inject link
            // Note: `insertBefore` is used instead of `appendChild`, for safety
            // re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
            ready(function() {
                ref.parentNode.insertBefore(link, ref.nextSibling);
            });
        });

        // Add our script to the stylesheetsLoaded array so other parts of the application
        // can determine whether a given stylesheet is / has been script loaded.
        stylesheetsLoaded.push({
            urlOrStyles: url,
            promise: stylesheetPromise,
        });

        return stylesheetPromise;
    }

    /**
     * Load the supplied styles into the head.
     * @param  {string} styles      The styles we wish to load
     * @param  {string} stylesClass An optional class that we'll attach to the
     *                              rendered <style> element.
     * @param  {string} media       The media to which we wish the stylesheet to apply.
     *                              Defaults to 'all'.
     * @return {window.Promise}     A promise that indicates when the styles are available.
     */
    function _loadStylesheetFromCSS(styles, stylesClass, media) {
        var stylesheetPromise = new window.Promise(function(resolve, reject) {

            var doc = window.document;
            var refs = (doc.body || doc.getElementsByTagName('head')[0]).childNodes;
            var ref = refs[ refs.length - 1];

            var stylesheet = document.createElement('style');

            try {
                stylesheet.type = 'text/css';

                if (stylesheet.styleSheet) {
                    stylesheet.styleSheet.cssText = styles;
                } else {
                    stylesheet.appendChild(document.createTextNode(styles));
                }

                if (stylesClass) {
                    stylesheet.classList.add(stylesClass);
                }

                stylesheet.media = media || 'all';
                ref.parentNode.insertBefore(stylesheet, ref.nextSibling);

                resolve(styles);
            } catch (error) {
                reject(error, styles);
            }
        });

        // Add our script to the stylesheetsLoaded array so other parts of the application
        // can determine whether a given stylesheet is / has been script loaded.
        stylesheetsLoaded.push({
            urlOrStyles: styles,
            promise: stylesheetPromise,
        });

        return stylesheetPromise;
    }

    /**
     * Return the promise of the supplied stylesheet.
     * If the stylesheet has not already been loaded, force the load.
     * @param  {string} urlOrStyles  A script URL
     * @param  {string} stylesClass  An optional class that we'll attach to the
     *                               rendered <style> element.
     * @param  {string} media        The media to which we wish the stylesheet to apply.
     *                               Defaults to 'all'.
     * @return {window.Promise}      The promise representing whether the stylesheet has loaded.
     */
    var ffxnzLoadStylesheet = function(urlOrStyles, stylesClass, media) {
        var promise;

        var stylesheets = stylesheetsLoaded.filter(function(item) {
            return item.urlOrStyles === urlOrStyles;
        });

        if (stylesheets.length) {
            promise = stylesheets[0].promise;
        } else {
            // Our stylesheet hasn't been previously requested for load,
            // so let's determine if it's a URL or a string of CSS and load it.
            if (/\.css$/.test(urlOrStyles)) {
                promise = _loadStylesheetFromURL(urlOrStyles, stylesClass, media);
            } else {
                promise = _loadStylesheetFromCSS(urlOrStyles, stylesClass, media);
            }
        }

        return promise;
    };

    if (typeof module !== 'undefined' && module.exports) {
        // commonjs
        exports = module.exports = ffxnzLoadStylesheet;
    } else {
        // browserland
        window.nz = window.nz || {};
        window.nz.ffx = window.nz.ffx || {};
        window.nz.ffx.loadStylesheet = ffxnzLoadStylesheet;
        window.nz.ffx.stylesheetsLoaded = stylesheetsLoaded;
    }

}(typeof global !== 'undefined' ? global : this));
<|endoftext|>