<commit_before>module.exports = ClientWorker;

var ioc = require('socket.io/node_modules/socket.io-client'),
    fs = require('fs-extra'),
    path = require('path'),
    extend = require('extend'),
    fileSize = require('filesize'),
    shortid = require('shortid'),
    Elapsed = require('elapsed'),
    CircularJSON = require('circular-json'),
    Build = require('../common/Build.js'),
    Msg = require('../common/Msg.js'),
    serverUtils = require('../common/serverUtils'),
    sig = 'CW';

function ClientWorker(conf) {
    this.conf = conf;
    this.id = shortid.generate();
    this.url = '{0}{1}{2}/{3}'.format(conf.protocol || 'http://', conf.server, conf.port === 80 ? '' : ':' + conf.port, 'client');
    this.location = path.resolve(conf.save || 'output');
    this.parseGroupFiles(conf);
    this.built = 0;
    process.on('exit', function() {
        if (this.socket.connected) {
            this.socket.disconnect();
        }
        this.socket.connected = false;
    }.bind(this));
}
ClientWorker.define({
    connect: function() {
        if (!this.socket) {
            console.log('Connecting client to:', this.url);
            this.socket = ioc.connect(this.url, {
                'reconnect': false,
                'force new connection': true, // <-- Add this!
                'sync disconnect on unload': true
            });
            this.socket.on('connect', this.onConnect.bind(this));
            this.socket.on('disconnect', this.onDisconnect.bind(this));
            this.socket.on('error', this.onError.bind(this));
            this.socket.on('build-success', this.onBuildSuccess.bind(this));
            this.socket.on('build-failed', this.onBuildFailed.bind(this));
            this.socket.on('log', function(msg) {
                    var message = new Msg(msg);
                    console.log(message.toString());
                }.bind(this));
        }
    },
    'onError': function(err) {
        console.log('Client Worker socket reported error:', err);
        if (this.conf.mode !== 'all' && !this.conf.agent) {
            console.log('Nothing to do. Exiting...');
            process.exit(1);
        } else {
            if (this.conf.agent) {
                console.log("However this process won't close because there is an agent worker on {0} which will be trying to reconnect".format(this.conf.agent));
            }
        }
    },
    'onConnect': function() {
        console.log('CLIENT CONNECTED requesting build on {0}'.format(this.conf.build));
        var client = this;
        var files = this.files;
        var platforms = this.conf.build;
        if (this.conf.number && this.conf.number.indexOf && this.conf.number.indexOf('0.') === 0) {
            this.conf.number = this.conf.number.substr(2);
        }
        var build = this.build = new Build({
            logs: [],
            status: 'uploading',
            name: this.conf.name,
            number: this.conf.number,
            started: new Date()
        }, client, null, platforms, files);
        build.id = client.id;
        extend(build.conf, this.conf);
        delete build.conf.agent;
        delete build.conf.serverInstance;
        console.log(build.conf);
        build.conf.logs.push(new Msg(build, this, sig, Msg.info, "The build is requested on {2}", platforms));

        client.socket.emit('register', {
            id: client.id,
            save: !!this.conf.save
        });
        if (!this.buildCompleted) {
            client.socket.emit('register-build', build.serialize());
            this.log(build, Msg.info, 'Reading {2} file{3}...', files.length, files.length === 1 ? '' : 's');
            serverUtils.readFiles(files, 'the servers-side sister of the cordova build client', function(err) {
                if (err) {
                    this.log(build, Msg.error, 'Error reading the input files\n{0}'.format(err));
                    client.socket.emit('fail-build', build.serialize());
                    throw 'Error reading the input files\n{0}'.format(err);
                }

                try {
                    //registering the client, sends our client id
                    var size = 0;
                    files.forEach(function(file) {
                        size += file && file.content && file.content.data && file.content.data.length || 0;
                    });
                    if (size) {
                        client.log(build, Msg.info, 'Uploading files to cordova build server...{2}', fileSize(size));
                    }
                    var serializedBuild = build.serialize({files: 1, content: 1});
                    client.socket.emit('upload-build', serializedBuild);
                } finally {
                    //free agent's memory of output files contents
                    serverUtils.freeMemFiles(files);
                }
            }.bind(this));
        }
    },
    'onDisconnect': function() {
        console.log('CLIENT DISCONNECTED');
        if (!this.conf.listen.server && !this.conf.listen.agent && !this.conf.listen.git) {
            process.exit();//the client worker should exit
        }
    },
    'onBuildFailed': function() {
        var client = this;
        if (++client.built >= client.conf.build.length) {
            client.disconnect();
        }
    },
    'onBuildSuccess': function(build) {
        var client = this;
        if (this.conf.save) {
            //var id = build.masterId || build.id;
            var files = build.outputFiles;
            var locationPath = path.resolve(client.location, build.id);//path.resolve(client.location, this.build.Id());
            var buildPath = path.resolve(locationPath, 'build.' + build.conf.platform + '.json');
            serverUtils.writeFiles(locationPath, files, 'the cordova build client {0}'.format(build.conf.platform), function(err) {
                if (err) {
                    client.log(build, Msg.error, 'error saving build output files on the cordova build server\n{3}', err);
                    return client.onBuildFailed();
                }
                serverUtils.cleanLastFolders(client.conf.keep, client.location + "/*", saveBuildLog);
            });
        } else {
            done();
        }
        function saveBuildLog(err) {
            if (err) {
                client.log(build, Msg.debug, 'Error while cleaning up last {2} folders in CLIENT output folder {3}:\n{4}', client.conf.keep, client.location, err);
            }
            fs.writeFile(buildPath, CircularJSON.stringify(build, null, 4), done);
        }
        function done(err) {
            if (err) {
                client.log(build, Msg.debug, 'Error while saving {2}:\n{3}', buildPath, err);
            }
            if (build.outputFiles) {
                serverUtils.freeMemFiles(build.outputFiles);
            }
            client.log(build, Msg.info, 'Build done! It took {2}.', new Date(build.conf.started).elapsed());
            if (++client.built >= client.conf.build.length) {
                client.disconnect();
            }
        }
    },
    disconnect: function() {
        try {
            this.buildCompleted = true;
            console.log('Client is disconnecting from the server since the build tasks completed.');
            this.socket.disconnect();
        } catch (e) {
            //@TODO: error-handling
        } finally {
            if (!this.conf.listen.server && !this.conf.listen.agent && !this.conf.listen.git) {
                process.exit(0);
            }
        }
    },
    log: function(build, priority, message, args) {
        if (/Command failed/i.test(message)) {
            try {
                throw new Error("client worker stack");
            } catch (e) {
                message += e.stack;
            }
        }
        Array.prototype.splice.call(arguments, 1, 0, this, sig);
        var msg = new Msg();
        msg.update.apply(msg, arguments);

        if (!this.socket.connected) {
            console.log(msg.toString());
        } else {
            this.socket.emit('log', msg);
        }
    },
    parseGroupFiles: function(conf) {
        var groups = ['files'].concat(conf.platforms);
        var files = [];
        groups.forEach(function(group, isGroup) {
            conf[group].forEach(function(file) {
                var f = file.split(/;|,/);
                f.forEach(function(file) {
                    if (file.length !== 0) {
                        files.push({file: file, group: isGroup ? group : null});
                    }
                });
            });
        });
        this.files = files;
    }
});<commit_msg>standardize and document ClientWorker<commit_after>/**
 * @name ClientWorker
 * @version 0.1
 * @fileoverview the client requests new builds from the server
 */

var ioc = require('socket.io/node_modules/socket.io-client');
var fs = require('fs-extra');
var path = require('path');
var extend = require('extend');
var fileSize = require('filesize');
var shortid = require('shortid');
var Elapsed = require('elapsed');
var CircularJSON = require('circular-json');
var Build = require('../common/Build.js');
var Msg = require('../common/Msg.js');
var serverUtils = require('../common/serverUtils');
var sig = 'CW';

/**
 * Initialise the ClientWorker
 *
 * @class
 * @param {Object} conf - command line options
 */
function ClientWorker(conf) {
    this.conf = conf;
    this.id = shortid.generate();
    this.url = '{0}{1}{2}/{3}'.format(conf.protocol || 'http://', conf.server, conf.port === 80 ? '' : ':' + conf.port, 'client');
    this.location = path.resolve(conf.save || 'output');
    this.parseGroupFiles(conf);
    this.built = 0;

    var _self = this;
    process.on('exit', function () {
        if (_self.socket.connected) {
            _self.socket.disconnect();
        }
        _self.socket.connected = false;
    });
}

/**
 * Connect to the server (create new socket)
 * and attach Listeners
 */
ClientWorker.prototype.connect = function () {
    if (!this.socket) {
        this.socket = ioc.connect(this.url, {
            'reconnect': false,
            'force new connection': true, // <-- Add this!
            'sync disconnect on unload': true
        });
        this.socket.on('connect', this.onConnect.bind(this));
        this.socket.on('disconnect', this.onDisconnect.bind(this));
        this.socket.on('error', this.onError.bind(this));
        this.socket.on('build-success', this.onBuildSuccess.bind(this));
        this.socket.on('build-failed', this.onBuildFailed.bind(this));
        this.socket.on('log', function (msg) {
            var message = new Msg(msg);
            console.log(message.toString());
        }.bind(this));
    }
};

