<commit_before>
(function(undefined) {

  // Calls Object.definePropert(y,ies) with configurable:true on all properties
  function define(obj, key, value) {
    if (typeof key == 'object') {
      Object.keys(key).forEach(function(name) {
        define(obj, name, key[name]);
      });
    } else {
      Object.defineProperty(obj, key, {
        configurable: true,
        value: value
      })
    }
  }

  // Add Array.prototype.view to create new views and
  // Array.prototype.updateViews to update all the views attached
  define(Array.prototype, {
    view: function view() {
      return createView(this);
    },
    updateViews: function updateViews() {
      if (!Array.isArray(this._views)) return false;
      this._views.forEach(function(view) {
        view.update();
      });
      return true;
    }
  });

  // create a new view on the source array (which may be a view itself)
  function createView(source) {
    var view = [];
    if (!source._views) initSource(source);
    initView(source, view);
    return view;
  }

  // add a _views array for updates and wrap the modifying functions
  function initSource(source) {
    define(source, '_views', []);
    define(source, modifiers);
  }

  // When the source is modified the views need to be updated
  function wrapModifier(modifier) {
    return function() {
      var results = modifier.apply(this, arguments);
      this.updateViews();
      return results;
    };
  }

  var proto = Array.prototype;
  var modifiers = {
    push: wrapModifier(proto.push),
    pop: wrapModifier(proto.pop),
    shift: wrapModifier(proto.shift),
    unshift: wrapModifier(proto.unshift),
    splice: wrapModifier(proto.splice)
  };

  // add view functions to the view array
  function initView(source, view) {
    attachToSource(source, view);
    define(view, viewMethods);
    view.update();
  }

  function attachToSource(source, view) {
    define(view, '_source', source);
    source._views.push(view);
    bindModifiers(source, view);
  }

  // bind the view's modifiers to the source, never modify the view
  function bindModifiers(source, view) {
    Object.keys(modifiers).forEach(function(modifier) {
      define(view, modifier, source[modifier].bind(source));
    });
  }

  // Remove an item from an array
  function remove(array, item) {
    var index = array.indexOf(item);
    if (index != -1) {
      array.splice(index, 1);
      return index;
    }
  }

  var sortDir = {
    asc: 1,
    desc: -1
  }

  function createGetter(field, dir) {
    var fields = field.split('.'),
        body = 'return ',
        ref = 0,
        refs = [],
        prevRef = 'item.';

    while (field = fields.shift()) {
      if (fields.length) {
        curRef = '_ref' + ref++;
        refs.push(curRef);
        body += '(' + curRef + ' = ' + prevRef + field + ') == null ? null : ';
        prevRef = curRef + '.';
      } else {
        body += prevRef + field;
      }
    }
    if (refs.length) body = 'var ' + refs.join(',') + ';\n' + body;
    var getter = new Function('item', body);
    getter.dir = dir || 1;
    return getter;
  }

  // create a sort function from arguments, see comments on `sort` below
  function createSort(sorts) {
    sorts = proto.slice.call(arguments);
    if (sorts[1] == 'asc' || sorts[1] == 'desc') sorts = [sorts];
    var getters = sorts.map(function(field) {
      var dir = 1;
      if (Array.isArray(field)) {
        dir = sortDir[field[1]] || field[1];
        field = field[0];
      }
      return createGetter(field, dir);
    });

    return function(a, b) {
      for (var i = 0; i < getters.length; i++) {
        var getter = getters[i];
        var valueA = getter(a);
        var valueB = getter(b);
        // handle nulls as less than
        if (valueB != null && valueA == null) return getter.dir;
        else if (valueA != null && valueB == null) return -getter.dir;
        else if (valueA > valueB) return getter.dir;
        else if (valueB > valueA) return -getter.dir;
      }
      return 0;
    }
  }

  // Adds the incredibly useful `createSort` function for any array to use.
  define(Array, 'createSort', createSort);

  var viewMethods = {

    // Adds a persistent filter function which takes a single argument, `item`,
    // and returns true or false if item should be allowed in the array. More
    // than one filter may be added and `name` (optional) can be used to remove
    // the filter later.
    addFilter: function addFilter(name, filter) {
      if (!this._filters) define(this, '_filters', []);
      if (typeof name == 'function') {
        filter = name;
        name = null;
      }
      if (typeof filter != 'function') return this;
      if (name) this._filters[name] = filter;
      this._filters.push(filter);
      return this.update();
    },

    // Removes a persistent filter which has been added. `filter` may be the
    // filter function or the name if `name` was used in `addFilter`. If
    // `filter` is not provided then `removeFilter()` will remove the last
    // filter added.
    removeFilter: function removeFilter(filter) {
      if (!this._filters) return this;
      if (typeof filter == 'string') {
        var name = filter;
        filter = this._filters[name];
        delete this._filters[name];
      } else if (typeof filter == 'undefined') {
        filter = this._filters[this._filters.length - 1];
      }
      if (typeof filter != 'function') return this;
      remove(this._filters, filter);
      return this.update();
    },

    // Sets a persistent sort on the array. Objects added or removed will
    // maintain the sort order of this sort function. Pass null to remove the
    // sort. You may use a sort function as Array.prototype.sort takes, or you
    // may use a special array view as the following examples:
    // ```
    // view.sort('dateCreated', 'desc');
    // view.sort('lastName', 'firstName', ['age', 'desc']);
    // view.sort('comments.length', ['postDate', 'desc']);
    // view.sort(['dateCreated', 'desc'], ['title', 'asc']);
    // ```
    // When only sorting by one property, you may use asc/desc as the second
    // parameter. 'asc' is always the default and is not necessary. When sorting
    // by multiple properties an array is required to add the sort order 'desc'.
    // chained properties (such as name.first or comments.length) may be as deep
    // as needed and will null-terminate, so no errors will occur. A null will
    // be treated as LESS THAN a non-null value.
    setSort: function setSort(sort) {
      if (sort && typeof sort != 'function') {
        sort = createSort.apply(null, arguments);
      }
      define(this, '_sort', sort || true);
      return this.update();
    },

    removeSort: function removeSort() {
      define(this, '_sort', undefined);
      return this.update();
    },

    // Paginates the view by splitting it up into pages. Each page has
    // `pageSize` items in it--or fewer if it is the last page. `page` is the
    // initial page the view will display (default is 1). The page can be set
    // later with the `page` method. Pass in null to remove pagination.
    paginate: function paginate(pageSize, page) {
      define(this, '_pageSize', pageSize);
      define(this, '_page', page || 1);
      return this.updatePagination();
    },

    // Sets or gets the page number of a paginated array view. If you pass in
    // a number it will set it, otherwise it will return the current page
    // number. Pages are 1-based, not zero-based. I.e. the first page is page 1.
    pageNumber: function pageNumber(page) {
      if (typeof page != 'undefined') {
        define(this, '_page', page || 1);
        return this.updatePagination();
      } else if (this._pageSize) {
        return this._page || 1;
      } else {
        return 1;
      }
    },

    // Gets the page count for a paginated array view.
    pageCount: function pageCount() {
      if (!this._pageSize) return 1;
      return Math.ceil(this._unpaginated.length / this._pageSize);
    },

    // Returns the total length of the array, before pagination
    total: function total() {
      return this._unpaginated.length;
    },

    // Returns the page number which an item will appear on for a paingated
    // array view.
    itemPageNumber: function itemsPageNumber(item) {
      var index = this._unpaginated.indexOf(item);
      if (index == -1) return 0;
      if (!this._pageSize) return 1;
      return Math.floor(index / this._pageSize) + 1;
    },

    // Updates the view to reflect the latest state. This array will contain
    // only items from the source that haven't been filtered if any filters were
    // added, in the sort order that has been set if any, and only the items on
    // the current page if pagination was set.
    update: function update() {
      var array = this._source.slice();
      if (this._filters) this._filters.forEach(function(filter) {
        array = array.filter(filter);
      });
      if (this._sort) array.sort(this._sort);
      define(this, '_unpaginated', array);
      return this.updatePagination();
    },

    // Updates only the pagination, skipping the filtering and sorting steps
    // for simple page changes.
    updatePagination: function updatePagination() {
      var array = this._unpaginated;
      if (this._pageSize) {
        var count = this.pageCount();
        define(this, '_page', Math.min(count, Math.max(1, this._page || 1)));
        var begin = (this._page - 1) * this._pageSize;
        var end = this._page * this._pageSize;
        array = array.slice(begin, end);
      }

      this.length = 0;
      // Use proto.push because this.push is bound to the source array
      proto.push.apply(this, array);
      // Allow views to have views
      this.updateViews();
      return this;
    },

    // Detach the view from its source array. Use this to clean up the view as
    // it will not be garbage collected as long as the source array is kept in
    // memory. If the view and source arrays are no longer referenced, both
    // should be garbage collected. This creates a new source array that can be
    // used to update the view or discarded for garbage collection.
    detach: function detach() {
      remove(this._source._views, this);
      var newSource = this._source.slice();
      initSource(newSource);
      attachToSource(newSource, this);
      return newSource;
    }
  }

})()
<commit_msg>Adds support for sorting numerical strings correctly<commit_after>
(function(undefined) {

  // Calls Object.definePropert(y,ies) with configurable:true on all properties
  function define(obj, key, value) {
    if (typeof key == 'object') {
      Object.keys(key).forEach(function(name) {
        define(obj, name, key[name]);
      });
    } else {
      Object.defineProperty(obj, key, {
        configurable: true,
        value: value
      })
    }
  }

  // Add Array.prototype.view to create new views and
  // Array.prototype.updateViews to update all the views attached
  define(Array.prototype, {
    view: function view() {
      return createView(this);
    },
    updateViews: function updateViews() {
      if (!Array.isArray(this._views)) return false;
      this._views.forEach(function(view) {
        view.update();
      });
      return true;
    }
  });

  // create a new view on the source array (which may be a view itself)
  function createView(source) {
    var view = [];
    if (!source._views) initSource(source);
    initView(source, view);
    return view;
  }

  // add a _views array for updates and wrap the modifying functions
  function initSource(source) {
    define(source, '_views', []);
    define(source, modifiers);
  }

  // When the source is modified the views need to be updated
  function wrapModifier(modifier) {
    return function() {
      var results = modifier.apply(this, arguments);
      this.updateViews();
      return results;
    };
  }

  var proto = Array.prototype;
  var modifiers = {
    push: wrapModifier(proto.push),
    pop: wrapModifier(proto.pop),
    shift: wrapModifier(proto.shift),
    unshift: wrapModifier(proto.unshift),
    splice: wrapModifier(proto.splice)
  };

  // add view functions to the view array
  function initView(source, view) {
    attachToSource(source, view);
    define(view, viewMethods);
    view.update();
  }

  function attachToSource(source, view) {
    define(view, '_source', source);
    source._views.push(view);
    bindModifiers(source, view);
  }

  // bind the view's modifiers to the source, never modify the view
  function bindModifiers(source, view) {
    Object.keys(modifiers).forEach(function(modifier) {
      define(view, modifier, source[modifier].bind(source));
    });
  }

  // Remove an item from an array
  function remove(array, item) {
    var index = array.indexOf(item);
    if (index != -1) {
      array.splice(index, 1);
      return index;
    }
  }

  var sortDir = {
    asc: 1,
    desc: -1
  }

  function createGetter(field, dir) {
    var fields = field.split('.'),
        body = 'return ',
        ref = 0,
        refs = [],
        prevRef = 'item.';

    while (field = fields.shift()) {
      if (fields.length) {
        curRef = '_ref' + ref++;
        refs.push(curRef);
        body += '(' + curRef + ' = ' + prevRef + field + ') == null ? null : ';
        prevRef = curRef + '.';
      } else {
        body += prevRef + field;
      }
    }
    if (refs.length) body = 'var ' + refs.join(',') + ';\n' + body;
    var getter = new Function('item', body);
    getter.dir = dir || 1;
    return getter;
  }

  // create a sort function from arguments, see comments on `sort` below
  function createSort(sorts) {
    sorts = proto.slice.call(arguments);
    if (sorts[1] == 'asc' || sorts[1] == 'desc') sorts = [sorts];
    var getters = sorts.map(function(field) {
      var dir = 1;
      if (Array.isArray(field)) {
        dir = sortDir[field[1]] || field[1];
        field = field[0];
      }
      return createGetter(field, dir);
    });

    return function(a, b) {
      for (var i = 0; i < getters.length; i++) {
        var getter = getters[i];
        var valueA = getter(a);
        var valueB = getter(b);
        if (!isNaN(valueA) && !isNaN(valueB)) {
          valueA = parseFloat(valueA);
          valueB = parseFloat(valueB);
        }
        // handle nulls as less than
        if (valueB != null && valueA == null) return getter.dir;
        else if (valueA != null && valueB == null) return -getter.dir;
        else if (valueA > valueB) return getter.dir;
        else if (valueB > valueA) return -getter.dir;
      }
      return 0;
    }
  }

  // Adds the incredibly useful `createSort` function for any array to use.
  define(Array, 'createSort', createSort);

  var viewMethods = {

    // Adds a persistent filter function which takes a single argument, `item`,
    // and returns true or false if item should be allowed in the array. More
    // than one filter may be added and `name` (optional) can be used to remove
    // the filter later.
    addFilter: function addFilter(name, filter) {
      if (!this._filters) define(this, '_filters', []);
      if (typeof name == 'function') {
        filter = name;
        name = null;
      }
      if (typeof filter != 'function') return this;
      if (name) this._filters[name] = filter;
      this._filters.push(filter);
      return this.update();
    },

    // Removes a persistent filter which has been added. `filter` may be the
    // filter function or the name if `name` was used in `addFilter`. If
    // `filter` is not provided then `removeFilter()` will remove the last
    // filter added.
    removeFilter: function removeFilter(filter) {
      if (!this._filters) return this;
      if (typeof filter == 'string') {
        var name = filter;
        filter = this._filters[name];
        delete this._filters[name];
      } else if (typeof filter == 'undefined') {
        filter = this._filters[this._filters.length - 1];
      }
      if (typeof filter != 'function') return this;
      remove(this._filters, filter);
      return this.update();
    },

    // Sets a persistent sort on the array. Objects added or removed will
    // maintain the sort order of this sort function. Pass null to remove the
    // sort. You may use a sort function as Array.prototype.sort takes, or you
    // may use a special array view as the following examples:
    // ```
    // view.sort('dateCreated', 'desc');
    // view.sort('lastName', 'firstName', ['age', 'desc']);
    // view.sort('comments.length', ['postDate', 'desc']);
    // view.sort(['dateCreated', 'desc'], ['title', 'asc']);
    // ```
    // When only sorting by one property, you may use asc/desc as the second
    // parameter. 'asc' is always the default and is not necessary. When sorting
    // by multiple properties an array is required to add the sort order 'desc'.
    // chained properties (such as name.first or comments.length) may be as deep
    // as needed and will null-terminate, so no errors will occur. A null will
    // be treated as LESS THAN a non-null value.
    setSort: function setSort(sort) {
      if (sort && typeof sort != 'function') {
        sort = createSort.apply(null, arguments);
      }
      define(this, '_sort', sort || true);
      return this.update();
    },

    removeSort: function removeSort() {
      define(this, '_sort', undefined);
      return this.update();
    },

    // Paginates the view by splitting it up into pages. Each page has
    // `pageSize` items in it--or fewer if it is the last page. `page` is the
    // initial page the view will display (default is 1). The page can be set
    // later with the `page` method. Pass in null to remove pagination.
    paginate: function paginate(pageSize, page) {
      define(this, '_pageSize', pageSize);
      define(this, '_page', page || 1);
      return this.updatePagination();
    },

    // Sets or gets the page number of a paginated array view. If you pass in
    // a number it will set it, otherwise it will return the current page
    // number. Pages are 1-based, not zero-based. I.e. the first page is page 1.
    pageNumber: function pageNumber(page) {
      if (typeof page != 'undefined') {
        define(this, '_page', page || 1);
        return this.updatePagination();
      } else if (this._pageSize) {
        return this._page || 1;
      } else {
        return 1;
      }
    },

    // Gets the page count for a paginated array view.
    pageCount: function pageCount() {
      if (!this._pageSize) return 1;
      return Math.ceil(this._unpaginated.length / this._pageSize);
    },

    // Returns the total length of the array, before pagination
    total: function total() {
      return this._unpaginated.length;
    },

    // Returns the page number which an item will appear on for a paingated
    // array view.
    itemPageNumber: function itemsPageNumber(item) {
      var index = this._unpaginated.indexOf(item);
      if (index == -1) return 0;
      if (!this._pageSize) return 1;
      return Math.floor(index / this._pageSize) + 1;
    },

    // Updates the view to reflect the latest state. This array will contain
    // only items from the source that haven't been filtered if any filters were
    // added, in the sort order that has been set if any, and only the items on
    // the current page if pagination was set.
    update: function update() {
      var array = this._source.slice();
      if (this._filters) this._filters.forEach(function(filter) {
        array = array.filter(filter);
      });
      if (this._sort) array.sort(this._sort);
      define(this, '_unpaginated', array);
      return this.updatePagination();
    },

    // Updates only the pagination, skipping the filtering and sorting steps
    // for simple page changes.
    updatePagination: function updatePagination() {
      var array = this._unpaginated;
      if (this._pageSize) {
        var count = this.pageCount();
        define(this, '_page', Math.min(count, Math.max(1, this._page || 1)));
        var begin = (this._page - 1) * this._pageSize;
        var end = this._page * this._pageSize;
        array = array.slice(begin, end);
      }

      this.length = 0;
      // Use proto.push because this.push is bound to the source array
      proto.push.apply(this, array);
      // Allow views to have views
      this.updateViews();
      return this;
    },

    // Detach the view from its source array. Use this to clean up the view as
    // it will not be garbage collected as long as the source array is kept in
    // memory. If the view and source arrays are no longer referenced, both
    // should be garbage collected. This creates a new source array that can be
    // used to update the view or discarded for garbage collection.
    detach: function detach() {
      remove(this._source._views, this);
      var newSource = this._source.slice();
      initSource(newSource);
      attachToSource(newSource, this);
      return newSource;
    }
  }

})()
<|endoftext|>


