<commit_before>// Copyright 2013-2020, University of Colorado Boulder

/**
 * An observable array of items which sends notifications when items are added or removed.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const inherit = require( 'PHET_CORE/inherit' );
  const merge = require( 'PHET_CORE/merge' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const ObservableArrayIO = require( 'AXON/ObservableArrayIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );

  // Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
  const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( options ) {
    assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

    options = merge( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      phetioType: DefaultObservableArrayIOType,
      tandem: Tandem.OPTIONAL
    }, options );

    assert && assert( options.phetioType && options.phetioType.parameterTypes, 'ObservableArray\'s phetioType should have a parameterType' );
    assert && assert( options.phetioType && options.phetioType.parameterTypes.length === 1, 'ObservableArray\'s phetioType should have a parameterType' );
    assert && assert( options.phetioType && options.phetioType.parameterTypes[ 0 ], 'ObservableArray\'s phetioType should have a parameterType' );

    this.allowDuplicates = options.allowDuplicates; // @private

    this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

    this._array = []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    // @public (read-only) observe this, but don't set it
    this.lengthProperty = new NumberProperty( this._array.length, {
      numberType: 'Integer',
      tandem: options.tandem.createTandem( 'lengthProperty' ),
      phetioReadOnly: true
    } );

    PhetioObject.call( this, options );
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( PhetioObject, ObservableArray, {

    // @public
    dispose: function() {
      this.lengthProperty.dispose();
      PhetioObject.prototype.dispose.call( this );
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      this.clear();
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1, 'listener is already registered' );
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      const index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1, 'listener is not registered' );
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'listener is already registered' );
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      const index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'listener is not registered' );
      this._removedListeners.splice( index, 1 );
    },

    // @private called when an item is added.
    _fireItemAdded: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemAdded', {
        getData: function() {
          return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
        }
      } );

      //Signify that an item was added to the list
      const copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    // @private called when an item is removed.
    _fireItemRemoved: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemRemoved', {
        getData: function() {
          return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
        }
      } );

      //Signify that an item was removed from the list
      const copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( let i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      const index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} array a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( array ) {
      assert && assert( _.isArray( array ), 'array should be an array' );
      for ( let i = 0; i < array.length; i++ ) {
        this.remove( array[ i ] );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {
      assert && assert( this.length > 0, 'cannot pop an empty ObservableArray' );

      const item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      assert && assert( this.length > 0, 'cannot shift an empty ObservableArray' );

      const item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item
     * @throws assertion Error if the index is out of bounds
     * @public
     */
    get: function( index ) {
      assert && assert( index >= 0 && index < this.length, `index out of bounds: ${index}` );
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      while ( this.length > 0 ) {
        this.pop();
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param {function(element:*):*} mapFunction - from Observable array element to something else
     * @returns {Object[]}
     * @public
     */
    map: function( mapFunction ) {
      return this._array.map( mapFunction );
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param {function(element):boolean} predicate
     * @returns {Object[]}
     * @public
     */
    filter: function( predicate ) {
      return this._array.filter( predicate );
    },

    /**
     * Count the number of items in this ObservableArray that satisfy the given Predicate.
     * @param {function} predicate
     * @returns {number}
     * @public
     */
    count: function( predicate ) {
      let count = 0;
      for ( let i = 0; i < this._array.length; i++ ) {
        if ( predicate( this._array[ i ] ) ) {
          count++;
        }
      }
      return count;
    },

    /**
     * Find the first element that matches the given predicate.
     * @param {function} predicate
     * @param {number} [fromIndex] - optional start index for the search
     * @returns {*} the first element that matches the given predicate, or undefined if no matching item can be found
     * @throws assertion Error if the fromIndex is specified and out of range
     * @public
     */
    find: function( predicate, fromIndex ) {
      assert && ( fromIndex !== undefined ) && assert( typeof fromIndex === 'number', 'fromIndex must be numeric, if provided' );
      assert && ( typeof fromIndex === 'number' ) && assert( fromIndex >= 0 && fromIndex < this.length,
        `fromIndex out of bounds: ${fromIndex}` );
      return _.find( this._array, predicate, fromIndex );
    },

    /**
     * Returns true if some element in this ObservableArray matches the predicate.
     * @param {function} predicate - the function to test elements
     * @returns {boolean}
     */
    some: function( predicate ) {
      return _.some( this._array, predicate );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( let i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array.  Most clients should be able to use the ObservableArray functions for array access
     * or filtering, mapping, etc.  If it is necessary to access the array directly, please document why it is necessary.
     * Also note this is not a defensive copy, so the client will need to take care not to disturb the elements of the
     * array.
     * @returns {Array}
     * @public
     */
    getArray: function() {
      return this._array;
    },

    /**
     * Add/remove elements from any point in the array
     * @param {number} start - the index to start adding/removing items
     * @param {number} deleteCount - the number of items to delete
     * @param {Object} [item1] - an item to add
     * @param {Object} [item2] - an item to add
     * @param {Object} [etc] - varargs items to add etc.
     * @returns {Object[]} the items that were deleted.
     * @public
     */
    splice: function( start, deleteCount, item1, item2, etc ) {
      const deleted = this._array.splice.apply( this._array, arguments );
      const args = Array.prototype.slice.call( arguments );
      for ( let i = 0; i < deleted.length; i++ ) {
        this._fireItemRemoved( deleted[ i ] );
      }

      for ( let k = 2; k < args.length; k++ ) {
        this._fireItemAdded( args[ k ] );
      }
      return deleted;
    },

    /**
     * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
     * No items are added or removed, and this method does not send out any notifications.
     * @param {Random} random - from dot
     */
    shuffle: function( random ) {
      assert && assert( random, 'random must be supplied' );

      // preserve the same _array reference in case any clients got a reference to it with getArray()
      const shuffled = random.shuffle( this._array );
      this._array.length = 0;
      Array.prototype.push.apply( this._array, shuffled );
    }
  } );
} );<commit_msg>Improve documentation about getArray, see https://github.com/phetsims/axon/issues/266<commit_after>// Copyright 2013-2020, University of Colorado Boulder

/**
 * An observable array of items which sends notifications when items are added or removed.
 *
 * Because the array is observable, we must be careful about the possibility of concurrent-modification errors.
 * Any time we iterate over the array, we must iterate over a copy, because callback may be modifying the array.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const inherit = require( 'PHET_CORE/inherit' );
  const merge = require( 'PHET_CORE/merge' );
  const NumberProperty = require( 'AXON/NumberProperty' );
  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const ObservableArrayIO = require( 'AXON/ObservableArrayIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );

  // Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
  const DefaultObservableArrayIOType = ObservableArrayIO( ObjectIO );

  /**
   * @param {Object} [options]
   * @constructor
   */
  function ObservableArray( options ) {
    assert && assert( !Array.isArray( options ), 'ObservableArray cannot be initialized with values' );

    options = merge( {
      allowDuplicates: false, // are duplicate items allowed in the array?
      phetioType: DefaultObservableArrayIOType,
      tandem: Tandem.OPTIONAL
    }, options );

    assert && assert( options.phetioType && options.phetioType.parameterTypes, 'ObservableArray\'s phetioType should have a parameterType' );
    assert && assert( options.phetioType && options.phetioType.parameterTypes.length === 1, 'ObservableArray\'s phetioType should have a parameterType' );
    assert && assert( options.phetioType && options.phetioType.parameterTypes[ 0 ], 'ObservableArray\'s phetioType should have a parameterType' );

    this.allowDuplicates = options.allowDuplicates; // @private

    this._options = options; // @private, for creating internal ObservableArray copies, see map/filter

    this._array = []; // @private internal, do not access directly
    this._addedListeners = []; // @private listeners called when an item is added
    this._removedListeners = []; // @private listeners called when an item is removed

    // @public (read-only) observe this, but don't set it
    this.lengthProperty = new NumberProperty( this._array.length, {
      numberType: 'Integer',
      tandem: options.tandem.createTandem( 'lengthProperty' ),
      phetioReadOnly: true
    } );

    PhetioObject.call( this, options );
  }

  axon.register( 'ObservableArray', ObservableArray );

  return inherit( PhetioObject, ObservableArray, {

    // @public
    dispose: function() {
      this.lengthProperty.dispose();
      PhetioObject.prototype.dispose.call( this );
    },

    /**
     * Restore the array back to its initial state
     * Note: if an item is in the current array and original array, it is removed and added back
     * This may or may not change in the future, see #4
     * @public
     */
    reset: function() {
      this.clear();
    },

    // @public
    get length() { return this._array.length; },

    /**
     * Adds a listener that will be notified when an item is added to the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemAddedListener: function( listener ) {
      assert && assert( this._addedListeners.indexOf( listener ) === -1, 'listener is already registered' );
      this._addedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemAddedListener.
     * @param listener
     * @public
     */
    removeItemAddedListener: function( listener ) {
      const index = this._addedListeners.indexOf( listener );
      assert && assert( index !== -1, 'listener is not registered' );
      this._addedListeners.splice( index, 1 );
    },

    /**
     * Adds a listener that will be notified when an item is removed from the list.
     * @param listener function( item, observableArray )
     * @public
     */
    addItemRemovedListener: function( listener ) {
      assert && assert( this._removedListeners.indexOf( listener ) === -1, 'listener is already registered' );
      this._removedListeners.push( listener );
    },

    /**
     * Removes a listener that was added via addItemRemovedListener.
     * @param listener
     * @public
     */
    removeItemRemovedListener: function( listener ) {
      const index = this._removedListeners.indexOf( listener );
      assert && assert( index !== -1, 'listener is not registered' );
      this._removedListeners.splice( index, 1 );
    },

    // @private called when an item is added.
    _fireItemAdded: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemAdded', {
        getData: function() {
          return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
        }
      } );

      //Signify that an item was added to the list
      const copy = this._addedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    // @private called when an item is removed.
    _fireItemRemoved: function( item ) {
      const self = this;
      this.phetioStartEvent( 'itemRemoved', {
        getData: function() {
          return self.phetioType.parameterTypes[ 0 ].toStateObject( item );
        }
      } );

      //Signify that an item was removed from the list
      const copy = this._removedListeners.slice( 0 ); // operate on a copy, firing could result in the listeners changing
      for ( let i = 0; i < copy.length; i++ ) {
        copy[ i ]( item, this );
      }

      this.phetioEndEvent();
    },

    /**
     * Adds an item to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param item
     * @public
     */
    add: function( item ) {
      this.push( item );
    },

    /**
     * Add items to the end of the array.
     * This is a convenience function, and is the same as push.
     * @param {Array} items
     * @public
     */
    addAll: function( items ) {
      for ( let i = 0; i < items.length; i++ ) {
        this.add( items[ i ] );
      }
    },

    /**
     * Removes the first occurrence of an item from the array.
     * If duplicates are allowed (see options.allowDuplicates) you may need to call this multiple
     * times to totally purge item from the array.
     * @param item
     * @public
     */
    remove: function( item ) {
      const index = this._array.indexOf( item );
      if ( index !== -1 ) {
        this._array.splice( index, 1 );
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
    },

    /**
     * Removes the first occurrence of each item in the specified array.
     * @param {Array} array a list of items to remove
     * @see ObservableArray.remove
     * @public
     */
    removeAll: function( array ) {
      assert && assert( _.isArray( array ), 'array should be an array' );
      for ( let i = 0; i < array.length; i++ ) {
        this.remove( array[ i ] );
      }
    },

    /**
     * Pushes an item onto the end of the array.
     * @param item
     * @throws Error if duplicates are not allowed (see options.allowDuplicates) and item is already in the array
     * @public
     */
    push: function( item ) {
      if ( !this.allowDuplicates && this.contains( item ) ) {
        throw new Error( 'duplicates are not allowed' );
      }
      this._array.push( item );
      this.lengthProperty.set( this._array.length );
      this._fireItemAdded( item );
    },

    /**
     * Removes an item from the end of the array and returns it.
     * @returns {*}
     * @public
     */
    pop: function() {
      assert && assert( this.length > 0, 'cannot pop an empty ObservableArray' );

      const item = this._array.pop();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Removes an item from the beginning of the array and returns it.
     * @returns {*}
     * @public
     */
    shift: function() {
      assert && assert( this.length > 0, 'cannot shift an empty ObservableArray' );

      const item = this._array.shift();
      if ( item !== undefined ) {
        this.lengthProperty.set( this._array.length );
        this._fireItemRemoved( item );
      }
      return item;
    },

    /**
     * Does the array contain the specified item?
     * @param item
     * @returns {boolean}
     * @public
     */
    contains: function( item ) {
      return this.indexOf( item ) !== -1;
    },

    /**
     * Gets an item at the specified index.
     * @param index
     * @returns {*} the item
     * @throws assertion Error if the index is out of bounds
     * @public
     */
    get: function( index ) {
      assert && assert( index >= 0 && index < this.length, `index out of bounds: ${index}` );
      return this._array[ index ];
    },

    /**
     * Gets the index of a specified item.
     * @param item
     * @returns {*} -1 if item is not in the array
     * @public
     */
    indexOf: function( item ) {
      return this._array.indexOf( item );
    },

    /**
     * Removes all items from the array.
     * @public
     */
    clear: function() {
      while ( this.length > 0 ) {
        this.pop();
      }
    },

    /**
     * Applies a callback function to each item in the array
     * @param callback function(item)
     * @public
     */
    forEach: function( callback ) {
      this._array.slice().forEach( callback ); // do this on a copy of the array, in case callbacks involve array modification
    },

    /**
     * Maps the values in this ObservableArray using the specified function, and returns a new ObservableArray for chaining.
     * @param {function(element:*):*} mapFunction - from Observable array element to something else
     * @returns {Object[]}
     * @public
     */
    map: function( mapFunction ) {
      return this._array.map( mapFunction );
    },

    /**
     * Filters the values in this ObservableArray using the predicate function, and returns a new ObservableArray for chaining.
     * @param {function(element):boolean} predicate
     * @returns {Object[]}
     * @public
     */
    filter: function( predicate ) {
      return this._array.filter( predicate );
    },

    /**
     * Count the number of items in this ObservableArray that satisfy the given Predicate.
     * @param {function} predicate
     * @returns {number}
     * @public
     */
    count: function( predicate ) {
      let count = 0;
      for ( let i = 0; i < this._array.length; i++ ) {
        if ( predicate( this._array[ i ] ) ) {
          count++;
        }
      }
      return count;
    },

    /**
     * Find the first element that matches the given predicate.
     * @param {function} predicate
     * @param {number} [fromIndex] - optional start index for the search
     * @returns {*} the first element that matches the given predicate, or undefined if no matching item can be found
     * @throws assertion Error if the fromIndex is specified and out of range
     * @public
     */
    find: function( predicate, fromIndex ) {
      assert && ( fromIndex !== undefined ) && assert( typeof fromIndex === 'number', 'fromIndex must be numeric, if provided' );
      assert && ( typeof fromIndex === 'number' ) && assert( fromIndex >= 0 && fromIndex < this.length,
        `fromIndex out of bounds: ${fromIndex}` );
      return _.find( this._array, predicate, fromIndex );
    },

    /**
     * Returns true if some element in this ObservableArray matches the predicate.
     * @param {function} predicate - the function to test elements
     * @returns {boolean}
     */
    some: function( predicate ) {
      return _.some( this._array, predicate );
    },

    /**
     * Starting with the initial value, combine values from this ObservableArray to come up with a composite result.
     * Same as foldLeft.  In underscore this is called _.reduce aka _.fold or _.inject
     * @param value
     * @param combiner
     * @returns {*}
     * @public
     */
    reduce: function( value, combiner ) {
      for ( let i = 0; i < this._array.length; i++ ) {
        value = combiner( value, this._array[ i ] );
      }
      return value;
    },

    /**
     * Return the underlying array.  Most clients should be able to use the ObservableArray functions for array access
     * or filtering, mapping, etc. instead of this function.  If it is necessary to access the array directly (say, for
     * performance reasons), please document why it is necessary. Also note this is not a defensive copy, so the client
     * will need to take care not to disturb the elements of the array.
     * @returns {Array}
     * @public
     */
    getArray: function() {
      return this._array;
    },

    /**
     * Add/remove elements from any point in the array
     * @param {number} start - the index to start adding/removing items
     * @param {number} deleteCount - the number of items to delete
     * @param {Object} [item1] - an item to add
     * @param {Object} [item2] - an item to add
     * @param {Object} [etc] - varargs items to add etc.
     * @returns {Object[]} the items that were deleted.
     * @public
     */
    splice: function( start, deleteCount, item1, item2, etc ) {
      const deleted = this._array.splice.apply( this._array, arguments );
      const args = Array.prototype.slice.call( arguments );
      for ( let i = 0; i < deleted.length; i++ ) {
        this._fireItemRemoved( deleted[ i ] );
      }

      for ( let k = 2; k < args.length; k++ ) {
        this._fireItemAdded( args[ k ] );
      }
      return deleted;
    },

    /**
     * Changes the ordering of elements in the array.  Requires a Random source so that shuffles can be reproducible.
     * No items are added or removed, and this method does not send out any notifications.
     * @param {Random} random - from dot
     */
    shuffle: function( random ) {
      assert && assert( random, 'random must be supplied' );

      // preserve the same _array reference in case any clients got a reference to it with getArray()
      const shuffled = random.shuffle( this._array );
      this._array.length = 0;
      Array.prototype.push.apply( this._array, shuffled );
    }
  } );
} );<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * A rectangular button.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var Button = require( 'SUN/Button' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  /**
   * @param {Node} content
   * @param {Function} callback
   * @param options
   * @constructor
   */
  function RectangleButton( content, callback, options ) {

    options = _.extend( {
        rectangleFill: 'white',
        rectangleFillDisabled: 'rgb(225,225,225)',
        rectangleStroke: 'black',
        rectangleStrokeDisabled: 'rgb(175,175,175)',
        rectangleLineWidth: 1,
        //TODO default margins and corner radius should be computed based on content dimensions
        rectangleXMargin: 5,
        rectangleYMargin: 5,
        rectangleCornerRadius: 10
      },
      options );

    var thisButton = this;

    // parent for content + rectangle
    var node = new Node();

    // rectangle around the content
    var rectangle = new Rectangle(
      0, 0, content.width + ( 2 * options.rectangleXMargin ), content.height + ( 2 * options.rectangleYMargin ), options.rectangleCornerRadius, options.rectangleCornerRadius,
      { rectangleLineWidth: options.lineWidth } );
    node.addChild( rectangle );

    // content centered in the rectangle
    content.centerX = rectangle.width / 2;
    content.centerY = rectangle.height / 2;
    node.addChild( content );

    Button.call( thisButton, node, callback );

    // enable/disable the pieces that are specific to this subtype
    thisButton._enabled.link( function( enabled ) {
      rectangle.fill = enabled ? options.rectangleFill : options.rectangleFillDisabled;
      rectangle.stroke = enabled ? options.rectangleStroke : options.rectangleStrokeDisabled;
    } );

    thisButton.mutate( options );
  }

  inherit( Button, RectangleButton );

  return RectangleButton;
} );
<commit_msg>add visual state feedback to RectangleButton, fix call sites to use 'rectangleFillUp' instead of 'rectangleFill', and to use Color instead of String for specifying the color (so that colors for other states can be computed)<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * A rectangular button. The background fill changes to give feedback about state. The content node is centered in the rectangle.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // imports
  var assert = require( 'ASSERT/assert' )( 'sun' );
  var Color = require( 'SCENERY/util/Color' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Node = require( 'SCENERY/nodes/Node' );
  var PushButton = require( 'SUN/PushButton' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );

  // Creates a node that represents the button is a specific state.
  var createNode = function( content, stroke, fill, rectangleLineWidth, rectangleXMargin, rectangleYMargin, rectangleCornerRadius ) {
    var node = new Node();
    var rectangle = new Rectangle(
      0, 0, content.width + rectangleXMargin + rectangleXMargin, content.height + rectangleYMargin + rectangleYMargin, rectangleCornerRadius, rectangleCornerRadius,
      { stroke: stroke, fill: fill, rectangleLineWidth: rectangleLineWidth } );
    node.addChild( rectangle );
    node.addChild( content );
    content.centerX = rectangle.centerX;
    content.centerY = rectangle.centerY;
    return node;
  };

  /**
   * @param {Node} content
   * @param {Function} callback
   * @param options
   * @constructor
   */
  function RectangleButton( content, callback, options ) {

    options = _.extend( {
        // stroke
        rectangleStroke: 'black',
        rectangleStrokeDisabled: 'rgb(175,175,175)',
        // fill
        rectangleFillUp: new Color( 255, 200, 0 ),
        rectangleFillDisabled: 'white',
        // options that apply to all states
        rectangleLineWidth: 1,
        rectangleXMargin: 5,
        rectangleYMargin: 5,
        rectangleCornerRadius: 10
      },
      options );

    // generated colors
    assert && assert( options.rectangleFillUp instanceof Color ); //TODO can we relax this requirement?
    options.rectangleFillOver = options.rectangleFillOver || options.rectangleFillUp.brighterColor( 0.9 );
    options.rectangleFillDown = options.rectangleFillDown || options.rectangleFillUp.darkerColor( 0.9 );

    PushButton.call( this,
      createNode( content, options.rectangleStroke, options.rectangleFillUp, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStroke, options.rectangleFillOver, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStroke, options.rectangleFillDown, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      createNode( content, options.rectangleStrokeDisabled, options.rectangleFillDisabled, options.rectangleLineWidth, options.rectangleXMargin, options.rectangleYMargin, options.rectangleCornerRadius ),
      callback, options );
  }

  return inherit( PushButton, RectangleButton );
} );
<|endoftext|>