/**
 * Handle socket errors
 *
 * @param {Object} [err] - error object or null
 */
ClientWorker.prototype.onError = function (err) {
    console.log('Client Worker socket reported error:', err);
    if (this.conf.mode !== 'all' && !this.conf.agent) {
        console.log('Nothing to do. Exiting...');
        process.exit(1);
    } else {
        if (this.conf.agent) {
            var _msg = "However this process won't close because there is an agent worker on {0} which will be trying to reconnect";
            console.log(_msg.format(this.conf.agent));
        }
    }
};

/**
 * Register client, create Build and initialise file uploads
 */
ClientWorker.prototype.onConnect = function () {
    console.log('CLIENT CONNECTED requesting build on {0}'.format(this.conf.build));
    var client = this;
    var files = this.files;
    var platforms = this.conf.build;
    if (this.conf.number && this.conf.number.indexOf && this.conf.number.indexOf('0.') === 0) {
        this.conf.number = this.conf.number.substr(2);
    }
    var build = this.build = new Build({
        logs: [],
        status: 'uploading',
        name: this.conf.name,
        number: this.conf.number,
        started: new Date()
    }, client, null, platforms, files);
    build.id = client.id;
    extend(build.conf, this.conf);
    delete build.conf.agent;
    delete build.conf.serverInstance;
    console.log(build.conf);
    build.conf.logs.push(new Msg(build, this, sig, Msg.info, "The build is requested on {2}", platforms));

    client.socket.emit('register', {
        id: client.id,
        save: !!this.conf.save
    });
    if (!this.buildCompleted) {
        client.socket.emit('register-build', build.serialize());
        this.log(build, Msg.info, 'Reading {2} file{3}...', files.length, files.length === 1 ? '' : 's');
        serverUtils.readFiles(files, 'the servers-side sister of the cordova build client', this.uploadFiles.bind(this));
    }
};

/**
 * Upload build files to server and free memory afterwards
 *
 * @param {Object} [err] - error object or null
 */
ClientWorker.prototype.uploadFiles = function (err) {
    if (err) {
        this.log(this.build, Msg.error, 'Error reading the input files\n{0}'.format(err));
        this.socket.emit('fail-build', build.serialize());
        throw 'Error reading the input files\n{0}'.format(err);
    }

    try {
        //registering the client, sends our client id
        var size = 0;
        this.files.forEach(function (file) {
            size += file && file.content && file.content.data && file.content.data.length || 0;
        });
        if (size) {
            this.log(this.build, Msg.info, 'Uploading files to cordova build server...{2}', fileSize(size));
        }
        var serializedBuild = this.build.serialize({files: 1, content: 1});
        this.socket.emit('upload-build', serializedBuild);
    } finally {
        //free agent's memory of output files contents
        serverUtils.freeMemFiles(this.files);
    }
};

/**
 * Exits the process when socket disconnects
 */
ClientWorker.prototype.onDisconnect = function () {
    console.log('CLIENT DISCONNECTED');
    if (!this.conf.listen.server && !this.conf.listen.agent && !this.conf.listen.git) {
        process.exit();//the client worker should exit
    }
};

/**
 * Disconnect the client when build fails
 */
ClientWorker.prototype.onBuildFailed = function () {
    var client = this;
    if (++client.built >= client.conf.build.length) {
        client.disconnect();
    }
};

/**
 * Disconnect the the socket and exit process
 */
ClientWorker.prototype.disconnect = function () {
    try {
        this.buildCompleted = true;
        console.log('Client is disconnecting from the server since the build tasks completed.');
        this.socket.disconnect();
    } catch (e) {
        //@TODO: error-handling
    } finally {
        if (!this.conf.listen.server && !this.conf.listen.agent && !this.conf.listen.git) {
            process.exit(0);
        }
    }
};

/**
 * Save outputfiles and build logs
 *
 * @param {Build} build - the suceeded build
 */
ClientWorker.prototype.onBuildSuccess = function (build) {
    var client = this;
    this.succBuild = build;
    if (this.conf.save) {
        //var id = build.masterId || build.id;
        var files = build.outputFiles;
        var locationPath = path.resolve(client.location, build.id);//path.resolve(client.location, this.build.Id());
        this.buildPath = path.resolve(locationPath, 'build.' + build.conf.platform + '.json');

        serverUtils.writeFiles(locationPath, files, 'the cordova build client {0}'.format(build.conf.platform), function (err) {
            if (err) {
                client.log(build, Msg.error, 'error saving build output files on the cordova build server\n{3}', err);
                return client.onBuildFailed();
            }
            serverUtils.cleanLastFolders(client.conf.keep, client.location + "/*", client.saveBuildLog.bind(this));
        });
    } else {
        this.done();
    }
};

/**
 * Write logfiles
 *
 * @param {Object} [err] - error object or null
 */
ClientWorker.prototype.saveBuildLog = function (err) {
    if (err) {
        this.log(this.succBuild, Msg.debug, 'Error while cleaning up last {2} folders in CLIENT output folder {3}:\n{4}', this.conf.keep, this.location, err);
    }
    fs.writeFile(this.buildPath, CircularJSON.stringify(this.succBuild, null, 4), this.done.bind(this));
};

/**
 * Free memory and disconnect if all builds are done
 *
 * @param {Object} [err] - error object or null
 */
ClientWorker.prototype.done = function (err) {
    if (err) {
        this.log(this.succBuild, Msg.debug, 'Error while saving {2}:\n{3}', this.buildPath, err);
    }
    if (this.succBuild.outputFiles) {
        serverUtils.freeMemFiles(this.succBuild.outputFiles);
    }
    this.log(this.succBuild, Msg.info, 'Build done! It took {2}.', new Date(this.succBuild.conf.started).elapsed());
    if (++this.built >= this.conf.build.length) {
        this.disconnect();
    }
};

/**
 * Output to console and emit to server
 *
 * @param {Build} build    - the build which the log message refers to
 * @param {int} priority   - priority of the log message (1-6)
 * @param {String} message - the log message
 * @param {*} args         - any additional arguments (passed to Msg.update
 */
ClientWorker.prototype.log = function (build, priority, message, args) {
    if (/Command failed/i.test(message)) {
        try {
            throw new Error("client worker stack");
        } catch (e) {
            message += e.stack;
        }
    }
    Array.prototype.splice.call(arguments, 1, 0, this, sig);
    var msg = new Msg();
    msg.update.apply(msg, arguments);

    if (!this.socket.connected) {
        console.log(msg.toString());
    } else {
        this.socket.emit('log', msg);
    }
};

/**
 * Parse the groupfiles (according to conf.platforms)
 *
 * @param {Object} conf - command line options
 */
ClientWorker.prototype.parseGroupFiles = function (conf) {
    var groups = ['files'].concat(conf.platforms);
    var files = [];
    groups.forEach(function (group, isGroup) {
        conf[group].forEach(function (file) {
            var f = file.split(/;|,/);
            f.forEach(function (file) {
                if (file.length !== 0) {
                    files.push({file: file, group: isGroup ? group : null});
                }
            });
        });
    });
    this.files = files;
};

module.exports = ClientWorker;<|endoftext|>


<commit_before>import React from 'react'
import { Route } from 'react-router'
import firebase from 'APP/fire'
const db = firebase.database()
import AdventureUsTimeline from './Timeline'
import moment from 'moment'

// this is where we make our connection to the database, we need:
// user name, user startDate for this trip, user endDate for this trip (currentTripUserStartDate, currentTripUserEndDate)
// loop over buddies in trip and grab info
// From trip (ie. tripRef) we need: buddiesid -> id, buddiesid -> group, availabilityStart -> start_time, availabilityEnd -> end_time
// Note: title in an bodyData group will be the name of a buddy or self