<commit_before>/**
 * @class TopNav
 */
function TopNav(iZhihu) {
    if ( typeof iZhihu === 'undefined' || !iZhihu) {
        return null
    }
    iZhihu.TopNav = this

    this.$topNav = $('body > .zu-top:first')
        .on('mouseover', function(event){
            this.style.top = '0'
            this.setAttribute('izh-mouseover', '1')
            $('#izhCSS_NotiNum').remove()
        })
        .on('mouseout', function(event){
            this.setAttribute('izh-mouseover', '0')
            iZhihu.TopNav.funcFold()
        })
    if (!this.$topNav.length) return

    this.topNavHeight = this.$topNav.height() - 5

    this.funcFold = function(event){
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0
          , _self = iZhihu.TopNav
          , isMouseOver = '1' === (_self.$topNav.attr('izh-mouseover') || '')
          , $head = $('head:first')
          , $cssNotiNum = $('#izhCSS_NotiNum')
          , $floatBar = $('body > .goog-scrollfloater')
        if (scrollTop === 0) {
            _self.$topNav.css({top:0})
            $floatBar.css({marginTop:0})
        } else if (!isMouseOver) {
            if (scrollTop < _self.topNavHeight) {
                _self.$topNav.css({top:-scrollTop})
                $floatBar.css({marginTop:-scrollTop})
            } else {
                _self.$topNav.css({top:-_self.topNavHeight})
                $floatBar.css({marginTop:-_self.topNavHeight})
            }
            if (scrollTop > 20) {
                if (!$cssNotiNum.length) {
                    $('<style>', {
                        id: 'izhCSS_NotiNum'
                      , type: 'text/css'
                      , html: '#zh-top-nav-count,#zh-top-nav-new-pm{position:absolute;top:40px;border-radius:0 !important}.top-nav-profile .zu-top-nav-userinfo{overflow:visible !important}'
                    }).appendTo('head:first')
                }
                return
            }
        }
        $cssNotiNum.remove()
    }

    iZhihu.$win.scroll(this.funcFold)

    return this
}
<commit_msg>Fixed floating bar which stopped floating<commit_after>/**
 * @class TopNav
 */