<commit_before>if (!window['YT']) {var YT = {loading: 0,loaded: 0};}if (!window['YTConfig']) {var YTConfig = {'host': 'https://www.youtube.com'};}if (!YT.loading) {YT.loading = 1;(function(){var l = [];YT.ready = function(f) {if (YT.loaded) {f();} else {l.push(f);}};window.onYTReady = function() {YT.loaded = 1;for (var i = 0; i < l.length; i++) {try {l[i]();} catch (e) {}}};YT.setConfig = function(c) {for (var k in c) {if (c.hasOwnProperty(k)) {YTConfig[k] = c[k];}}};var a = document.createElement('script');a.type = 'text/javascript';a.id = 'www-widgetapi-script';a.src = '/js/api/youtube/www-widgetapi.js';a.async = true;var b = document.getElementsByTagName('script')[0];b.parentNode.insertBefore(a, b);})();}<commit_msg>Delete api.js<commit_after><|endoftext|>


<commit_before>imports.gi.versions.WebKit2 = '4.0';

const Eknc = imports.gi.EosKnowledgeContent;
const Endless = imports.gi.Endless;
const EvinceDocument = imports.gi.EvinceDocument;
const Format = imports.format;
const Gdk = imports.gi.Gdk;
const Gettext = imports.gettext;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const System = imports.system;