export default class TimelineIndex extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      groups: [],
      items: []
    }
    this.listenFor = this.listenFor.bind(this)
  }
  componentWillUnmount() {
    this.unsubscribe && this.unsubscribe()
  }
  componentDidMount() {
    this.props.whichTab && this.props.whichTab === 'Buddies' ?
      this.listenFor('buddies') // For Buddies Slider Timelne View
      :
      this.listenFor('ideas') // For Ideas Box Timeline View
  }

  /* For mysteriosu reasons,  DidMount works but WillReceiveProps below did not .... Learning lesson?? */
  // It's not getting in oldProps too
  componentWillReceiveProps(nextProps) {
    nextProps.whichTab && nextProps.whichTab === 'Buddies' ?
      this.listenFor('buddies') // For Buddies Slider Timelne View
      :
      this.listenFor('ideas') // For Ideas Box Timeline View
  }
  buddiesData = (snapshot) => {
    const dbObject = snapshot.val()['buddies']
    let branchIds = [], bodyData = [], namesData = []
    if (dbObject) {
      branchIds = Object.keys(dbObject)
      bodyData = branchIds.map(key => {
        return {
          id: key,
          group: key,
          title: dbObject[key].status.text,
          start_time: moment(dbObject[key].startDate),
          end_time: moment(dbObject[key].endDate),
          canResize: key === this.props.userId ? 'both' : false,
          canChangeGroup: false
        }
      })
      namesData = Object.keys(dbObject).map((key) => ({ id: key, title: dbObject[key].name }))
    }
    return { bodyData, namesData }
  }

  ideasData = (snapshot) => {
    const dbObject = snapshot.val()['ideas']
    let branchIds = [], bodyData = [], namesData = []
    if (dbObject) {
      branchIds = Object.keys(dbObject)
      // now map over the db Ids and grab info as necessary depending
      // on the branch typs.
      bodyData = branchIds.map(key => {
        return {
          id: key,
          group: dbObject[key].category.text,
          title: dbObject[key].ideaName,
          start_time: moment(dbObject[key].startDate),
          end_time: moment(dbObject[key].endDate),
          canResize: dbObject[key].addedBy === this.props.userId ? 'both' : false,
          canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
        }
      })
      const categoryArr = []
      namesData = Object.keys(dbObject).map((key) => {
        if (!categoryArr.includes(dbObject[key].category.text)) {
          categoryArr.push(dbObject[key].category.text)
          return ({
            id: dbObject[key].category.text,
            title: dbObject[key].category.text
          })
        }
      })
        .filter((key) => key !== undefined)
    }
    return { bodyData, namesData }
  }

  listenFor(branch) {
    if (this.unsubscribe) this.unsubscribe()
    // Getting data from branch part of db

    let bodyData = [], namesData = []
    const listener = this.props.tripRef.on('value', snapshot => {
      const dbObject = snapshot.val()[branch]
      var branchIds = []
      if (dbObject) {
        branchIds = Object.keys(dbObject)
      }
      // now map over the db Ids and grab info as necessary depending
      // on the branch typs.
      if (branch === 'buddies') { // Make Buddies Timeline Data
        bodyData = branchIds.map(key => {
          //
          return {
            id: key,
            group: key,
            title: dbObject[key].status.text,
            start_time: moment(dbObject[key].startDate),
            end_time: moment(dbObject[key].endDate),
            canResize: key === this.props.userId ? 'both' : false,
            canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
          }
        })
        if (dbObject) {
          namesData = Object.keys(dbObject).map((key) =>
            ({
              id: key,
              title: dbObject[key].name
            }))
        }
      } else { //  Make Ideas Timeline Data
        bodyData = branchIds.map(key => {
          return {
            id: key,
            group: dbObject[key].category.text,
            title: dbObject[key].ideaName,
            start_time: moment(dbObject[key].startDate),
            end_time: moment(dbObject[key].endDate),
            canResize: dbObject[key].addedBy === this.props.userId ? 'both' : false,
            canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
          }
        })
        // use this categoryArr to hold the categories,
        // if it does not contain the category on the dbObject,
        //    then add the object to the namesData array in the map
        // otherwise it is a duplicte, so do not add that dbObject item in the map
        const categoryArr = []
        if (dbObject) {
          namesData = Object.keys(dbObject).map((key) => {
            if (!categoryArr.includes(dbObject[key].category.text)) {
              categoryArr.push(dbObject[key].category.text)
              return ({
                // id: key,
                id: dbObject[key].category.text,
                title: dbObject[key].category.text
              })
            }
          })
            .filter((key) => key !== undefined)
        }
      }
      //       this.setState({groups: namesData})
      //       this.setState({items: bodyData})
      //     })
      //     this.unsubscribe = () => this.props.tripRef.off('value', listener)
      // HACK: the following setTimeout is a hack to force initial rendering of the timeline data. The data is there and shows as soon as we do anything to the view
      setTimeout(() => {
        const listener = this.props.tripRef.on('value', snapshot => {
          const data = (branch === 'buddies' ? this.buddiesData(snapshot) : this.ideasData(snapshot))
          this.setState({
            groups: data['namesData'],
            items: data['bodyData']
          })
        })
        this.unsubscribe = () => this.props.tripRef.off('value', listener)
      }, 10)

      render() {
        return (

            <AdventureUsTimeline
              groups={this.state.groups}
              items={this.state.items}
              tripRef={this.props.tripRef}
              whichTab={this.props.whichTab}
            />

        )
      }
    }
<commit_msg>functioning timeline<commit_after>import React from 'react'
import { Route } from 'react-router'
import firebase from 'APP/fire'
const db = firebase.database()
import AdventureUsTimeline from './Timeline'
import moment from 'moment'

// this is where we make our connection to the database, we need:
// user name, user startDate for this trip, user endDate for this trip (currentTripUserStartDate, currentTripUserEndDate)
// loop over buddies in trip and grab info
// From trip (ie. tripRef) we need: buddiesid -> id, buddiesid -> group, availabilityStart -> start_time, availabilityEnd -> end_time
// Note: title in an bodyData group will be the name of a buddy or self

export default class TimelineIndex extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      groups: [],
      items: []
    }
    this.listenFor = this.listenFor.bind(this)
  }
  componentWillUnmount() {
    this.unsubscribe && this.unsubscribe()
  }
  componentDidMount() {
    this.props.whichTab && this.props.whichTab === 'Buddies' ?
      this.listenFor('buddies') // For Buddies Slider Timelne View
      :
      this.listenFor('ideas') // For Ideas Box Timeline View
  }

  /* For mysteriosu reasons,  DidMount works but WillReceiveProps below did not .... Learning lesson?? */
  // It's not getting in oldProps too
  componentWillReceiveProps(nextProps) {
    nextProps.whichTab && nextProps.whichTab === 'Buddies' ?
      this.listenFor('buddies') // For Buddies Slider Timelne View
      :
      this.listenFor('ideas') // For Ideas Box Timeline View
  }
  buddiesData = (snapshot) => {
    const dbObject = snapshot.val()['buddies']
    let branchIds = [], bodyData = [], namesData = []
    if (dbObject) {
      branchIds = Object.keys(dbObject)
      bodyData = branchIds.map(key => {
        return {
          id: key,
          group: key,
          title: dbObject[key].status.text,
          start_time: moment(dbObject[key].startDate),
          end_time: moment(dbObject[key].endDate),
          canResize: key === this.props.userId ? 'both' : false,
          canChangeGroup: false
        }
      })
      namesData = Object.keys(dbObject).map((key) => ({id: key, title: dbObject[key].name}))
    }
    return {bodyData, namesData}
  }

  ideasData = (snapshot) => {
    const dbObject = snapshot.val()['ideas']
    let branchIds = [], bodyData = [], namesData = []
    if (dbObject) {
      branchIds = Object.keys(dbObject)
      // now map over the db Ids and grab info as necessary depending
      // on the branch typs.
      bodyData = branchIds.map(key => {
        return {
          id: key,
          group: dbObject[key].category.text,
          title: dbObject[key].ideaName,
          start_time: moment(dbObject[key].startDate),
          end_time: moment(dbObject[key].endDate),
          canResize: dbObject[key].addedBy === this.props.userId ? 'both' : false,
          canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
        }
      })
      const categoryArr = []
      namesData = Object.keys(dbObject).map((key) => {
        if (!categoryArr.includes(dbObject[key].category.text)) {
          categoryArr.push(dbObject[key].category.text)
          return ({
            id: dbObject[key].category.text,
            title: dbObject[key].category.text
          })
        }
      })
      .filter((key) => key !== undefined)
    }
    return {bodyData, namesData}
  }

  listenFor(branch) {
    if (this.unsubscribe) this.unsubscribe()
    // Getting data from branch part of db

    let bodyData = [], namesData = []
    const listener = this.props.tripRef.on('value', snapshot => {
      const dbObject = snapshot.val()[branch]
      var branchIds = []
      if (dbObject) {
        branchIds = Object.keys(dbObject)
      }
      // now map over the db Ids and grab info as necessary depending
      // on the branch typs.
      if (branch === 'buddies') { // Make Buddies Timeline Data
        bodyData = branchIds.map(key => {
          //
          return {
            id: key,
            group: key,
            title: dbObject[key].status.text,
            start_time: moment(dbObject[key].startDate),
            end_time: moment(dbObject[key].endDate),
            canResize: key === this.props.userId ? 'both' : false,
            canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
          }
        })
        if (dbObject) {
          namesData = Object.keys(dbObject).map((key) =>
            ({
              id: key,
              title: dbObject[key].name
            }))
        }
      } else { //  Make Ideas Timeline Data
        bodyData = branchIds.map(key => {
          return {
            id: key,
            group: dbObject[key].category.text,
            title: dbObject[key].ideaName,
            start_time: moment(dbObject[key].startDate),
            end_time: moment(dbObject[key].endDate),
            canResize: dbObject[key].addedBy === this.props.userId ? 'both' : false,
            canChangeGroup: false // if we oneday get to items do conditional checks for item categories here
          }
        })
        // use this categoryArr to hold the categories,
        // if it does not contain the category on the dbObject,
        //    then add the object to the namesData array in the map
        // otherwise it is a duplicte, so do not add that dbObject item in the map
        const categoryArr = []
        if (dbObject) {
          namesData = Object.keys(dbObject).map((key) => {
            if (!categoryArr.includes(dbObject[key].category.text)) {
              categoryArr.push(dbObject[key].category.text)
              return ({
                // id: key,
                id: dbObject[key].category.text,
                title: dbObject[key].category.text
              })
            }
          })
            .filter((key) => key !== undefined)
        }
      }
//       this.setState({groups: namesData})
//       this.setState({items: bodyData})
    })
//     this.unsubscribe = () => this.props.tripRef.off('value', listener)
    // HACK: the following setTimeout is a hack to force initial rendering of the timeline data. The data is there and shows as soon as we do anything to the view
    setTimeout(() => {
      const listener = this.props.tripRef.on('value', snapshot => {
        const data = (branch === 'buddies' ? this.buddiesData(snapshot) : this.ideasData(snapshot))
        this.setState({
          groups: data['namesData'],
          items: data['bodyData']
        })
      })
      this.unsubscribe = () => this.props.tripRef.off('value', listener) // make sure we are removing both listeners here.
    }, 10)
  }

  render() {
    return (

        <AdventureUsTimeline
          groups={this.state.groups}
          items={this.state.items}
          tripRef={this.props.tripRef}
          whichTab={this.props.whichTab}
          />

    )
  }
}
<|endoftext|>


