<commit_before>/**
 Copyright (c) 2012 aixigo AG <info@aixigo.de>
 All rights reserved.

 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the 'Software'), to deal in
 the Software without restriction, including without limitation the rights to
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 the Software, and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
define( [
   'lib/underscore/underscore',
   'lib/utilities/object'
], function( _, objectUtils ) {
   'use strict';

   var Q_;
   var nextTick_;
   var timeoutFunction_;

   var PART_SEPARATOR = '.';
   var SUB_PART_SEPARATOR = '-';
   var REQUEST_MATCHER = /^(.)(.*)Request(\..+)?$/;

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function EventBus( optionalConfiguration ) {
      this.config_ = _.defaults( optionalConfiguration || {}, {
         pendingDidTimeout: 3000
      } );

      this.cycleCounter_ = 0;
      this.eventQueue_ = [];
      this.subscribers_ = [];
      this.waitingDeferreds_ = [];
      this.currentCycle_ = -1;
      this.errorHandler_ = defaultErrorHandler;
      this.mediator_ = defaultMediator;
      this.inspector_ = defaultInspector;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setErrorHandler = function( optionalErrorHandler ) {
      this.errorHandler_ = _.isFunction( optionalErrorHandler ) ? optionalErrorHandler : defaultErrorHandler;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setMediator = function( optionalMediator ) {
      this.mediator_ = _.isFunction( optionalMediator ) ? optionalMediator : defaultMediator;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setInspector = function( optionalInspector ) {
      this.inspector_ = _.isFunction( optionalInspector ) ? optionalInspector : defaultInspector;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Subscribes to a specific event.
    *
    * @param {String} eventName the name of the event to subscribe to
    * @param {Function} subscriber a function to call when the event is published
    * @param {String=} optionalSubscriberName a name for the subscriber (for debugging purpose)
    */
   EventBus.prototype.subscribe = function subscribe( eventName, subscriber, optionalSubscriberName ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }
      if( !_.isFunction( subscriber ) ) {
         throw new Error( 'Expected listener to be a function but got ' + subscriber );
      }

      var subscriberName = _.isString( optionalSubscriberName ) ? optionalSubscriberName : '';
      this.subscribers_.push( {
         name: eventName,
         subscriber: subscriber,
         subscriberName: subscriberName,
         subscriptionWeight: calculateSubscriptionWeight( eventName )
      } );

      this.inspector_( {
         action: 'subscribe',
         source: subscriberName,
         target: '-',
         event: eventName,
         cycleId: this.currentCycle_
      } );
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Unsubscribes a subscriber from all subscribed events.
    *
    * NEEDS FIX B: Currently this only removes the subscriber for future events but ignores queued events
    *
    * @param {Function} subscriber the function to unsubscribe
    */
   EventBus.prototype.unsubscribe = function( subscriber ) {
      if( !_.isFunction( subscriber ) ) {
         throw new Error( 'Expected listener to be a function but got ' + subscriber );
      }

      var self = this;
      this.subscribers_ = _.filter( this.subscribers_, function( subscriberItem ) {
         if( subscriberItem.subscriber !== subscriber ) {
            return true;
         }

         self.inspector_( {
            action: 'unsubscribe',
            source: subscriberItem.subscriberName,
            target: '-',
            event: subscriberItem.name,
            cycleId: self.currentCycle_
         } );
         return false;
      } );
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Asynchronously publishes an event on the event bus.
    *
    * @param {String} eventName the name of the event to publish
    * @param {Object=} optionalEvent the event to publish
    * @return {Promise}
    */
   EventBus.prototype.publish = function publish( eventName, optionalEvent ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }

      var item = _.isObject( optionalEvent ) ? objectUtils.deepClone( optionalEvent ) : {};
      var eventItem = _.defaults( item, {
            data: {},
            cycleId: this.currentCycle_ > -1 ? this.currentCycle_ : this.cycleCounter_++,
            sender: null,
            initiator: null
         } );
      eventItem.publishedDeferred = Q_.defer(),
      eventItem.name = eventName;
      enqueueEvent( this, eventItem );

      this.inspector_( {
         action: 'publish',
         source: eventItem.sender,
         target: '-',
         event: eventName,
         eventObject: eventItem,
         cycleId: eventItem.cycleId
      } );

      return eventItem.publishedDeferred.promise;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Asynchronously publishes an event on the event bus.
    *
    * @param {String} eventName the name of the event to publish
    * @param {Object=} optionalEvent the event to publish
    * @return {Promise}
    */
   EventBus.prototype.publishAndGatherReplies = function publishAndGatherReplies( eventName, optionalEvent ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }
      var matches = REQUEST_MATCHER.exec( eventName );
      if( !matches ) {
         throw new Error( 'Expected eventName to end with "Request" but got ' + eventName );
      }

      var self = this;
      var sender = optionalEvent ? optionalEvent.sender : undefined;
      var eventNameSuffix = matches[1].toUpperCase() + matches[2];
      if( matches[ 3 ] ) {
         eventNameSuffix += matches[ 3 ];
      }
      var deferred = Q_.defer();
      var willWaitingForDid = [];
      var givenDidResponses = [];
      var cycleFinished = false;

      function willCollector( event ) {
         if( typeof event.sender !== 'string' ) {
            throw new Error( 'A response with will to a request-event must contain a sender.' );
         }

         willWaitingForDid.push( event.sender );
      }
      this.subscribe( 'will' + eventNameSuffix, willCollector, sender );

      function didCollector( event ) {
         givenDidResponses.push( event );

         var senderIndex = _.indexOf( willWaitingForDid, event.sender );
         if( senderIndex !== -1 ) {
            willWaitingForDid.splice( senderIndex, 1 );
         }

         if( willWaitingForDid.length === 0 && cycleFinished ) {
            finish();
         }
      }
      this.subscribe( 'did' + eventNameSuffix, didCollector, sender );

      var timeoutRef = timeoutFunction_( function() {
         if( willWaitingForDid.length > 0 ) {
            var message = 'Timeout while waiting for pending did' + eventNameSuffix + ' on ' + eventName +
               ' from ' + sender + ' after ' + self.config_.pendingDidTimeout + 'ms.\n' +
               '  Response missing from: ' + willWaitingForDid.join( ', ' ) + '.';
            self.errorHandler_( message );
            finish();
         }
      }, this.config_.pendingDidTimeout );

      this.publish( eventName, optionalEvent ).then( function() {
         if( willWaitingForDid.length === 0 ) {
            // either there was no will or all did reponses were already given in the same cycle as the will
            return finish();
         }

         cycleFinished = true;
      } );

      function finish() {
         clearTimeout( timeoutRef );
         self.unsubscribe( willCollector );
         self.unsubscribe( didCollector );
         deferred.resolve( givenDidResponses );
      }

      return deferred.promise;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function enqueueEvent( self, eventItem ) {
      if( self.eventQueue_.length === 0 ) {
         nextTick_( function() {
            var queuedEvents = self.eventQueue_;

            self.eventQueue_ = [];

            processWaitingDeferreds( self, processQueue( self, queuedEvents ) );
         } );
      }
      self.eventQueue_.push( eventItem );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function processQueue( self, queuedEvents ) {
      return _.map( self.mediator_( queuedEvents ), function( eventItem ) {

         self.currentCycle_ = eventItem.cycleId;

         var deferred = eventItem.publishedDeferred;
         delete eventItem.publishedDeferred;

         _.each( findSubscribers( self, eventItem.name ), function( subscriberItem ) {
            self.inspector_( {
               action: 'deliver',
               source: eventItem.sender,
               target: subscriberItem.subscriberName,
               event: eventItem.name,
               subscribedTo: subscriberItem.name,
               cycleId: eventItem.cycleId
            } );

            try {
               subscriberItem.subscriber( objectUtils.deepClone( eventItem ), {
                  unsubscribe: function() {
                     self.unsubscribe( subscriberItem.subscriber );
                  }
               } );

            }
            catch( e ) {
               var message = 'error while calling subscriber for event ' + eventItem.name +
                  ' (subscribed to: ' + subscriberItem.name + ')';
               self.errorHandler_( message, { 'Exception': e, 'Published event': eventItem } );
            }
         } );

         self.currentCycle_ = -1;

         return deferred;
      } );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function processWaitingDeferreds( self, newDeferreds ) {
      var waitingDeferreds = self.waitingDeferreds_;
      self.waitingDeferreds_ = newDeferreds;

      _.each( waitingDeferreds, function( deferred ) {
         deferred.resolve();
      } );

      if( self.eventQueue_.length === 0 ) {
         // nothing was queued by any subscriber. The publishers can instantly be notified of delivery.
         _.each( newDeferreds, function( deferred ) {
            deferred.resolve();
         } );
         self.waitingDeferreds_ = [];
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function findSubscribers( self, eventName ) {
      var subscribers = [];

      _.each( self.subscribers_, function( subscriberItem ) {
         if( isValidSubscriber( subscriberItem, eventName ) ) {
            var weight = subscriberItem.subscriptionWeight;
            for( var index = 0; index < subscribers.length; ++index ) {
               var pushedWeight = subscribers[index].subscriptionWeight;
               if( weight[0] > pushedWeight[0] ) {
                  subscribers.splice( index, 0, subscriberItem );
                  return;
               }

               if( weight[0] === pushedWeight[0] ) {
                  if( weight[1] > pushedWeight[1] ) {
                     subscribers.splice( index, 0, subscriberItem );
                     return;
                  }
               }
            }
            subscribers.push( subscriberItem );
         }
      } );

      return subscribers;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function isValidSubscriber( subscriber, eventName ) {
      var subscribedTo = subscriber.name;
      if( subscribedTo === eventName ) {
         return true;
      }

      var subscribedToParts = subscribedTo.split( PART_SEPARATOR );
      var eventNameParts = eventName.split( PART_SEPARATOR );
      if( subscribedToParts.length > eventNameParts.length ) {
         return false;
      }

      for( var i = 0, len = eventNameParts.length; i < len; ++i ) {
         // subscribedTo is a prefix of event name
         if( i >= subscribedToParts.length ) {
            return true;
         }

         if( !matchesPart( subscribedToParts[ i ], eventNameParts[ i ] ) ) {
            return false;
         }
      }

      return true;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function matchesPart( subscribedPart, eventPart ) {
      if( subscribedPart === '' ) {
         return true;
      }
      else if( subscribedPart === eventPart ) {
         return true;
      }

      return ( eventPart.indexOf( subscribedPart + SUB_PART_SEPARATOR ) === 0 );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function calculateSubscriptionWeight( eventName ) {
      var parts = eventName.split( '.' );
      var weight = [ 0, 0 ];
      _.each( parts, function( part ) {
         if( part.length > 0 ) {
            weight[0]++;
            weight[1] += part.split( '-' ).length - 1;
         }
      } );
      return weight;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultErrorHandler( message, optionalErrorInformation ) {
      /*global console*/
      if( !console || !_.isFunction( console.log ) ) {
         return;
      }

      var errFunc = _.isFunction( console.error ) ? 'error' : 'log';
      console[ errFunc ]( message );
      if( optionalErrorInformation ) {
         console.log( optionalErrorInformation );
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultMediator( eventItems ) {
      return eventItems;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultInspector( inspectionEvent ) {}

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   return {

      create: function( optionalConfiguration ) {
         if( !Q_ ) {
            throw new Error( 'Need a promise implementation like $q or Q' );
         }
         if( !nextTick_ ) {
            throw new Error( 'Need a next tick implementation like $timeout' );
         }

         return new EventBus( optionalConfiguration );
      },

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      init: function( Q, nextTick, timeoutFunction ) {
         Q_ = Q;
         nextTick_ = nextTick;
         timeoutFunction_ = timeoutFunction;
      }

   };

} );
<commit_msg>jira ATP-6393: set event bus timeout to 10s.<commit_after>/**
 Copyright (c) 2012 aixigo AG <info@aixigo.de>
 All rights reserved.

 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the 'Software'), to deal in
 the Software without restriction, including without limitation the rights to
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 the Software, and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
define( [
   'lib/underscore/underscore',
   'lib/utilities/object'
], function( _, objectUtils ) {
   'use strict';

   var Q_;
   var nextTick_;
   var timeoutFunction_;

   var PART_SEPARATOR = '.';
   var SUB_PART_SEPARATOR = '-';
   var REQUEST_MATCHER = /^(.)(.*)Request(\..+)?$/;

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function EventBus( optionalConfiguration ) {
      this.config_ = _.defaults( optionalConfiguration || {}, {
         pendingDidTimeout: 20000
      } );

      this.cycleCounter_ = 0;
      this.eventQueue_ = [];
      this.subscribers_ = [];
      this.waitingDeferreds_ = [];
      this.currentCycle_ = -1;
      this.errorHandler_ = defaultErrorHandler;
      this.mediator_ = defaultMediator;
      this.inspector_ = defaultInspector;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setErrorHandler = function( optionalErrorHandler ) {
      this.errorHandler_ = _.isFunction( optionalErrorHandler ) ? optionalErrorHandler : defaultErrorHandler;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setMediator = function( optionalMediator ) {
      this.mediator_ = _.isFunction( optionalMediator ) ? optionalMediator : defaultMediator;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   EventBus.prototype.setInspector = function( optionalInspector ) {
      this.inspector_ = _.isFunction( optionalInspector ) ? optionalInspector : defaultInspector;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Subscribes to a specific event.
    *
    * @param {String} eventName the name of the event to subscribe to
    * @param {Function} subscriber a function to call when the event is published
    * @param {String=} optionalSubscriberName a name for the subscriber (for debugging purpose)
    */
   EventBus.prototype.subscribe = function subscribe( eventName, subscriber, optionalSubscriberName ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }
      if( !_.isFunction( subscriber ) ) {
         throw new Error( 'Expected listener to be a function but got ' + subscriber );
      }

      var subscriberName = _.isString( optionalSubscriberName ) ? optionalSubscriberName : '';
      this.subscribers_.push( {
         name: eventName,
         subscriber: subscriber,
         subscriberName: subscriberName,
         subscriptionWeight: calculateSubscriptionWeight( eventName )
      } );

      this.inspector_( {
         action: 'subscribe',
         source: subscriberName,
         target: '-',
         event: eventName,
         cycleId: this.currentCycle_
      } );
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Unsubscribes a subscriber from all subscribed events.
    *
    * NEEDS FIX B: Currently this only removes the subscriber for future events but ignores queued events
    *
    * @param {Function} subscriber the function to unsubscribe
    */
   EventBus.prototype.unsubscribe = function( subscriber ) {
      if( !_.isFunction( subscriber ) ) {
         throw new Error( 'Expected listener to be a function but got ' + subscriber );
      }

      var self = this;
      this.subscribers_ = _.filter( this.subscribers_, function( subscriberItem ) {
         if( subscriberItem.subscriber !== subscriber ) {
            return true;
         }

         self.inspector_( {
            action: 'unsubscribe',
            source: subscriberItem.subscriberName,
            target: '-',
            event: subscriberItem.name,
            cycleId: self.currentCycle_
         } );
         return false;
      } );
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Asynchronously publishes an event on the event bus.
    *
    * @param {String} eventName the name of the event to publish
    * @param {Object=} optionalEvent the event to publish
    * @return {Promise}
    */
   EventBus.prototype.publish = function publish( eventName, optionalEvent ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }

      var item = _.isObject( optionalEvent ) ? objectUtils.deepClone( optionalEvent ) : {};
      var eventItem = _.defaults( item, {
            data: {},
            cycleId: this.currentCycle_ > -1 ? this.currentCycle_ : this.cycleCounter_++,
            sender: null,
            initiator: null
         } );
      eventItem.publishedDeferred = Q_.defer(),
      eventItem.name = eventName;
      enqueueEvent( this, eventItem );

      this.inspector_( {
         action: 'publish',
         source: eventItem.sender,
         target: '-',
         event: eventName,
         eventObject: eventItem,
         cycleId: eventItem.cycleId
      } );

      return eventItem.publishedDeferred.promise;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   /**
    * Asynchronously publishes an event on the event bus.
    *
    * @param {String} eventName the name of the event to publish
    * @param {Object=} optionalEvent the event to publish
    * @return {Promise}
    */
   EventBus.prototype.publishAndGatherReplies = function publishAndGatherReplies( eventName, optionalEvent ) {
      if( !_.isString( eventName ) ) {
         throw new Error( 'Expected eventName to be a String but got ' + eventName );
      }
      var matches = REQUEST_MATCHER.exec( eventName );
      if( !matches ) {
         throw new Error( 'Expected eventName to end with "Request" but got ' + eventName );
      }

      var self = this;
      var sender = optionalEvent ? optionalEvent.sender : undefined;
      var eventNameSuffix = matches[1].toUpperCase() + matches[2];
      if( matches[ 3 ] ) {
         eventNameSuffix += matches[ 3 ];
      }
      var deferred = Q_.defer();
      var willWaitingForDid = [];
      var givenDidResponses = [];
      var cycleFinished = false;

      function willCollector( event ) {
         if( typeof event.sender !== 'string' ) {
            throw new Error( 'A response with will to a request-event must contain a sender.' );
         }

         willWaitingForDid.push( event.sender );
      }
      this.subscribe( 'will' + eventNameSuffix, willCollector, sender );

      function didCollector( event ) {
         givenDidResponses.push( event );

         var senderIndex = _.indexOf( willWaitingForDid, event.sender );
         if( senderIndex !== -1 ) {
            willWaitingForDid.splice( senderIndex, 1 );
         }

         if( willWaitingForDid.length === 0 && cycleFinished ) {
            finish();
         }
      }
      this.subscribe( 'did' + eventNameSuffix, didCollector, sender );

      var timeoutRef = timeoutFunction_( function() {
         if( willWaitingForDid.length > 0 ) {
            var message = 'Timeout while waiting for pending did' + eventNameSuffix + ' on ' + eventName +
               ' from ' + sender + ' after ' + self.config_.pendingDidTimeout + 'ms.\n' +
               '  Response missing from: ' + willWaitingForDid.join( ', ' ) + '.';
            self.errorHandler_( message );
            finish();
         }
      }, this.config_.pendingDidTimeout );

      this.publish( eventName, optionalEvent ).then( function() {
         if( willWaitingForDid.length === 0 ) {
            // either there was no will or all did reponses were already given in the same cycle as the will
            return finish();
         }

         cycleFinished = true;
      } );

      function finish() {
         clearTimeout( timeoutRef );
         self.unsubscribe( willCollector );
         self.unsubscribe( didCollector );
         deferred.resolve( givenDidResponses );
      }

      return deferred.promise;
   };

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function enqueueEvent( self, eventItem ) {
      if( self.eventQueue_.length === 0 ) {
         nextTick_( function() {
            var queuedEvents = self.eventQueue_;

            self.eventQueue_ = [];

            processWaitingDeferreds( self, processQueue( self, queuedEvents ) );
         } );
      }
      self.eventQueue_.push( eventItem );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function processQueue( self, queuedEvents ) {
      return _.map( self.mediator_( queuedEvents ), function( eventItem ) {

         self.currentCycle_ = eventItem.cycleId;

         var deferred = eventItem.publishedDeferred;
         delete eventItem.publishedDeferred;

         _.each( findSubscribers( self, eventItem.name ), function( subscriberItem ) {
            self.inspector_( {
               action: 'deliver',
               source: eventItem.sender,
               target: subscriberItem.subscriberName,
               event: eventItem.name,
               subscribedTo: subscriberItem.name,
               cycleId: eventItem.cycleId
            } );

            try {
               subscriberItem.subscriber( objectUtils.deepClone( eventItem ), {
                  unsubscribe: function() {
                     self.unsubscribe( subscriberItem.subscriber );
                  }
               } );

            }
            catch( e ) {
               var message = 'error while calling subscriber for event ' + eventItem.name +
                  ' (subscribed to: ' + subscriberItem.name + ')';
               self.errorHandler_( message, { 'Exception': e, 'Published event': eventItem } );
            }
         } );

         self.currentCycle_ = -1;

         return deferred;
      } );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function processWaitingDeferreds( self, newDeferreds ) {
      var waitingDeferreds = self.waitingDeferreds_;
      self.waitingDeferreds_ = newDeferreds;

      _.each( waitingDeferreds, function( deferred ) {
         deferred.resolve();
      } );

      if( self.eventQueue_.length === 0 ) {
         // nothing was queued by any subscriber. The publishers can instantly be notified of delivery.
         _.each( newDeferreds, function( deferred ) {
            deferred.resolve();
         } );
         self.waitingDeferreds_ = [];
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function findSubscribers( self, eventName ) {
      var subscribers = [];

      _.each( self.subscribers_, function( subscriberItem ) {
         if( isValidSubscriber( subscriberItem, eventName ) ) {
            var weight = subscriberItem.subscriptionWeight;
            for( var index = 0; index < subscribers.length; ++index ) {
               var pushedWeight = subscribers[index].subscriptionWeight;
               if( weight[0] > pushedWeight[0] ) {
                  subscribers.splice( index, 0, subscriberItem );
                  return;
               }

               if( weight[0] === pushedWeight[0] ) {
                  if( weight[1] > pushedWeight[1] ) {
                     subscribers.splice( index, 0, subscriberItem );
                     return;
                  }
               }
            }
            subscribers.push( subscriberItem );
         }
      } );

      return subscribers;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function isValidSubscriber( subscriber, eventName ) {
      var subscribedTo = subscriber.name;
      if( subscribedTo === eventName ) {
         return true;
      }

      var subscribedToParts = subscribedTo.split( PART_SEPARATOR );
      var eventNameParts = eventName.split( PART_SEPARATOR );
      if( subscribedToParts.length > eventNameParts.length ) {
         return false;
      }

      for( var i = 0, len = eventNameParts.length; i < len; ++i ) {
         // subscribedTo is a prefix of event name
         if( i >= subscribedToParts.length ) {
            return true;
         }

         if( !matchesPart( subscribedToParts[ i ], eventNameParts[ i ] ) ) {
            return false;
         }
      }

      return true;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function matchesPart( subscribedPart, eventPart ) {
      if( subscribedPart === '' ) {
         return true;
      }
      else if( subscribedPart === eventPart ) {
         return true;
      }

      return ( eventPart.indexOf( subscribedPart + SUB_PART_SEPARATOR ) === 0 );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function calculateSubscriptionWeight( eventName ) {
      var parts = eventName.split( '.' );
      var weight = [ 0, 0 ];
      _.each( parts, function( part ) {
         if( part.length > 0 ) {
            weight[0]++;
            weight[1] += part.split( '-' ).length - 1;
         }
      } );
      return weight;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultErrorHandler( message, optionalErrorInformation ) {
      /*global console*/
      if( !console || !_.isFunction( console.log ) ) {
         return;
      }

      var errFunc = _.isFunction( console.error ) ? 'error' : 'log';
      console[ errFunc ]( message );
      if( optionalErrorInformation ) {
         console.log( optionalErrorInformation );
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultMediator( eventItems ) {
      return eventItems;
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function defaultInspector( inspectionEvent ) {}

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   return {

      create: function( optionalConfiguration ) {
         if( !Q_ ) {
            throw new Error( 'Need a promise implementation like $q or Q' );
         }
         if( !nextTick_ ) {
            throw new Error( 'Need a next tick implementation like $timeout' );
         }

         return new EventBus( optionalConfiguration );
      },

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      init: function( Q, nextTick, timeoutFunction ) {
         Q_ = Q;
         nextTick_ = nextTick;
         timeoutFunction_ = timeoutFunction;
      }

   };

} );
<|endoftext|>


<commit_before>var parser = require('./parser');
var Interpreter = require('./interpreter');

function TileAction(tile, tileX, tileY, key, data, renderer, callback) {
  this.tile = tile;
  this.key = key;
  this.data = data;
  this.tileX = tileX;
  this.tileY = tileY;
  this.renderer = renderer;
  this.callback = callback;
}

TileAction.prototype.exec = function() {
  this.before = this.tile[this.key];
  this.tile[this.key] = this.data;
  this.update();
}

TileAction.prototype.undo = function() {
  this.tile[this.key] = this.before;
  this.update();
}

TileAction.prototype.update = function() {
  // Fill the data with 0 if it's required and null.
  if(Interpreter.CommandMap[this.tile.command].argument &&
    this.tile.data == null) {
    this.tile.data = 0;
  }
  this.tile.original = parser.encodeSyllable(this.tile);
  this.renderer.map.set(this.tileX, this.tileY, this.tile);
  this.renderer.updateTile(this.tileX, this.tileY);
  if(this.callback) this.callback();
}

module.exports = TileAction;
<commit_msg>Fix not being able to edit commandless tile<commit_after>var parser = require('./parser');
var Interpreter = require('./interpreter');

function TileAction(tile, tileX, tileY, key, data, renderer, callback) {
  this.tile = tile;
  this.key = key;
  this.data = data;
  this.tileX = tileX;
  this.tileY = tileY;
  this.renderer = renderer;
  this.callback = callback;
}

TileAction.prototype.exec = function() {
  this.before = this.tile[this.key];
  this.tile[this.key] = this.data;
  this.update();
}

TileAction.prototype.undo = function() {
  this.tile[this.key] = this.before;
  this.update();
}

TileAction.prototype.update = function() {
  // Fill the data with 0 if it's required and null.
  var command = Interpreter.CommandMap[this.tile.command];
  if(command != null && command.argument && this.tile.data == null) {
    this.tile.data = 0;
  }
  this.tile.original = parser.encodeSyllable(this.tile);
  this.renderer.map.set(this.tileX, this.tileY, this.tile);
  this.renderer.updateTile(this.tileX, this.tileY);
  if(this.callback) this.callback();
}

module.exports = TileAction;
<|endoftext|>


<commit_before>// ***********************************************
// WINE LIFE
// DISCOVER WINDOW - DiscoverWindow.js
//
// MEN+1
// Programmer: Matthew Johnson
// ***********************************************

function DiscoverWindow(title) {

	var global = require('ui/common/globals');
	var dropdown = require('ui/common/elements/dropdown');
	var self = global.createWindow(title);
	self.barImage='images/iPhone_Nav_Bar_With_Bkgrd.png';
	var TU = require ('/TitanUp/TitanUp');

	// This view holds the wine list
	var view = Ti.UI.createView({
		top:'40%',
		left:0,
		width:'100%',
		height:'60%',
		layout: 'vertical'
	});

	var label_title = Ti.UI.createLabel({
		color: global.colors.dark,
		text:"Today's Featured Wines",
		font:{
            fontFamily:'Helvetica Neue',
            fontSize:18,
            fontWeight:'Bold'
           }
	});
//***********************************************************************************
//  This original list in the future will display advertised wines / picks of the day
//***********************************************************************************
	self.addEventListener('open', function(e){
		global.api.search_with_filter("Zinfandel", '124', function(search_results){
            table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
			});
		   view.add(label_title);
           view.add(table);
      });
	});

	self.add(view);

//************************************************************************
// Arrays with wines for picker menu's
//************************************************************************

 var allwines = ['Type', 'Amarone', 'Blanc', 'Carmenere', 'Cabernet', 'Chenin',
 'Grand', 'Grenache', 'Grigio', 'Gris', 'Merlot', 'Murray', 'Muscat', 'Pinot Noir', 'Riesling',
  'Sangiovese', 'Shiraz', 'Syrah', 'Traminer', 'Verdicchio', 'Zinfandel'];

 var redwines =  ['Type','Amarone', 'Cabernet', 'Carmenere',
   'Grand', 'Grenache', 'Merlot', 'Pinot Noir',
  'Sangiovese', 'Shiraz', 'Syrah', 'Zinfandel'];

 var whitewines = ['Type','Blanc', 'Chenin',
  'Grigio', 'Gris', 'Murray', 'Muscat', 'Riesling',
  'Traminer', 'Verdicchio'];

// wines with filters not working
//  'Barolo', 'Carmenere', 'Chianti', 'Bordeaux',

// Drop down view
	var view_discover = Ti.UI.createView({
			width: Ti.UI.FILL,
			height: Ti.UI.SIZE,
			layout: 'vertical',
			left: 10,
			right: 10,
		});

	var wine_review = null;
	var table = null;

	// Dropdown menu components
	var search_bar = Ti.UI.createTextField({
		top:0,
		width:'90%',
		left:'5%',
		hintText:' Discover Wine...',
		borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
	});

	var picker_color = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Color",
		values: ['Color','Red', 'White']
	});

	var picker_allwines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: allwines
	});

	var picker_redwines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: redwines
	});

	var picker_whitewines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: whitewines
	});

	// These variables store picker selected values
	var winecolor = null; // this is for color id to refine search
	var wineColor = null;  // this is for actual wine color
	var winetype = null;

//*****************************************************************
//  Associative array used for server query filtering
//*****************************************************************

	var wines = new Object;
	wines['Amarone'] = '145';
	wines['Australian'] = '145';
	wines['Barolo'] = '170';
	wines['Blanc'] = '144';
	wines['Bordeaux'] = '-2147483648'; // Filter doesnt work
	wines['Cabernet'] = '139';
	wines['Carmenere'] = '10081';
	wines['Chenin'] = '163'; // Filter DNE
	wines['Chile'] = '10081';
	wines['Chianti'] = '139';
	wines['Grand'] = '10082';
	wines['Grenache'] = '10080';
	wines['Grigio'] = '194';
	wines['Gris'] = '194';
	wines['Merlot'] = '138';
	wines['Murray'] = '162';
	wines['Muscat'] = '173';
	wines['Pinot Noir'] = '143';
	wines['Riesling'] = '153';
	wines['Rose'] = '147';
	wines['Sangiovese'] = '163';
	wines['Shiraz'] = '146';
	wines['Syrah'] = '146';
	wines['Traminer'] = '148';
	wines['Verdicchio'] = '148';
	wines['Zinfandel'] = '141';

//******************************************************************************
// Event listener for wine color picker.  This function adds and removes views
// based off of what is selected in the picker.
//******************************************************************************
	picker_color.addEventListener ('TUchange', function (e) {
		if(e.value == 'Red'){
			winecolor = '124';
			wineColor = e.value;
			view_discover.remove(picker_allwines);
			view_discover.remove(picker_whitewines);
			view_discover.add(picker_redwines);

		picker_redwines.addEventListener ('TUchange', function (e) {
		winetype =  e.value;
		});

		}
		else if(e.value == "White"){
			winecolor = '125';
			wineColor = e.value;
			view_discover.remove(picker_allwines);
			view_discover.remove(picker_redwines);
			view_discover.add(picker_whitewines);

			picker_whitewines.addEventListener ('TUchange', function (e) {
			winetype =  e.value;
			});
		}
		else{
			winecolor = null;
			wineColor = null;
			view_discover.remove(picker_whitewines);
			view_discover.remove(picker_redwines);
			view_discover.add(picker_allwines);

			picker_allwines.addEventListener ('TUchange', function (e) {
			winetype =  e.value;
			});
		}

	});

	picker_allwines.addEventListener ('TUchange', function (e) {
	winetype =  e.value;
	});

	view_discover.add(search_bar);
	view_discover.add(picker_color);
	view_discover.add(picker_allwines);

//***********************************************************************************
// Dropdown menu area
// This function controls the dropdown menu and the logic for the pickers and
// search bar.
//***********************************************************************************
	dropdown(view_discover, self, "Find", "Browse", "down", function(){

		view.setTop('10%');
		view.setHeight('90%');
		view.remove(table);
		view.remove(label_title);
		self.remove(view);
		label_title.setText("Search Results");
		view.add(label_title);

		if(search_bar.value != null && search_bar.value != ""){

		if(winetype == 'Type'){winetype = null;}

			if(winecolor != null && winetype != null)
			{
				global.api.search_with_filter(search_bar.value, winecolor + '+' + wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor == null && winetype != null)
			{
				global.api.search_with_filter(search_bar.value, wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor != null && winetype == null)
			{
				global.api.search_with_filter(search_bar.value, winecolor, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}
			else
			{
				global.api.search(search_bar.value, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}

        self.add(view);

     }
     else{
     //	if nothing entered in textfield do these
     	if(winetype == 'Type'){winetype = null;}

			if(winecolor != null && winetype != null)
			{
				global.api.search_with_filter(winetype, winecolor + '+' + wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor == null && winetype != null)
			{
				global.api.search_with_filter(winetype, wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor != null && winetype == null)
			{
				global.api.search_with_filter(wineColor, winecolor, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}
			else if(winecolor == null && winetype == null)
			{
				//alert('Please Enter a Wine or Select a Filter to Search');
			}

     	self.add(view);
     }
	});

global.outputHook(self);
return self;
};

module.exports = DiscoverWindow;<commit_msg>baralo filter fixed/added discover window<commit_after>// ***********************************************
// WINE LIFE
// DISCOVER WINDOW - DiscoverWindow.js
//
// MEN+1
// Programmer: Matthew Johnson
// ***********************************************

function DiscoverWindow(title) {

	var global = require('ui/common/globals');
	var dropdown = require('ui/common/elements/dropdown');
	var self = global.createWindow(title);
	self.barImage='images/iPhone_Nav_Bar_With_Bkgrd.png';
	var TU = require ('/TitanUp/TitanUp');

	// This view holds the wine list
	var view = Ti.UI.createView({
		top:'40%',
		left:0,
		width:'100%',
		height:'60%',
		layout: 'vertical'
	});

	var label_title = Ti.UI.createLabel({
		color: global.colors.dark,
		text:"Today's Featured Wines",
		font:{
            fontFamily:'Helvetica Neue',
            fontSize:18,
            fontWeight:'Bold'
           }
	});
//***********************************************************************************
//  This original list in the future will display advertised wines / picks of the day
//***********************************************************************************
	self.addEventListener('open', function(e){
		global.api.search_with_filter("Zinfandel", '124', function(search_results){
            table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
			});
		   view.add(label_title);
           view.add(table);
      });
	});

	self.add(view);

//************************************************************************
// Arrays with wines for picker menu's
//************************************************************************

 var allwines = ['Type', 'Amarone', 'Barolo', 'Blanc', 'Carmenere', 'Cabernet', 'Chenin',
 'Grand', 'Grenache', 'Grigio', 'Gris', 'Merlot', 'Murray', 'Muscat', 'Pinot Noir', 'Riesling',
  'Sangiovese', 'Shiraz', 'Syrah', 'Traminer', 'Verdicchio', 'Zinfandel'];

 var redwines =  ['Type','Amarone', 'Barolo', 'Cabernet', 'Carmenere',
   'Grand', 'Grenache', 'Merlot', 'Pinot Noir',
  'Sangiovese', 'Shiraz', 'Syrah', 'Zinfandel'];

 var whitewines = ['Type','Blanc', 'Chenin',
  'Grigio', 'Gris', 'Murray', 'Muscat', 'Riesling',
  'Traminer', 'Verdicchio'];

// wines with filters not working
//  'Barolo', 'Carmenere', 'Chianti', 'Bordeaux',

// Drop down view
	var view_discover = Ti.UI.createView({
			width: Ti.UI.FILL,
			height: Ti.UI.SIZE,
			layout: 'vertical',
			left: 10,
			right: 10,
		});

	var wine_review = null;
	var table = null;

	// Dropdown menu components
	var search_bar = Ti.UI.createTextField({
		top:0,
		width:'90%',
		left:'5%',
		hintText:' Discover Wine...',
		borderStyle: Ti.UI.INPUT_BORDERSTYLE_ROUNDED
	});

	var picker_color = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Color",
		values: ['Color','Red', 'White']
	});

	var picker_allwines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: allwines
	});

	var picker_redwines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: redwines
	});

	var picker_whitewines = TU.UI.createSimplePicker ({
		left: '5%',
		right: '5%',
		title: "Type",
		values: whitewines
	});

	// These variables store picker selected values
	var winecolor = null; // this is for color id to refine search
	var wineColor = null;  // this is for actual wine color
	var winetype = null;

//*****************************************************************
//  Associative array used for server query filtering
//*****************************************************************

	var wines = new Object;
	wines['Amarone'] = '145';
	wines['Australian'] = '145';
	wines['Barolo'] = '170';
	wines['Blanc'] = '144';
	wines['Bordeaux'] = '-2147483648'; // Filter doesnt work
	wines['Cabernet'] = '139';
	wines['Carmenere'] = '10081';
	wines['Chenin'] = '163'; // Filter DNE
	wines['Chile'] = '10081';
	wines['Chianti'] = '139';
	wines['Grand'] = '10082';
	wines['Grenache'] = '10080';
	wines['Grigio'] = '194';
	wines['Gris'] = '194';
	wines['Merlot'] = '138';
	wines['Murray'] = '162';
	wines['Muscat'] = '173';
	wines['Pinot Noir'] = '143';
	wines['Riesling'] = '153';
	wines['Rose'] = '147';
	wines['Sangiovese'] = '163';
	wines['Shiraz'] = '146';
	wines['Syrah'] = '146';
	wines['Traminer'] = '148';
	wines['Verdicchio'] = '148';
	wines['Zinfandel'] = '141';

//******************************************************************************
// Event listener for wine color picker.  This function adds and removes views
// based off of what is selected in the picker.
//******************************************************************************
	picker_color.addEventListener ('TUchange', function (e) {
		if(e.value == 'Red'){
			winecolor = '124';
			wineColor = e.value;
			view_discover.remove(picker_allwines);
			view_discover.remove(picker_whitewines);
			view_discover.add(picker_redwines);

		picker_redwines.addEventListener ('TUchange', function (e) {
		winetype =  e.value;
		});

		}
		else if(e.value == "White"){
			winecolor = '125';
			wineColor = e.value;
			view_discover.remove(picker_allwines);
			view_discover.remove(picker_redwines);
			view_discover.add(picker_whitewines);

			picker_whitewines.addEventListener ('TUchange', function (e) {
			winetype =  e.value;
			});
		}
		else{
			winecolor = null;
			wineColor = null;
			view_discover.remove(picker_whitewines);
			view_discover.remove(picker_redwines);
			view_discover.add(picker_allwines);

			picker_allwines.addEventListener ('TUchange', function (e) {
			winetype =  e.value;
			});
		}

	});

	picker_allwines.addEventListener ('TUchange', function (e) {
	winetype =  e.value;
	});

	view_discover.add(search_bar);
	view_discover.add(picker_color);
	view_discover.add(picker_allwines);

//***********************************************************************************
// Dropdown menu area
// This function controls the dropdown menu and the logic for the pickers and
// search bar.
//***********************************************************************************
	dropdown(view_discover, self, "Find", "Browse", "down", function(){

		view.setTop('10%');
		view.setHeight('90%');
		view.remove(table);
		view.remove(label_title);
		self.remove(view);
		label_title.setText("Search Results");
		view.add(label_title);

		if(search_bar.value != null && search_bar.value != ""){

		if(winetype == 'Type'){winetype = null;}

			if(winecolor != null && winetype != null)
			{
				global.api.search_with_filter(search_bar.value, winecolor + '+' + wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor == null && winetype != null)
			{
				global.api.search_with_filter(search_bar.value, wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor != null && winetype == null)
			{
				global.api.search_with_filter(search_bar.value, winecolor, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}
			else
			{
				global.api.search(search_bar.value, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}

        self.add(view);

     }
     else{
     //	if nothing entered in textfield do these
     	if(winetype == 'Type'){winetype = null;}

			if(winecolor != null && winetype != null)
			{
				global.api.search_with_filter(winetype, winecolor + '+' + wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor == null && winetype != null)
			{
				global.api.search_with_filter(winetype, wines[winetype], function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			  });
			}
			else if(winecolor != null && winetype == null)
			{
				global.api.search_with_filter(wineColor, winecolor, function(search_results){
         		table = global.api.search_results(search_results, function(wine){
				wine_review = require('ui/handheld/WineReview');
				self.containingTab.open(wine_review(wine));
				});
				view.add(table);
			 });
			}
			else if(winecolor == null && winetype == null)
			{
				//alert('Please Enter a Wine or Select a Filter to Search');
			}

     	self.add(view);
     }
	});

global.outputHook(self);
return self;
};

module.exports = DiscoverWindow;<|endoftext|>


<commit_before>window.Helpers = {
  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      return window.Helpers.apiCall(
        typeof url !== 'undefined' ? url : "https://api.spotify.com/v1/users/" + userId + "/playlists",
        this.props.access_token
      )
    }.bind(this)).done(function(response) {
      if (this.isMounted()) {
        this.setState({
          playlists: response.items,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  render: function() {
    playlist = this.props.playlist

    return (

        {playlist.name}
        {playlist.owner.id}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var requests = [];
      var limit = 20;

      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + response.id + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) { $.merge(playlists, response[0].items) })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob(["\uFEFF" + data], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist));
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0].href == 'undefined') {
        responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
      } else {
        responses = [arguments[0]];
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function(artist) { return artist.name }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ].map(function(track) { return '"' + track + '"'; })
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function(infoArray, index){
        dataString = infoArray.join(",");
        csvContent += index < tracks.length ? dataString+ "\n" : dataString;
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[^a-z0-9\- ]/gi, '').replace(/[ ]/gi, '_').toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<commit_msg>Stop column widths from changing when paginating<commit_after>window.Helpers = {
  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      return window.Helpers.apiCall(
        typeof url !== 'undefined' ? url : "https://api.spotify.com/v1/users/" + userId + "/playlists",
        this.props.access_token
      )
    }.bind(this)).done(function(response) {
      if (this.isMounted()) {
        this.setState({
          playlists: response.items,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  render: function() {
    playlist = this.props.playlist

    return (

        {playlist.name}
        {playlist.owner.id}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var requests = [];
      var limit = 20;

      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + response.id + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) { $.merge(playlists, response[0].items) })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob(["\uFEFF" + data], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist));
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0].href == 'undefined') {
        responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
      } else {
        responses = [arguments[0]];
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function(artist) { return artist.name }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ].map(function(track) { return '"' + track + '"'; })
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function(infoArray, index){
        dataString = infoArray.join(",");
        csvContent += index < tracks.length ? dataString+ "\n" : dataString;
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[^a-z0-9\- ]/gi, '').replace(/[ ]/gi, '_').toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<|endoftext|>


<commit_before>window.Helpers = {
  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      return window.Helpers.apiCall(
        typeof url !== 'undefined' ? url : "https://api.spotify.com/v1/users/" + userId + "/playlists",
        this.props.access_token
      )
    }.bind(this)).done(function(response) {
      if (this.isMounted()) {
        this.setState({
          playlists: response.items,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  render: function() {
    playlist = this.props.playlist

    return (

        {playlist.name}
        {playlist.owner.id}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var requests = [];
      var limit = 20;

      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + response.id + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) { $.merge(playlists, response[0].items) })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob(["\uFEFF" + data], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist));
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0].href == 'undefined') {
        responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
      } else {
        responses = [arguments[0]];
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function(artist) { return artist.name }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ].map(function(track) { return '"' + track + '"'; })
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function(infoArray, index){
        dataString = infoArray.join(",");
        csvContent += index < tracks.length ? dataString+ "\n" : dataString;
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[^a-z0-9\- ]/gi, '').replace(/[ ]/gi, '_').toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<commit_msg>Allow starred tracks to be exported [Fixes #5]<commit_after>window.Helpers = {
  apiCall: function(url, access_token) {
    return $.ajax({
      url: url,
      headers: {
        'Authorization': 'Bearer ' + access_token
      }
    }).fail(function (jqXHR, textStatus) {
      if (jqXHR.status == 401) {
        // Return to home page after auth token expiry
        window.location = window.location.href.split('#')[0]
      } else {
        // Otherwise report the error so user can raise an issue
        alert(jqXHR.responseText);
      }
    })
  }
}

var PlaylistTable = React.createClass({
  getInitialState: function() {
    return {
      playlists: [],
      playlistCount: 0,
      nextURL: null,
      prevURL: null
    };
  },

  loadPlaylists: function(url) {
    var userId = '';
    var firstPage = typeof url === 'undefined' || url.indexOf('offset=0') > -1;

    window.Helpers.apiCall("https://api.spotify.com/v1/me", this.props.access_token).then(function(response) {
      userId = response.id;

      // Show starred playlist if viewing first page
      if (firstPage) {
        return $.when.apply($, [
          window.Helpers.apiCall(
            "https://api.spotify.com/v1/users/" + userId + "/starred",
            this.props.access_token
          ),
          window.Helpers.apiCall(
            "https://api.spotify.com/v1/users/" + userId + "/playlists",
            this.props.access_token
          )
        ])
      } else {
        return window.Helpers.apiCall(url, this.props.access_token);
      }
    }.bind(this)).done(function() {
      var response;
      var playlists = [];

      if (arguments[1] === 'success') {
        response = arguments[0];
        playlists = arguments[0].items;
      } else {
        response = arguments[1][0];
        playlists = $.merge([arguments[0][0]], arguments[1][0].items);
      }

      if (this.isMounted()) {
        this.setState({
          playlists: playlists,
          playlistCount: response.total,
          nextURL: response.next,
          prevURL: response.previous
        });

        $('#playlists').fadeIn();
        $('#subtitle').text((response.offset + 1) + '-' + (response.offset + response.items.length) + ' of ' + response.total + ' playlists for ' + userId)
      }
    }.bind(this))
  },

  exportPlaylists: function() {
    PlaylistsExporter.export(this.props.access_token, this.state.playlistCount);
  },

  componentDidMount: function() {
    this.loadPlaylists(this.props.url);
  },

  render: function() {
    if (this.state.playlists.length > 0) {
      return (

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

                Name
                Owner
                Tracks
                Public?
                Collaborative?
                <button className="btn btn-default btn-xs" type="submit" onClick={this.exportPlaylists}> Export All</button>

              {this.state.playlists.map(function(playlist, i) {
                return <PlaylistRow playlist={playlist} key={playlist.id} access_token={this.props.access_token}/>;
              }.bind(this))}

          <Paginator nextURL={this.state.nextURL} prevURL={this.state.prevURL} loadPlaylists={this.loadPlaylists}/>

      );
    } else {
      return

    }
  }
});

var PlaylistRow = React.createClass({
  exportPlaylist: function() {
    PlaylistExporter.export(this.props.access_token, this.props.playlist);
  },

  renderTickCross: function(condition) {
    if (condition) {
      return
    } else {
      return
    }
  },

  renderIcon: function(playlist) {
    if (playlist.name == 'Starred') {
      return ;
    } else {
      return ;
    }
  },

  render: function() {
    playlist = this.props.playlist

    return (

        {this.renderIcon(playlist)}
        {playlist.name}
        {playlist.owner.id}
        {playlist.tracks.total}
        {this.renderTickCross(playlist.public)}
        {this.renderTickCross(playlist.collaborative)}
        <button className="btn btn-default btn-xs btn-success" type="submit" onClick={this.exportPlaylist}> Export</button>

    );
  }
});

var Paginator = React.createClass({
  nextClick: function(e) {
    e.preventDefault()

    if (this.props.nextURL != null) {
      this.props.loadPlaylists(this.props.nextURL)
    }
  },

  prevClick: function(e) {
    e.preventDefault()

    if (this.props.prevURL != null) {
      this.props.loadPlaylists(this.props.prevURL)
    }
  },

  render: function() {
    if (this.props.nextURL != null || this.props.prevURL != null) {
      return (

                «

                »

      )
    } else {
      return

    }
  }
});

// Handles exporting all playlist data as a zip file
var PlaylistsExporter = {
  export: function(access_token, playlistCount) {
    var playlistFileNames = [];

    window.Helpers.apiCall("https://api.spotify.com/v1/me", access_token).then(function(response) {
      var limit = 20;
      var userId = response.id;

      // Initialize requests with starred playlist
      var requests = [
        window.Helpers.apiCall(
          "https://api.spotify.com/v1/users/" + userId + "/starred",
          access_token
        )
      ];

      // Add other playlists
      for (var offset = 0; offset < playlistCount; offset = offset + limit) {
        var url = "https://api.spotify.com/v1/users/" + userId + "/playlists";
        requests.push(
          window.Helpers.apiCall(url + '?offset=' + offset + '&limit=' + limit, access_token)
        )
      }

      $.when.apply($, requests).then(function() {
        var playlists = [];
        var playlistExports = [];

        // Handle either single or multiple responses
        if (typeof arguments[0].href == 'undefined') {
          $(arguments).each(function(i, response) {
            if (typeof response[0].items === 'undefined') {
              // Single playlist
              playlists.push(response[0]);
            } else {
              // Page of playlists
              $.merge(playlists, response[0].items);
            }
          })
        } else {
          playlists = arguments[0].items
        }

        $(playlists).each(function(i, playlist) {
          playlistFileNames.push(PlaylistExporter.fileName(playlist));
          playlistExports.push(PlaylistExporter.csvData(access_token, playlist));
        });

        return $.when.apply($, playlistExports);
      }).then(function() {
        var zip = new JSZip();
        var responses = [];

        $(arguments).each(function(i, response) {
          zip.file(playlistFileNames[i], response)
        });

        var content = zip.generate({ type: "blob" });
        saveAs(content, "spotify_playlists.zip");
      });
    });
  }
}

// Handles exporting a single playlist as a CSV file
var PlaylistExporter = {
  export: function(access_token, playlist) {
    this.csvData(access_token, playlist).then(function(data) {
      var blob = new Blob(["\uFEFF" + data], { type: "text/csv;charset=utf-8" });
      saveAs(blob, this.fileName(playlist));
    }.bind(this))
  },

  csvData: function(access_token, playlist) {
    var requests = [];
    var limit = 100;

    for (var offset = 0; offset < playlist.tracks.total; offset = offset + limit) {
      requests.push(
        window.Helpers.apiCall(playlist.tracks.href.split('?')[0] + '?offset=' + offset + '&limit=' + limit, access_token)
      )
    }

    return $.when.apply($, requests).then(function() {
      var responses = [];

      // Handle either single or multiple responses
      if (typeof arguments[0].href == 'undefined') {
        responses = Array.prototype.slice.call(arguments).map(function(a) { return a[0] });
      } else {
        responses = [arguments[0]];
      }

      var tracks = responses.map(function(response) {
        return response.items.map(function(item) {
          return [
            item.track.uri,
            item.track.name,
            item.track.artists.map(function(artist) { return artist.name }).join(', '),
            item.track.album.name,
            item.track.disc_number,
            item.track.track_number,
            item.track.duration_ms,
            item.added_by == null ? '' : item.added_by.uri,
            item.added_at
          ].map(function(track) { return '"' + track + '"'; })
        });
      });

      // Flatten the array of pages
      tracks = $.map(tracks, function(n) { return n })

      tracks.unshift([
        "Spotify URI",
        "Track Name",
        "Artist Name",
        "Album Name",
        "Disc Number",
        "Track Number",
        "Track Duration (ms)",
        "Added By",
        "Added At"
      ]);

      csvContent = '';
      tracks.forEach(function(infoArray, index){
        dataString = infoArray.join(",");
        csvContent += index < tracks.length ? dataString+ "\n" : dataString;
      });

      return csvContent;
    });
  },

  fileName: function(playlist) {
    return playlist.name.replace(/[^a-z0-9\- ]/gi, '').replace(/[ ]/gi, '_').toLowerCase() + ".csv";
  }
}

$(function() {
  var vars = window.location.hash.substring(1).split('&');
  var key = {};
  for (i=0; i<vars.length; i++) {
    var tmp = vars[i].split('=');
    key[tmp[0]] = tmp[1];
  }

  if (typeof key['access_token'] === 'undefined') {
    $('#loginButton').css('display', 'inline-block')
  } else {
    React.render(<PlaylistTable access_token={key['access_token']} />, playlistsContainer);
  }
});
<|endoftext|>


<commit_before>import React from 'react';
import Analyzer from 'parser/core/Analyzer';
import SPELLS from 'common/SPELLS';
import { formatPercentage } from 'common/format';
import TalentStatisticBox from 'interface/others/TalentStatisticBox';
import STATISTIC_ORDER from 'interface/others/STATISTIC_ORDER';

const OSSUARY_RUNICPOWER_REDUCTION = 5;

class Ossuary extends Analyzer {
  dsWithOS = 0;
  dsWithoutOS = 0;

  get uptime() {
    return this.selectedCombatant.getBuffUptime(SPELLS.OSSUARY.id) / this.owner.fightDuration;
  }

  get buffedDeathStrikes() {
    return this.dsWithOS / (this.dsWithOS + this.dsWithoutOS);
  }

  on_byPlayer_cast(event) {
    if (event.ability.guid !== SPELLS.DEATH_STRIKE.id) return;

    if (this.selectedCombatant.hasBuff(SPELLS.OSSUARY.id)) {
      this.dsWithOS += 1;
    } else {
      this.dsWithoutOS += 1;

      event.meta = event.meta || {};
      event.meta.isInefficientCast = true;
      event.meta.inefficientCastReason = `This Death Strike cast was without Ossuary.`;
    }
  }

  get efficiencySuggestionThresholds() {
    return {
      actual: this.buffedDeathStrikes,
      isLessThan: {
        minor: 1,
        average: 0.9,
        major: .85,
      },
      style: 'percentage',
    };
  }

  get uptimeSuggestionThresholds() {
    return {
      actual: this.uptime,
      isLessThan: {
        minor: 0.95,
        average: 0.9,
        major: .8,
      },
      style: 'percentage',
    };
  }

  suggestions(when) {
    when(this.efficiencySuggestionThresholds)
        .addSuggestion((suggest, actual, recommended) => {
          return suggest('Your Ossuary usage can be improved. Avoid casting Death Strike while not having Ossuary up as you lose Runic Power by doing so.')
            .icon(SPELLS.OSSUARY.icon)
            .actual(`${formatPercentage(actual)}% Ossuary efficiency`)
            .recommended(`${formatPercentage(recommended)}% is recommended`);
        });
  }

  statistic() {

    return (
      <TalentStatisticBox
        talent={SPELLS.OSSUARY.id}
        position={STATISTIC_ORDER.OPTIONAL(3)}
        value={`${ this.dsWithoutOS } / ${ this.dsWithOS + this.dsWithoutOS }`}
        label="Death Strikes without Ossuary"
        tooltip={(
          <>
            {this.dsWithoutOS * OSSUARY_RUNICPOWER_REDUCTION} RP wasted by casting them without Ossuary up.

            {this.dsWithOS * OSSUARY_RUNICPOWER_REDUCTION} RP saved by casting them with Ossuary up.

            {formatPercentage(this.uptime)}% uptime.
          </>
        )}
      />
    );
  }
}

export default Ossuary;
<commit_msg>updates to ossuary module<commit_after>import React from 'react';
import Analyzer from 'parser/core/Analyzer';
import SPELLS from 'common/SPELLS';
import { formatPercentage } from 'common/format';
import StatisticBox, { STATISTIC_ORDER } from 'interface/others/StatisticBox';
import SpellIcon from 'common/SpellIcon';

const OSSUARY_RUNICPOWER_REDUCTION = 5;

class Ossuary extends Analyzer {
  dsWithOS = 0;
  dsWithoutOS = 0;

  get uptime() {
    return this.selectedCombatant.getBuffUptime(SPELLS.OSSUARY.id) / this.owner.fightDuration;
  }

  get buffedDeathStrikes() {
    return this.dsWithOS / (this.dsWithOS + this.dsWithoutOS);
  }

  on_byPlayer_cast(event) {
    if (event.ability.guid !== SPELLS.DEATH_STRIKE.id) return;

    if (this.selectedCombatant.hasBuff(SPELLS.OSSUARY.id)) {
      this.dsWithOS += 1;
    } else {
      this.dsWithoutOS += 1;

      event.meta = event.meta || {};
      event.meta.isInefficientCast = true;
      event.meta.inefficientCastReason = `This Death Strike cast was without Ossuary.`;
    }
  }

  get efficiencySuggestionThresholds() {
    return {
      actual: this.buffedDeathStrikes,
      isLessThan: {
        minor: 1,
        average: 0.9,
        major: .85,
      },
      style: 'percentage',
    };
  }

  get uptimeSuggestionThresholds() {
    return {
      actual: this.uptime,
      isLessThan: {
        minor: 0.95,
        average: 0.9,
        major: .8,
      },
      style: 'percentage',
    };
  }

  suggestions(when) {
    when(this.efficiencySuggestionThresholds)
        .addSuggestion((suggest, actual, recommended) => {
          return suggest('Your Ossuary usage can be improved. Avoid casting Death Strike while not having Ossuary up as you lose Runic Power by doing so.')
            .icon(SPELLS.OSSUARY.icon)
            .actual(`${formatPercentage(actual)}% Ossuary efficiency`)
            .recommended(`${formatPercentage(recommended)}% is recommended`);
        });
  }

  statistic() {
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.OSSUARY.id} />}
        value={`${ this.dsWithoutOS } / ${ this.dsWithOS + this.dsWithoutOS }`}
        label="Death Strikes without Ossuary"
        tooltip={(
          <>
            {this.dsWithoutOS * OSSUARY_RUNICPOWER_REDUCTION} RP wasted by casting them without Ossuary up.

            {this.dsWithOS * OSSUARY_RUNICPOWER_REDUCTION} RP saved by casting them with Ossuary up.

            {formatPercentage(this.uptime)}% uptime.
          </>
        )}
      />
    );
  }
  statisticOrder = STATISTIC_ORDER.CORE(3);
}

export default Ossuary;
<|endoftext|>