const Actions = imports.app.actions;
const Config = imports.app.config;
const Dispatcher = imports.app.dispatcher;
const Downloader = imports.app.downloader;
const Knowledge = imports.app.knowledge;
const ModuleFactory = imports.app.moduleFactory;
const MoltresEngine = imports.app.moltresEngine;

let _ = Gettext.dgettext.bind(null, Config.GETTEXT_PACKAGE);

// Initialize libraries
EvinceDocument.init();

const KnowledgeSearchIface = '\
<node> \
  <interface name="com.endlessm.KnowledgeSearch"> \
    <method name="LoadItem"> \
      <arg type="s" name="EknID" direction="in" /> \
      <arg type="s" name="Query" direction="in" /> \
      <arg type="u" name="Timestamp" direction="in" /> \
    </method> \
    <method name="LoadQuery"> \
      <arg type="s" name="Query" direction="in" /> \
      <arg type="u" name="Timestamp" direction="in" /> \
    </method> \
  </interface> \
</node>';

const CREDITS_URI = 'resource:///app/credits.json';
const APP_JSON_URI = 'resource:///app/app.json';
const APP_YAML_URI = 'resource:///app/app.yaml';
const OVERRIDES_CSS_URI = 'resource:///app/overrides.css';
const OVERRIDES_SCSS_URI = 'resource:///app/overrides.scss';

const AUTOBAHN_COMMAND = 'autobahn -I ' + Config.YAML_PRESET_DIR + ' ';
const SCSS_COMMAND = 'scss --compass -E utf-8 --stop-on-error --sourcemap=none -I ' + Config.TOP_THEME_DIR + ' ';

/**
 * Class: Application
 */