function TopNav(iZhihu) {
    if ( typeof iZhihu === 'undefined' || !iZhihu) {
        return null
    }
    iZhihu.TopNav = this

    this.$topNav = $('body > .zu-top:first')
        .on('mouseover', function(event){
            this.style.top = '0'
            this.setAttribute('izh-mouseover', '1')
            $('#izhCSS_NotiNum').remove()
        })
        .on('mouseout', function(event){
            this.setAttribute('izh-mouseover', '0')
            iZhihu.TopNav.funcFold()
        })
    if (!this.$topNav.length) return

    this.topNavHeight = this.$topNav.height() - 5

    this.funcFold = function(event){
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0
          , _self = iZhihu.TopNav
          , isMouseOver = '1' === (_self.$topNav.attr('izh-mouseover') || '')
          , $head = $('head:first')
          , $cssNotiNum = $('#izhCSS_NotiNum')
          , $floatingBar = $('body > .goog-scrollfloater-floating')
        if (scrollTop === 0) {
            _self.$topNav.css({top:0})
        } else if (!isMouseOver) {
            if (scrollTop < _self.topNavHeight) {
                _self.$topNav.css({top:-scrollTop})
                $floatingBar.css({marginTop:-scrollTop})
            } else {
                _self.$topNav.css({top:-_self.topNavHeight})
                $floatingBar.css({marginTop:-_self.topNavHeight})
            }
            if (scrollTop > 20) {
                if (!$cssNotiNum.length) {
                    $('<style>', {
                        id: 'izhCSS_NotiNum'
                      , type: 'text/css'
                      , html: '#zh-top-nav-count,#zh-top-nav-new-pm{position:absolute;top:40px;border-radius:0 !important}.top-nav-profile .zu-top-nav-userinfo{overflow:visible !important}'
                    }).appendTo('head:first')
                }
                return
            }
        }
        $cssNotiNum.remove()
    }

    iZhihu.$win.scroll(this.funcFold)

    utils.observeDOMNodeAdded(document.body,function(event){
        $.each(event.removedNodes, function(i, e){
            var $floatingBar = $(e).filter('.goog-scrollfloater').css({marginTop:''})
        })
    })

    return this
}
<|endoftext|>


<commit_before>goog.provide('annotorious.modules.Module');

/**
 * An base class for Annotorious Module implementations.
 * @constructor
 */
annotorious.modules.Module = function() { }

/**
 * Initializes the module instance's fields. Note that subclasses *must*
 * ensure by themselves that this method is called on initialization.
 * @protected
 */
annotorious.modules.Module.prototype._initFields = function() {
  /** @private **/
  this._annotators = new goog.structs.Map();

  /** @private **/
  this._eventHandlers = [];

  /** @private **/
  this._plugins = [];

  /** @private **/
  this._allItems = [];

  /** @private **/
  this._itemsToLoad = [];

  /** @private **/
  this._bufferedForAdding = [];

  /** @private **/
  this._bufferedForRemoval = [];

  /** @private **/
  this._cachedGlobalSettings = { hide_selection_widget: false, hide_annotations: false };

  /** @private **/
  this._cachedItemSettings = new goog.structs.Map();
}

/**
 * Returns the settings for the specified annotator. If there is no cached settings object yet,
 * a new one will be created (initialized with defaults).
 * @param {string} item_url the URL of the item controlled by the annotator
 * @private
 */
annotorious.modules.Module.prototype._getSettings = function(item_url) {
  var settings = this._cachedSettings(item_url);
  if (!settings) {
    settings = { hide_selection_widget: false, hide_annotations: false };
    settings.set(item_url, settings);
  }
  return settings;
}

/**
 *
 */
annotorious.modules.Module.prototype._init = function(opt_predef_items) {
  if (opt_predef_items)
    goog.array.extend(this._allItems, opt_predef_items);

  if (opt_predef_items)
    goog.array.extend(this._itemsToLoad, opt_predef_items);

  // Make items in viewport annotatable
  this._lazyLoad();

  // Attach a listener to make items annotatable as they scroll into view
  var self = this;
  var key = goog.events.listen(window, goog.events.EventType.SCROLL, function() {
    if (self._itemsToLoad.length > 0)
      self._lazyLoad();
    else
      goog.events.unlistenByKey(key);
  });
}

/**
 * @private
 */
annotorious.modules.Module.prototype._lazyLoad = function() {
  var self = this;
  goog.array.forEach(this._itemsToLoad, function(item) {
    if (annotorious.dom.isInViewport(item)) {
      self._initAnnotator(item);
    }
  });
}

/**
 * @private
 */
annotorious.modules.Module.prototype._initAnnotator = function(item) {
  var self = this;

  // Keep track of changes
  var addedAnnotations = [];
  var removedAnnotations = [];

  var annotator = this.newAnnotator(item);

  var item_src = this.getItemURL(item);

  // Attach handlers that are already registered
  goog.array.forEach(this._eventHandlers, function(eventHandler) {
    annotator.addHandler(eventHandler.type, eventHandler.handler);
  });

  // Callback to registered plugins
  goog.array.forEach(this._plugins, function(plugin) {
    self._initPlugin(plugin, annotator);
  });

  // Cross-check with annotation add/remove buffers
  goog.array.forEach(this._bufferedForAdding, function(annotation) {
    if (annotation.src == item_src) {
      annotator.addAnnotation(annotation);
      addedAnnotations.push(annotation);
    }
  });

  goog.array.forEach(this._bufferedForRemoval, function(annotation) {
    if (annotation.src == item_src) {
      annotator.removeAnnotation(annotation);
      removedAnnotations.push(annotation);
    }
  });

  // Apply changes
  goog.array.forEach(addedAnnotations, function(annotation) {
    goog.array.remove(self._bufferedForAdding, annotation);
  });

  goog.array.forEach(removedAnnotations, function(annotation) {
    goog.array.remove(self._bufferedForRemoval, annotation);
  });

  // Apply cached settings
  var settings = this._cachedItemSettings.get(item_src);
  if (settings) {
    if (settings.hide_selection_widget)
      annotator.hideSelectionWidget();

    if (settings.hide_annotations)
      annotator.hideAnnotations();

    this._cachedItemSettings.remove(item_src);
  } else {
    if (this._cachedGlobalSettings.hide_selection_widget)
      annotator.hideSelectionWidget();

    if (this._cachedGlobalSettings.hide_annotations)
      annotator.hideAnnotations();
  }

  // Update _annotators and _imagesToLoad lists
  this._annotators.set(item_src, annotator);
  goog.array.remove(this._itemsToLoad, item);
}

/**
 * @private
 */
annotorious.modules.Module.prototype._initPlugin = function(plugin, annotator) {
  if (plugin.onInitAnnotator)
    plugin.onInitAnnotator(annotator);
}

annotorious.modules.Module.prototype.activateSelector = function(opt_item_url_or_callback, opt_callback) {
  var item_url = undefined,
      callback = undefined;

  if (goog.isString(opt_item_url_or_callback)) {
    item_url = opt_item_url_or_callback;
    callback = opt_callback;
  } else if (goog.isFunction(opt_item_url_or_callback)) {
    callback = opt_item_url_or_callback;
  }

  if (item_url) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.activateSelector(callback);
  } else {
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      annotator.activateSelector(callback);
    });
  }
}

/**
 * Adds an annotation to an item managed by this module.
 * @param {Annotation} annotation the annotation
 * @param {Annotation} opt_replace optionally, an existing annotation to replace
 */
annotorious.modules.Module.prototype.addAnnotation = function(annotation, opt_replace) {
  if (this.annotatesItem(annotation.src)) {
    var annotator = this._annotators.get(annotation.src);
    if (annotator) {
      annotator.addAnnotation(annotation, opt_replace)
    } else {
      this._bufferedForAdding.push(annotation);
      if (opt_replace)
        goog.array.remove(this._bufferedForAdding, opt_replace);
    }
  }
}

/**
 * Adds a lifecycle event handler to this module.
 * @param {yuma.events.EventType} type the event type
 * @param {function} handler the handler function
 */
annotorious.modules.Module.prototype.addHandler = function(type, handler) {
  goog.array.forEach(this._annotators.getValues(), function(annotator, idx, array) {
    annotator.addHandler(type, handler);
  });

  this._eventHandlers.push({ type: type, handler: handler });
}

/**
 * Adds a plugin to this module.
 * @param {Plugin} plugin the plugin
 */
annotorious.modules.Module.prototype.addPlugin = function(plugin) {
  this._plugins.push(plugin);

  var self = this;
  goog.array.forEach(this._annotators.getValues(), function(annotator) {
    self._initPlugin(plugin, annotator);
  });
}

/**
 * Adds a selector to an item managed by this module.
 *
 * !! TEMPORARY !!
 *
 * TODO selectors should be added to annotators directly, from within a plugin
 * which will make this method unecessary
 */
annotorious.modules.Module.prototype.addSelector = function(item_url, selector) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.addSelector(selector);
  }
}

/**
 * Tests if this module is in charge of managing the item with the specified URL.
 * @param {string} item_url the URL of the item
 * @return {boolean} true if this module is in charge of the media
 */