<commit_before>// this function Copyright (C) 2015 Ryan Morr
// http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/
(function(win){
    'use strict';

    var listeners = [],
    doc = win.document,
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;

    function ready(selector, fn){
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if(!observer){
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }

    function check(){
        // Check the DOM for elements matching a stored selector
        for(var i = 0, len = listeners.length, listener, elements; i < len; i++){
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for(var j = 0, jLen = elements.length, element; j < jLen; j++){
                element = elements[j];
                // Make sure the callback isn't invoked with the
                // same element more than once
                if(!element.ready){
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;

})(this);

if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) {
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

function pad(num, size) {
    var s = num + "";
    while (s.length < size) s = "0" + s;
    return s;
}

// for date suffix checking
function include(arr, obj) {
    return (arr.indexOf(obj) != -1);
}

// human readable notation for days and months
// to be used for the projected time and date
const all_days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const all_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

function main() {
	// remove the total duration container before injecting it
	var total_duration = document.getElementById('total-duration-container');
	if (total_duration) { total_duration.parentNode.removeChild(total_duration); }

	// sum all timestamps to get the playlist's total number of seconds
	var total_seconds = 0;
	var timestamps = document.querySelectorAll(".timestamp > *");

	Array.prototype.forEach.call(timestamps, function(element, i) {
		var value = element.innerText;
		var parts = value.split(":");
		if (parts.length == 3) { total_seconds += parseInt(parts[0]*60*60) + parseInt(parts[1]*60) + parseInt(parts[2]); }
		else { total_seconds += parseInt(parts[0]*60) + parseInt(parts[1]); }
	});

	// ignore playlists without any videos
	if (total_seconds == 0) { return; }

	// add a new container with the playlist's total duration
	var years = Math.floor(total_seconds / 31536000);
	var days = Math.floor((total_seconds % 31536000) / 86400);
	var hours = Math.floor(((total_seconds % 31536000) % 86400) / 3600);
	var minutes = Math.floor((((total_seconds % 31536000) % 86400) % 3600) / 60);
	var seconds = (((total_seconds % 31536000) % 86400) % 3600) % 60;

	// version 1: the numerical version on the righthand side of the container
	var total_duration_short = "";
	if (years > 0) { total_duration_short += years + ":"; }
	if (days > 0) { total_duration_short += days + ":"; }
	if (hours > 0) { total_duration_short += pad(hours, 2) + ":"; }
	total_duration_short += pad(minutes, 2) + ":";
	total_duration_short += pad(seconds, 2);

	// version 2: the human readable version on the lefthand side of the container
	play_all_link = document.getElementsByClassName("playlist-play-all")[0].getAttribute("href");
	var total_duration_long = "
";
	if (years > 0) { total_duration_long += years + " years, "; }
	if (days > 0) { total_duration_long += days + " days, "; }
	if (hours > 0) { total_duration_long += hours + " hours, "; }
	total_duration_long += minutes + " minutes and ";
	total_duration_long += seconds + " seconds.";
	total_duration_long += ' That\'s for a total of  videos ( deleted and/or private videos were excluded).
';
	total_duration_long += '
Start watching now and you will finish before you know it. Click here to begin.
';

	// inject container
	document.querySelector('div.branded-page-v2-primary-col > div.yt-card').insertAdjacentHTML('afterend', '\

 \

 \

 \

 \

 \
						Total playlist time \

 \
						' + total_duration_long + ' \
						\

 \
						' + total_duration_short + ' \

 \

 \

 \

 \

 \
	');

	// use the playlist's total number of seconds to calculate the point in time at which the user will have finished watching the entire playlist
	var finished_watching = new Date();
	finished_watching.setSeconds(finished_watching.getSeconds() + total_seconds);

	// 1nd, 2nd, 3rd, 4th, etc.
	var suffix = "th";
	if (include([1, 21], finished_watching.getDate())) { suffix = "st"; }
	else if (include([2, 22], finished_watching.getDate())) { suffix = "nd"; }
	else if (include([3, 23], finished_watching.getDate())) { suffix = "rd"; }

	// inject projected time and date in the new container
	var finished_watching_formatted = "{0} {1} {2}{3}, {4} at {5}:{6}:{7}".format(
		all_days[finished_watching.getDay()],
		all_months[finished_watching.getMonth()],
		finished_watching.getDate(),
		suffix,
		finished_watching.getFullYear(),
		pad(finished_watching.getHours(), 2),
		pad(finished_watching.getMinutes(), 2),
		pad(finished_watching.getSeconds(), 2)
	);
	document.getElementById("finished_watching").innerText = "on " + finished_watching_formatted;

	// to do: refresh every second/minute
	// setTimeout(fn, 1000, total_seconds);

	// inject video counts in the new container
	var available_videos = document.getElementsByClassName("timestamp").length;
	var total_videos = document.getElementsByClassName("pl-video").length;
	var unavailable_videos = total_videos - available_videos;
	document.getElementById('available_videos').innerText = available_videos;
	document.getElementById('unavailable_videos').innerText = unavailable_videos;
}

ready('#pl-video-list', function(element) {
	// deal with the possibility of a node with id 'pl-video-list' on a page other than /playlist
	if (/^\/playlist.*/.test(window.location.pathname) === false) {	return;	}

	// execute on element load
	main();

	// execute on element change
	var videos = document.getElementById('pl-video-list');
	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			main();
		});
	});
	observer.observe(videos, { childList: true });
});<commit_msg>extra comments<commit_after>// this function Copyright (C) 2015 Ryan Morr
// http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/
(function(win){
    'use strict';

    var listeners = [],
    doc = win.document,
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;

    function ready(selector, fn){
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if(!observer){
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }

    function check(){
        // Check the DOM for elements matching a stored selector
        for(var i = 0, len = listeners.length, listener, elements; i < len; i++){
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for(var j = 0, jLen = elements.length, element; j < jLen; j++){
                element = elements[j];
                // Make sure the callback isn't invoked with the
                // same element more than once
                if(!element.ready){
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;

})(this);

if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) {
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

function pad(num, size) {
    var s = num + "";
    while (s.length < size) s = "0" + s;
    return s;
}

// for date suffix checking
function include(arr, obj) {
    return (arr.indexOf(obj) != -1);
}

// human readable notation for days and months
// to be used for the projected time and date
const all_days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const all_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

function main() {
	// remove the total duration container before injecting it
	var total_duration = document.getElementById('total-duration-container');
	if (total_duration) { total_duration.parentNode.removeChild(total_duration); }

	// sum all timestamps to get the playlist's total number of seconds
	var total_seconds = 0;
	var timestamps = document.querySelectorAll(".timestamp > *");

	Array.prototype.forEach.call(timestamps, function(element, i) {
		var value = element.innerText;

		// finding 3 parts means H:M:SS
		// finding 2 parts means M:SS
		var parts = value.split(":");
		if (parts.length == 3) { total_seconds += parseInt(parts[0]*60*60) + parseInt(parts[1]*60) + parseInt(parts[2]); }
		else { total_seconds += parseInt(parts[0]*60) + parseInt(parts[1]); }
	});

	// ignore playlists without any videos
	if (total_seconds == 0) { return; }

	// add a new container with the playlist's total duration
	var years = Math.floor(total_seconds / 31536000);
	var days = Math.floor((total_seconds % 31536000) / 86400);
	var hours = Math.floor(((total_seconds % 31536000) % 86400) / 3600);
	var minutes = Math.floor((((total_seconds % 31536000) % 86400) % 3600) / 60);
	var seconds = (((total_seconds % 31536000) % 86400) % 3600) % 60;

	// version 1: the numerical version on the righthand side of the container
	var total_duration_short = "";
	if (years > 0) { total_duration_short += years + ":"; }
	if (days > 0) { total_duration_short += days + ":"; }
	if (hours > 0) { total_duration_short += pad(hours, 2) + ":"; }
	total_duration_short += pad(minutes, 2) + ":";
	total_duration_short += pad(seconds, 2);

	// version 2: the human readable version on the lefthand side of the container
	play_all_link = document.getElementsByClassName("playlist-play-all")[0].getAttribute("href");
	var total_duration_long = "
";
	if (years > 0) { total_duration_long += years + " years, "; }
	if (days > 0) { total_duration_long += days + " days, "; }
	if (hours > 0) { total_duration_long += hours + " hours, "; }
	total_duration_long += minutes + " minutes and ";
	total_duration_long += seconds + " seconds.";
	total_duration_long += ' That\'s for a total of  videos ( deleted and/or private videos were excluded).
';
	total_duration_long += '
Start watching now and you will finish before you know it. Click here to begin.
';

	// inject container
	document.querySelector('div.branded-page-v2-primary-col > div.yt-card').insertAdjacentHTML('afterend', '\

 \

 \

 \

 \

 \
						Total playlist time \

 \
						' + total_duration_long + ' \
						\

 \
						' + total_duration_short + ' \

 \

 \

 \

 \

 \
	');

	// use the playlist's total number of seconds to calculate the point in time at which the user will have finished watching the entire playlist
	var finished_watching = new Date();
	finished_watching.setSeconds(finished_watching.getSeconds() + total_seconds);

	// 1nd, 2nd, 3rd, 4th, etc.
	var suffix = "th";
	if (include([1, 21], finished_watching.getDate())) { suffix = "st"; }
	else if (include([2, 22], finished_watching.getDate())) { suffix = "nd"; }
	else if (include([3, 23], finished_watching.getDate())) { suffix = "rd"; }

	// inject projected time and date in the new container
	var finished_watching_formatted = "{0} {1} {2}{3}, {4} at {5}:{6}:{7}".format(
		all_days[finished_watching.getDay()],
		all_months[finished_watching.getMonth()],
		finished_watching.getDate(),
		suffix,
		finished_watching.getFullYear(),
		pad(finished_watching.getHours(), 2),
		pad(finished_watching.getMinutes(), 2),
		pad(finished_watching.getSeconds(), 2)
	);
	document.getElementById("finished_watching").innerText = "on " + finished_watching_formatted;

	// to do: refresh every second/minute
	// setTimeout(fn, 1000, total_seconds);

	// inject video counts in the new container
	var available_videos = document.getElementsByClassName("timestamp").length;
	var total_videos = document.getElementsByClassName("pl-video").length;
	var unavailable_videos = total_videos - available_videos;
	document.getElementById('available_videos').innerText = available_videos;
	document.getElementById('unavailable_videos').innerText = unavailable_videos;
}

ready('#pl-video-list', function(element) {
	// deal with the possibility of a node with id 'pl-video-list' on a page other than /playlist
	if (/^\/playlist.*/.test(window.location.pathname) === false) {	return;	}

	// execute on element load
	main();

	// execute on element change
	var videos = document.getElementById('pl-video-list');
	var observer = new MutationObserver(function(mutations) {
		mutations.forEach(function(mutation) {
			main();
		});
	});
	observer.observe(videos, { childList: true });
});<|endoftext|>


<commit_before>// NOTE: maybe need to reset these somewhere? For now just going with this
jest.mock('app/services/Firebase', () => {
    return {
        analytics: () => {
            return {
                logEvent: () => {

                }
            };
        }
    };
});

import * as sut from 'app/utility/Analytics';
import { AppState } from 'react-native';
import * as AppStateSelectors from 'app/redux/selectors/AppStateSelectors';
import * as ScannedDevicesSelectors from 'app/redux/selectors/ScannedDevicesSelectors';
import * as ConnectedDeviceStatusSelectors from 'app/redux/selectors/ConnectedDeviceStatusSelectors';
import * as SetsSelectors from 'app/redux/selectors/SetsSelectors';

describe('Analytics', () => {
    let params = null;
    const realTron = console.tron;
    const realGetScreenStatus = AppStateSelectors.getScreenStatus;
    const realGetScannedDevices = ScannedDevicesSelectors.getScannedDevices;
    const realGetConnectedDeviceStatus = ConnectedDeviceStatusSelectors.getConnectedDeviceStatus;
    const realGetIsWorkoutEmpty = SetsSelectors.getIsWorkoutEmpty;

    beforeAll(() => {
        console.tron = console.tron = {
            log: () => null,
            display: () => null
        };
    });

    beforeEach(() => {
        params = {};
    });

    afterEach(() => {
        AppStateSelectors.getScreenStatus = realGetScreenStatus;
        ScannedDevicesSelectors.getScannedDevices = realGetScannedDevices;
        ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = realGetConnectedDeviceStatus;
        SetsSelectors.getIsWorkoutEmpty = realGetIsWorkoutEmpty;
    });

    afterAll(() => {
        console.tron = realTron;
    });

    describe('logEventWithAppState', () => {
        describe('is_screen_locked', () => {
            beforeEach(() => {
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('false', () => {
                AppStateSelectors.getScreenStatus = () => 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_screen_locked).toBe(false);
            });

            test('true', () => {
                AppStateSelectors.getScreenStatus = () => '';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_screen_locked).toBe(true);
            });
        });

        describe('is_app_active', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('true if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(true);
            });

            test('false if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(false);
            });

            test('false is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(false);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBeUndefined();
            });
        });

        describe('is_app_in_background', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('false if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(false);
            });

            test('true if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(true);
            });

            test('true is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(true);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBeUndefined();
            });
        });

        describe('is_app_inactive', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('false if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(false);
            });

            test('true if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(true);
            });

            test('false is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(false);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBeUndefined();
            });
        });

        describe('scanned_devices', () => {
            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('0', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('');
            });

            test('1', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000'];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('3000');
            });

            test('2', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000', 'OB 3021'];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('30003021');
            });

            test('26', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [
                    'OB 3001', 'OB 3002', 'OB 3003', 'OB 3004', 'OB 3005', 'OB 3006', 'OB 3007', 'OB 3008', 'OB 3009', 'OB 3010',
                    'OB 2001', 'OB 2002', 'OB 2003', 'OB 2004', 'OB 2005', 'OB 2006', 'OB 2007', 'OB 2008', 'OB 2009', 'OB 2010',
                    'OB 1001', 'OB 1002', 'OB 1003', 'OB 1004', 'OB 1005', 'OB 1006'
                ];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('3001300230033004300530063007300830093010200120022003200420052006200720082009201010011002100310041005');
            });
        });

        describe('num_scanned_devices', () => {

        });

        describe('is_bluetooth_on', () => {

            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('false if bluetooth off', () => {
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => 'BLUETOOTH_OFF';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_bluetooth_on).toBe(false);
            });

            test('true otherwise', () => {
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_bluetooth_on).toBe(true);
            });
        });

        describe('is_workout_in_progress', () => {
            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
            });

            test('true', () => {
                SetsSelectors.getIsWorkoutEmpty = () => false;

                sut.logEventWithAppState(null, params, null);

                expect(params.is_workout_in_progress).toBe(true);
            });

            test('false', () => {
                SetsSelectors.getIsWorkoutEmpty = () => true;

                sut.logEventWithAppState(null, params, null);

                expect(params.is_workout_in_progress).toBe(false);
            });
        });
    });
});
<commit_msg>tested num_scanned_devices<commit_after>// NOTE: maybe need to reset these somewhere? For now just going with this
jest.mock('app/services/Firebase', () => {
    return {
        analytics: () => {
            return {
                logEvent: () => {

                }
            };
        }
    };
});