const Application = new Knowledge.Class({
    Name: 'Application',
    Extends: Endless.Application,

    Properties: {
        /**
         * Property: resource-path
         * Path to applications gresource.
         */
        'resource-path': GObject.ParamSpec.string('resource-path',
            'Resource Path', 'Resource Path',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ''),
    },

    _init: function (props={}) {
        this.parent(props);
        this._controller = null;
        this._knowledge_search_impl = Gio.DBusExportedObject.wrapJSObject(KnowledgeSearchIface, this);
        this.image_attribution_file = Gio.File.new_for_uri(CREDITS_URI);

        Eknc.Engine.get_default().default_app_id = this.application_id;

        this.add_main_option('theme-name', 't'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.STRING,
                             'Use a stock theme with given name instead of any application theme overrides', null);
        this.add_main_option('default-theme', 'd'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Same as --theme-name=default', null);
        this.add_main_option('recompile-theme-overrides', 'o'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Recompile the applications overrides.css file from the source scss', null);
        this.add_main_option('recompile-app-json', 'j'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Recompile the app json file from the source yaml', null);
        this.add_main_option('recompile-all', 'r'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Same as --recompile-overrides --recompile-app-json', null);
        this.add_main_option('resource-path', 'R'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a different gresource to use with the application', null);
        this.add_main_option('theme-overrides-path', 'O'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a overrides scss or css file to theme the application', null);
        this.add_main_option('app-json-path', 'J'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a yaml or json file to use as a preset', null);
        this.add_main_option('dummy-content', 'C'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Whether to use dummy content from Moltres', null);
    },

    vfunc_handle_local_options: function (options) {
        function has_option (option) {
            return options.lookup_value(option, null) !== null;
        }
        function get_option_string (option) {
            return options.lookup_value(option, null).deep_unpack().toString();
        }

        if (has_option('resource-path'))
            this.resource_path = get_option_string('resource-path');
        let app_resource = Gio.Resource.load(this.resource_path);
        app_resource._register();

        if (has_option('default-theme'))
            this._theme = 'default';
        if (has_option('theme-name'))
            this._theme = get_option_string('theme-name');
        if (has_option('default-theme') && has_option('theme-name'))
            logError(new Error('Both --default-theme and --theme-name set; using theme ' + this._theme));

        this._recompile_overrides = has_option('recompile-all') || has_option('recompile-theme-overrides');
        this._recompile_app_json = has_option('recompile-all') || has_option('recompile-app-json');

        if (has_option('theme-overrides-path'))
            this._overrides_path = get_option_string('theme-overrides-path');
        if (has_option('app-json-path'))
            this._app_json_path = get_option_string('app-json-path');

        if (has_option('dummy-content'))
            MoltresEngine.override_engine();

        return -1;
    },

    vfunc_dbus_register: function (connection, path) {
        this.parent(connection, path);
        this._knowledge_search_impl.export(connection, path);
        return true;
    },

    vfunc_dbus_unregister: function (connection, path) {
        this.parent(connection, path);
        this._knowledge_search_impl.unexport_from_connection(connection);
    },

    _check_for_content: function () {
        let engine = Eknc.Engine.get_default();
        if (engine instanceof MoltresEngine.MoltresEngine)
            return;

        this._check_for_update();
        try {
            let shards = engine.get_domain().get_shards();
            Eknc.default_vfs_set_shards(shards);
        } catch (e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
            // No content. If updates are pending then show a nice dialog
            let subs = engine.get_domain().get_subscription_entries();
            if (subs.some(entry => !entry.disable_updates)) {
                let dialog = new Gtk.MessageDialog({
                    message_type: Gtk.MessageType.ERROR,
                    text: _("Oops! No content."),
                    secondary_text: _("If you have an internet connection, we are downloading more content right now. Try again in a few minutes after this message disappears."),
                    urgency_hint: true,
                });
                dialog.show();
                this._controller = {};  // i.e. don't load the real controller
            }
            // if no updates pending, then proceed and let Xapian queries fail
        }
    },

    _check_for_update: function () {
        if (GLib.getenv('EKN_DISABLE_UPDATES'))
            return;

        let downloader = Downloader.get_default();
        let subs = Eknc.get_subscriptions_json(this.application_id, null).subscriptions;
        subs.forEach(function (entry) {
            let id = entry.id;

            if (entry.disable_updates)
                return;

            // Synchronously apply any update we have.
            downloader.apply_update_sync(id);
            // Regardless of whether or not we applied an update,
            // let's see about fetching a new one...
            downloader.fetch_update(id, null, (downloader, result) => {
                try {
                    downloader.fetch_update_finish(result);
                } catch(e) {
                    logError(e, Format.vprintf("Could not update subscription ID: %s", [id]));
                }
            });
        });
    },

    vfunc_startup: function () {
        this.parent();
        this._check_for_content();
    },

    LoadItem: function (ekn_id, query, timestamp) {
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.DBUS_LOAD_ITEM_CALLED,
            ekn_id: ekn_id,
            query: query,
            timestamp: timestamp,
        });
    },

    LoadQuery: function (query, timestamp) {
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.DBUS_LOAD_QUERY_CALLED,
            query: query,
            timestamp: timestamp,
        });
    },

    vfunc_activate: function () {
        this.parent();
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.LAUNCHED_FROM_DESKTOP,
            timestamp: Gdk.CURRENT_TIME,
        });
    },

    _ensure_controller: function () {
        if (this._controller === null) {
            let factory = new ModuleFactory.ModuleFactory({
                app_json: JSON.parse(this._get_app_json()),
            });

            let controller_props = {
                application: this,
            };
            if (this._theme)
                controller_props.theme = this._theme;
            else
                controller_props.css = this._get_overrides_css();
            this._controller = factory.create_root_module(controller_props);
            this._controller.make_ready();
        }
    },

    _get_overrides_css: function () {
        let contents, theme_file;
        if (this._overrides_path)
            theme_file = Gio.File.new_for_path(this._overrides_path);
        else
            theme_file = Gio.File.new_for_uri(this._recompile_overrides ? OVERRIDES_SCSS_URI : OVERRIDES_CSS_URI);
        try {
            [, contents] = theme_file.load_contents(null);
            contents = contents.toString();
        } catch (error if !this._overrides_path && error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
            // No overrides, fallback to stock theme
            return '';
        }
        if (!theme_file.get_uri().endsWith('.scss'))
            return contents;
        // This uri might be a gresource, and the scss command cannot read
        // from a gresource, so save the file contents to a tmp file.
        [theme_file,] = Gio.File.new_tmp(null);
        theme_file.replace_contents(contents, null, false, 0, null);
        let [, stdout, stderr, status] = GLib.spawn_command_line_sync(SCSS_COMMAND + theme_file.get_path());
        if (status !== 0) {
            printerr(new Error(stderr.toString()));
            System.exit(1);
        }
        return stdout.toString();
    },

    _get_app_json: function () {
        let contents, app_json_file;
        if (this._app_json_path)
            app_json_file = Gio.File.new_for_path(this._app_json_path);
        else
            app_json_file = Gio.File.new_for_uri(this._recompile_app_json ? APP_YAML_URI : APP_JSON_URI);
        [, contents] = app_json_file.load_contents(null);
        contents = contents.toString();
        if (!app_json_file.get_uri().endsWith('.yaml'))
            return contents;
        // This uri might be a gresource, and the autobahn command cannot read
        // from a gresource, so save the file contents to a tmp file.
        [app_json_file,] = Gio.File.new_tmp(null);
        app_json_file.replace_contents(contents, null, false, 0, null);
        let [, stdout, stderr, status] = GLib.spawn_command_line_sync(AUTOBAHN_COMMAND + app_json_file.get_path());
        if (status !== 0) {
            printerr(new Error(stderr.toString()));
            System.exit(1);
        }
        return stdout.toString();
    },

    vfunc_shutdown: function () {
        Dispatcher.get_default().pause();
        this.parent();
    },
});
<commit_msg>application: css file supersedes theme<commit_after>imports.gi.versions.WebKit2 = '4.0';

const Eknc = imports.gi.EosKnowledgeContent;
const Endless = imports.gi.Endless;
const EvinceDocument = imports.gi.EvinceDocument;
const Format = imports.format;
const Gdk = imports.gi.Gdk;
const Gettext = imports.gettext;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const System = imports.system;

const Actions = imports.app.actions;
const Config = imports.app.config;
const Dispatcher = imports.app.dispatcher;
const Downloader = imports.app.downloader;
const Knowledge = imports.app.knowledge;
const ModuleFactory = imports.app.moduleFactory;
const MoltresEngine = imports.app.moltresEngine;

let _ = Gettext.dgettext.bind(null, Config.GETTEXT_PACKAGE);

// Initialize libraries
EvinceDocument.init();

const KnowledgeSearchIface = '\
<node> \
  <interface name="com.endlessm.KnowledgeSearch"> \
    <method name="LoadItem"> \
      <arg type="s" name="EknID" direction="in" /> \
      <arg type="s" name="Query" direction="in" /> \
      <arg type="u" name="Timestamp" direction="in" /> \
    </method> \
    <method name="LoadQuery"> \
      <arg type="s" name="Query" direction="in" /> \
      <arg type="u" name="Timestamp" direction="in" /> \
    </method> \
  </interface> \
</node>';

const CREDITS_URI = 'resource:///app/credits.json';
const APP_JSON_URI = 'resource:///app/app.json';
const APP_YAML_URI = 'resource:///app/app.yaml';
const OVERRIDES_CSS_URI = 'resource:///app/overrides.css';
const OVERRIDES_SCSS_URI = 'resource:///app/overrides.scss';

const AUTOBAHN_COMMAND = 'autobahn -I ' + Config.YAML_PRESET_DIR + ' ';
const SCSS_COMMAND = 'scss --compass -E utf-8 --stop-on-error --sourcemap=none -I ' + Config.TOP_THEME_DIR + ' ';

/**
 * Class: Application
 */
const Application = new Knowledge.Class({
    Name: 'Application',
    Extends: Endless.Application,

    Properties: {
        /**
         * Property: resource-path
         * Path to applications gresource.
         */
        'resource-path': GObject.ParamSpec.string('resource-path',
            'Resource Path', 'Resource Path',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ''),
    },

    _init: function (props={}) {
        this.parent(props);
        this._controller = null;
        this._knowledge_search_impl = Gio.DBusExportedObject.wrapJSObject(KnowledgeSearchIface, this);
        this.image_attribution_file = Gio.File.new_for_uri(CREDITS_URI);

        Eknc.Engine.get_default().default_app_id = this.application_id;

        this.add_main_option('theme-name', 't'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.STRING,
                             'Use a stock theme with given name instead of any application theme overrides', null);
        this.add_main_option('default-theme', 'd'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Same as --theme-name=default', null);
        this.add_main_option('recompile-theme-overrides', 'o'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Recompile the applications overrides.css file from the source scss', null);
        this.add_main_option('recompile-app-json', 'j'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Recompile the app json file from the source yaml', null);
        this.add_main_option('recompile-all', 'r'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Same as --recompile-overrides --recompile-app-json', null);
        this.add_main_option('resource-path', 'R'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a different gresource to use with the application', null);
        this.add_main_option('theme-overrides-path', 'O'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a overrides scss or css file to theme the application', null);
        this.add_main_option('app-json-path', 'J'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
                             'Path to a yaml or json file to use as a preset', null);
        this.add_main_option('dummy-content', 'C'.charCodeAt(), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
                             'Whether to use dummy content from Moltres', null);
    },

    vfunc_handle_local_options: function (options) {
        function has_option (option) {
            return options.lookup_value(option, null) !== null;
        }
        function get_option_string (option) {
            return options.lookup_value(option, null).deep_unpack().toString();
        }

        if (has_option('resource-path'))
            this.resource_path = get_option_string('resource-path');
        let app_resource = Gio.Resource.load(this.resource_path);
        app_resource._register();

        if (has_option('default-theme'))
            this._theme = 'default';
        if (has_option('theme-name'))
            this._theme = get_option_string('theme-name');
        if (has_option('default-theme') && has_option('theme-name'))
            logError(new Error('Both --default-theme and --theme-name set; using theme ' + this._theme));

        this._recompile_overrides = has_option('recompile-all') || has_option('recompile-theme-overrides');
        this._recompile_app_json = has_option('recompile-all') || has_option('recompile-app-json');

        if (has_option('theme-overrides-path'))
            this._overrides_path = get_option_string('theme-overrides-path');
        if (has_option('app-json-path'))
            this._app_json_path = get_option_string('app-json-path');

        if (has_option('dummy-content'))
            MoltresEngine.override_engine();

        return -1;
    },

    vfunc_dbus_register: function (connection, path) {
        this.parent(connection, path);
        this._knowledge_search_impl.export(connection, path);
        return true;
    },

    vfunc_dbus_unregister: function (connection, path) {
        this.parent(connection, path);
        this._knowledge_search_impl.unexport_from_connection(connection);
    },

    _check_for_content: function () {
        let engine = Eknc.Engine.get_default();
        if (engine instanceof MoltresEngine.MoltresEngine)
            return;

        this._check_for_update();
        try {
            let shards = engine.get_domain().get_shards();
            Eknc.default_vfs_set_shards(shards);
        } catch (e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
            // No content. If updates are pending then show a nice dialog
            let subs = engine.get_domain().get_subscription_entries();
            if (subs.some(entry => !entry.disable_updates)) {
                let dialog = new Gtk.MessageDialog({
                    message_type: Gtk.MessageType.ERROR,
                    text: _("Oops! No content."),
                    secondary_text: _("If you have an internet connection, we are downloading more content right now. Try again in a few minutes after this message disappears."),
                    urgency_hint: true,
                });
                dialog.show();
                this._controller = {};  // i.e. don't load the real controller
            }
            // if no updates pending, then proceed and let Xapian queries fail
        }
    },

    _check_for_update: function () {
        if (GLib.getenv('EKN_DISABLE_UPDATES'))
            return;

        let downloader = Downloader.get_default();
        let subs = Eknc.get_subscriptions_json(this.application_id, null).subscriptions;
        subs.forEach(function (entry) {
            let id = entry.id;

            if (entry.disable_updates)
                return;

            // Synchronously apply any update we have.
            downloader.apply_update_sync(id);
            // Regardless of whether or not we applied an update,
            // let's see about fetching a new one...
            downloader.fetch_update(id, null, (downloader, result) => {
                try {
                    downloader.fetch_update_finish(result);
                } catch(e) {
                    logError(e, Format.vprintf("Could not update subscription ID: %s", [id]));
                }
            });
        });
    },

    vfunc_startup: function () {
        this.parent();
        this._check_for_content();
    },

    LoadItem: function (ekn_id, query, timestamp) {
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.DBUS_LOAD_ITEM_CALLED,
            ekn_id: ekn_id,
            query: query,
            timestamp: timestamp,
        });
    },

    LoadQuery: function (query, timestamp) {
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.DBUS_LOAD_QUERY_CALLED,
            query: query,
            timestamp: timestamp,
        });
    },

    vfunc_activate: function () {
        this.parent();
        this._ensure_controller();
        Dispatcher.get_default().dispatch({
            action_type: Actions.LAUNCHED_FROM_DESKTOP,
            timestamp: Gdk.CURRENT_TIME,
        });
    },

    _ensure_controller: function () {
        if (this._controller === null) {
            let factory = new ModuleFactory.ModuleFactory({
                app_json: JSON.parse(this._get_app_json()),
            });

            let controller_props = {
                application: this,
            };
            if (this._theme)
                controller_props.theme = this._theme;
            let css = this._get_overrides_css();
            if (css)
                controller_props.css = css;
            this._controller = factory.create_root_module(controller_props);
            this._controller.make_ready();
        }
    },

    _get_overrides_css: function () {
        let contents, theme_file;
        if (this._overrides_path)
            theme_file = Gio.File.new_for_path(this._overrides_path);
        else
            theme_file = Gio.File.new_for_uri(this._recompile_overrides ? OVERRIDES_SCSS_URI : OVERRIDES_CSS_URI);
        try {
            [, contents] = theme_file.load_contents(null);
            contents = contents.toString();
        } catch (error if !this._overrides_path && error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
            // No overrides, fallback to stock theme
            return '';
        }
        if (!theme_file.get_uri().endsWith('.scss'))
            return contents;
        // This uri might be a gresource, and the scss command cannot read
        // from a gresource, so save the file contents to a tmp file.
        [theme_file,] = Gio.File.new_tmp(null);
        theme_file.replace_contents(contents, null, false, 0, null);
        let [, stdout, stderr, status] = GLib.spawn_command_line_sync(SCSS_COMMAND + theme_file.get_path());
        if (status !== 0) {
            printerr(new Error(stderr.toString()));
            System.exit(1);
        }
        return stdout.toString();
    },

    _get_app_json: function () {
        let contents, app_json_file;
        if (this._app_json_path)
            app_json_file = Gio.File.new_for_path(this._app_json_path);
        else
            app_json_file = Gio.File.new_for_uri(this._recompile_app_json ? APP_YAML_URI : APP_JSON_URI);
        [, contents] = app_json_file.load_contents(null);
        contents = contents.toString();
        if (!app_json_file.get_uri().endsWith('.yaml'))
            return contents;
        // This uri might be a gresource, and the autobahn command cannot read
        // from a gresource, so save the file contents to a tmp file.
        [app_json_file,] = Gio.File.new_tmp(null);
        app_json_file.replace_contents(contents, null, false, 0, null);
        let [, stdout, stderr, status] = GLib.spawn_command_line_sync(AUTOBAHN_COMMAND + app_json_file.get_path());
        if (status !== 0) {
            printerr(new Error(stderr.toString()));
            System.exit(1);
        }
        return stdout.toString();
    },

    vfunc_shutdown: function () {
        Dispatcher.get_default().pause();
        this.parent();
    },
});
<|endoftext|>


<commit_before>/*
 * articleSwitcher.js
 *
 * This This toggles articles
 *
 * It should:
 * - Close an article if it's title isn't visible
 * - Hide all closed articles of canvas
 * - Show the title of the next article
 * - open the next article when the user scrolls to the bottom of the page or clicks it's title
 * - Put an animated line next to the article that is in focus
 *
 * articleSwitcher = {}
 * - articleIndex = [Article, Article, ...]
 * - config = {}
 *   - article = {}
 *     - class
 *     - openClass
 *     - focusClass
 *     - offsetTop
 *     - titleClass
 *   - focus
 *     - class
 *     - animationTime
 *   - container
 *     - tolarance
 *
 * Article = {}
 * - _node = DOMNode
 * - state = {focus: bool, open: bool}
 * - size = (height: integer, width: integer)
 * - getTitlePosition() returns (x: integer, y: integer)
 * - close()
 * - open()
 * - focus()
 * - unFocus()
 * - updatePos()
 * - getHeight()
 * - appendChild()
 *
 * Focus = {}
 *   This should be the line which jumps from title to title
 * Should have
 * - flyingThingi = DOMNode
 * - _position = integer
 * - goto() uses scrollTo, sets position
 *
 * Control
 *   When articleSwitcher is initialized Control will create all Articles and create the index for Focus
 * Should have:
 * - _container
 * - switchPos = integer the position in pixels where the focus should switch
 * - current
 * - init()
 *   - _createIndex
 *   - _setScrollListener()
 *     - _updateSwitchPos()
 *
 * getOffset(node)
 */

articleSwitcher = {
  articleIndex: [],
  config: {
    article: {
      class: 'article',
      openClass: 'article--open',
      focusClass: 'article--focus',
      offsetTop: '20',
      titleClass: 'article__title',
    },
    focus: {
      class: 'focusLine',
      animationTime: 200,
      triggerTime: 500,
    },
    container: {
      class: 'main',
      correction: 60,
    }
  }
};

articleSwitcher.createArticle = (function (node) {
  var _node = node,
  _state = {
    focus: false,
    open: false
  },
  _titlePosition = getOffset(_node);
  _size = {
    height: _node.height,
    width: _node.width
  };

  return {
    open: function () {
      if(!_state.open) {
        _node.className += ' ' + articleSwitcher.config.article.openClass;
        _node.style.height = (60 + _node.children[1].clientHeight) + 'px';
        _state.open = true;
      }
    },

    close: function () {
      if(_state.open) {
        _node.className = _node.className.replace(' ' + articleSwitcher.config.article.openClass, '');
        _node.style.height = '60px'
        _state.open = false;
      }
    },

    focus: function () {
      _node.className += ' ' + articleSwitcher.config.article.focusClass;
    },

    unFocus: function () {
      _node.className = _node.className.replace('' + articleSwitcher.config.article.focusClass, '');
    },

    getHeight: function () {
      return _size.height;
    },

    appendElement: function (element) {
      _node.appendChild(element);
    },

    getTitlePosition: function () {
      return _titlePosition;
    },

    updatePos: function () {
     _titlePosition = getOffset(_node);
    },
  }
});

articleSwitcher.createFocus = (function () {
  var _position = 0,
  _flyingThingi = document.createElement('div');

  _flyingThingi.className = articleSwitcher.config.focus.class;
  document.body.appendChild(_flyingThingi);
  return {
    goto: function (index) {
      _flyingThingi.remove();
      articleSwitcher.articleIndex[index].appendElement(_flyingThingi);
      var moveBy = articleSwitcher.articleIndex[_position].getHeight() + articleSwitcher.config.article.offsetTop;
      _flyingThingi.style.transform = 'translateY: '+ moveBy + 'px';
      setTimeout(function () {
        _flyingThingi.remove();
        articleSwitcher.articleIndex[index].appendElement(_flyingThingi);
      }, articleSwitcher.config.focus.animationTime);
    }
  }
});

// ToDo
articleSwitcher.createControl = (function () {
  var _container = document.querySelector(articleSwitcher.config.container.class),
  current = 0,
  _switchPos = 0,
  _blocked = true,
  _blockCheck = false;

  function _createIndex () {
    var allArticleNodes = document.querySelectorAll(articleSwitcher.config.config.article.class);
    for(var key in allArticleNodes) {
      if(allArticleNodes.hasOwnProperty(key)) {
        var thisNode = allArticleNodes[key];
        articleSwitcher.articleIndex.push(createArticle(thisNode));
      }
    }
    _updateSwitchPos();
  }

  function _setScrollListener () {
    window.addEventListener('scroll', function () {
      if(!_blockCheck && _blocked) {
        _blocked = false;
      }
    });
    setInterval(function () {
      if(!_blocked) {
        _checkScrollPos();
        console.log('check');
      }
    }, 200);
    window.addEventListener('resize', function () {
      _updateSwitchPos();
    });
  }

  function _checkScrollPos () {
    _shouldSwitch('next', function () {
      setTimeout(function () {
        _shouldSwitch('next', function () {
          _blockCheck = true;
          articleSwitcher.articleIndex[current + 1].open();
          articleSwitcher.focus.goto(current + 1);
          articleSwitcher.articleIndex[current].close();
          current = current + 1;
          _updateSwitchPos();
          setTimeout(function () {
            articleSwitcher.articleIndex[current].updatePos();
            _animScrollTo(articleSwitcher.articleIndex[current].getTitlePosition().y, function () {
              _blockCheck = false;
            });
          }, (articleSwitcher.config.focus.animationTime * 2));
        });
      },articleSwitcher.config.focus.triggerTime);
    });
    _shouldSwitch('prev', function () {
      setTimeout(function () {
        _shouldSwitch('prev', function () {
          _blockCheck = true;
          articleSwitcher.articleIndex[current - 1].open();
          articleSwitcher.focus.goto(current - 1);
          articleSwitcher.articleIndex[current].close();
          current = current - 1;
          _updateSwitchPos();
          articleSwitcher.articleIndex[current].updatePos();
          setTimeout(function () {
            articleSwitcher.articleIndex[current].updatePos();
            _animScrollTo(articleSwitcher.articleIndex[current].getTitlePosition().y, function () {
              _blockCheck = false;
            });
          }, (articleSwitcher.config.focus.animationTime * 2));
        });
      },articleSwitcher.config.focus.triggerTime);
    });
  }

  function _shouldSwitch (condition, callback) {
    setTimeout(function () {
      if(condition === 'prev' && current !== 0 && window.pageYOffset < articleSwitcher.articleIndex[current - 1].getTitlePosition().y) {
        _blocked = true;
        callback();
        _updateSwitchPos();
      } else if (condition === 'next' && current !== articleSwitcher.articleIndex.length - 1 && window.pageYOffset >= (_switchPos + articleSwitcher.config.container.correction)) {
        _blocked = true;
        callback();
        _updateSwitchPos();
      }
    }, 1);
  }

  function _animScrollTo (position, callback) {
    var currentState = pageYOffset,
    dir = pageYOffset > position ? 'up' : 'down'
    scrollInterval = setInterval( function () {
      if(currentState !== pageYOffset) {
        clearInterval(scrollInterval);
        callback();
      }
      if((dir == 'down' && position < pageYOffset) || (dir == 'up' && position > pageYOffset)) {
        clearInterval(scrollInterval);
        callback();
      } else {
        scrollTo(0, pageYOffset > position ? pageYOffset - 60 : pageYOffset + 60);
        currentState = pageYOffset;
      }
    }, 10);
  }

  function _checkIfAnyShouldOpen (index) {
    var index = index || 0;
    articleSwitcher.articleIndex[index].updatePos();
    if(innerHeight >= articleSwitcher.articleIndex[index].getTitlePosition().y) {
      articleSwitcher.articleIndex[index].open();
      setTimeout(function () {
        _checkIfAnyShouldOpen(index + 1);
      }, articleSwitcher.config.focus.animationTime);
    }
  }

  function _updateSwitchPos () {
    if(articleSwitcher.articleIndex[current + 1]) {
      setTimeout(function () {
        articleSwitcher.articleIndex[current + 1].updatePos();
        _switchPos = articleSwitcher.articleIndex[current + 1].getTitlePosition().y - innerHeight;
      },1);
    } else {
      _switchPos = 0;
    }
  }

  function _createIndex (callback) {
    setTimeout( function () {
      var allArticles = document.querySelectorAll('.' + articleSwitcher.config.article.class);
      for(prop in allArticles) {
        if(allArticles.hasOwnProperty(prop)) {
          articleSwitcher.articleIndex.push(articleSwitcher.createArticle(allArticles[prop]));
        }
      }
      callback();
    }, 1);
  }

  return {
    init: function () {
      _createIndex(function () {
        _setScrollListener ();
        _updateSwitchPos();
        _checkIfAnyShouldOpen();
      });
    }
  }
});

// Initializing everything
document.addEventListener('readystatechange', function () {
  if(document.readyState == 'interactive') {
    articleSwitcher.control = articleSwitcher.createControl();
    articleSwitcher.focus = articleSwitcher.createFocus();
    articleSwitcher.control.init();
  }
});

function getOffset (thisNode) {
  var pos = {
    x: thisNode.offsetLeft,
    y: thisNode.offsetTop
  },
  child = thisNode;
  while(child = child.offsetParent) {
    pos.x += child.offsetLeft;
    pos.y += child.offsetTop;
  }
  return pos;
};<commit_msg>Fixing some issues with the timeout<commit_after>/*
 * articleSwitcher.js
 *
 * This This toggles articles
 *
 * It should:
 * - Close an article if it's title isn't visible
 * - Hide all closed articles of canvas
 * - Show the title of the next article
 * - open the next article when the user scrolls to the bottom of the page or clicks it's title
 * - Put an animated line next to the article that is in focus
 *
 * articleSwitcher = {}
 * - articleIndex = [Article, Article, ...]
 * - config = {}
 *   - article = {}
 *     - class
 *     - openClass
 *     - focusClass
 *     - offsetTop
 *     - titleClass
 *   - focus
 *     - class
 *     - animationTime
 *   - container
 *     - tolarance
 *
 * Article = {}
 * - _node = DOMNode
 * - state = {focus: bool, open: bool}
 * - size = (height: integer, width: integer)
 * - getTitlePosition() returns (x: integer, y: integer)
 * - close()
 * - open()
 * - focus()
 * - unFocus()
 * - updatePos()
 * - getHeight()
 * - appendChild()
 *
 * Focus = {}
 *   This should be the line which jumps from title to title
 * Should have
 * - flyingThingi = DOMNode
 * - _position = integer
 * - goto() uses scrollTo, sets position
 *
 * Control
 *   When articleSwitcher is initialized Control will create all Articles and create the index for Focus
 * Should have:
 * - _container
 * - switchPos = integer the position in pixels where the focus should switch
 * - current
 * - init()
 *   - _createIndex
 *   - _setScrollListener()
 *     - _updateSwitchPos()
 *
 * getOffset(node)
 */

articleSwitcher = {
  articleIndex: [],
  config: {
    article: {
      class: 'article',
      openClass: 'article--open',
      focusClass: 'article--focus',
      offsetTop: '20',
      titleClass: 'article__title',
    },
    focus: {
      class: 'focusLine',
      animationTime: 200,
      triggerTime: 500,
    },
    container: {
      class: 'main',
      correction: 60,
    }
  }
};

articleSwitcher.createArticle = (function (node) {
  var _node = node,
  _state = {
    focus: false,
    open: false
  },
  _titlePosition = getOffset(_node);
  _size = {
    height: _node.height,
    width: _node.width
  };

  return {
    open: function () {
      if(!_state.open) {
        _node.className += ' ' + articleSwitcher.config.article.openClass;
        _node.style.height = (60 + _node.children[1].clientHeight) + 'px';
        _state.open = true;
      }
    },

    close: function () {
      if(_state.open) {
        _node.className = _node.className.replace(' ' + articleSwitcher.config.article.openClass, '');
        _node.style.height = '60px'
        _state.open = false;
      }
    },

    focus: function () {
      _node.className += ' ' + articleSwitcher.config.article.focusClass;
    },

    unFocus: function () {
      _node.className = _node.className.replace('' + articleSwitcher.config.article.focusClass, '');
    },

    getHeight: function () {
      return _size.height;
    },

    appendElement: function (element) {
      _node.appendChild(element);
    },

    getTitlePosition: function () {
      return _titlePosition;
    },

    updatePos: function () {
     _titlePosition = getOffset(_node);
    },
  }
});

articleSwitcher.createFocus = (function () {
  var _position = 0,
  _flyingThingi = document.createElement('div');

  _flyingThingi.className = articleSwitcher.config.focus.class;
  document.body.appendChild(_flyingThingi);
  return {
    goto: function (index) {
      _flyingThingi.remove();
      articleSwitcher.articleIndex[index].appendElement(_flyingThingi);
      var moveBy = articleSwitcher.articleIndex[_position].getHeight() + articleSwitcher.config.article.offsetTop;
      _flyingThingi.style.transform = 'translateY: '+ moveBy + 'px';
      setTimeout(function () {
        _flyingThingi.remove();
        articleSwitcher.articleIndex[index].appendElement(_flyingThingi);
      }, articleSwitcher.config.focus.animationTime);
    }
  }
});

// ToDo
articleSwitcher.createControl = (function () {
  var _container = document.querySelector(articleSwitcher.config.container.class),
  current = 0,
  _switchPos = 0,
  _blocked = true,
  _blockCheck = false;

  function _createIndex () {
    var allArticleNodes = document.querySelectorAll(articleSwitcher.config.config.article.class);
    for(var key in allArticleNodes) {
      if(allArticleNodes.hasOwnProperty(key)) {
        var thisNode = allArticleNodes[key];
        articleSwitcher.articleIndex.push(createArticle(thisNode));
      }
    }
    _updateSwitchPos();
  }

  function _setScrollListener () {
    window.addEventListener('scroll', function () {
      if(!_blockCheck && _blocked) {
        _blocked = false;
      }
    });
    setInterval(function () {
      if(!_blocked) {
        _checkScrollPos();
      }
    }, 200);
    window.addEventListener('resize', function () {
      _updateSwitchPos();
    });
  }

  function _checkScrollPos () {
    _shouldSwitch('next', function () {
      setTimeout(function () {
        _shouldSwitch('next', function () {
          _blockCheck = true;
          articleSwitcher.articleIndex[current + 1].open();
          articleSwitcher.focus.goto(current + 1);
          articleSwitcher.articleIndex[current].close();
          current = current + 1;
          _updateSwitchPos();
          setTimeout(function () {
            articleSwitcher.articleIndex[current].updatePos();
            _animScrollTo(articleSwitcher.articleIndex[current].getTitlePosition().y, function () {
                _blockCheck = false;
            });
            setTimeout(function () {
            }, 1000);
          }, (articleSwitcher.config.focus.animationTime));
        });
      },articleSwitcher.config.focus.triggerTime);
    });
    _shouldSwitch('prev', function () {
      setTimeout(function () {
        _shouldSwitch('prev', function () {
          _blockCheck = true;
          articleSwitcher.articleIndex[current - 1].open();
          articleSwitcher.focus.goto(current - 1);
          articleSwitcher.articleIndex[current].close();
          current = current - 1;
          _updateSwitchPos();
          articleSwitcher.articleIndex[current].updatePos();
          setTimeout(function () {
            articleSwitcher.articleIndex[current].updatePos();
            _animScrollTo(articleSwitcher.articleIndex[current].getTitlePosition().y, function () {
                _blockCheck = false;
            });
            setTimeout(function () {
            }, 1000);
          }, (articleSwitcher.config.focus.animationTime));
        });
      },articleSwitcher.config.focus.triggerTime);
    });
  }

  function _shouldSwitch (condition, callback) {
    setTimeout(function () {
      if(condition === 'prev' && current !== 0 && window.pageYOffset < articleSwitcher.articleIndex[current - 1].getTitlePosition().y) {
        _blocked = true;
        callback();
        _updateSwitchPos();
      } else if (condition === 'next' && current !== articleSwitcher.articleIndex.length - 1 && window.pageYOffset >= (_switchPos + articleSwitcher.config.container.correction)) {
        _blocked = true;
        callback();
        _updateSwitchPos();
      }
    }, 1);
  }

  function _animScrollTo (position, callback) {
    var currentState = pageYOffset,
    dir = pageYOffset > position ? 'up' : 'down',
    step = pageYOffset > position ? (pageYOffset - position) / 20 : (position - pageYOffset) / 20,
    scrollInterval = setInterval( function () {
      if(currentState !== pageYOffset) {
        clearInterval(scrollInterval);
        callback();
        return false;
      }
      if((dir == 'down' && position < pageYOffset) || (dir == 'up' && position > pageYOffset)) {
        clearInterval(scrollInterval);
        callback();
        return false;
      } else {
        scrollTo(0, pageYOffset > position ? pageYOffset - step : pageYOffset + step);
        currentState = pageYOffset;
      }
    }, 10);
  }

  function _checkIfAnyShouldOpen (index) {
    var index = index || 0;
    articleSwitcher.articleIndex[index].updatePos();
    if(innerHeight >= articleSwitcher.articleIndex[index].getTitlePosition().y) {
      articleSwitcher.articleIndex[index].open();
      setTimeout(function () {
        _checkIfAnyShouldOpen(index + 1);
      }, articleSwitcher.config.focus.animationTime);
    }
  }

  function _updateSwitchPos () {
    if(articleSwitcher.articleIndex[current + 1]) {
      setTimeout(function () {
        articleSwitcher.articleIndex[current + 1].updatePos();
        _switchPos = articleSwitcher.articleIndex[current + 1].getTitlePosition().y - innerHeight;
      },1);
    } else {
      _switchPos = 0;
    }
  }

  function _createIndex (callback) {
    setTimeout( function () {
      var allArticles = document.querySelectorAll('.' + articleSwitcher.config.article.class);
      for(prop in allArticles) {
        if(allArticles.hasOwnProperty(prop)) {
          articleSwitcher.articleIndex.push(articleSwitcher.createArticle(allArticles[prop]));
        }
      }
      callback();
    }, 1);
  }

  return {
    init: function () {
      _createIndex(function () {
        _setScrollListener ();
        _updateSwitchPos();
        _checkIfAnyShouldOpen();
      });
    }
  }
});

// Initializing everything
document.addEventListener('readystatechange', function () {
  if(document.readyState == 'interactive') {
    articleSwitcher.control = articleSwitcher.createControl();
    articleSwitcher.focus = articleSwitcher.createFocus();
    articleSwitcher.control.init();
  }
});

function getOffset (thisNode) {
  var pos = {
    x: thisNode.offsetLeft,
    y: thisNode.offsetTop
  },
  child = thisNode;
  while(child = child.offsetParent) {
    pos.x += child.offsetLeft;
    pos.y += child.offsetTop;
  }
  return pos;
};<|endoftext|>


<commit_before>//BYGGER VIDERE PÅ services.js SOM ER LAGET I FRONT-END
//BURDE KANSKJE LEGGES INN I services.js? ELLERS MÅ MODULEN ('starter.services')
//FÅ NYTT NAVN :)
angular.module('backend.services', ['ngSanitize'])
.factory("Story", function ($sce) {

	 /**
	 * Constructor, with class name
	 */
	 function Story(storyData) {

	 	this.storyId = storyData.storyId;
	 	this.title = storyData.title;

		//Returns an array, use author[0]
		this.author = storyData.creatorList;
		this.imageList = storyData.imageList;
		this.introduction = storyData.introduction;

		//EXAMPLE VIDEO-URL: "http://mm01.dimu.no/multimedia/012FwwCj.mp4?mmid=012FwwCj"
		this.videoList = storyData.videoList;

		// NEEDS

 where
 {{story.text}}
 is in story.html
		// to make html tags from story work
		this.text = $sce.trustAsHtml(storyData.theStory);

		//EXAMPLE AUDIO-URL: "http://mm01.dimu.no/multimedia/012QsXh9.mp3?mmid=012QsXh9"
		this.audioList = storyData.audioList;
		this.rights = storyData.rights;
		this.municipality = storyData.municipality;
		this.county = storyData.county;
		this.institution = storyData.institution;

		this.subcategoryList = storyData.subCategoryNames;

		//Tror ikke denne trenger å hentes her??
		this.subcategoryIDs = storyData.subCategoryList;
		this.subjectList = storyData.subjectList;
		this.url = $sce.trustAsUrl("http://www.digitaltfortalt.no/things/thing/H-DF/"+this.storyId);

		this.updateMedia();
	}

	//TRENGER VI Å HENTE UT BILDELISTE I DET HELE TATT NÅR BILDEVISNING BRUKER INDEKS OG IKKE ID?
	//BARE HENTE ANTALL BILDER?
	/** Adds the imageurl to imageList */
	Story.prototype.updateMedia = function(){
		//BLIR LITT RART, HELE BILDET VISES IKKE PÅ STORYVIEW. BØR EKSPERIMENTERES MED HEIGHTxWIDHT?
		if(this.imageList != null)
			for(var i = 0; i < this.imageList.length; i++){
				this.imageList[i] = $sce.trustAsResourceUrl("http://media31.dimu.no/media/image/H-DF/"+this.storyId+"/"+i+"?byIndex=true&height=400&width=400");
			}
		}

		/** Public method, assigned to prototype */
		Story.prototype.getStoryId = function () {
			return this.storyId;
		};

		/** Return the constructor function */
		return Story;
	})
.factory('User', function (){
	function User(userData){
		this.userId = userData.userId;
		this.email = userData.email;
		this.age_group = userData.age_group;
		this.gender = userData.gender;
		this.use_of_location = userData.use_of_location;
		this.category_preference = userData.category_preference;
	};
	User.prototype.getUser = function() {
		var userData;
		userData.userId = this.userId;
		userData.email = this.email;
		userData.age_group = this.age_group;
		userData.gender = this.gender;
		userData.use_of_location = this.use_of_location;
		userData.category_preference = this.category_preference;
		return userData;
	};
	return User;
})

/**Handles communication with backend*/
.factory("Requests", function ($http) {
	var req = {
		method: 'POST',
		url: '../../requests/controller.php',
		headers: {'Content-Type': 'application/json'} // 'Content-Type': application/json???
	}

	/* DETTE MÅ BRUKES I Controllere:
 	Requests."metode"().then(function(response){
    		$scope."detsomskalbrukes" = new Story(response.data); eller bare response.data
    	}); TUNGVINT MÅTE?? :/*/

	return {
		/**Retrieves single story from digitalt fortalt*/
		getStory: function(id){
			req.data = {
				type: "getStory",
				storyId: id };
			//Burde sikkert bruke .success osv, men det så mye finere ut uten :)
			return $http(req);
		},

		//PRØVER Å HENTE DE 20 FØRSTE HISTORIENE FRA DATABASEN NÅ OG LEGGE TIL I LISTE
		//BRUKER GETALLSTORIES METODE I DBHELPER. SKAL SLETTES ETTERPÅ

		/**Retrieves multiple stories from the database, now returns 500 error when
		* story doesn't have pictures*/
		getMultipleStories: function(idArray) {
			req.data = { type: "getStories" };
			return $http(req);
		},

		/** Adds user rating to a story */
		addRating: function(storyId, userId, rating){
			req.data = {
				type: "rating",
				storyId: storyId,
				userId: userId,
				rating: rating};
			$http(req);
		},

		addTag: function(tagName, userId, storyId){
			req.data = {
				type: "addTag",
				userId: userId,
				storyId: storyId,
				tagName: tagName
			};
			$http(req);
		}
		addUpdateUser: function (userData){
			req.data = {type: "addUpdateUser",
				userData};
			$http(req);
		},
		getUser: function(mail){
			req.data = {type: "getUser",
				'email': mail};
			return $http(req);
		}
	}
});<commit_msg>nothingspecial<commit_after>angular.module('backend.services', ['ngSanitize'])
.factory("Story", function ($sce) {

	 /**
	 * Constructor, with class name
	 */
	 function Story(storyData) {

	 	this.storyId = storyData.storyId;
	 	this.title = storyData.title;

		//Returns an array, use author[0]
		this.author = storyData.creatorList;
		this.imageList = storyData.imageList;
		this.introduction = storyData.introduction;

		//EXAMPLE VIDEO-URL: "http://mm01.dimu.no/multimedia/012FwwCj.mp4?mmid=012FwwCj"
		this.videoList = storyData.videoList;

		// NEEDS

 where
 {{story.text}}
 is in story.html
		// to make html tags from story work
		this.text = $sce.trustAsHtml(storyData.theStory);

		//EXAMPLE AUDIO-URL: "http://mm01.dimu.no/multimedia/012QsXh9.mp3?mmid=012QsXh9"
		this.audioList = storyData.audioList;
		this.rights = storyData.rights;
		this.municipality = storyData.municipality;
		this.county = storyData.county;
		this.institution = storyData.institution;

		this.subcategoryList = storyData.subCategoryNames;

		//Tror ikke denne trenger å hentes her??
		this.subcategoryIDs = storyData.subCategoryList;
		this.subjectList = storyData.subjectList;
		this.url = $sce.trustAsUrl("http://www.digitaltfortalt.no/things/thing/H-DF/"+this.storyId);

		this.updateMedia();
	}

	//TRENGER VI Å HENTE UT BILDELISTE I DET HELE TATT NÅR BILDEVISNING BRUKER INDEKS OG IKKE ID?
	//BARE HENTE ANTALL BILDER?
	/** Adds the imageurl to imageList */
	Story.prototype.updateMedia = function(){
		//BLIR LITT RART, HELE BILDET VISES IKKE PÅ STORYVIEW. BØR EKSPERIMENTERES MED HEIGHTxWIDHT?
		if(this.imageList != null)
			for(var i = 0; i < this.imageList.length; i++){
				this.imageList[i] = $sce.trustAsResourceUrl("http://media31.dimu.no/media/image/H-DF/"+this.storyId+"/"+i+"?byIndex=true&height=400&width=400");
			}
		}

		/** Public method, assigned to prototype */
		Story.prototype.getStoryId = function () {
			return this.storyId;
		};

		/** Return the constructor function */
		return Story;
	})
.factory('User', function (){
	function User(userData){
		this.userId = userData.userId;
		this.email = userData.email;
		this.age_group = userData.age_group;
		this.gender = userData.gender;
		this.use_of_location = userData.use_of_location;
		this.category_preference = userData.category_preference;
	};
	User.prototype.getUser = function() {
		var userData;
		userData.userId = this.userId;
		userData.email = this.email;
		userData.age_group = this.age_group;
		userData.gender = this.gender;
		userData.use_of_location = this.use_of_location;
		userData.category_preference = this.category_preference;
		return userData;
	};
	return User;
})

/**Handles communication with backend*/
.factory("Requests", function ($http) {
	var req = {
		method: 'POST',
		url: '../../requests/controller.php',
		headers: {'Content-Type': 'application/json'} // 'Content-Type': application/json???
	}

	/* DETTE MÅ BRUKES I Controllere:
 	Requests."metode"().then(function(response){
    		$scope."detsomskalbrukes" = new Story(response.data); eller bare response.data
    	}); TUNGVINT MÅTE?? :/*/

	return {
		/**Retrieves single story from digitalt fortalt*/
		getStory: function(id){
			req.data = {
				type: "getStory",
				storyId: id };
			//Burde sikkert bruke .success osv, men det så mye finere ut uten :)
			return $http(req);
		},

		//PRØVER Å HENTE DE 20 FØRSTE HISTORIENE FRA DATABASEN NÅ OG LEGGE TIL I LISTE
		//BRUKER GETALLSTORIES METODE I DBHELPER

		/**Retrieves multiple stories from the database, now returns 500 error when
		* story doesn't have pictures*/
		getMultipleStories: function(idArray) {
			req.data = { type: "getStories" };
			return $http(req);
		},

		/** Adds user rating to a story */
		addRating: function(storyId, userId, rating){
			req.data = {
				type: "rating",
				storyId: storyId,
				userId: userId,
				rating: rating};
			$http(req);
		},

		addTag: function(tagName, userId, storyId){
			req.data = {
				type: "addTag",
				userId: userId,
				storyId: storyId,
				tagName: tagName
			};
			$http(req);
		}
		addUpdateUser: function (userData){
			req.data = {type: "addUpdateUser",
				userData};
			$http(req);
		},
		getUser: function(mail){
			req.data = {type: "getUser",
				'email': mail};
			return $http(req);
		}
	}
});<|endoftext|>