annotorious.modules.Module.prototype.annotatesItem = function(item_url) {
  if (this._annotators.containsKey(item_url)) {
    return true;
  } else {
    var self = this;
    var item = goog.array.find(this._itemsToLoad, function(item) {
      return self.getItemURL(item) == item_url;
    });

    return goog.isDefAndNotNull(item);
  }
}

annotorious.modules.Module.prototype.hideAnnotations = function(opt_item_url) {
  this._setAnnotationVisibility(opt_item_url, false);
}

annotorious.modules.Module.prototype.hideSelectionWidget = function(opt_item_url) {
  this._setSelectionWidgetVisibility(opt_item_url, false);
}

annotorious.modules.Module.prototype.showSelectionWidget = function(opt_item_url) {
  this._setSelectionWidgetVisibility(opt_item_url, true);
}

annotorious.modules.Module.prototype._setSelectionWidgetVisibility = function(opt_item_url, visibility) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      // Item URL is provided, and item is loaded - set directly
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideSelectionWidget();
    } else {
      // Item URL is provided, but item not yet loaded - cache for later
      this._getSettings(opt_item_url).hide_selection_widget = visibility;
    }
  } else {
    // Item URL is not provided - update all annotators...
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideSelectionWidget();
    });

    // ...cache global settings...
    this._cachedGlobalSettings.hide_selection_widget = visibility;

    // ...and update all cached item settings
    goog.array.forEach(this._cachedItemSettings.getValues(), function(settings) {
      settings.hide_selection_widget = visibility;
    });
  }
}

/**
 * Returns the name of the selector that is currently activated on the item
 * with the specified URL (if managed by this module).
 * @param {string} item_url the URL of the item to query for the active selector
 * @return {string | undefined} the name of the active selector (or undefined)
 */
annotorious.modules.Module.prototype.getActiveSelector = function(item_url) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      return annotator.getActiveSelector().getName();
  }
  return undefined;
}

/**
 * Returns all annotations on the item with the specified URL (if managed by this
 * module) or all annotations from this module in case no URL is specified.
 * @param {string | undefined} opt_item_url an item URL (optional)
 * @return {Array.<Annotation>} the annotations
 */
annotorious.modules.Module.prototype.getAnnotations = function(opt_item_url) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      return annotator.getAnnotations();
    } else {
      return goog.array.filter(this._bufferedForAdding, function(annotation) {
        return annotation.src == opt_item_url;
      });
    }
  } else {
    var annotations = [];
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      goog.array.extend(annotations, annotator.getAnnotations());
    });
    goog.array.extend(annotations, this._bufferedForAdding);
    return annotations;
  }
}

/**
 * Returns the list of available shape selectors for a particular item.
 * @param {string} item_url the URL of the item to query for available selectors
 * @returns {List.<string> | undefined} the list of selector names
 */
annotorious.modules.Module.prototype.getAvailableSelectors = function(item_url) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator) {
      return goog.array.map(annotator.getAvailableSelectors(), function(selector) {
        return selector.getName();
      });
    }
  }
  return undefined;
}

/**
 * Highlights the specified annotation.
 * @param {Annotation} annotation the annotation
 */
annotorious.modules.Module.prototype.highlightAnnotation = function(annotation) {
  if (annotation) {
    if (this.annotatesItem(annotation.src)) {
      var annotator = this._annotators.get(annotation.src);
      if (annotator)
        annotator.highlightAnnotation(annotation);
    }
  } else {
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      annotator.highlightAnnotation();
    });
  }
}

/**
 * Makes an item annotatable, if it is supported by this module.
 * @param {object} item the annotatable item
 */
annotorious.modules.Module.prototype.makeAnnotatable = function(item) {
  if (this.supports(item)) {
    this._allItems.push(item);
    this._initAnnotator(item);
  }
}

/**
 * Removes an annotation from the item with the specified URL.
 * @param {Annotation} annotation the annotation
 */
annotorious.modules.Module.prototype.removeAnnotation = function(annotation) {
  if (this.annotatesItem(annotation.src)) {
    var annotator = this._annotators.get(annotation.src);
    if (annotator)
      annotator.removeAnnotation(annotation);
    else
      this._bufferedForRemoval.push(annotation);
  }
}

annotorious.modules.Module.prototype.showAnnotations = function(opt_item_url) {
  this._setAnnotationVisibility(opt_item_url, true);
}

annotorious.modules.Module.prototype._setAnnotationVisibility = function(opt_item_url, visibility) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      // Item URL is provided, and item is loaded - set directly
      if (visibility)
        annotator.showAnnotations();
      else
        annotator.hideAnnotations();
    } else {
      // Item URL is provided, but item not yet loaded - cache for later
      this._getSettings(opt_item_url).hide_annotations = visibility;
    }
  } else {
    // Item URL is not provided - update all annotators...
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideAnnotations();
    });

    // ...cache global settings...
    this._cachedGlobalSettings.hide_annotations = visibility;

    // ...and update all cached item settings
    goog.array.forEach(this._cachedItemSettings.getValues(), function(settings) {
      settings.hide_annotations = visibility;
    });
  }
}

/**
 * Sets a specific selector on a particular item.
 * @param {string} item_url the URL of the item on which to set the selector
 * @param {string} selector the name of the selector to set on the item
 */
annotorious.modules.Module.prototype.setActiveSelector = function(item_url, selector) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.setActiveSelector(selector);
  }
}

/** Methods that must be implemented by subclasses of annotorious.modules.Module **/

/**
 * Returns the identifying URL of the specified item.
 * @param {object} item the item.
 * @return {string} the URL
 */
annotorious.modules.Module.prototype.getItemURL = goog.abstractMethod;

/**
 * This function is called from the framework when the module is initialized.
 * Subclasses of annotorious.modules.Module must perform all subclass-specific
 * initialization actions in this function, and then make sure this._init (note
 * the leading _!) gets called with a list of all items that should be made
 * annotatable immediately!
 */
annotorious.modules.Module.prototype.init = goog.abstractMethod;

/**
 * Returns a new annotator for the specified item.
 * @param {object} item the item
 * @return {object} an annotator for this item
 */
annotorious.modules.Module.prototype.newAnnotator = goog.abstractMethod;

/**
 * Tests if this module supports the specified item's media type.
 * @param {object} item the item
 * @return {boolean} true if this module supports the item
 */
annotorious.modules.Module.prototype.supports = goog.abstractMethod;
<commit_msg>Cleanup/JSDoc<commit_after>goog.provide('annotorious.modules.Module');

/**
 * An base class for Annotorious Module implementations.
 * @constructor
 */
annotorious.modules.Module = function() { }

/**
 * Initializes the module instance's fields. Note that subclasses *must*
 * ensure by themselves that this method is called on initialization.
 * @protected
 */
annotorious.modules.Module.prototype._initFields = function() {
  /** @private **/
  this._annotators = new goog.structs.Map();

  /** @private **/
  this._eventHandlers = [];

  /** @private **/
  this._plugins = [];

  /** @private **/
  this._allItems = [];

  /** @private **/
  this._itemsToLoad = [];

  /** @private **/
  this._bufferedForAdding = [];

  /** @private **/
  this._bufferedForRemoval = [];

  /** @private **/
  this._cachedGlobalSettings = { hide_selection_widget: false, hide_annotations: false };

  /** @private **/
  this._cachedItemSettings = new goog.structs.Map();
}

/**
 * Returns the settings for the specified annotator. If there is no cached settings object yet,
 * a new one will be created (initialized with defaults).
 * @param {string} item_url the URL of the item controlled by the annotator
 * @private
 */
annotorious.modules.Module.prototype._getSettings = function(item_url) {
  var settings = this._cachedSettings(item_url);
  if (!settings) {
    settings = { hide_selection_widget: false, hide_annotations: false };
    settings.set(item_url, settings);
  }
  return settings;
}

/**
 * @private
 */
annotorious.modules.Module.prototype._init = function(opt_predef_items) {
  if (opt_predef_items) {
    goog.array.extend(this._allItems, opt_predef_items);
    goog.array.extend(this._itemsToLoad, opt_predef_items);
  }

  this._lazyLoad();

  var self = this;
  var key = goog.events.listen(window, goog.events.EventType.SCROLL, function() {
    if (self._itemsToLoad.length > 0)
      self._lazyLoad();
    else
      goog.events.unlistenByKey(key);
  });
}

/**
 * @private
 */
annotorious.modules.Module.prototype._lazyLoad = function() {
  var self = this;
  goog.array.forEach(this._itemsToLoad, function(item) {
    if (annotorious.dom.isInViewport(item)) {
      self._initAnnotator(item);
    }
  });
}

/**
 * @private
 */
annotorious.modules.Module.prototype._initAnnotator = function(item) {
  var self = this;
  var annotator = this.newAnnotator(item);
  var item_src = this.getItemURL(item);

  // Keep track of changes
  var addedAnnotations = [];
  var removedAnnotations = [];

  // Attach handlers that are already registered
  goog.array.forEach(this._eventHandlers, function(eventHandler) {
    annotator.addHandler(eventHandler.type, eventHandler.handler);
  });

  // Callback to registered plugins
  goog.array.forEach(this._plugins, function(plugin) {
    self._initPlugin(plugin, annotator);
  });

  // Cross-check with annotation add/remove buffers
  goog.array.forEach(this._bufferedForAdding, function(annotation) {
    if (annotation.src == item_src) {
      annotator.addAnnotation(annotation);
      addedAnnotations.push(annotation);
    }
  });

  goog.array.forEach(this._bufferedForRemoval, function(annotation) {
    if (annotation.src == item_src) {
      annotator.removeAnnotation(annotation);
      removedAnnotations.push(annotation);
    }
  });

  // Apply changes
  goog.array.forEach(addedAnnotations, function(annotation) {
    goog.array.remove(self._bufferedForAdding, annotation);
  });

  goog.array.forEach(removedAnnotations, function(annotation) {
    goog.array.remove(self._bufferedForRemoval, annotation);
  });

  // Apply cached settings
  var settings = this._cachedItemSettings.get(item_src);
  if (settings) {
    if (settings.hide_selection_widget)
      annotator.hideSelectionWidget();

    if (settings.hide_annotations)
      annotator.hideAnnotations();

    this._cachedItemSettings.remove(item_src);
  } else {
    if (this._cachedGlobalSettings.hide_selection_widget)
      annotator.hideSelectionWidget();

    if (this._cachedGlobalSettings.hide_annotations)
      annotator.hideAnnotations();
  }

  // Update _annotators and _imagesToLoad lists
  this._annotators.set(item_src, annotator);
  goog.array.remove(this._itemsToLoad, item);
}