import * as sut from 'app/utility/Analytics';
import { AppState } from 'react-native';
import * as AppStateSelectors from 'app/redux/selectors/AppStateSelectors';
import * as ScannedDevicesSelectors from 'app/redux/selectors/ScannedDevicesSelectors';
import * as ConnectedDeviceStatusSelectors from 'app/redux/selectors/ConnectedDeviceStatusSelectors';
import * as SetsSelectors from 'app/redux/selectors/SetsSelectors';

describe('Analytics', () => {
    let params = null;
    const realTron = console.tron;
    const realGetScreenStatus = AppStateSelectors.getScreenStatus;
    const realGetScannedDevices = ScannedDevicesSelectors.getScannedDevices;
    const realGetConnectedDeviceStatus = ConnectedDeviceStatusSelectors.getConnectedDeviceStatus;
    const realGetIsWorkoutEmpty = SetsSelectors.getIsWorkoutEmpty;

    beforeAll(() => {
        console.tron = console.tron = {
            log: () => null,
            display: () => null
        };
    });

    beforeEach(() => {
        params = {};
    });

    afterEach(() => {
        AppStateSelectors.getScreenStatus = realGetScreenStatus;
        ScannedDevicesSelectors.getScannedDevices = realGetScannedDevices;
        ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = realGetConnectedDeviceStatus;
        SetsSelectors.getIsWorkoutEmpty = realGetIsWorkoutEmpty;
    });

    afterAll(() => {
        console.tron = realTron;
    });

    describe('logEventWithAppState', () => {
        describe('is_screen_locked', () => {
            beforeEach(() => {
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('false', () => {
                AppStateSelectors.getScreenStatus = () => 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_screen_locked).toBe(false);
            });

            test('true', () => {
                AppStateSelectors.getScreenStatus = () => '';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_screen_locked).toBe(true);
            });
        });

        describe('is_app_active', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('true if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(true);
            });

            test('false if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(false);
            });

            test('false is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBe(false);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_active).toBeUndefined();
            });
        });

        describe('is_app_in_background', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('false if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(false);
            });

            test('true if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(true);
            });

            test('true is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBe(true);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_in_background).toBeUndefined();
            });
        });

        describe('is_app_inactive', () => {
            let realAppCurrentState = null;

            beforeEach(() => {
                realAppCurrentState = AppState.currentState;
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            afterEach(() => {
                AppState.currentState = realAppCurrentState;
            });

            test('false if active', () => {
                AppState.currentState = 'active';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(false);
            });

            test('true if inactive', () => {
                AppState.currentState = 'inactive';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(true);
            });

            test('false is background', () => {
                AppState.currentState = 'background';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBe(false);
            });

            test('undefined otherwise', () => {
                AppState.currentState = 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_app_inactive).toBeUndefined();
            });
        });

        describe('scanned_devices', () => {
            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('0', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('');
            });

            test('1', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000'];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('3000');
            });

            test('2', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000', 'OB 3021'];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('30003021');
            });

            test('25', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [
                    'OB 3001', 'OB 3002', 'OB 3003', 'OB 3004', 'OB 3005', 'OB 3006', 'OB 3007', 'OB 3008', 'OB 3009', 'OB 3010',
                    'OB 2001', 'OB 2002', 'OB 2003', 'OB 2004', 'OB 2005', 'OB 2006', 'OB 2007', 'OB 2008', 'OB 2009', 'OB 2010',
                    'OB 1001', 'OB 1002', 'OB 1003', 'OB 1004', 'OB 1005'
                ];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('3001300230033004300530063007300830093010200120022003200420052006200720082009201010011002100310041005');
            });

            test('26', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [
                    'OB 3001', 'OB 3002', 'OB 3003', 'OB 3004', 'OB 3005', 'OB 3006', 'OB 3007', 'OB 3008', 'OB 3009', 'OB 3010',
                    'OB 2001', 'OB 2002', 'OB 2003', 'OB 2004', 'OB 2005', 'OB 2006', 'OB 2007', 'OB 2008', 'OB 2009', 'OB 2010',
                    'OB 1001', 'OB 1002', 'OB 1003', 'OB 1004', 'OB 1005', 'OB 1006'
                ];

                sut.logEventWithAppState(null, params, null);

                expect(params.scanned_devices).toBe('3001300230033004300530063007300830093010200120022003200420052006200720082009201010011002100310041005');
            });
        });

        describe('num_scanned_devices', () => {
            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('0', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [];

                sut.logEventWithAppState(null, params, null);

                expect(params.num_scanned_devices).toBe(0);
            });

            test('1', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000'];

                sut.logEventWithAppState(null, params, null);

                expect(params.num_scanned_devices).toBe(1);
            });

            test('2', () => {
                ScannedDevicesSelectors.getScannedDevices = () => ['OB 3000', 'OB 3021'];

                sut.logEventWithAppState(null, params, null);

                expect(params.num_scanned_devices).toBe(2);
            });

            test('25', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [
                    'OB 3001', 'OB 3002', 'OB 3003', 'OB 3004', 'OB 3005', 'OB 3006', 'OB 3007', 'OB 3008', 'OB 3009', 'OB 3010',
                    'OB 2001', 'OB 2002', 'OB 2003', 'OB 2004', 'OB 2005', 'OB 2006', 'OB 2007', 'OB 2008', 'OB 2009', 'OB 2010',
                    'OB 1001', 'OB 1002', 'OB 1003', 'OB 1004', 'OB 1005'
                ];

                sut.logEventWithAppState(null, params, null);

                expect(params.num_scanned_devices).toBe(25);
            });

            test('26', () => {
                ScannedDevicesSelectors.getScannedDevices = () => [
                    'OB 3001', 'OB 3002', 'OB 3003', 'OB 3004', 'OB 3005', 'OB 3006', 'OB 3007', 'OB 3008', 'OB 3009', 'OB 3010',
                    'OB 2001', 'OB 2002', 'OB 2003', 'OB 2004', 'OB 2005', 'OB 2006', 'OB 2007', 'OB 2008', 'OB 2009', 'OB 2010',
                    'OB 1001', 'OB 1002', 'OB 1003', 'OB 1004', 'OB 1005', 'OB 1006'
                ];

                sut.logEventWithAppState(null, params, null);

                expect(params.num_scanned_devices).toBe(25);
            });
        });

        describe('is_bluetooth_on', () => {

            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                SetsSelectors.getIsWorkoutEmpty = () => true;
            });

            test('false if bluetooth off', () => {
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => 'BLUETOOTH_OFF';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_bluetooth_on).toBe(false);
            });

            test('true otherwise', () => {
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => 'foobar';

                sut.logEventWithAppState(null, params, null);

                expect(params.is_bluetooth_on).toBe(true);
            });
        });

        describe('is_workout_in_progress', () => {
            beforeEach(() => {
                AppStateSelectors.getScreenStatus = () => '';
                ScannedDevicesSelectors.getScannedDevices = () => [];
                ConnectedDeviceStatusSelectors.getConnectedDeviceStatus = () => '';
            });

            test('true', () => {
                SetsSelectors.getIsWorkoutEmpty = () => false;

                sut.logEventWithAppState(null, params, null);

                expect(params.is_workout_in_progress).toBe(true);
            });

            test('false', () => {
                SetsSelectors.getIsWorkoutEmpty = () => true;

                sut.logEventWithAppState(null, params, null);

                expect(params.is_workout_in_progress).toBe(false);
            });
        });
    });
});
<|endoftext|>