/**
 * @private
 */
annotorious.modules.Module.prototype._initPlugin = function(plugin, annotator) {
  if (plugin.onInitAnnotator)
    plugin.onInitAnnotator(annotator);
}

annotorious.modules.Module.prototype.activateSelector = function(opt_item_url_or_callback, opt_callback) {
  var item_url = undefined,
      callback = undefined;

  if (goog.isString(opt_item_url_or_callback)) {
    item_url = opt_item_url_or_callback;
    callback = opt_callback;
  } else if (goog.isFunction(opt_item_url_or_callback)) {
    callback = opt_item_url_or_callback;
  }

  if (item_url) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.activateSelector(callback);
  } else {
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      annotator.activateSelector(callback);
    });
  }
}

/**
 * Adds an annotation to an item managed by this module.
 * @param {Annotation} annotation the annotation
 * @param {Annotation} opt_replace optionally, an existing annotation to replace
 */
annotorious.modules.Module.prototype.addAnnotation = function(annotation, opt_replace) {
  if (this.annotatesItem(annotation.src)) {
    var annotator = this._annotators.get(annotation.src);
    if (annotator) {
      annotator.addAnnotation(annotation, opt_replace)
    } else {
      this._bufferedForAdding.push(annotation);
      if (opt_replace)
        goog.array.remove(this._bufferedForAdding, opt_replace);
    }
  }
}

/**
 * Adds a lifecycle event handler to this module.
 * @param {yuma.events.EventType} type the event type
 * @param {function} handler the handler function
 */
annotorious.modules.Module.prototype.addHandler = function(type, handler) {
  goog.array.forEach(this._annotators.getValues(), function(annotator, idx, array) {
    annotator.addHandler(type, handler);
  });
  this._eventHandlers.push({ type: type, handler: handler });
}

/**
 * Adds a plugin to this module.
 * @param {Plugin} plugin the plugin
 */
annotorious.modules.Module.prototype.addPlugin = function(plugin) {
  this._plugins.push(plugin);

  var self = this;
  goog.array.forEach(this._annotators.getValues(), function(annotator) {
    self._initPlugin(plugin, annotator);
  });
}

/**
 * Adds a selector to an item managed by this module.
 *
 * !! TEMPORARY !!
 *
 * TODO selectors should be added to annotators directly, from within a plugin
 * which will make this method unecessary
 */
annotorious.modules.Module.prototype.addSelector = function(item_url, selector) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.addSelector(selector);
  }
}

/**
 * Tests if this module is in charge of managing the item with the specified URL.
 * @param {string} item_url the URL of the item
 * @return {boolean} true if this module is in charge of the media
 */
annotorious.modules.Module.prototype.annotatesItem = function(item_url) {
  if (this._annotators.containsKey(item_url)) {
    return true;
  } else {
    var self = this;
    var item = goog.array.find(this._itemsToLoad, function(item) {
      return self.getItemURL(item) == item_url;
    });

    return goog.isDefAndNotNull(item);
  }
}

annotorious.modules.Module.prototype.hideAnnotations = function(opt_item_url) {
  this._setAnnotationVisibility(opt_item_url, false);
}

annotorious.modules.Module.prototype.hideSelectionWidget = function(opt_item_url) {
  this._setSelectionWidgetVisibility(opt_item_url, false);
}

annotorious.modules.Module.prototype.showSelectionWidget = function(opt_item_url) {
  this._setSelectionWidgetVisibility(opt_item_url, true);
}

annotorious.modules.Module.prototype._setSelectionWidgetVisibility = function(opt_item_url, visibility) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      // Item URL is provided, and item is loaded - set directly
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideSelectionWidget();
    } else {
      // Item URL is provided, but item not yet loaded - cache for later
      this._getSettings(opt_item_url).hide_selection_widget = visibility;
    }
  } else {
    // Item URL is not provided - update all annotators...
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideSelectionWidget();
    });

    // ...cache global settings...
    this._cachedGlobalSettings.hide_selection_widget = visibility;

    // ...and update all cached item settings
    goog.array.forEach(this._cachedItemSettings.getValues(), function(settings) {
      settings.hide_selection_widget = visibility;
    });
  }
}

/**
 * Returns the name of the selector that is currently activated on the item
 * with the specified URL (if managed by this module).
 * @param {string} item_url the URL of the item to query for the active selector
 * @return {string | undefined} the name of the active selector (or undefined)
 */
annotorious.modules.Module.prototype.getActiveSelector = function(item_url) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      return annotator.getActiveSelector().getName();
  }
  return undefined;
}

/**
 * Returns all annotations on the item with the specified URL (if managed by this
 * module) or all annotations from this module in case no URL is specified.
 * @param {string | undefined} opt_item_url an item URL (optional)
 * @return {Array.<Annotation>} the annotations
 */
annotorious.modules.Module.prototype.getAnnotations = function(opt_item_url) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      return annotator.getAnnotations();
    } else {
      return goog.array.filter(this._bufferedForAdding, function(annotation) {
        return annotation.src == opt_item_url;
      });
    }
  } else {
    var annotations = [];
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      goog.array.extend(annotations, annotator.getAnnotations());
    });
    goog.array.extend(annotations, this._bufferedForAdding);
    return annotations;
  }
}

/**
 * Returns the list of available shape selectors for a particular item.
 * @param {string} item_url the URL of the item to query for available selectors
 * @returns {List.<string> | undefined} the list of selector names
 */
annotorious.modules.Module.prototype.getAvailableSelectors = function(item_url) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator) {
      return goog.array.map(annotator.getAvailableSelectors(), function(selector) {
        return selector.getName();
      });
    }
  }
  return undefined;
}

/**
 * Highlights the specified annotation.
 * @param {Annotation} annotation the annotation
 */
annotorious.modules.Module.prototype.highlightAnnotation = function(annotation) {
  if (annotation) {
    if (this.annotatesItem(annotation.src)) {
      var annotator = this._annotators.get(annotation.src);
      if (annotator)
        annotator.highlightAnnotation(annotation);
    }
  } else {
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      annotator.highlightAnnotation();
    });
  }
}

/**
 * Makes an item annotatable, if it is supported by this module.
 * @param {object} item the annotatable item
 */
annotorious.modules.Module.prototype.makeAnnotatable = function(item) {
  if (this.supports(item)) {
    this._allItems.push(item);
    this._initAnnotator(item);
  }
}

/**
 * Removes an annotation from the item with the specified URL.
 * @param {Annotation} annotation the annotation
 */
annotorious.modules.Module.prototype.removeAnnotation = function(annotation) {
  if (this.annotatesItem(annotation.src)) {
    var annotator = this._annotators.get(annotation.src);
    if (annotator)
      annotator.removeAnnotation(annotation);
    else
      this._bufferedForRemoval.push(annotation);
  }
}

annotorious.modules.Module.prototype.showAnnotations = function(opt_item_url) {
  this._setAnnotationVisibility(opt_item_url, true);
}

annotorious.modules.Module.prototype._setAnnotationVisibility = function(opt_item_url, visibility) {
  if (opt_item_url) {
    var annotator = this._annotators.get(opt_item_url);
    if (annotator) {
      // Item URL is provided, and item is loaded - set directly
      if (visibility)
        annotator.showAnnotations();
      else
        annotator.hideAnnotations();
    } else {
      // Item URL is provided, but item not yet loaded - cache for later
      this._getSettings(opt_item_url).hide_annotations = visibility;
    }
  } else {
    // Item URL is not provided - update all annotators...
    goog.array.forEach(this._annotators.getValues(), function(annotator) {
      if (visibility)
        annotator.showSelectionWidget();
      else
        annotator.hideAnnotations();
    });

    // ...cache global settings...
    this._cachedGlobalSettings.hide_annotations = visibility;

    // ...and update all cached item settings
    goog.array.forEach(this._cachedItemSettings.getValues(), function(settings) {
      settings.hide_annotations = visibility;
    });
  }
}

/**
 * Sets a specific selector on a particular item.
 * @param {string} item_url the URL of the item on which to set the selector
 * @param {string} selector the name of the selector to set on the item
 */
annotorious.modules.Module.prototype.setActiveSelector = function(item_url, selector) {
  if (this.annotatesItem(item_url)) {
    var annotator = this._annotators.get(item_url);
    if (annotator)
      annotator.setActiveSelector(selector);
  }
}

/** Methods that must be implemented by subclasses of annotorious.modules.Module **/

/**
 * Returns the identifying URL of the specified item.
 * @param {object} item the item.
 * @return {string} the URL
 */
annotorious.modules.Module.prototype.getItemURL = goog.abstractMethod;

/**
 * This function is called from the framework when the module is initialized.
 * Subclasses of annotorious.modules.Module must perform all subclass-specific
 * initialization actions in this function, and then make sure this._init (note
 * the leading _!) gets called with a list of all items that should be made
 * annotatable immediately!
 */
annotorious.modules.Module.prototype.init = goog.abstractMethod;

/**
 * Returns a new annotator for the specified item.
 * @param {object} item the item
 * @return {object} an annotator for this item
 */
annotorious.modules.Module.prototype.newAnnotator = goog.abstractMethod;