<commit_before>/* knod 02/28/14
* texteditor.js
* Manages the changing inputs of the visualizer's editor
*
* Sources:
* 1. http://stackoverflow.com/questions/7745867/how-do-you-get-the-cursor-position-in-a-textarea
* 2. http://stackoverflow.com/questions/6683046/how-do-i-move-the-cursor-to-the-front-of-a-textbox-which-has-text-in-it
*
* ToDo:
* - Figure out why .num-row height doesn't change when
* pasting text or deleting selected text. (on keyup?)
* - [Not done, somewhat better somehow] Figure out why
* height of .text-row changes slightly
* after creation so that we don't have to cycle through
* all the .text-rows to get the right height.
* - Do we want the bottom line of the editor to remain
* blank for the evaluate button
*
* DONE:
* - [DONE, keypress propagation or automatic padding/
* height] FIGURE OUT WHY A TEXTAREA HAS AN EXTRA SPACE
*  AT THE BOTTOM AND HOW TO GET RID OF IT.
* - [DONE, keypress propagation] Make textareas empty
* on creation so that you don't have to delete twice to
* delete an area you click into, though this should be
* better once we make the initial textarea just one line
* high. .val("") doesn't work.
* [DONE] - The first text input does not have row one
* as it's data value
*
*/

var textEditor = {
	/* Enclosure for text editor functions */

	// The colors for an activated row
	activeNumRow: "#dcdcdc",
	activeTextRow: "#f0f0f0",

	firstRow: function () {
		/* (None) -> None

		Creates the first text editor row. Why here?
		Because we want to have the .num-row and the
		.text-row paired.
		*/

		// Create the .num-row div
		var $newNumRow = $("

");
		// Create the .text-row input
		var $newTextRow = $("<textarea class='text-row'></textarea>")
		// Store the .num-row as the .text-row's data value
		.data("numRow", $newNumRow);

		// Append them as the first in their divs
		$("#text-areas").append($newTextRow);
		$("#row-num-col").append($newNumRow);

		// Size textarea and it's .num-row to contents
		textEditor.resizeRow($newTextRow);

		// Do we want to focus the mouse here at the start?
		$newTextRow.focus();
		// Somehow the focus() is not triggering activateRow()

		textEditor.activateRow($newTextRow);
	},

	keyFilter: function (key, $textRow) {
		/* (int, jQuery collection) -> None

		Resizes current .num-row on any keypress, calls
		further function depending on value of key. Also
		resizes current row on
		enter: 13, delete: 8, up: 38, down: 40,
		*/

		// Size textarea and it's .num-row to contents
		// for when a keypress rolls wraps to the next line down
		textEditor.resizeRow($textRow);

		// ENTER
		if (key.keyCode == 13) {
			// Add a line and updates the row numbers
			textEditor.addRow($textRow);
			// Don't make a new paragraph
			key.stopPropagation();
			key.preventDefault();
		}

		// DELETE
		else if (key.keyCode == 8) {

			// Do not remove the first row
			if ($(".text-row").length - 1) {
				// If there's no text in the row
				if (!$textRow.val()) {
					// Run a function in texteditor.js that removes a
					// line and updates the row numbers
					textEditor.removeRow($textRow);
				}
			}
			// Don't delete more than you should or something
			key.stopPropagation();
			// .preventDefault would prevent all deleting
		}

		// UP ARROW
		else if (key.keyCode == 38) {
			// If this isn't the first row
			if ( Math.max(0, $(".text-row").index($textRow)) ) {

				// Get the cursor position. Sources (1)
				var cursorPos = $textRow.prop("selectionStart");

				// If the cursor is at the start position
				if (!cursorPos) {

					// Get the previous .text-row element
					// for some reason this doesn't work
					// Comes out as undefined
					var $prevTextRow = $textRow.prev();

					// Get the length of the prev textarea
					var textLength = $textRow.prev().val().length;

					$textRow.prev()
					// Move the cursor to the prev input field
					.focus()
					// Set cursor position to end of prev textarea.
					// Sources (2)
					.prop("setSelectionRange", textLength, textLength);

					// For when textarea has/had multiple lines
					key.stopPropagation();
					key.preventDefault();
				}
			}
		}

		// DOWN ARROW
		else if (key.keyCode == 40) {
			// If this isn't the last row
			if ( $(".text-row").index($textRow) !=
				($(".text-row").length - 1) ) {

				// Get the length of the text in the textarea
				var textLength = $textRow.val().length;
				// Get the cursor position. Sources (1)
				// Selection end in case they had something selected
				var cursorPos = $textRow.prop("selectionEnd");

				// If the cursor is at the end of the text area
				if (cursorPos == textLength) {
					$textRow.next()
					// Move the cursor to the next input field
					.focus()
					// Set cursor position to start of next textarea.
					// Sources (2)
					.prop("setSelectionRange", 0, 0);

					// For when textarea has/had multiple lines
					key.stopPropagation();
					key.preventDefault();
				}
			}
		}
	},

	addRow: function ($textRow) {
		/* (element) -> None

		Adds divs below current input, one for the
		rows, one for the text, then numbers the
		rows appropriately. Moves curosr to the new
		input field
		*/

		// Create the .num-row div
		var $newNumRow = $("

");
		// Create the .text-row input
		var $newTextRow = $("<textarea class='text-row'></textarea>")
		// Store the .num-row as the .text-row's data value
		.data("numRow", $newNumRow);

		// Append new text input under this text input
		$textRow.after($newTextRow);
		// Append new .num-row using the current .text-row's data
		$textRow.data("numRow").after($newNumRow);

		// Expands the input textarea size to show all text
		textEditor.resizeTextArea($newTextRow);
		// Should this also resize row number?
		// textEditor.resizeRow($newTextRow);

		// Still getting weird extra padding, so resizing is
		// needed, but it doesn't need to happen to all of them
		// anymore, just one at a time. Why?

		// Re-number the rows
		textEditor.updateNums();
		// Move the cursor to the new .text-row input
		$newTextRow.focus();
	},

	removeRow: function ($textRow) {
		/* (element) -> None

		Deletes this input and it's matching .num-row,
		then numbers the rows appropriately. Moves curosr
		to the previous input field.
		*/

		// Move the cursor to the previous input field
		$textRow.prev().focus();
		// Remove the .num-row in this .text-row's data value
		$textRow.data("numRow").remove();
		// Remove this .text-row
		$textRow.remove();
		// Update the row numbers
		textEditor.updateNums();
	},

	updateNums: function () {
		/* (None) -> None

		Cycles through all the .num-row rows and
		numbers them in order
		*/

		$(".num-row").each(
			function (thisIndex) {
				$(this).text(thisIndex + 1);
			});
	},

	activateRow: function ($textRow) {
		/* (element) -> None

		Change the colors of $textRow to the
		active colors.
		*/

		// Change $textRow's color
		$textRow.css("background", this.activeTextRow);
		// Change color of $textRow's numRow data value
		$textRow.data("numRow").css("background", this.activeNumRow);
	},

	deactivateRow: function ($textRow) {
		/* (element) -> None

		Remove the colors of $textRow.
		*/

		// Remove $textRow's color
		$textRow.css("background", "");
		// Remove color of $textRow's numRow data value
		$textRow.data("numRow").css("background", "");
	},

	resizeTextArea: function ($elemToSize) {
		// The other places that say (element) may need to say
		// (jQuery element) or collection or something

		/* (jQuery collection) -> (None)

		Resizes the jQuery element/collection elemToSize to
		fit it's contents, minimum one row high.
		*/

		// Too stupid to figure out how to pass DOM elements
		// instead of jQuery collections, so we'll do it the
		// messier jQuery way

		// For some reason it's necessary to set it to 1px first
	    // elemToSize.style.height = "1px";
	    // jQuery way:
	    $elemToSize.css("height","1px");
	    // Gets, basically, the height of the contents with some minimum
	    // Don't know if this will actually make it the size we want,
	    // but meh, it'll be close
	    // elemToSize.style.height = (elemToSize.scrollHeight)+"px";
	    // jQuery way:
		$elemToSize.css("height", ($elemToSize.prop("scrollHeight"))+"px");
	},

	resizeRow: function ($rowToSize) {
		/* ($ collection) -> None

		Resizes $rowToSize to fit contents, minimum of one
		row, then resize's that row's row number.
		*/

		textEditor.resizeTextArea($rowToSize);
		$rowToSize.data("numRow").outerHeight($rowToSize.outerHeight());
	},
}
<commit_msg>Added question mark... yeah, that's all, so sue me<commit_after>/* knod 02/28/14
* texteditor.js
* Manages the changing inputs of the visualizer's editor
*
* Sources:
* 1. http://stackoverflow.com/questions/7745867/how-do-you-get-the-cursor-position-in-a-textarea
* 2. http://stackoverflow.com/questions/6683046/how-do-i-move-the-cursor-to-the-front-of-a-textbox-which-has-text-in-it
*
* ToDo:
* - Figure out why .num-row height doesn't change when
* pasting text or deleting selected text. (on keyup?)
* - [Not done, somewhat better somehow] Figure out why
* height of .text-row changes slightly
* after creation so that we don't have to cycle through
* all the .text-rows to get the right height.
* - Do we want the bottom line of the editor to remain
* blank for the evaluate button?
*
* DONE:
* - [DONE, keypress propagation or automatic padding/
* height] FIGURE OUT WHY A TEXTAREA HAS AN EXTRA SPACE
*  AT THE BOTTOM AND HOW TO GET RID OF IT.
* - [DONE, keypress propagation] Make textareas empty
* on creation so that you don't have to delete twice to
* delete an area you click into, though this should be
* better once we make the initial textarea just one line
* high. .val("") doesn't work.
* [DONE] - The first text input does not have row one
* as it's data value
*
*/

var textEditor = {
	/* Enclosure for text editor functions */

	// The colors for an activated row
	activeNumRow: "#dcdcdc",
	activeTextRow: "#f0f0f0",

	firstRow: function () {
		/* (None) -> None

		Creates the first text editor row. Why here?
		Because we want to have the .num-row and the
		.text-row paired.
		*/

		// Create the .num-row div
		var $newNumRow = $("

");
		// Create the .text-row input
		var $newTextRow = $("<textarea class='text-row'></textarea>")
		// Store the .num-row as the .text-row's data value
		.data("numRow", $newNumRow);

		// Append them as the first in their divs
		$("#text-areas").append($newTextRow);
		$("#row-num-col").append($newNumRow);

		// Size textarea and it's .num-row to contents
		textEditor.resizeRow($newTextRow);

		// Do we want to focus the mouse here at the start?
		$newTextRow.focus();
		// Somehow the focus() is not triggering activateRow()

		textEditor.activateRow($newTextRow);
	},

	keyFilter: function (key, $textRow) {
		/* (int, jQuery collection) -> None

		Resizes current .num-row on any keypress, calls
		further function depending on value of key. Also
		resizes current row on
		enter: 13, delete: 8, up: 38, down: 40,
		*/

		// Size textarea and it's .num-row to contents
		// for when a keypress rolls wraps to the next line down
		textEditor.resizeRow($textRow);

		// ENTER
		if (key.keyCode == 13) {
			// Add a line and updates the row numbers
			textEditor.addRow($textRow);
			// Don't make a new paragraph
			key.stopPropagation();
			key.preventDefault();
		}

		// DELETE
		else if (key.keyCode == 8) {

			// Do not remove the first row
			if ($(".text-row").length - 1) {
				// If there's no text in the row
				if (!$textRow.val()) {
					// Run a function in texteditor.js that removes a
					// line and updates the row numbers
					textEditor.removeRow($textRow);
				}
			}
			// Don't delete more than you should or something
			key.stopPropagation();
			// .preventDefault would prevent all deleting
		}

		// UP ARROW
		else if (key.keyCode == 38) {
			// If this isn't the first row
			if ( Math.max(0, $(".text-row").index($textRow)) ) {

				// Get the cursor position. Sources (1)
				var cursorPos = $textRow.prop("selectionStart");

				// If the cursor is at the start position
				if (!cursorPos) {

					// Get the previous .text-row element
					// for some reason this doesn't work
					// Comes out as undefined
					var $prevTextRow = $textRow.prev();

					// Get the length of the prev textarea
					var textLength = $textRow.prev().val().length;

					$textRow.prev()
					// Move the cursor to the prev input field
					.focus()
					// Set cursor position to end of prev textarea.
					// Sources (2)
					.prop("setSelectionRange", textLength, textLength);

					// For when textarea has/had multiple lines
					key.stopPropagation();
					key.preventDefault();
				}
			}
		}

		// DOWN ARROW
		else if (key.keyCode == 40) {
			// If this isn't the last row
			if ( $(".text-row").index($textRow) !=
				($(".text-row").length - 1) ) {

				// Get the length of the text in the textarea
				var textLength = $textRow.val().length;
				// Get the cursor position. Sources (1)
				// Selection end in case they had something selected
				var cursorPos = $textRow.prop("selectionEnd");

				// If the cursor is at the end of the text area
				if (cursorPos == textLength) {
					$textRow.next()
					// Move the cursor to the next input field
					.focus()
					// Set cursor position to start of next textarea.
					// Sources (2)
					.prop("setSelectionRange", 0, 0);

					// For when textarea has/had multiple lines
					key.stopPropagation();
					key.preventDefault();
				}
			}
		}
	},

	addRow: function ($textRow) {
		/* (element) -> None

		Adds divs below current input, one for the
		rows, one for the text, then numbers the
		rows appropriately. Moves curosr to the new
		input field
		*/

		// Create the .num-row div
		var $newNumRow = $("

");
		// Create the .text-row input
		var $newTextRow = $("<textarea class='text-row'></textarea>")
		// Store the .num-row as the .text-row's data value
		.data("numRow", $newNumRow);

		// Append new text input under this text input
		$textRow.after($newTextRow);
		// Append new .num-row using the current .text-row's data
		$textRow.data("numRow").after($newNumRow);

		// Expands the input textarea size to show all text
		textEditor.resizeTextArea($newTextRow);
		// Should this also resize row number?
		// textEditor.resizeRow($newTextRow);

		// Still getting weird extra padding, so resizing is
		// needed, but it doesn't need to happen to all of them
		// anymore, just one at a time. Why?

		// Re-number the rows
		textEditor.updateNums();
		// Move the cursor to the new .text-row input
		$newTextRow.focus();
	},

	removeRow: function ($textRow) {
		/* (element) -> None

		Deletes this input and it's matching .num-row,
		then numbers the rows appropriately. Moves curosr
		to the previous input field.
		*/

		// Move the cursor to the previous input field
		$textRow.prev().focus();
		// Remove the .num-row in this .text-row's data value
		$textRow.data("numRow").remove();
		// Remove this .text-row
		$textRow.remove();
		// Update the row numbers
		textEditor.updateNums();
	},

	updateNums: function () {
		/* (None) -> None

		Cycles through all the .num-row rows and
		numbers them in order
		*/

		$(".num-row").each(
			function (thisIndex) {
				$(this).text(thisIndex + 1);
			});
	},

	activateRow: function ($textRow) {
		/* (element) -> None

		Change the colors of $textRow to the
		active colors.
		*/

		// Change $textRow's color
		$textRow.css("background", this.activeTextRow);
		// Change color of $textRow's numRow data value
		$textRow.data("numRow").css("background", this.activeNumRow);
	},

	deactivateRow: function ($textRow) {
		/* (element) -> None

		Remove the colors of $textRow.
		*/

		// Remove $textRow's color
		$textRow.css("background", "");
		// Remove color of $textRow's numRow data value
		$textRow.data("numRow").css("background", "");
	},

	resizeTextArea: function ($elemToSize) {
		// The other places that say (element) may need to say
		// (jQuery element) or collection or something

		/* (jQuery collection) -> (None)

		Resizes the jQuery element/collection elemToSize to
		fit it's contents, minimum one row high.
		*/

		// Too stupid to figure out how to pass DOM elements
		// instead of jQuery collections, so we'll do it the
		// messier jQuery way

		// For some reason it's necessary to set it to 1px first
	    // elemToSize.style.height = "1px";
	    // jQuery way:
	    $elemToSize.css("height","1px");
	    // Gets, basically, the height of the contents with some minimum
	    // Don't know if this will actually make it the size we want,
	    // but meh, it'll be close
	    // elemToSize.style.height = (elemToSize.scrollHeight)+"px";
	    // jQuery way:
		$elemToSize.css("height", ($elemToSize.prop("scrollHeight"))+"px");
	},

	resizeRow: function ($rowToSize) {
		/* ($ collection) -> None

		Resizes $rowToSize to fit contents, minimum of one
		row, then resize's that row's row number.
		*/

		textEditor.resizeTextArea($rowToSize);
		$rowToSize.data("numRow").outerHeight($rowToSize.outerHeight());
	},
}
<|endoftext|>


<commit_before><commit_msg>rebuild<commit_after><|endoftext|>


<commit_before>var Node = require('./node');
var Sequence = require('./sequence');
var Load = require('./load');
var Parse = require('./parse');

module.exports = function Link (scope, event, options) {

    var node = Node();

    // load instance, parse event and get flow stream sequences
    Load(scope, node, event[0], options, function (instance) {
        Parse(scope, node, instance, event[1], options, function (event) {

            // pipe event transform streams
            event.s.forEach(function (sequence) {

                options._ = sequence.a || {};

                // create sequence transform
                if (sequence.s) {
                    node._seq(Sequence(options, sequence.s));
                }

                // link to next node
                if (sequence.p) {

                    // pipe to flow event
                    if (typeof sequence.p[1] === 'string') {
                        node._seq(sequence.p[0].flow(sequence.p[1], options));

                    // call stream handler
                    } else {
                        node._seq(sequence.p[1].call(sequence.p[0], options, node._o));
                    }
                }
            });

            // link sequence to node
            node._link(instance, event);

            // end event
            if (event.e) {
                node.on('end', function () {
                    var eventName = event.e;
                    var options;
                    if (eventName instanceof Array) {
                        options = eventName[1];
                        eventName = eventName[0];
                    }
                    var ee = instance.inst.flow(eventName, options);
                    ee.end();
                    if (node._re) {
                        node._re.end();
                    }
                });
            }

            // error events
            if (event.r) {
                node.on('error', function (err) {
                    if (!node._re) {
                        var eventName = event.r;
                        var options;
                        if (eventName instanceof Array) {
                            options = eventName[1];
                            eventName = eventName[0];
                        }
                        node._re = instance.inst.flow(eventName, options);
                    }
                    node._re.write(err)
                });
            }
        });
    });

    return node;
};
<commit_msg>prevent process crash, by listening to error event<commit_after>var Node = require('./node');
var Sequence = require('./sequence');
var Load = require('./load');
var Parse = require('./parse');

module.exports = function Link (scope, _event, options) {

    var node = Node(_event);

    // load instance, parse event and get flow stream sequences
    Load(scope, node, _event[0], options, function (instance) {
        Parse(scope, node, instance, _event[1], options, function (event) {

            // pipe event transform streams
            event.s.forEach(function (sequence) {

                options._ = sequence.a || {};

                // create sequence transform
                if (sequence.s) {
                    node._seq(Sequence(options, sequence.s));
                }

                // link to next node
                if (sequence.p) {

                    // pipe to flow event
                    if (typeof sequence.p[1] === 'string') {
                        node._seq(sequence.p[0].flow(sequence.p[1], options));

                    // call stream handler
                    } else {
                        node._seq(sequence.p[1].call(sequence.p[0], options, node._o || node));
                    }
                }
            });

            // link sequence to node
            node._link(instance, event);

            // end event
            if (event.e) {
                node.on('end', function () {
                    var eventName = event.e;
                    var options;
                    if (eventName instanceof Array) {
                        options = eventName[1];
                        eventName = eventName[0];
                    }
                    var ee = instance.inst.flow(eventName, options);
                    ee.end();

                    // end also error event
                    if (node._re) {
                        node._re.end();
                    }
                });
            }

            // error events
            if (event.r) {
                node.on('error', function (err) {
                    if (!node._re) {
                        var eventName = event.r;
                        var options;
                        if (eventName instanceof Array) {
                            options = eventName[1];
                            eventName = eventName[0];
                        }
                        node._re = instance.inst.flow(eventName, options);
                    }
                    node._re.write(err)
                });
            } else {
                node.on('error', function (err) {
                    console.log(_event, 'no err event error', err);
                });
            }
        });
    });

    return node;
};
<|endoftext|>