/**
 * Tests if this module supports the specified item's media type.
 * @param {object} item the item
 * @return {boolean} true if this module supports the item
 */
annotorious.modules.Module.prototype.supports = goog.abstractMethod;
<|endoftext|>


<commit_before>var cadence = require('cadence/redux')
var url = require('url')
var ok = require('assert').ok
var assert = require('assert')
var logger = require('../monitor/logger')('http.ua')
var Binder = require('../net/binder')
var typer = require('media-typer')
var accum = require('accum')
var Window = require('../monitor/window')
var __slice = [].slice

require('cadence/ee')

function UserAgent (log) {
    this._log = arguments.length == 0 ? true : log
    this._tokens = {}
}

UserAgent.durations = {
    1: new Window(60000),
    5: new Window(300000),
    15: new Window(900000)
}

function collectAverages (time) {
    for (var key in UserAgent.durations) {
       UserAgent.durations[key].sample(time)
    }
}

UserAgent.prototype.fetch = cadence(function (async) {
    var request = {
        options: { headers: {} }
    }

    function override (object) {
        if (Array.isArray(object)) {
            object.forEach(override)
        } else {
            if (object instanceof Binder) {
                object = {
                    url: object.location,
                    ca: object.options.ca,
                    cert: object.options.cert,
                    key: object.options.key
                }
            }
            for (var key in object) {
                if (key == 'url') {
                    if (request.options.url) {
                        request.options.url = url.resolve(request.options.url, object.url)
                    } else {
                        request.options[key] = object[key]
                        request.baseUrl = url.parse(object.url)
                    }
                } else if (key == 'headers') {
                    for (var header in object.headers) {
                        request.options.headers[header.toLowerCase()] = object.headers[header]
                    }
                } else if (/^(context|payload|grant|token|timeout)$/.test(key)) {
                    request[key] = object[key]
                } else {
                    request.options[key] = object[key]
                }
            }
        }
    }

    function log (name, object) {
        if (this._log) {
            logger.debug(name, object, request.context || {})
        }
    }

    __slice.call(arguments, 1).forEach(override)
    if (!request.options.method) {
        request.options.method = request.payload ? 'POST' : 'GET'
    }
    if (request.options.method == 'POST' && !request.options.headers['content-type']) {
        request.options.headers['content-type'] = 'application/json'
    }
    request.options.headers['accept'] = 'application/json'
    request.url = url.parse(request.options.url)

    request.options.hostname = request.url.hostname
    request.options.port = request.url.port
    request.options.path = url.format({
        pathname: request.url.pathname,
        search: request.url.search,
        hash: request.url.hash
    })

    request.key = request.url.hostname + ':' + request.url.port

    if (request.grant == 'cc') {
        request.token = this._tokens[request.key]
    }

    async(function () {
        if (request.grant == 'cc' && !request.token) {
            assert.ok(request.baseUrl.auth)
            async(function () {
                this.fetch({
                    url: url.format(request.url),
                    ca: request.options.ca
                }, {
                    url: '/token',
                    headers: {
                        authorization: 'Basic ' + new Buffer(request.baseUrl.auth).toString('base64')
                    },
                    payload: {
                        grant_type: 'client_credentials'
                    }
                }, async())
            }, function (body, response) {
                if (body.token_type == 'Bearer' && body.access_token) {
                    request.token = this._tokens[request.key] = body.access_token
                }
            })
        } else {
            return [ null, { statusCode: 200 } ]
        }
    }, function (body, response) {
        if (Math.floor(response.statusCode / 100) != 2) return
        var http, options = {}
        if (request.token) {
            request.options.headers.authorization = 'Bearer ' + request.token
        }
        for (var key in request.options) {
            if (key == 'ca') {
                options[key] = true
            } else {
                options[key] = request.options[key]
            }
        }
        log.call(this, 'request', {
            url: request.url,
            options: options,
            sent: request.payload
        })
        if (request.url.protocol == 'https:') {
            http = require('https')
        } else {
            http = require('http')
        }
        http.globalAgent.maxSockets = 5000
        var payload = request.payload
        if (payload && !Buffer.isBuffer(payload)) {
            payload = new Buffer(JSON.stringify(payload))
        }
        if (payload) {
            request.options.headers['content-length'] = payload.length
        }

        var stopwatch = Date.now()
        var fetch = async([function () {
            var client = http.request(request.options)
            async.ee(client).end('response').error()
            if (payload) {
                client.write(payload)
            }
            if (request.timeout) {
                client.setTimeout(request.timeout, function () {
                    client.abort()
                })
            }
            client.end()
        }, function (error) {
            collectAverages(Date.now() - stopwatch)
            var body = new Buffer(JSON.stringify({ message: error.message, errno: error.code }))
            var response = {
                statusCode: 599,
                errno: error.code,
                okay: false,
                headers: {
                    'content-length': body.length,
                    'content-type': 'application/json'
                }
            }
            log.call(this, 'response', {
                status: 'exceptional',
                options: options,
                sent: request.payload,
                received: JSON.parse(body.toString()),
                statusCode: response.statusCode,
                headers: response.headers
            })
            return [ fetch, JSON.parse(body.toString()), response, body ]
        }], function (response) {
            var chunks = []
            async(function () {
                async.ee(response)
                     .on('data', function (chunk) {
                        chunks.push(chunk)
                      })
                     .end('end')
                     .error()
            }, function () {
                var body = Buffer.concat(chunks)
                collectAverages(Date.now() - stopwatch)
                var parsed = body
                var display = null
                var type = typer.parse(response.headers['content-type'] || 'application/octet-stream')
                switch (type.type + '/' + type.subtype) {
                case 'application/json':
                    try {
                        display = parsed = JSON.parse(body.toString())
                    } catch (e) {
                        display = parsed = body.toString()
                        log.call(this, 'unparsable', { toString: display })
                    }
                    break
                case 'text/html':
                case 'text/plain':
                    display = body.toString()
                    break
                }
                response.okay = Math.floor(response.statusCode / 100) == 2
                log.call(this, 'response', {
                    status: 'responded',
                    options: options,
                    sent: request.payload,
                    received: display,
                    statusCode: response.statusCode,
                    headers: response.headers
                })
                if (request.grant == 'cc' && response.statusCode == 401) {
                    delete this._tokens[request.key]
                }
                return [ fetch, parsed, response, body ]
            })
        })()
    })
})

module.exports = UserAgent
<commit_msg>Do not log `agent` in `UserAgent`.<commit_after>var cadence = require('cadence/redux')
var url = require('url')
var ok = require('assert').ok
var assert = require('assert')
var logger = require('../monitor/logger')('http.ua')
var Binder = require('../net/binder')
var typer = require('media-typer')
var accum = require('accum')
var Window = require('../monitor/window')
var __slice = [].slice

require('cadence/ee')

function UserAgent (log) {
    this._log = arguments.length == 0 ? true : log
    this._tokens = {}
}

UserAgent.durations = {
    1: new Window(60000),
    5: new Window(300000),
    15: new Window(900000)
}

function collectAverages (time) {
    for (var key in UserAgent.durations) {
       UserAgent.durations[key].sample(time)
    }
}

UserAgent.prototype.fetch = cadence(function (async) {
    var request = {
        options: { headers: {} }
    }

    function override (object) {
        if (Array.isArray(object)) {
            object.forEach(override)
        } else {
            if (object instanceof Binder) {
                object = {
                    url: object.location,
                    ca: object.options.ca,
                    cert: object.options.cert,
                    key: object.options.key
                }
            }
            for (var key in object) {
                if (key == 'url') {
                    if (request.options.url) {
                        request.options.url = url.resolve(request.options.url, object.url)
                    } else {
                        request.options[key] = object[key]
                        request.baseUrl = url.parse(object.url)
                    }
                } else if (key == 'headers') {
                    for (var header in object.headers) {
                        request.options.headers[header.toLowerCase()] = object.headers[header]
                    }
                } else if (/^(context|payload|grant|token|timeout)$/.test(key)) {
                    request[key] = object[key]
                } else {
                    request.options[key] = object[key]
                }
            }
        }
    }

    function log (name, object) {
        if (this._log) {
            logger.debug(name, object, request.context || {})
        }
    }

    __slice.call(arguments, 1).forEach(override)
    if (!request.options.method) {
        request.options.method = request.payload ? 'POST' : 'GET'
    }
    if (request.options.method == 'POST' && !request.options.headers['content-type']) {
        request.options.headers['content-type'] = 'application/json'
    }
    request.options.headers['accept'] = 'application/json'
    request.url = url.parse(request.options.url)

    request.options.hostname = request.url.hostname
    request.options.port = request.url.port
    request.options.path = url.format({
        pathname: request.url.pathname,
        search: request.url.search,
        hash: request.url.hash
    })

    request.key = request.url.hostname + ':' + request.url.port

    if (request.grant == 'cc') {
        request.token = this._tokens[request.key]
    }

    async(function () {
        if (request.grant == 'cc' && !request.token) {
            assert.ok(request.baseUrl.auth)
            async(function () {
                this.fetch({
                    url: url.format(request.url),
                    ca: request.options.ca
                }, {
                    url: '/token',
                    headers: {
                        authorization: 'Basic ' + new Buffer(request.baseUrl.auth).toString('base64')
                    },
                    payload: {
                        grant_type: 'client_credentials'
                    }
                }, async())
            }, function (body, response) {
                if (body.token_type == 'Bearer' && body.access_token) {
                    request.token = this._tokens[request.key] = body.access_token
                }
            })
        } else {
            return [ null, { statusCode: 200 } ]
        }
    }, function (body, response) {
        if (Math.floor(response.statusCode / 100) != 2) return
        var http, options = {}
        if (request.token) {
            request.options.headers.authorization = 'Bearer ' + request.token
        }
        for (var key in request.options) {
            if (key == 'ca' || key == 'agent') {
                options[key] = true
            } else {
                options[key] = request.options[key]
            }
        }
        log.call(this, 'request', {
            url: request.url,
            options: options,
            sent: request.payload
        })
        if (request.url.protocol == 'https:') {
            http = require('https')
        } else {
            http = require('http')
        }
        http.globalAgent.maxSockets = 5000
        var payload = request.payload
        if (payload && !Buffer.isBuffer(payload)) {
            payload = new Buffer(JSON.stringify(payload))
        }
        if (payload) {
            request.options.headers['content-length'] = payload.length
        }

        var stopwatch = Date.now()
        var fetch = async([function () {
            var client = http.request(request.options)
            async.ee(client).end('response').error()
            if (payload) {
                client.write(payload)
            }
            if (request.timeout) {
                client.setTimeout(request.timeout, function () {
                    client.abort()
                })
            }
            client.end()
        }, function (error) {
            collectAverages(Date.now() - stopwatch)
            var body = new Buffer(JSON.stringify({ message: error.message, errno: error.code }))
            var response = {
                statusCode: 599,
                errno: error.code,
                okay: false,
                headers: {
                    'content-length': body.length,
                    'content-type': 'application/json'
                }
            }
            log.call(this, 'response', {
                status: 'exceptional',
                options: options,
                sent: request.payload,
                received: JSON.parse(body.toString()),
                statusCode: response.statusCode,
                headers: response.headers
            })
            return [ fetch, JSON.parse(body.toString()), response, body ]
        }], function (response) {
            var chunks = []
            async(function () {
                async.ee(response)
                     .on('data', function (chunk) {
                        chunks.push(chunk)
                      })
                     .end('end')
                     .error()
            }, function () {
                var body = Buffer.concat(chunks)
                collectAverages(Date.now() - stopwatch)
                var parsed = body
                var display = null
                var type = typer.parse(response.headers['content-type'] || 'application/octet-stream')
                switch (type.type + '/' + type.subtype) {
                case 'application/json':
                    try {
                        display = parsed = JSON.parse(body.toString())
                    } catch (e) {
                        display = parsed = body.toString()
                        log.call(this, 'unparsable', { toString: display })
                    }
                    break
                case 'text/html':
                case 'text/plain':
                    display = body.toString()
                    break
                }
                response.okay = Math.floor(response.statusCode / 100) == 2
                log.call(this, 'response', {
                    status: 'responded',
                    options: options,
                    sent: request.payload,
                    received: display,
                    statusCode: response.statusCode,
                    headers: response.headers
                })
                if (request.grant == 'cc' && response.statusCode == 401) {
                    delete this._tokens[request.key]
                }
                return [ fetch, parsed, response, body ]
            })
        })()
    })
})

module.exports = UserAgent
<|endoftext|>


<commit_before>/**
 * Just flat-out fail the test with the given message
 */
function fail(message) {
  throw message;
}

/**
 * Perform an assertion several times. If the assertion passes before the
 * maximum number of iterations, the assertion passes. Otherwise the
 * assertion fails
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 * @param delay The amount of time to pause between attempts
 */
function retryFunctionWithDelay(f, maxTries, target, delay) {
  var tries = 0;
  var exception = null;
  while (tries < maxTries) {
    try {
      f();
      return;  // if we get here, our function must have passed (no exceptions)
    }
    catch(e) {
      exception = e;
      tries++;
      target.delay(delay);
    }
  }
  throw exception;
}

/**
 * A simplified version of 'retryFunctionWithDelay' with an implicit delay of
 * 0.5 seconds.
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 */
function retryFunction(f, maxTries, target) {
  retryFunctionWithDelay(f, maxTries, target, 0.5);
}

/**
 * Asserts that the given expression is true and throws an exception with
 * a default message, or the optional +message+ parameter
 */
function assertTrue(expression, message) {
  if (! expression) {
    if (! message) { message = "Assertion failed"; }
    throw message;
  }
}

/**
 * Asserts that the given regular expression matches the result of the
 * given message.
 * @param pattern - the pattern to match
 * @param expression - the expression to match against
 * @param message - an optional string message
 */
function assertMatch(regExp, expression, message) {
  var defMessage = "'" + expression + "' does not match '" + regExp + "'";
  assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the +received+ object matches the +expected+ object (using
 * plain ol' ==). If it doesn't, this method throws an exception with either
 * a default message, or the one given as the last (optional) argument
 */
function assertEquals(expected, received, message) {
  var defMessage = "Expected <" + expected + "> but received <" + received + ">";
  assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given expression is false and otherwise throws an
 * exception with a default message, or the optional +message+ parameter
 */
function assertFalse(expression, message) {
  assertTrue(! expression, message);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is not null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If it is null, an exception is thrown with
 * a default message or the given optional +message+ parameter
 */
function assertNotNull(thingie, message) {
  var defMessage = "Expected not null object";
  assertTrue(thingie !== null && thingie.toString() != "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the given window definition matches the current main window. The
 * window definition is a JavaScript object whose property hierarchy matches
 * the main UIAWindow.  Property names in the given definition that match a
 * method will cause that method to be invoked and the matching to be performed
 * and the result. For example, the UIAWindow exposes all UITableViews through
 * the tableViews() method. You only need to specify a 'tableViews' property to
 * cause the method to be invoked.
 *
 * PROPERTY HIERARCHY Property definitions can be nested as deeply as
 * necessary. Matching is done by traversing the same path in the main
 * UIAWindow as your screen definition. For example, to make assertions about
 * the left and right buttons in a UINavigationBar you can do this:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   }
 * });
 *
 * PROPERTY MATCHERS For each property you wish to make an assertion about, you
 * can specify a string, number regular expression or function. Strings and
 * numbers are matches using the assertEquals() method. Regular expressions are
 * matches using the assertMatch() method.
 *
 * If you specify 'null' for a property, it means you don't care to match.
 * Typically this is done inside of arrays where you need to match the number
 * of elements, but don't necessarily care to make assertions about each one.
 *
 * Functions are given the matching property as the single argument. For
 * example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: function(button) {
 *       // make custom assertions here
 *     }
 *   }
 * });
 *
 * ARRAYS
 * If a property you want to match is an array (e.g. tableViews()), you can
 * specify one of the above matchers for each element of the array. If the
 * number of provided matchers does not match the number of given elements, the
 * assertion will fail (throw an exception)
 *
 * In any case, you specify another object definition for each property to
 * drill-down into the atomic properties you wish to test. For example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   },
 *   tableViews: [
 *     {
 *       groups: [
 *         { name: "First Group" },
 *         { name: "Second Group" }
 *       ],
 *       cells: [
 *         { name: "Cell 1" },
 *         { name: "Cell 2" },
 *         { name: "Cell 3" },
 *         { name: "Cell 4" }
 *       ]
 *     }
 *   ]
 * });
 *
 * HANDLING FAILURE If any match fails, an appropriate exception will be
 * thrown. If you are using the test structure provided by tuneup, this will be
 * caught and detailed correctly in Instruments.
 *
 * POST-PROCESSING If your screen definition provides an 'onPass' property that
 * points to a function, that function will be invoked after all matching has
 * been peformed on the current window and all assertions have passed. This
 * means you can assert the structure of your screen and operate on it in one
 * pass:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" }
 *   },
 *   onPass: function(window) {
 *     var leftButton = window.navigationBar().leftButton();
 *     leftButton.tap();
 *   }
 * });
 */
function assertWindow(window) {
  target = UIATarget.localTarget();
  application = target.frontMostApp();
  mainWindow = application.mainWindow();

  var onPass = null;
  if (window.onPass) {
    onPass = window.onPass;
    delete window.onPass;
  }

  try {
    assertPropertiesMatch(window, mainWindow, 0);
  }
  catch(badProp) {
    fail("Failed to match " + badProp[0] + ": " + badProp[1]);
  }

  if (onPass) {
    try {
      onPass(mainWindow);
    }
    catch(e) {
      throw "Failed to execute 'onPass' callback: " + e;
    }
  }
};

/**
 * Asserts that the +expected+ object matches the +given+ object by making
 * assertions appropriate based on the type of each property in the
 * +expected+ object. This method will recurse through the structure,
 * applying assertions for each matching property path. See the description
 * for +assertWindow+ for details on the matchers.
 */
function assertPropertiesMatch(expected, given, level) {
  for (var propName in expected) {
    if (propName.match(/~iphone$/) && UIATarget.localTarget().model().match(/^iPhone/) === null) {
      return;
    }
    else if (propName.match(/~ipad$/) && UIATarget.localTarget().model().match(/^iPad/) === null) {
      return;
    }

    if (expected.hasOwnProperty(propName)) {
      try {
        var expectedProp = expected[propName];
        var givenProp = given[propName];

        if (typeof(givenProp) == "function") {
          try {
            // We have to use eval (shudder) because calling functions on
            // UIAutomation objects with () operator crashes
            // See Radar bug 8496138
            givenProp = eval("given." + propName + "()");
          }
          catch (e) {
            UIALogger.logDebug("Unable to evaluate given." + propName + "() against " + given);
            continue;
          }
        }

        // null indicates we don't care to match
        if (expectedProp == null) {
          continue;
        }

        if (givenProp == null) {
          throw propName;
        }

        if (typeof(expectedProp) == "string") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "number") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "function") {
          if (expectedProp.constructor == RegExp) {
            assertMatch(expectedProp, givenProp);
          }
          else {
            expectedProp(givenProp);
          }
        }
        else if (typeof(expectedProp) == "object") {
          if (expectedProp.constructor == Array) {
            assertEquals(expectedProp.length, givenProp.length, "Incorrect number of elements in array");
            for (var i = 0; i < expectedProp.length; i++) {
              var exp = expectedProp[i];
              var giv = givenProp[i];
              assertPropertiesMatch(exp, giv, level + 1);
            };
          }
          else if (typeof(givenProp) == "object") {
            assertPropertiesMatch(expectedProp, givenProp, level + 1);
          }
          else {
            throw propName;
          }
        }
      }
      catch(e) {
        if (e.constructor == Array) {
          e[0] = propName + "." + e[0];
          throw e;
        }
        else {
          var err = [propName, e];
          throw err;
        }
      }
    }
  }
};

<commit_msg>Bug-fixes and optional debugging in assertPropertiesMatch()<commit_after>var TuneupJS = {
  debug: false
};

/**
 * Just flat-out fail the test with the given message
 */
function fail(message) {
  throw message;
}

/**
 * Perform an assertion several times. If the assertion passes before the
 * maximum number of iterations, the assertion passes. Otherwise the
 * assertion fails
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 * @param delay The amount of time to pause between attempts
 */
function retryFunctionWithDelay(f, maxTries, target, delay) {
  var tries = 0;
  var exception = null;
  while (tries < maxTries) {
    try {
      f();
      return;  // if we get here, our function must have passed (no exceptions)
    }
    catch(e) {
      exception = e;
      tries++;
      target.delay(delay);
    }
  }
  throw exception;
}

/**
 * A simplified version of 'retryFunctionWithDelay' with an implicit delay of
 * 0.5 seconds.
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 */
function retryFunction(f, maxTries, target) {
  retryFunctionWithDelay(f, maxTries, target, 0.5);
}

/**
 * Asserts that the given expression is true and throws an exception with
 * a default message, or the optional +message+ parameter
 */
function assertTrue(expression, message) {
  if (! expression) {
    if (! message) { message = "Assertion failed"; }
    throw message;
  }
}

/**
 * Asserts that the given regular expression matches the result of the
 * given message.
 * @param pattern - the pattern to match
 * @param expression - the expression to match against
 * @param message - an optional string message
 */
function assertMatch(regExp, expression, message) {
  var defMessage = "'" + expression + "' does not match '" + regExp + "'";
  assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the +received+ object matches the +expected+ object (using
 * plain ol' ==). If it doesn't, this method throws an exception with either
 * a default message, or the one given as the last (optional) argument
 */
function assertEquals(expected, received, message) {
  var defMessage = "Expected <" + expected + "> but received <" + received + ">";
  assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given expression is false and otherwise throws an
 * exception with a default message, or the optional +message+ parameter
 */
function assertFalse(expression, message) {
  assertTrue(! expression, message);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is not null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If it is null, an exception is thrown with
 * a default message or the given optional +message+ parameter
 */
function assertNotNull(thingie, message) {
  var defMessage = "Expected not null object";
  assertTrue(thingie !== null && thingie.toString() != "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the given window definition matches the current main window. The
 * window definition is a JavaScript object whose property hierarchy matches
 * the main UIAWindow.  Property names in the given definition that match a
 * method will cause that method to be invoked and the matching to be performed
 * and the result. For example, the UIAWindow exposes all UITableViews through
 * the tableViews() method. You only need to specify a 'tableViews' property to
 * cause the method to be invoked.
 *
 * PROPERTY HIERARCHY Property definitions can be nested as deeply as
 * necessary. Matching is done by traversing the same path in the main
 * UIAWindow as your screen definition. For example, to make assertions about
 * the left and right buttons in a UINavigationBar you can do this:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   }
 * });
 *
 * PROPERTY MATCHERS For each property you wish to make an assertion about, you
 * can specify a string, number regular expression or function. Strings and
 * numbers are matches using the assertEquals() method. Regular expressions are
 * matches using the assertMatch() method.
 *
 * If you specify 'null' for a property, it means you don't care to match.
 * Typically this is done inside of arrays where you need to match the number
 * of elements, but don't necessarily care to make assertions about each one.
 *
 * Functions are given the matching property as the single argument. For
 * example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: function(button) {
 *       // make custom assertions here
 *     }
 *   }
 * });
 *
 * ARRAYS
 * If a property you want to match is an array (e.g. tableViews()), you can
 * specify one of the above matchers for each element of the array. If the
 * number of provided matchers does not match the number of given elements, the
 * assertion will fail (throw an exception)
 *
 * In any case, you specify another object definition for each property to
 * drill-down into the atomic properties you wish to test. For example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   },
 *   tableViews: [
 *     {
 *       groups: [
 *         { name: "First Group" },
 *         { name: "Second Group" }
 *       ],
 *       cells: [
 *         { name: "Cell 1" },
 *         { name: "Cell 2" },
 *         { name: "Cell 3" },
 *         { name: "Cell 4" }
 *       ]
 *     }
 *   ]
 * });
 *
 * HANDLING FAILURE If any match fails, an appropriate exception will be
 * thrown. If you are using the test structure provided by tuneup, this will be
 * caught and detailed correctly in Instruments.
 *
 * POST-PROCESSING If your screen definition provides an 'onPass' property that
 * points to a function, that function will be invoked after all matching has
 * been peformed on the current window and all assertions have passed. This
 * means you can assert the structure of your screen and operate on it in one
 * pass:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" }
 *   },
 *   onPass: function(window) {
 *     var leftButton = window.navigationBar().leftButton();
 *     leftButton.tap();
 *   }
 * });
 */
function assertWindow(window) {
  target = UIATarget.localTarget();
  application = target.frontMostApp();
  mainWindow = application.mainWindow();

  var onPass = null;
  if (window.onPass) {
    onPass = window.onPass;
    delete window.onPass;
  }

  try {
    assertPropertiesMatch(window, mainWindow, 0);
  }
  catch(badProp) {
    fail("Failed to match " + badProp[0] + ": " + badProp[1]);
  }

  if (onPass) {
    try {
      onPass(mainWindow);
    }
    catch(e) {
      throw "Failed to execute 'onPass' callback: " + e;
    }
  }
}

function debugMessage(message) {
  if (TuneupJS.debug === true) {
    UIALogger.logMessage(message);
  }
}

/**
 * Asserts that the +expected+ object matches the +given+ object by making
 * assertions appropriate based on the type of each property in the
 * +expected+ object. This method will recurse through the structure,
 * applying assertions for each matching property path. See the description
 * for +assertWindow+ for details on the matchers.
 */
function assertPropertiesMatch(expected, given, level) {
  for (var propName in expected) {
    debugMessage("Attempting to match '" + propName + "'...");
    var expectedProp = expected[propName];

    if (propName.match(/~iphone$/)) {
      if (UIATarget.localTarget().model().match(/^iPhone/) === null) {
        debugMessage("[" + propName + "]: Ignoring iPhone-specific property '" +
                     "' on " + UIATarget.localTarget().model());
        continue;  // we're on the wrong platform, ignore
      }
      else {
        propName = propName.match(/^(.*)~iphone/)[1];
      }
    }
    else if (propName.match(/~ipad$/)) {
      if (UIATarget.localTarget().model().match(/^iPad/) === null) {
        debugMessage("[" + propName + "]: Ignoring iPad-specific property '" +
                     "' on " + UIATarget.localTarget().model());
        continue;  // we're on the wrong platform, ignore
      }
      else {
        propName = propName.match(/^(.*)~ipad/)[1];
      }
    }

    var givenProp = given[propName];

    if (typeof(givenProp) == "function") {
      try {
        // We have to use eval (shudder) because calling functions on
        // UIAutomation objects with () operator crashes
        // See Radar bug 8496138
        givenProp = eval("given." + propName + "()");
      }
      catch (e) {
        debugMessage("[" + propName + "]: Unable to evaluate against " + given);
        continue;
      }
    }

    if (givenProp === null) {
      debugMessage("[" + propName + "]: Could not find given property");
      throw propName;
    }
    else {
      debugMessage("[" + propName + "]: givenProp = " + givenProp);
    }

    try {
      // null indicates we don't care to match
      if (expectedProp === null) {
        debugMessage("[" + propName + "]: Ignoring null-match");
        continue;
      }

      // debugMessage("[" + propName + "]: expectedProp type: " + typeof(expectedProp) +
      //              " constructor: " + expectedProp.constructor);

      if (typeof(expectedProp) == "string") {
        debugMessage("[" + propName + "]: matching string '" + givenProp + "'");
        assertEquals(expectedProp, givenProp);
      }
      else if (typeof(expectedProp) == "number") {
        debugMessage("[" + propName + "]: matching number " + givenProp);
        assertEquals(expectedProp, givenProp);
      }
      else if (typeof(expectedProp) == "function") {
        if (expectedProp.constructor == RegExp) {
          debugMessage("[" + propName + "]: matching RegExp " + givenProp);
          assertMatch(expectedProp, givenProp);
        }
        else {
          debugMessage("[" + propName + "]: matching function " + givenProp);
          expectedProp(givenProp);
        }
      }
      else if (typeof(expectedProp) == "object") {
        if (expectedProp.constructor == Array) {
          debugMessage("[" + propName + "]: matching array " + givenProp);
          assertEquals(expectedProp.length, givenProp.length, "Incorrect number of elements in array");
          for (var i = 0; i < expectedProp.length; i++) {
            var exp = expectedProp[i];
            var giv = givenProp[i];
            assertPropertiesMatch(exp, giv, level + 1);
          }
        }
        else if (typeof(givenProp) == "object") {
          debugMessage("[" + propName + "]: matching object " + givenProp);
          assertPropertiesMatch(expectedProp, givenProp, level + 1);
        }
        else {
          debugMessage("[" + propName + "]: Unknown type of object constructor: " + expectedProp.constructor);
          throw propName;
        }
      }
      else {
        debugMessage("[" + propName + "]: unknown type for expectedProp: " + typeof(expectedProp));
      }
    }
    catch(e1) {
      debugMessage("Got an exception: " + e1);
      if (e1.constructor == Array) {
        e1[0] = propName + "." + e1[0];
        throw e1;
      }
      else {
        var err = [propName, e1];
        throw err;
      }
    }
  }
}

<|endoftext|>