<commit_before>/**
 * Tabs module.
 * @module foundation.tabs
 * @requires foundation.util.keyboard
 */
!function($, Foundation) {
  'use strict';

  /**
   * Creates a new instance of tabs.
   * @class
   * @fires Tabs#init
   * @param {jQuery} element - jQuery object to make into tabs.
   * @param {Object} options - Overrides to the default plugin settings.
   */
  function Tabs(element){
    this.$element = element;
    this.options = $.extend({}, Tabs.defaults, this.$element.data());

    this._init();
    Foundation.registerPlugin(this);
    // /**
    //  * Fires when the plugin has been successfuly initialized.
    //  * @event Tabs#init
    //  */
    // this.$element.trigger('init.zf.tabs');
  }

  Tabs.defaults = {
    deepLinking: false,
    scrollToContent: false,
    autoFocus: false,
    wrapOnKeys: true,
    matchHeight: true,
    linkClass: 'tabs-title',
    contentClass: 'tabs-content',
    panelClass: 'tabs-panel'
  };

  /**
   * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
   * @private
   */
  Tabs.prototype._init = function(){
    var _this = this,
        tabIndex = 1;
    this.$tabTitles = this.$element.find('.' + this.options.linkClass);
    this.$tabContent = $('[data-tabs-content="' + this.$element[0].id + '"]');
    this.$tabTitles.each(function(){
      var $elem = $(this),
          $link = $elem.find('a'),
          isActive = $elem.hasClass('is-active'),
          hash = $link.attr('href').slice(1),
          linkId = hash + '-label',
          $tabContent = $(hash);

      $elem.attr({'role': 'presentation'});

      $link.attr({
        'role': 'tab',
        'aria-controls': hash,
        'aria-selected': isActive,
        'id': linkId
      });

      $tabContent.attr({
        'role': 'tabpanel',
        'aria-hidden': !isActive,
        'aria-labelledby': linkId
      });

      if(isActive && _this.options.autoFocus){
        $link.focus();
      }
      tabIndex++
    });
    if(this.options.matchHeight){
      this.setHeight();
    }
    this._events();
  };
  /**
   * Adds event handlers for items within the tabs.
   * @private
   */
   Tabs.prototype._events = function(){
    this._addKeyHandler();
    this._addClickHandler();
    if(this.options.matchHeight){
      $(window).on('changed.zf.mediaquery', this.setHeight.bind(this));
    }
  };

  /**
   * Adds click handlers for items within the tabs.
   * @private
   */
  Tabs.prototype._addClickHandler = function(){
    var _this = this;
    this.$tabTitles.off('click.zf.tabs')
                   .on('click.zf.tabs', function(e){
                     e.preventDefault();
                     e.stopPropagation();
                     if($(this).hasClass('is-active')){
                       return;
                     }
                     _this._handleTabChange($(this));
                   });
  };

  /**
   * Adds keyboard event handlers for items within the tabs.
   * @private
   */
  Tabs.prototype._addKeyHandler = function(){
    var _this = this;
    var $firstTab = _this.$element.find('li:first-of-type');
    var $lastTab = _this.$element.find('li:last-of-type');

    this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
      e.stopPropagation();
      e.preventDefault();
      var $tabTitle = $(this),
          $prev = $tabTitle.prev(),
          $next = $tabTitle.next();
      if(checkClass($prev) || checkClass($next)){
        return;
      }
      if(_this.options.wrapOnKeys){
        $prev = $prev.length ? $prev : $lastTab;
        $next = $next.length ? $next : $firstTab;
        if(checkClass($prev) || checkClass($next)){
          return;
        }
      }

      // handle keyboard event with keyboard util
      Foundation.handleKey(e, _this, {
        open: function() {
          $tabTitle.focus();
          _this._handleTabChange($tabTitle);
        },
        previous: function() {
          if(checkClass($prev)){ return; }
          $prev.focus();
          _this._handleTabChange($prev)
        },
        next: function() {
          if(checkClass($next)){ return; }
          $next.focus();
          _this._handleTabChange($next)
        }
      });
    });
  };

  function checkClass($elem){
    return $elem.hasClass('is-active');
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to open.
   * @param {jQuery} $targetContent - Content pane to open.
   * @fires Tabs#change
   * @function
   */
  Tabs.prototype._handleTabChange = function($target){
    var $tabLink = $target.find('[role="tab"]'),
        hash = $tabLink.attr('href'),
        $targetContent = $(hash),

        $oldTab = this.$element.find('.' + this.options.linkClass + '.is-active')
                  .removeClass('is-active').find('[role="tab"]')
                  .attr({'aria-selected': 'false'}).attr('href');

    $($oldTab).removeClass('is-active').attr({'aria-hidden': 'true'});

    $target.addClass('is-active');

    $tabLink.attr({'aria-selected': 'true'});

    $targetContent
      .addClass('is-active')
      .attr({'aria-hidden': 'false'});

    /**
     * Fires when the plugin has successfully changed tabs.
     * @event Tabs#change
     */
    this.$element.trigger('change.zf.tabs', [$target]);
    // console.log(this.$element.find('.tabs-title, .tabs-panel'));
    // Foundation.reflow(this.$element, 'tabs');
  };
  /**
   * Sets the height of each panel to the height of the tallest panel.
   * If enabled in options, gets called on media query change.
   * If loading content via external source, can be called directly or with _reflow.
   * @function
   */
  Tabs.prototype.setHeight = function(){
    var max = 0;
    this.$tabContent.find('.' + this.options.panelClass)
                    .css('height', '')
                    .each(function(){
                      var panel = $(this),
                          isActive = panel.hasClass('is-active');

                      if(!isActive){
                        panel.css({'visibility': 'hidden', 'display': 'block'});
                      }
                      var temp = this.getBoundingClientRect().height;

                      if(!isActive){
                        panel.css({'visibility': '', 'display': ''});
                      }

                      max = temp > max ? temp : max;
                    })
                    .css('height', max + 'px');
  };

  /**
   * Destroys an instance of an tabs.
   * @fires Tabs#destroyed
   */
  Tabs.prototype.destroy = function() {
    this.$element.find('.' + this.options.linkClass)
                 .off('.zf.tabs').hide().end()
                 .find('.' + this.options.panelClass)
                 .hide();
    if(this.options.matchHeight){
      $(window).off('changed.zf.mediaquery');
    }
    Foundation.unregisterPlugin(this);
    // /**
    //  * Fires when the plugin has been destroyed.
    //  * @event Tabs#destroyed
    //  */
    // this.$element.trigger('destroyed.zf.tabs');
  }

  Foundation.plugin(Tabs);
}(jQuery, window.Foundation);
<commit_msg>cleans up tabs a smidge<commit_after>/**
 * Tabs module.
 * @module foundation.tabs
 * @requires foundation.util.keyboard
 */
!function($, Foundation) {
  'use strict';

  /**
   * Creates a new instance of tabs.
   * @class
   * @fires Tabs#init
   * @param {jQuery} element - jQuery object to make into tabs.
   * @param {Object} options - Overrides to the default plugin settings.
   */
  function Tabs(element){
    this.$element = element;
    this.options = $.extend({}, Tabs.defaults, this.$element.data());

    this._init();
    Foundation.registerPlugin(this);
    // /**
    //  * Fires when the plugin has been successfuly initialized.
    //  * @event Tabs#init
    //  */
    // this.$element.trigger('init.zf.tabs');
  }

  Tabs.defaults = {
    deepLinking: false,
    scrollToContent: false,
    autoFocus: false,
    wrapOnKeys: true,
    matchHeight: true,
    linkClass: 'tabs-title',
    contentClass: 'tabs-content',
    panelClass: 'tabs-panel'
  };

  /**
   * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
   * @private
   */
  Tabs.prototype._init = function(){
    var _this = this;

    this.$tabTitles = this.$element.find('.' + this.options.linkClass);
    this.$tabContent = $('[data-tabs-content="' + this.$element[0].id + '"]');

    this.$tabTitles.each(function(){
      var $elem = $(this),
          $link = $elem.find('a'),
          isActive = $elem.hasClass('is-active'),
          hash = $link.attr('href').slice(1),
          linkId = hash + '-label',
          $tabContent = $(hash);

      $elem.attr({'role': 'presentation'});

      $link.attr({
        'role': 'tab',
        'aria-controls': hash,
        'aria-selected': isActive,
        'id': linkId
      });

      $tabContent.attr({
        'role': 'tabpanel',
        'aria-hidden': !isActive,
        'aria-labelledby': linkId
      });

      if(isActive && _this.options.autoFocus){
        $link.focus();
      }
    });
    if(this.options.matchHeight){
      this.setHeight();
    }
    this._events();
  };
  /**
   * Adds event handlers for items within the tabs.
   * @private
   */
   Tabs.prototype._events = function(){
    this._addKeyHandler();
    this._addClickHandler();
    if(this.options.matchHeight){
      $(window).on('changed.zf.mediaquery', this.setHeight.bind(this));
    }
  };

  /**
   * Adds click handlers for items within the tabs.
   * @private
   */
  Tabs.prototype._addClickHandler = function(){
    var _this = this;
    this.$tabTitles.off('click.zf.tabs')
                   .on('click.zf.tabs', function(e){
                     e.preventDefault();
                     e.stopPropagation();
                     if($(this).hasClass('is-active')){
                       return;
                     }
                     _this._handleTabChange($(this));
                   });
  };

  /**
   * Adds keyboard event handlers for items within the tabs.
   * @private
   */
  Tabs.prototype._addKeyHandler = function(){
    var _this = this;
    var $firstTab = _this.$element.find('li:first-of-type');
    var $lastTab = _this.$element.find('li:last-of-type');

    this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
      e.stopPropagation();
      e.preventDefault();
      var $tabTitle = $(this),
          $prev = $tabTitle.prev(),
          $next = $tabTitle.next();
      if(checkClass($prev) || checkClass($next)){
        return;
      }
      if(_this.options.wrapOnKeys){
        $prev = $prev.length ? $prev : $lastTab;
        $next = $next.length ? $next : $firstTab;
        if(checkClass($prev) || checkClass($next)){
          return;
        }
      }

      // handle keyboard event with keyboard util
      Foundation.handleKey(e, _this, {
        open: function() {
          $tabTitle.focus();
          _this._handleTabChange($tabTitle);
        },
        previous: function() {
          if(checkClass($prev)){ return; }
          $prev.focus();
          _this._handleTabChange($prev)
        },
        next: function() {
          if(checkClass($next)){ return; }
          $next.focus();
          _this._handleTabChange($next)
        }
      });
    });
  };

  function checkClass($elem){
    return $elem.hasClass('is-active');
  }

  /**
   * Opens the tab `$targetContent` defined by `$target`.
   * @param {jQuery} $target - Tab to open.
   * @param {jQuery} $targetContent - Content pane to open.
   * @fires Tabs#change
   * @function
   */
  Tabs.prototype._handleTabChange = function($target){
    var $tabLink = $target.find('[role="tab"]'),
        hash = $tabLink.attr('href'),
        $targetContent = $(hash),

        $oldTab = this.$element.find('.' + this.options.linkClass + '.is-active')
                  .removeClass('is-active').find('[role="tab"]')
                  .attr({'aria-selected': 'false'}).attr('href');

    $($oldTab).removeClass('is-active').attr({'aria-hidden': 'true'});

    $target.addClass('is-active');

    $tabLink.attr({'aria-selected': 'true'});

    $targetContent
      .addClass('is-active')
      .attr({'aria-hidden': 'false'});

    /**
     * Fires when the plugin has successfully changed tabs.
     * @event Tabs#change
     */
    this.$element.trigger('change.zf.tabs', [$target]);
    // console.log(this.$element.find('.tabs-title, .tabs-panel'));
    // Foundation.reflow(this.$element, 'tabs');
  };
  /**
   * Sets the height of each panel to the height of the tallest panel.
   * If enabled in options, gets called on media query change.
   * If loading content via external source, can be called directly or with _reflow.
   * @function
   */
  Tabs.prototype.setHeight = function(){
    var max = 0;
    this.$tabContent.find('.' + this.options.panelClass)
                    .css('height', '')
                    .each(function(){
                      var panel = $(this),
                          isActive = panel.hasClass('is-active');

                      if(!isActive){
                        panel.css({'visibility': 'hidden', 'display': 'block'});
                      }
                      var temp = this.getBoundingClientRect().height;

                      if(!isActive){
                        panel.css({'visibility': '', 'display': ''});
                      }

                      max = temp > max ? temp : max;
                    })
                    .css('height', max + 'px');
  };

  /**
   * Destroys an instance of an tabs.
   * @fires Tabs#destroyed
   */
  Tabs.prototype.destroy = function() {
    this.$element.find('.' + this.options.linkClass)
                 .off('.zf.tabs').hide().end()
                 .find('.' + this.options.panelClass)
                 .hide();
    if(this.options.matchHeight){
      $(window).off('changed.zf.mediaquery');
    }
    Foundation.unregisterPlugin(this);
    // /**
    //  * Fires when the plugin has been destroyed.
    //  * @event Tabs#destroyed
    //  */
    // this.$element.trigger('destroyed.zf.tabs');
  }

  Foundation.plugin(Tabs);
}(jQuery, window.Foundation);
<|endoftext|>


<commit_before>import test from 'tape-catch'

import navItems from 'content/navItems'
import pageMetaReducer, {
	updatePageMeta,

	initialState,
} from './pageMeta'

const getPathWithoutLeadingSlash = path => path.replace(/^\//, '')

test('Page Meta: Update Page Meta from `/`', t => {
	const currentPath = '/'

	const pathWithoutSlash = getPathWithoutLeadingSlash(currentPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const action = updatePageMeta(currentPath)

	const { description, title } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		title,
		navItem.title,
		"Page title was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from `/about`', t => {
	const currentPath = '/about'

	const pathWithoutSlash = getPathWithoutLeadingSlash(currentPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const action = updatePageMeta(currentPath)

	const { description, title } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		title,
		navItem.title,
		"Page title was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from `/no-match`', t => {
	const currentPath = '/no-match'

	const navItem = navItems.find(({ to }) => to === '404')

	const action = updatePageMeta(currentPath)

	const { description, title } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		title,
		navItem.title,
		"Page title was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from Many Actions', t => {
	const finalPath = '/about'

	const pathWithoutSlash = getPathWithoutLeadingSlash(finalPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const actions = [
		updatePageMeta('/no-match'),
		updatePageMeta('/'),
		updatePageMeta(finalPath),
	]

	const { description, title } = (
		actions
		.reduce(
			pageMetaReducer,
			initialState
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		title,
		navItem.title,
		"Page title was updated"
	)

	t.end()
})
<commit_msg>Fixed pageMeta tests<commit_after>import test from 'tape-catch'

import navItems from 'content/navItems'
import pageMetaReducer, {
	updatePageMeta,

	initialState,
} from './pageMeta'

const getPathWithoutLeadingSlash = path => path.replace(/^\//, '')

test('Page Meta: Update Page Meta from `/`', t => {
	const currentPath = '/'

	const pathWithoutSlash = getPathWithoutLeadingSlash(currentPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const action = updatePageMeta(currentPath)

	const { description, name } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		name,
		navItem.name,
		"Page name was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from `/about`', t => {
	const currentPath = '/about'

	const pathWithoutSlash = getPathWithoutLeadingSlash(currentPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const action = updatePageMeta(currentPath)

	const { description, name } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		name,
		navItem.name,
		"Page name was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from `/no-match`', t => {
	const currentPath = '/no-match'

	const navItem = navItems.find(({ to }) => to === '404')

	const action = updatePageMeta(currentPath)

	const { description, name } = (
		pageMetaReducer(
			initialState,
			action
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		name,
		navItem.name,
		"Page name was updated"
	)

	t.end()
})

test('Page Meta: Update Page Meta from Many Actions', t => {
	const finalPath = '/about'

	const pathWithoutSlash = getPathWithoutLeadingSlash(finalPath)
	const navItem = navItems.find(({ to }) => to === pathWithoutSlash)

	const actions = [
		updatePageMeta('/no-match'),
		updatePageMeta('/'),
		updatePageMeta(finalPath),
	]

	const { description, name } = (
		actions
		.reduce(
			pageMetaReducer,
			initialState
		)
	)

	t.equal(
		description,
		navItem.description,
		"Page description was updated"
	)

	t.equal(
		name,
		navItem.name,
		"Page name was updated"
	)

	t.end()
})
<|endoftext|>


<commit_before>const Runtime = require('../../engine/runtime');

const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Clone = require('../../util/clone');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const Video = require('../../io/video');

const VideoMotion = require('./library');

/**
 * Sensor attribute video sensor block should report.
 * @readonly
 * @enum {string}
 */
const SensingAttribute = {
    /** The amount of motion. */
    MOTION: 'motion',

    /** The direction of the motion. */
    DIRECTION: 'direction'
};

/**
 * Subject video sensor block should report for.
 * @readonly
 * @enum {string}
 */
const SensingSubject = {
    /** The sensor traits of the whole stage. */
    STAGE: 'Stage',

    /** The senosr traits of the area overlapped by this sprite. */
    SPRITE: 'this sprite'
};

/**
 * States the video sensing activity can be set to.
 * @readonly
 * @enum {string}
 */
const VideoState = {
    /** Video turned off. */
    OFF: 'off',

    /** Video turned on with default y axis mirroring. */
    ON: 'on',

    /** Video turned on without default y axis mirroring. */
    ON_FLIPPED: 'on-flipped'
};

/**
 * Class for the motion-related blocks in Scratch 3.0
 * @param {Runtime} runtime - the runtime instantiating this block package.
 * @constructor
 */
class Scratch3VideoSensingBlocks {
    constructor (runtime) {
        /**
         * The runtime instantiating this block package.
         * @type {Runtime}
         */
        this.runtime = runtime;

        /**
         * The motion detection algoritm used to power the motion amount and
         * direction values.
         * @type {VideoMotion}
         */
        this.detect = new VideoMotion();

        /**
         * The last millisecond epoch timestamp that the video stream was
         * analyzed.
         * @type {number}
         */
        this._lastUpdate = null;

        if (this.runtime.ioDevices) {
            // Clear target motion state values when the project starts.
            this.runtime.on(Runtime.PROJECT_RUN_START, this.reset.bind(this));

            // Kick off looping the analysis logic.
            this._loop();

            // Configure the video device with values from a globally stored
            // location.
            this.setVideoTransparency({
                TRANSPARENCY: this.globalVideoTransparency
            });
            this.videoToggle({
                VIDEO_STATE: this.globalVideoState
            });
        }
    }

    /**
     * After analyzing a frame the amount of milliseconds until another frame
     * is analyzed.
     * @type {number}
     */
    static get INTERVAL () {
        return 33;
    }

    /**
     * Dimensions the video stream is analyzed at after its rendered to the
     * sample canvas.
     * @type {Array.<number>}
     */
    static get DIMENSIONS () {
        return [480, 360];
    }

    /**
     * The key to load & store a target's motion-related state.
     * @type {string}
     */
    static get STATE_KEY () {
        return 'Scratch.videoSensing';
    }

    /**
     * The default motion-related state, to be used when a target has no existing motion state.
     * @type {MotionState}
     */
    static get DEFAULT_MOTION_STATE () {
        return {
            motionFrameNumber: 0,
            motionAmount: 0,
            motionDirection: 0
        };
    }

    /**
     * The transparency setting of the video preview stored in a value
     * accessible by any object connected to the virtual machine.
     * @type {number}
     */
    get globalVideoTransparency () {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            return stage.videoTransparency;
        }
        return 50;
    }

    set globalVideoTransparency (transparency) {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            stage.videoTransparency = transparency;
        }
        return transparency;
    }

    /**
     * The video state of the video preview stored in a value accessible by any
     * object connected to the virtual machine.
     * @type {number}
     */
    get globalVideoState () {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            return stage.videoState;
        }
        return VideoState.ON;
    }

    set globalVideoState (state) {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            stage.videoState = state;
        }
        return state;
    }

    /**
     * Reset the extension's data motion detection data. This will clear out
     * for example old frames, so the first analyzed frame will not be compared
     * against a frame from before reset was called.
     */
    reset () {
        this.detect.reset();

        const targets = this.runtime.targets;
        for (let i = 0; i < targets.length; i++) {
            const state = targets[i].getCustomState(Scratch3VideoSensingBlocks.STATE_KEY);
            if (state) {
                state.motionAmount = 0;
                state.motionDirection = 0;
            }
        }
    }

    /**
     * Occasionally step a loop to sample the video, stamp it to the preview
     * skin, and add a TypedArray copy of the canvas's pixel data.
     * @private
     */
    _loop () {
        setTimeout(this._loop.bind(this), Math.max(this.runtime.currentStepTime, Scratch3VideoSensingBlocks.INTERVAL));

        // Add frame to detector
        const time = Date.now();
        if (this._lastUpdate === null) {
            this._lastUpdate = time;
        }
        const offset = time - this._lastUpdate;
        if (offset > Scratch3VideoSensingBlocks.INTERVAL) {
            const frame = this.runtime.ioDevices.video.getFrame({
                format: Video.FORMAT_IMAGE_DATA,
                dimensions: Scratch3VideoSensingBlocks.DIMENSIONS
            });
            if (frame) {
                this._lastUpdate = time;
                this.detect.addFrame(frame.data);
            }
        }
    }

    /**
     * Create data for a menu in scratch-blocks format, consisting of an array
     * of objects with text and value properties. The text is a translated
     * string, and the value is one-indexed.
     * @param {object[]} info - An array of info objects each having a name
     *   property.
     * @return {array} - An array of objects with text and value properties.
     * @private
     */
    _buildMenu (info) {
        return info.map((entry, index) => {
            const obj = {};
            obj.text = entry.name;
            obj.value = entry.value || String(index + 1);
            return obj;
        });
    }

    /**
     * @param {Target} target - collect motion state for this target.
     * @returns {MotionState} the mutable motion state associated with that
     *   target. This will be created if necessary.
     * @private
     */
    _getMotionState (target) {
        let motionState = target.getCustomState(Scratch3VideoSensingBlocks.STATE_KEY);
        if (!motionState) {
            motionState = Clone.simple(Scratch3VideoSensingBlocks.DEFAULT_MOTION_STATE);
            target.setCustomState(Scratch3VideoSensingBlocks.STATE_KEY, motionState);
        }
        return motionState;
    }

    static get SensingAttribute () {
        return SensingAttribute;
    }

    /**
     * An array of choices of whether a reporter should return the frame's
     * motion amount or direction.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in sensor
     *   attribute menu
     * @param {string} value - the serializable value of the attribute
     */
    get ATTRIBUTE_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.motion',
                    default: 'motion',
                    description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingAttribute.MOTION
            },
            {
                name: formatMessage({
                    id: 'videoSensing.direction',
                    default: 'direction',
                    description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingAttribute.DIRECTION
            }
        ];
    }

    static get SensingSubject () {
        return SensingSubject;
    }

    /**
     * An array of info about the subject choices.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in the subject menu
     * @param {string} value - the serializable value of the subject
     */
    get SUBJECT_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.sprite',
                    default: 'sprite',
                    description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingSubject.SPRITE
            },
            {
                name: formatMessage({
                    id: 'videoSensing.stage',
                    default: 'stage',
                    description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingSubject.STAGE
            }
        ];
    }

    /**
     * States the video sensing activity can be set to.
     * @readonly
     * @enum {string}
     */
    static get VideoState () {
        return VideoState;
    }

    /**
     * An array of info on video state options for the "turn video [STATE]" block.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in the video state menu
     * @param {string} value - the serializable value stored in the block
     */
    get VIDEO_STATE_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.off',
                    default: 'off',
                    description: 'Option for the "turn video [STATE]" block'
                }),
                value: VideoState.OFF
            },
            {
                name: formatMessage({
                    id: 'videoSensing.on',
                    default: 'on',
                    description: 'Option for the "turn video [STATE]" block'
                }),
                value: VideoState.ON
            },
            {
                name: formatMessage({
                    id: 'videoSensing.onFlipped',
                    default: 'on flipped',
                    description: 'Option for the "turn video [STATE]" block'
                }),
                value: VideoState.ON_FLIPPED
            }
        ];
    }

    /**
     * @returns {object} metadata for this extension and its blocks.
     */
    getInfo () {
        // Enable the video layer
        this.runtime.ioDevices.video.enableVideo();

        // Return extension definition
        return {
            id: 'videoSensing',
            name: 'Video Motion',
            blocks: [
                {
                    // @todo this hat needs to be set itself to restart existing
                    // threads like Scratch 2's behaviour.
                    opcode: 'whenMotionGreaterThan',
                    text: formatMessage({
                        id: 'videoSensing.whenMotionGreaterThan',
                        default: 'when video motion > [REFERENCE]',
                        description: 'Event that triggers when the amount of motion is greater than [REFERENCE]'
                    }),
                    blockType: BlockType.HAT,
                    arguments: {
                        REFERENCE: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 10
                        }
                    }
                },
                {
                    opcode: 'videoOn',
                    blockType: BlockType.REPORTER,
                    text: formatMessage({
                        id: 'videoSensing.videoOn',
                        default: 'video [ATTRIBUTE] on [SUBJECT]',
                        description: 'Reporter that returns the amount of [ATTRIBUTE] for the selected [SUBJECT]'
                    }),
                    arguments: {
                        ATTRIBUTE: {
                            type: ArgumentType.NUMBER,
                            menu: 'ATTRIBUTE',
                            defaultValue: SensingAttribute.MOTION
                        },
                        SUBJECT: {
                            type: ArgumentType.NUMBER,
                            menu: 'SUBJECT',
                            defaultValue: SensingSubject.SPRITE
                        }
                    }
                },
                {
                    opcode: 'videoToggle',
                    text: formatMessage({
                        id: 'videoSensing.videoToggle',
                        default: 'turn video [VIDEO_STATE]',
                        description: 'Controls dispay of the video preview layer'
                    }),
                    arguments: {
                        VIDEO_STATE: {
                            type: ArgumentType.NUMBER,
                            menu: 'VIDEO_STATE',
                            defaultValue: VideoState.ON
                        }
                    }
                },
                {
                    opcode: 'setVideoTransparency',
                    text: formatMessage({
                        id: 'videoSensing.setVideoTransparency',
                        default: 'set video transparency to [TRANSPARENCY]',
                        description: 'Controls transparency of the video preview layer'
                    }),
                    arguments: {
                        TRANSPARENCY: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 50
                        }
                    }
                }
            ],
            menus: {
                ATTRIBUTE: this._buildMenu(this.ATTRIBUTE_INFO),
                SUBJECT: this._buildMenu(this.SUBJECT_INFO),
                VIDEO_STATE: this._buildMenu(this.VIDEO_STATE_INFO)
            }
        };
    }

    /**
     * Analyze a part of the frame that a target overlaps.
     * @param {Target} target - a target to determine where to analyze
     * @returns {MotionState} the motion state for the given target
     */
    _analyzeLocalMotion (target) {
        const drawable = this.runtime.renderer._allDrawables[target.drawableID];
        const state = this._getMotionState(target);
        this.detect.getLocalMotion(drawable, state);
        return state;
    }

    /**
     * A scratch reporter block handle that analyzes the last two frames and
     * depending on the arguments, returns the motion or direction for the
     * whole stage or just the target sprite.
     * @param {object} args - the block arguments
     * @param {BlockUtility} util - the block utility
     * @returns {number} the motion amount or direction of the stage or sprite
     */
    videoOn (args, util) {
        this.detect.analyzeFrame();

        let state = this.detect;
        if (args.SUBJECT === SensingSubject.SPRITE) {
            state = this._analyzeLocalMotion(util.target);
        }

        if (args.ATTRIBUTE === SensingAttribute.MOTION) {
            return state.motionAmount;
        }
        return state.motionDirection;
    }

    /**
     * A scratch hat block edge handle that analyzes the last two frames where
     * the target sprite overlaps and if it has more motion than the given
     * reference value.
     * @param {object} args - the block arguments
     * @param {BlockUtility} util - the block utility
     * @returns {boolean} true if the sprite overlaps more motion than the
     *   reference
     */
    whenMotionGreaterThan (args, util) {
        this.detect.analyzeFrame();
        const state = this._analyzeLocalMotion(util.target);
        return state.motionAmount > Number(args.REFERENCE);
    }

    /**
     * A scratch command block handle that configures the video state from
     * passed arguments.
     * @param {object} args - the block arguments
     * @param {VideoState} args.VIDEO_STATE - the video state to set the device to
     */
    videoToggle (args) {
        const state = args.VIDEO_STATE;
        this.globalVideoState = state;
        if (state === VideoState.OFF) {
            this.runtime.ioDevices.video.disableVideo();
        } else {
            this.runtime.ioDevices.video.enableVideo();
            // Mirror if state is ON. Do not mirror if state is ON_FLIPPED.
            this.runtime.ioDevices.video.mirror = state === VideoState.ON;
        }
    }

    /**
     * A scratch command block handle that configures the video preview's
     * transparency from passed arguments.
     * @param {object} args - the block arguments
     * @param {number} args.TRANSPARENCY - the transparency to set the video
     *   preview to
     */
    setVideoTransparency (args) {
        const transparency = Cast.toNumber(args.TRANSPARENCY);
        this.globalVideoTransparency = transparency;
        this.runtime.ioDevices.video.setPreviewGhost(transparency);
    }
}

module.exports = Scratch3VideoSensingBlocks;
<commit_msg>Fix typo and add clarification to translation note<commit_after>const Runtime = require('../../engine/runtime');

const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Clone = require('../../util/clone');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const Video = require('../../io/video');

const VideoMotion = require('./library');

/**
 * Sensor attribute video sensor block should report.
 * @readonly
 * @enum {string}
 */
const SensingAttribute = {
    /** The amount of motion. */
    MOTION: 'motion',

    /** The direction of the motion. */
    DIRECTION: 'direction'
};

/**
 * Subject video sensor block should report for.
 * @readonly
 * @enum {string}
 */
const SensingSubject = {
    /** The sensor traits of the whole stage. */
    STAGE: 'Stage',

    /** The senosr traits of the area overlapped by this sprite. */
    SPRITE: 'this sprite'
};

/**
 * States the video sensing activity can be set to.
 * @readonly
 * @enum {string}
 */
const VideoState = {
    /** Video turned off. */
    OFF: 'off',

    /** Video turned on with default y axis mirroring. */
    ON: 'on',

    /** Video turned on without default y axis mirroring. */
    ON_FLIPPED: 'on-flipped'
};

/**
 * Class for the motion-related blocks in Scratch 3.0
 * @param {Runtime} runtime - the runtime instantiating this block package.
 * @constructor
 */
class Scratch3VideoSensingBlocks {
    constructor (runtime) {
        /**
         * The runtime instantiating this block package.
         * @type {Runtime}
         */
        this.runtime = runtime;

        /**
         * The motion detection algoritm used to power the motion amount and
         * direction values.
         * @type {VideoMotion}
         */
        this.detect = new VideoMotion();

        /**
         * The last millisecond epoch timestamp that the video stream was
         * analyzed.
         * @type {number}
         */
        this._lastUpdate = null;

        if (this.runtime.ioDevices) {
            // Clear target motion state values when the project starts.
            this.runtime.on(Runtime.PROJECT_RUN_START, this.reset.bind(this));

            // Kick off looping the analysis logic.
            this._loop();

            // Configure the video device with values from a globally stored
            // location.
            this.setVideoTransparency({
                TRANSPARENCY: this.globalVideoTransparency
            });
            this.videoToggle({
                VIDEO_STATE: this.globalVideoState
            });
        }
    }

    /**
     * After analyzing a frame the amount of milliseconds until another frame
     * is analyzed.
     * @type {number}
     */
    static get INTERVAL () {
        return 33;
    }

    /**
     * Dimensions the video stream is analyzed at after its rendered to the
     * sample canvas.
     * @type {Array.<number>}
     */
    static get DIMENSIONS () {
        return [480, 360];
    }

    /**
     * The key to load & store a target's motion-related state.
     * @type {string}
     */
    static get STATE_KEY () {
        return 'Scratch.videoSensing';
    }

    /**
     * The default motion-related state, to be used when a target has no existing motion state.
     * @type {MotionState}
     */
    static get DEFAULT_MOTION_STATE () {
        return {
            motionFrameNumber: 0,
            motionAmount: 0,
            motionDirection: 0
        };
    }

    /**
     * The transparency setting of the video preview stored in a value
     * accessible by any object connected to the virtual machine.
     * @type {number}
     */
    get globalVideoTransparency () {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            return stage.videoTransparency;
        }
        return 50;
    }

    set globalVideoTransparency (transparency) {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            stage.videoTransparency = transparency;
        }
        return transparency;
    }

    /**
     * The video state of the video preview stored in a value accessible by any
     * object connected to the virtual machine.
     * @type {number}
     */
    get globalVideoState () {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            return stage.videoState;
        }
        return VideoState.ON;
    }

    set globalVideoState (state) {
        const stage = this.runtime.getTargetForStage();
        if (stage) {
            stage.videoState = state;
        }
        return state;
    }

    /**
     * Reset the extension's data motion detection data. This will clear out
     * for example old frames, so the first analyzed frame will not be compared
     * against a frame from before reset was called.
     */
    reset () {
        this.detect.reset();

        const targets = this.runtime.targets;
        for (let i = 0; i < targets.length; i++) {
            const state = targets[i].getCustomState(Scratch3VideoSensingBlocks.STATE_KEY);
            if (state) {
                state.motionAmount = 0;
                state.motionDirection = 0;
            }
        }
    }

    /**
     * Occasionally step a loop to sample the video, stamp it to the preview
     * skin, and add a TypedArray copy of the canvas's pixel data.
     * @private
     */
    _loop () {
        setTimeout(this._loop.bind(this), Math.max(this.runtime.currentStepTime, Scratch3VideoSensingBlocks.INTERVAL));

        // Add frame to detector
        const time = Date.now();
        if (this._lastUpdate === null) {
            this._lastUpdate = time;
        }
        const offset = time - this._lastUpdate;
        if (offset > Scratch3VideoSensingBlocks.INTERVAL) {
            const frame = this.runtime.ioDevices.video.getFrame({
                format: Video.FORMAT_IMAGE_DATA,
                dimensions: Scratch3VideoSensingBlocks.DIMENSIONS
            });
            if (frame) {
                this._lastUpdate = time;
                this.detect.addFrame(frame.data);
            }
        }
    }

    /**
     * Create data for a menu in scratch-blocks format, consisting of an array
     * of objects with text and value properties. The text is a translated
     * string, and the value is one-indexed.
     * @param {object[]} info - An array of info objects each having a name
     *   property.
     * @return {array} - An array of objects with text and value properties.
     * @private
     */
    _buildMenu (info) {
        return info.map((entry, index) => {
            const obj = {};
            obj.text = entry.name;
            obj.value = entry.value || String(index + 1);
            return obj;
        });
    }

    /**
     * @param {Target} target - collect motion state for this target.
     * @returns {MotionState} the mutable motion state associated with that
     *   target. This will be created if necessary.
     * @private
     */
    _getMotionState (target) {
        let motionState = target.getCustomState(Scratch3VideoSensingBlocks.STATE_KEY);
        if (!motionState) {
            motionState = Clone.simple(Scratch3VideoSensingBlocks.DEFAULT_MOTION_STATE);
            target.setCustomState(Scratch3VideoSensingBlocks.STATE_KEY, motionState);
        }
        return motionState;
    }

    static get SensingAttribute () {
        return SensingAttribute;
    }

    /**
     * An array of choices of whether a reporter should return the frame's
     * motion amount or direction.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in sensor
     *   attribute menu
     * @param {string} value - the serializable value of the attribute
     */
    get ATTRIBUTE_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.motion',
                    default: 'motion',
                    description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingAttribute.MOTION
            },
            {
                name: formatMessage({
                    id: 'videoSensing.direction',
                    default: 'direction',
                    description: 'Attribute for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingAttribute.DIRECTION
            }
        ];
    }

    static get SensingSubject () {
        return SensingSubject;
    }

    /**
     * An array of info about the subject choices.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in the subject menu
     * @param {string} value - the serializable value of the subject
     */
    get SUBJECT_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.sprite',
                    default: 'sprite',
                    description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingSubject.SPRITE
            },
            {
                name: formatMessage({
                    id: 'videoSensing.stage',
                    default: 'stage',
                    description: 'Subject for the "video [ATTRIBUTE] on [SUBJECT]" block'
                }),
                value: SensingSubject.STAGE
            }
        ];
    }

    /**
     * States the video sensing activity can be set to.
     * @readonly
     * @enum {string}
     */
    static get VideoState () {
        return VideoState;
    }

    /**
     * An array of info on video state options for the "turn video [STATE]" block.
     * @type {object[]} an array of objects
     * @param {string} name - the translatable name to display in the video state menu
     * @param {string} value - the serializable value stored in the block
     */
    get VIDEO_STATE_INFO () {
        return [
            {
                name: formatMessage({
                    id: 'videoSensing.off',
                    default: 'off',
                    description: 'Option for the "turn video [STATE]" block'
                }),
                value: VideoState.OFF
            },
            {
                name: formatMessage({
                    id: 'videoSensing.on',
                    default: 'on',
                    description: 'Option for the "turn video [STATE]" block'
                }),
                value: VideoState.ON
            },
            {
                name: formatMessage({
                    id: 'videoSensing.onFlipped',
                    default: 'on flipped',
                    description: 'Option for the "turn video [STATE]" block that causes the video to be flipped' +
                        ' horizontally (reversed as in a mirror)'
                }),
                value: VideoState.ON_FLIPPED
            }
        ];
    }

    /**
     * @returns {object} metadata for this extension and its blocks.
     */
    getInfo () {
        // Enable the video layer
        this.runtime.ioDevices.video.enableVideo();

        // Return extension definition
        return {
            id: 'videoSensing',
            name: 'Video Motion',
            blocks: [
                {
                    // @todo this hat needs to be set itself to restart existing
                    // threads like Scratch 2's behaviour.
                    opcode: 'whenMotionGreaterThan',
                    text: formatMessage({
                        id: 'videoSensing.whenMotionGreaterThan',
                        default: 'when video motion > [REFERENCE]',
                        description: 'Event that triggers when the amount of motion is greater than [REFERENCE]'
                    }),
                    blockType: BlockType.HAT,
                    arguments: {
                        REFERENCE: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 10
                        }
                    }
                },
                {
                    opcode: 'videoOn',
                    blockType: BlockType.REPORTER,
                    text: formatMessage({
                        id: 'videoSensing.videoOn',
                        default: 'video [ATTRIBUTE] on [SUBJECT]',
                        description: 'Reporter that returns the amount of [ATTRIBUTE] for the selected [SUBJECT]'
                    }),
                    arguments: {
                        ATTRIBUTE: {
                            type: ArgumentType.NUMBER,
                            menu: 'ATTRIBUTE',
                            defaultValue: SensingAttribute.MOTION
                        },
                        SUBJECT: {
                            type: ArgumentType.NUMBER,
                            menu: 'SUBJECT',
                            defaultValue: SensingSubject.SPRITE
                        }
                    }
                },
                {
                    opcode: 'videoToggle',
                    text: formatMessage({
                        id: 'videoSensing.videoToggle',
                        default: 'turn video [VIDEO_STATE]',
                        description: 'Controls display of the video preview layer'
                    }),
                    arguments: {
                        VIDEO_STATE: {
                            type: ArgumentType.NUMBER,
                            menu: 'VIDEO_STATE',
                            defaultValue: VideoState.ON
                        }
                    }
                },
                {
                    opcode: 'setVideoTransparency',
                    text: formatMessage({
                        id: 'videoSensing.setVideoTransparency',
                        default: 'set video transparency to [TRANSPARENCY]',
                        description: 'Controls transparency of the video preview layer'
                    }),
                    arguments: {
                        TRANSPARENCY: {
                            type: ArgumentType.NUMBER,
                            defaultValue: 50
                        }
                    }
                }
            ],
            menus: {
                ATTRIBUTE: this._buildMenu(this.ATTRIBUTE_INFO),
                SUBJECT: this._buildMenu(this.SUBJECT_INFO),
                VIDEO_STATE: this._buildMenu(this.VIDEO_STATE_INFO)
            }
        };
    }

    /**
     * Analyze a part of the frame that a target overlaps.
     * @param {Target} target - a target to determine where to analyze
     * @returns {MotionState} the motion state for the given target
     */
    _analyzeLocalMotion (target) {
        const drawable = this.runtime.renderer._allDrawables[target.drawableID];
        const state = this._getMotionState(target);
        this.detect.getLocalMotion(drawable, state);
        return state;
    }

    /**
     * A scratch reporter block handle that analyzes the last two frames and
     * depending on the arguments, returns the motion or direction for the
     * whole stage or just the target sprite.
     * @param {object} args - the block arguments
     * @param {BlockUtility} util - the block utility
     * @returns {number} the motion amount or direction of the stage or sprite
     */
    videoOn (args, util) {
        this.detect.analyzeFrame();

        let state = this.detect;
        if (args.SUBJECT === SensingSubject.SPRITE) {
            state = this._analyzeLocalMotion(util.target);
        }

        if (args.ATTRIBUTE === SensingAttribute.MOTION) {
            return state.motionAmount;
        }
        return state.motionDirection;
    }

    /**
     * A scratch hat block edge handle that analyzes the last two frames where
     * the target sprite overlaps and if it has more motion than the given
     * reference value.
     * @param {object} args - the block arguments
     * @param {BlockUtility} util - the block utility
     * @returns {boolean} true if the sprite overlaps more motion than the
     *   reference
     */
    whenMotionGreaterThan (args, util) {
        this.detect.analyzeFrame();
        const state = this._analyzeLocalMotion(util.target);
        return state.motionAmount > Number(args.REFERENCE);
    }

    /**
     * A scratch command block handle that configures the video state from
     * passed arguments.
     * @param {object} args - the block arguments
     * @param {VideoState} args.VIDEO_STATE - the video state to set the device to
     */
    videoToggle (args) {
        const state = args.VIDEO_STATE;
        this.globalVideoState = state;
        if (state === VideoState.OFF) {
            this.runtime.ioDevices.video.disableVideo();
        } else {
            this.runtime.ioDevices.video.enableVideo();
            // Mirror if state is ON. Do not mirror if state is ON_FLIPPED.
            this.runtime.ioDevices.video.mirror = state === VideoState.ON;
        }
    }

    /**
     * A scratch command block handle that configures the video preview's
     * transparency from passed arguments.
     * @param {object} args - the block arguments
     * @param {number} args.TRANSPARENCY - the transparency to set the video
     *   preview to
     */
    setVideoTransparency (args) {
        const transparency = Cast.toNumber(args.TRANSPARENCY);
        this.globalVideoTransparency = transparency;
        this.runtime.ioDevices.video.setPreviewGhost(transparency);
    }
}

module.exports = Scratch3VideoSensingBlocks;
<|endoftext|>


<commit_before>/* search_autocomplete.js.  Add autocomplete to Umlaut journal title search. */
jQuery(document).ready(function($) {

 // We override typeahead's 'render' function to NOT have the first
 // item selected. We simply copy and pasted it, and removed the line
 // `items.first().addClass('active')`, then set the prototype to our
 // own function. Yes, this changes typeahead globally, sorry we don't have
 // a way to change it just for certain typeaheads.
 //
 // The default first-item-selected behavior has been hated by users
 // in the journal search scenario, since when they hit return they
 // get it even if they didn't want it.
 var newRender = function(items) {
      var that = this

      items = $(items).map(function (i, item) {
        i = $(that.options.item).attr('data-value', item)
        i.find('a').html(that.highlighter(item))
        return i[0]
      })

      this.$menu.html(items)
      return this
 };
 $.fn.typeahead.Constructor.prototype.render = newRender;

  $(document).on("submit", "form.OpenURL", function() {
    var form = $(this);
    if ( form.find(".rft_title").val() != $(this).val()) {
      form.find(".rft_object_id").val("");
      form.find(".rft_title").val("");
    }
  });

  // Search for the title with the current form. Only search
  // if there are more than two chars though!
  //
  var search_title = function(query, process) {
    if (query.length > 2) {
      var form = this.$element.closest("form");
      var url = form.attr("action").replace("journal_search", "auto_complete_for_journal_title");
      // Get JSON from
      $.getJSON(
        form.attr("action").replace("journal_search", "auto_complete_for_journal_title"),
        form.serialize(),
        function(data) {
          process(data)
        }
      )
    }
  }

  var lookup_limit = 300; //ms
  // Uses a timer to only do a lookup at most once every
  // 300ms . Based on rejected pull request at:
  // https://github.com/twitter/bootstrap/pull/6320
  var throttled_search_title = function(query, process) {
    if(this.lookupTimer) {
      clearTimeout(this.lookupTimer);
    }

    this.lookupTimer = setTimeout($.proxy(search_title, this, query, process), lookup_limit);
    return this;
  }

  $("input.title_search").typeahead({
    items: 10,
    minLength: 3,
    source: throttled_search_title,
    highlighter: function(item) {
      // Bootstrap updates the item as it passes through the callback chain
      // so this is a hack to ensure we get the proper values.
      return ""+ item.title + "";
    },
    sorter: function(items) { return items },
    matcher: function(item) { return true; },
    updater: function(item) {
      // Get the selected item via our hack.
      var selected_item = this.$menu.find('.active .title');
      // We set the id attribute as the object id
      var object_id = selected_item.attr("id");
      // We set the inner text with the title
      var title = selected_item.text();
      var form = this.$element.closest("form");
      form.find("input.rft_object_id").val(object_id);
      form.find("input.rft_title").val(title);
      form.find("select.title_search_type").val("exact");
      return title;
    }
  });
});<commit_msg>autocmplete suggestions do not need to be 'strong'd<commit_after>/* search_autocomplete.js.  Add autocomplete to Umlaut journal title search. */
jQuery(document).ready(function($) {

 // We override typeahead's 'render' function to NOT have the first
 // item selected. We simply copy and pasted it, and removed the line
 // `items.first().addClass('active')`, then set the prototype to our
 // own function. Yes, this changes typeahead globally, sorry we don't have
 // a way to change it just for certain typeaheads.
 //
 // The default first-item-selected behavior has been hated by users
 // in the journal search scenario, since when they hit return they
 // get it even if they didn't want it.
 var newRender = function(items) {
      var that = this

      items = $(items).map(function (i, item) {
        i = $(that.options.item).attr('data-value', item)
        i.find('a').html(that.highlighter(item))
        return i[0]
      })

      this.$menu.html(items)
      return this
 };
 $.fn.typeahead.Constructor.prototype.render = newRender;

  $(document).on("submit", "form.OpenURL", function() {
    var form = $(this);
    if ( form.find(".rft_title").val() != $(this).val()) {
      form.find(".rft_object_id").val("");
      form.find(".rft_title").val("");
    }
  });

  // Search for the title with the current form. Only search
  // if there are more than two chars though!
  //
  var search_title = function(query, process) {
    if (query.length > 2) {
      var form = this.$element.closest("form");
      var url = form.attr("action").replace("journal_search", "auto_complete_for_journal_title");
      // Get JSON from
      $.getJSON(
        form.attr("action").replace("journal_search", "auto_complete_for_journal_title"),
        form.serialize(),
        function(data) {
          process(data)
        }
      )
    }
  }

  var lookup_limit = 300; //ms
  // Uses a timer to only do a lookup at most once every
  // 300ms . Based on rejected pull request at:
  // https://github.com/twitter/bootstrap/pull/6320
  var throttled_search_title = function(query, process) {
    if(this.lookupTimer) {
      clearTimeout(this.lookupTimer);
    }

    this.lookupTimer = setTimeout($.proxy(search_title, this, query, process), lookup_limit);
    return this;
  }

  $("input.title_search").typeahead({
    items: 10,
    minLength: 3,
    source: throttled_search_title,
    highlighter: function(item) {
      // Bootstrap updates the item as it passes through the callback chain
      // so this is a hack to ensure we get the proper values.
      return ""+ item.title + "";
    },
    sorter: function(items) { return items },
    matcher: function(item) { return true; },
    updater: function(item) {
      // Get the selected item via our hack.
      var selected_item = this.$menu.find('.active .title');
      // We set the id attribute as the object id
      var object_id = selected_item.attr("id");
      // We set the inner text with the title
      var title = selected_item.text();
      var form = this.$element.closest("form");
      form.find("input.rft_object_id").val(object_id);
      form.find("input.rft_title").val(title);
      form.find("select.title_search_type").val("exact");
      return title;
    }
  });
});<|endoftext|>


<commit_before>/*jslint nomen:true, regexp: true */
/*global define: false */

define(['jquery', 'lib/underscore', 'lib/backbone', 'unicode_utils'], function ($, _, Backbone, unicode) {
	"use strict";

	// Return a slice of genesets over the current position.
	var genesetVisibleSlice = function (genesets, pos) {
		// If no pos, return all genesets.
		// If no genesets, don't try to slice them.
		if (!pos || !genesets || genesets.length === 0) {
			return genesets;
		}

		var gsnames = _(genesets).pluck('name'),
			gss = gsnames.indexOf(pos.genesetStart),
			gse = gsnames.indexOf(pos.genesetEnd),
			gsl = genesets.slice(gss, gse + 1), // array of references to gs objects
			gl;

		gsl[0] = $.extend(true, {}, gsl[0]); // deep copy
		gl = gsl[0].genes;
		gsl[0].genes = gl.slice(gl.indexOf(pos.geneStart));

		gsl[gsl.length - 1] = $.extend(true, {}, gsl[gsl.length - 1]); // deep copy
		gl = gsl[gsl.length - 1].genes;
		gsl[gsl.length - 1].genes = gl.slice(0, gl.indexOf(pos.geneEnd) + 1);

		return gsl;
	},

		genesetParse = function (text) {
			// Handle carriage return. Strip comment lines. Allow implicit comma at newline. Strip whitespace.
			var list = unicode.normalize(text).replace(/\r/g, '\n').replace(/#[^\n]*/g, "").replace(/^\s+/, '').replace(/\s+$/, '').split(/[\s,]+/);
			return _(list).without('');
		},

		genomicPositionError = function (msg) {
			var e = new Error(msg);
			e.name = 'genomicPositionError';
			return e;
		};

/*
		},

		GenomicPosition = Backbone.PersistingModel.extend({
			defaults: {
				ntList: [],
				ntActive: [],
				genesetsPos: null
			},
			ajax: function () {
				if (this.get('mode') === 'chrom') {
					return {
						hgh2_type: 'chrom',
						hgh2_chromStart: _.template('<%= chromStart %>:<%= baseStart %>', this.get('chromPos')),
						hgh2_chromEnd: _.template('<%= chromEnd %>:<%= baseEnd %>', this.get('chromPos'))
					};
				} else {
					var ret = {
						hgh2_type: 'geneset',
						hgh2_userGeneset_0: "set=" + _(this.genesetsVisible).chain().pluck('genes')
							.reduce(function (a, b) { return a.concat(b); }, [])
							.value().join(',')
					};

					return ret;
				}
			},
			assembly: function (a) {
				if (a && !_(this._assembly).isEqual(a)) {
					this._assembly = a;
				}
				return this._assembly;
			},
			clipChromPosition: function () {
				var cs, ce, chromInfo,
					assembly = this.assembly(),
					chromPos = this.get('chromPos'),
					newPos,
					set = GenomicPosition.__super__.set;

				if (!chromPos) { // Set default position
					if (!assembly) {
						return;
					}
					chromInfo = assembly.chromInfo;
					cs = 0;
					ce = chromInfo.length - 1;
					set.call(this, 'chromPos', {
						chromStart : chromInfo[cs].name,
						baseStart: 0,
						chromEnd: chromInfo[ce].name,
						baseEnd: chromInfo[ce].size
					}, { silent: true });
					return;
				}

				if (assembly) {
					chromInfo = assembly.chromInfo;
					newPos = _(chromPos).clone();

					// clip coords
					cs = _.pluck(chromInfo, 'name').indexOf(chromPos.chromStart);
					if (cs === -1) { // XXX unit test test
						throw genomicPositionError("Invalid start chromosome");
					}

					ce = _.pluck(chromInfo, 'name').indexOf(chromPos.chromEnd);
					if (ce === -1 || ce < cs) { // XXX unit test test
						throw genomicPositionError("Invalid end chromosome");
					}

					if (!newPos.baseStart || newPos.baseStart < 0) {
						newPos.baseStart = 0;
					}
					newPos.baseStart = Math.min(newPos.baseStart, chromInfo[cs].size - 1);

					if (!newPos.baseEnd || newPos.baseEnd > chromInfo[ce].size) {
						newPos.baseEnd = chromInfo[ce].size;
					}
					newPos.baseEnd = Math.max(newPos.baseEnd, 1);

					if (cs === ce && newPos.baseEnd <= newPos.baseStart) {
						throw genomicPositionError("Invalid end base");
					}

					if (!_(chromPos).isEqual(newPos)) {
						set.call(this, 'chromPos', newPos, { silent: true });
					}
				}
			},
			defaultGenesetsPosition: function () {
				var genesetsPos = this.get('genesetsPos'),
					genesets = this.genesetsParsed,
					set = GenomicPosition.__super__.set;

				if (genesets && genesets.length) {
					set.call(this, 'genesetsPos', {
						genesetStart: genesets[0].name,
						geneStart: genesets[0].genes[0],
						genesetEnd: _(genesets).last().name,
						geneEnd: _(_(genesets).last().genes).last()
					}, { silent: true});
				}
			},
			genesetsVisibleSet: function () {
				this.genesetsVisible = genesetVisibleSlice(this.genesetsParsed, this.get('genesetsPos'));
			},
			genesetsPosValid: function () {
				var pos = this.get('genesetsPos');
				return _(this.genesetsParsed).find(function (gs) { return gs.name === pos.genesetStart; })
					&& _(this.genesetsParsed).find(function (gs) { return gs.name === pos.genesetEnd; });
			},
			genesetsParse: function () {
				var genesets = this.get('ntList');
				this.genesetsParsed = _(this.get('ntActive')).map(function (name) {
					var gs = _(genesets).find(function (gs) { return gs.name === name; });
					return {
						name: name,
						genes: genesetParse(gs.text)
					};
				});
			},
			chromZoomed: function () {
				var chromPos = this.get('chromPos'),
					assembly = this.assembly();

				return chromPos && !(chromPos.chromStart === assembly.chromInfo[0].name &&
						chromPos.baseStart === 0 &&
						chromPos.chromEnd === _(assembly.chromInfo).last().name &&
						chromPos.baseEnd === _(assembly.chromInfo).last().size);
			},
			genesetsZoomed: function () {
				var p = this.get('genesetsPos'),
					gslist = this.genesetsParsed;

				return gslist && gslist.length && p && !(p.genesetStart === gslist[0].name &&
						p.geneStart === gslist[0].genes[0] &&
						p.genesetEnd === _(gslist).last().name &&
						p.geneEnd === _(_(gslist).last().genes).last());
			},
			zoomed: function () {
				if (this.get('mode') === 'genesets') {
					return this.genesetsZoomed();
				}
				return this.chromZoomed();
			},
			set: function (key, value, options) {
				var changed,
					zoomed = this.genesetsZoomed(),
					attrs;

				// Handle both `"key", value` and `{key: value}` -style arguments.
				if (_.isObject(key) || key === null) {
					attrs = key;
					options = value;
				} else {
					attrs = {};
					attrs[key] = value;
				}
				// XXX User might accidentally override 'silent'
				GenomicPosition.__super__.set.call(this, attrs, _({silent: true}).extend(options || {}));
				this.clipChromPosition();
				changed = _(this.changedAttributes());
				if (changed.has('ntList') || changed.has('ntActive')) {
					this.genesetsParse();
				}
				if (this.get('genesetsPos') === null
						|| !this.genesetsPosValid()
						|| (!zoomed && !changed.has('genesetsPos'))) {
					this.defaultGenesetsPosition();
				}
				if (changed.has('ntList') || changed.has('ntActive') || changed.has('genesetsPos')) {
					this.genesetsVisibleSet();
				}
				if (!options || !options.silent) {
					this.change();
				}
				return this;
			}
		});
*/

	return {
/*
		factory : function (options) {
			return new GenomicPosition(null, options);
		},
*/
		genesetParse : genesetParse
	};

});
<commit_msg>Cut Backbone dependency + unused code.<commit_after>/*jslint nomen:true, regexp: true */
/*global define: false */

define(['jquery', 'lib/underscore', 'unicode_utils'], function ($, _, unicode) {
	"use strict";

	// Return a slice of genesets over the current position.
	var genesetParse = function (text) {
			// Handle carriage return. Strip comment lines. Allow implicit comma at newline. Strip whitespace.
			var list = unicode.normalize(text).replace(/\r/g, '\n').replace(/#[^\n]*/g, "").replace(/^\s+/, '').replace(/\s+$/, '').split(/[\s,]+/);
			return _(list).without('');
		};

	return {
		genesetParse : genesetParse
	};

});
<|endoftext|>


<commit_before>import { createAction, handleActions } from 'redux-actions';
import {
  fireRef,
  fetch,
  fetchOrFail,
  fetchUser,
  doesExist } from '../utils/firebase';

// ------------------------------------
// Constants
// ------------------------------------
const REG_QUESTION_LISTENER = 'REG_QUESTION_LISTENER';
const VOID_QUESTION_LISTENER = 'VOID_QUESTION_LISTENER';

const JOIN_REQUEST = 'JOIN_REQUEST';
const JOIN_SUCCESS = 'JOIN_SUCCESS';
const JOIN_ERROR = 'JOIN_ERROR';

const QUESTION_REQUEST = 'QUESTION_REQUEST';
const QUESTION_SUCCESS = 'QUESTION_SUCCESS';
const QUESTION_ERROR = 'QUESTION_ERROR';

const VOTE_REQUEST = 'VOTE_REQUEST';
const PARTICIPANTS_UPDATE = 'PARTICIPANTS_UPDATE';
const VOTES_UPDATE = 'VOTES_UPDATE';

// ------------------------------------
// Actions
// ------------------------------------
const regListener =
  createAction(REG_QUESTION_LISTENER, (name, ref) => ({name, ref}));
const voidListener =
  createAction(VOID_QUESTION_LISTENER, () => {});

const joinRequest =
  createAction(JOIN_REQUEST, (name) => name);
const joinSuccess =
  createAction(JOIN_SUCCESS, (name) => name);
const joinError =
  createAction(JOIN_ERROR, (error) => error);

const questionRequest =
  createAction(QUESTION_REQUEST, (name) => name);
const questionSuccess =
  createAction(QUESTION_SUCCESS, (questionData) => questionData);
const questionError =
  createAction(QUESTION_ERROR, (error) => error);

const voteRequest =
  createAction(VOTE_REQUEST, (question, date, period) => ({question, date, period}));
const participantsUpdate =
  createAction(PARTICIPANTS_UPDATE, (participants) => participants);
const votesUpdate =
  createAction(VOTES_UPDATE, (votes, myVotes) => ({votes, myVotes}));

const joinAsync = (name) => {
  return (dispatch, getState) => {
    const authData = getState().auth.authData;
    const questionRef = fireRef.child('questions').child(name);

    return new Promise((resolve, reject) => {
      dispatch(joinRequest(name));

      doesExist(questionRef)
        .then((questionData) => {
          console.log('join a question', questionData);
          // join a new question, and assign the default vote and add himself as one of the participants
          // set participants
          const participantsRef = questionRef.child('participants').child(authData.uid);
          participantsRef.set(true);

          // set votes
          const votesRef = questionRef.child('votes').child(authData.uid);
          votesRef.set(questionData.structure.map(x => ({
            date: x.date,
            periods: x.periods
          })));

          dispatch(joinSuccess(name));
          resolve(name);
        })
        .catch(() => {
          dispatch(joinError('question not found'));
          reject('question not found');
        });
    });
  };
};

const questionAsync = (name) => {
  return (dispatch, getState) => {
    dispatch(questionRequest(name));
    const questionRef = fireRef
      .child('questions')
      .child(name);

    return new Promise((resolve, reject) => {
      // fetch question
      fetchOrFail(questionRef)
        .then((questionData) =>
          Promise.all([
            questionData,
            // fetch owner
            fetchUser(questionData.owner)
          ].concat(
            // fetch all participants
            Object.keys(questionData.participants)
              .map((uid) => fetchUser(uid))
          )))
        .then((values) => {
          const [questionData, owner, ...participants] = values;
          const qData = {
            ...questionData,
            owner: owner,
            participants: participants.reduce((acc, x) => {
              // turns array into object (with uid as its indices)
              acc[x.uid] = x;
              return acc;
            }, {})
          };
          dispatch(questionSuccess(qData));
          resolve(qData);
        })
        .catch((error) => {
          dispatch(questionError(error));
          reject(error);
        });
    });
  };
};

const vote = (question, date, period) => {
  return (dispatch, getState) => {
    dispatch(voteRequest(question, date, period));
    const authData = getState().auth.authData;
    const currentVote =
      getState().question.votes[date].periods[period];
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('votes')
        .child(authData.uid)
        .child(date)
        .child('periods')
        .child(period);
    ref.set((currentVote + 1) % 3);
  };
};

const participantsListener = (authData, question) => {
  return (dispatch, getState) => {
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('participants');
    dispatch(regListener('participants', ref));
    ref.on('value', (snapshot) => {
      const participants = snapshot.val();
      Promise.all(participants.map(uid => fetchUser(uid)))
        .then((participants) => {
          dispatch(participantsUpdate(
            // turn an array to object with uid as its indices
            participants.reduce((acc, x) => {
              acc[x.uid] = x;
              return acc;
            }, {})
          ));
        });
    });
  };
};

const votesListener = (authData, question) => {
  return (dispatch, getState) => {
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('votes');
    dispatch(regListener('question', ref));
    ref.on('value', (snapshot) => {
      const votes = snapshot.val();
      const myVotes = votes[authData.uid];
      dispatch(votesUpdate(votes, myVotes));
    });
  };
};

const voidQuestionListener = () => {
  return (dispatch, getState) => {
    const listeners = getState().question.listeners;
    Object.keys(listeners).forEach((name) => {
      // turn off each listeners
      listeners[name].off();
    });
    dispatch(voidListener());
  };
};

export const actions = {
  joinAsync,
  questionAsync,
  vote,
};

export const listeners = {
  participantsListener,
  votesListener,
  voidQuestionListener,
};

// ------------------------------------
// Reducer
// ------------------------------------
const calculateScore = (structure, votes) => {
  // [[0, 0, 0], [0, 1, 2]] => avg => [0, 0.5, 1]
  const avg = (arr) => {
    const result = [];
    for (let j = 0; j < arr[0].length; ++j) {
      let sum = 0;
      for (let i = 0; i < arr.length; ++i) {
        sum += arr[i][j];
      }
      result.push(sum / arr.length);
    }
    return result;
  };
  const allDates = Object.keys(structure);
  const result = {};
  for (let date of allDates) {
    result[date] = {
      date,
      periods: avg(
        Object.keys(votes)
          .map((uid) => votes[uid][date].periods))
    };
  }
  return result;
};

export default handleActions({
  [REG_QUESTION_LISTENER]: (state, {payload}) => {
    return {
      ...state,
      listeners: {
        ...state.listeners,
        [payload.name]: payload.ref,
      },
    };
  },

  [VOID_QUESTION_LISTENER]: (state, {payload}) => {
    return {
      ...state,
      listeners: {},
    };
  },

  [JOIN_REQUEST]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: true,
        name: payload,
      }
    };
  },

  [JOIN_SUCCESS]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: false,
      },
    };
  },

  [JOIN_ERROR]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: false,
        error: payload
      },
    };
  },

  [QUESTION_REQUEST]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: true,
        name: payload
      },
    };
  },

  [QUESTION_SUCCESS]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: false,
      },
      questionData: payload,
    };
  },

  [QUESTION_ERROR]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: false,
        error: payload,
      },
      questionData: null,
    };
  },

  [PARTICIPANTS_UPDATE]: (state, { payload }) => {
    return {
      ...state,
      questionData: {
        ...state.questionData,
        participants: payload
      },
    };
  },

  [VOTES_UPDATE]: (state, {payload}) => {
    const q = state.questionData;
    const {votes, myVotes} = payload;
    return {
      ...state,
      questionData: {
        ...state.questionData,
        votes: votes,
      },
      votes: myVotes,
      score: calculateScore(q.structure, votes),
    };
  },

}, {
  // default state
  listeners: {},

  _join: {
    loading: false,
    error: null,
    name: null,
  },

  _question: {
    loading: false,
    error: null,
    name: null,
  },

  questionData: undefined,
  votes: null,
  score: null,
});
<commit_msg>calculateScore with functional style<commit_after>import { createAction, handleActions } from 'redux-actions';
import {
  fireRef,
  fetch,
  fetchOrFail,
  fetchUser,
  doesExist } from '../utils/firebase';

// ------------------------------------
// Constants
// ------------------------------------
const REG_QUESTION_LISTENER = 'REG_QUESTION_LISTENER';
const VOID_QUESTION_LISTENER = 'VOID_QUESTION_LISTENER';

const JOIN_REQUEST = 'JOIN_REQUEST';
const JOIN_SUCCESS = 'JOIN_SUCCESS';
const JOIN_ERROR = 'JOIN_ERROR';

const QUESTION_REQUEST = 'QUESTION_REQUEST';
const QUESTION_SUCCESS = 'QUESTION_SUCCESS';
const QUESTION_ERROR = 'QUESTION_ERROR';

const VOTE_REQUEST = 'VOTE_REQUEST';
const PARTICIPANTS_UPDATE = 'PARTICIPANTS_UPDATE';
const VOTES_UPDATE = 'VOTES_UPDATE';

// ------------------------------------
// Actions
// ------------------------------------
const regListener =
  createAction(REG_QUESTION_LISTENER, (name, ref) => ({name, ref}));
const voidListener =
  createAction(VOID_QUESTION_LISTENER, () => {});

const joinRequest =
  createAction(JOIN_REQUEST, (name) => name);
const joinSuccess =
  createAction(JOIN_SUCCESS, (name) => name);
const joinError =
  createAction(JOIN_ERROR, (error) => error);

const questionRequest =
  createAction(QUESTION_REQUEST, (name) => name);
const questionSuccess =
  createAction(QUESTION_SUCCESS, (questionData) => questionData);
const questionError =
  createAction(QUESTION_ERROR, (error) => error);

const voteRequest =
  createAction(VOTE_REQUEST, (question, date, period) => ({question, date, period}));
const participantsUpdate =
  createAction(PARTICIPANTS_UPDATE, (participants) => participants);
const votesUpdate =
  createAction(VOTES_UPDATE, (votes, myVotes) => ({votes, myVotes}));

const joinAsync = (name) => {
  return (dispatch, getState) => {
    const authData = getState().auth.authData;
    const questionRef = fireRef.child('questions').child(name);

    return new Promise((resolve, reject) => {
      dispatch(joinRequest(name));

      doesExist(questionRef)
        .then((questionData) => {
          console.log('join a question', questionData);
          // join a new question, and assign the default vote and add himself as one of the participants
          // set participants
          const participantsRef = questionRef.child('participants').child(authData.uid);
          participantsRef.set(true);

          // set votes
          const votesRef = questionRef.child('votes').child(authData.uid);
          votesRef.set(questionData.structure.map(x => ({
            date: x.date,
            periods: x.periods
          })));

          dispatch(joinSuccess(name));
          resolve(name);
        })
        .catch(() => {
          dispatch(joinError('question not found'));
          reject('question not found');
        });
    });
  };
};

const questionAsync = (name) => {
  return (dispatch, getState) => {
    dispatch(questionRequest(name));
    const questionRef = fireRef
      .child('questions')
      .child(name);

    return new Promise((resolve, reject) => {
      // fetch question
      fetchOrFail(questionRef)
        .then((questionData) =>
          Promise.all([
            questionData,
            // fetch owner
            fetchUser(questionData.owner)
          ].concat(
            // fetch all participants
            Object.keys(questionData.participants)
              .map((uid) => fetchUser(uid))
          )))
        .then((values) => {
          const [questionData, owner, ...participants] = values;
          const qData = {
            ...questionData,
            owner: owner,
            participants: participants.reduce((acc, x) => {
              // turns array into object (with uid as its indices)
              acc[x.uid] = x;
              return acc;
            }, {})
          };
          dispatch(questionSuccess(qData));
          resolve(qData);
        })
        .catch((error) => {
          dispatch(questionError(error));
          reject(error);
        });
    });
  };
};

const vote = (question, date, period) => {
  return (dispatch, getState) => {
    dispatch(voteRequest(question, date, period));
    const authData = getState().auth.authData;
    const currentVote =
      getState().question.votes[date].periods[period];
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('votes')
        .child(authData.uid)
        .child(date)
        .child('periods')
        .child(period);
    ref.set((currentVote + 1) % 3);
  };
};

const participantsListener = (authData, question) => {
  return (dispatch, getState) => {
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('participants');
    dispatch(regListener('participants', ref));
    ref.on('value', (snapshot) => {
      const participants = snapshot.val();
      Promise.all(participants.map(uid => fetchUser(uid)))
        .then((participants) => {
          dispatch(participantsUpdate(
            // turn an array to object with uid as its indices
            participants.reduce((acc, x) => {
              acc[x.uid] = x;
              return acc;
            }, {})
          ));
        });
    });
  };
};

const votesListener = (authData, question) => {
  return (dispatch, getState) => {
    const ref =
      fireRef
        .child('questions')
        .child(question)
        .child('votes');
    dispatch(regListener('question', ref));
    ref.on('value', (snapshot) => {
      const votes = snapshot.val();
      const myVotes = votes[authData.uid];
      dispatch(votesUpdate(votes, myVotes));
    });
  };
};

const voidQuestionListener = () => {
  return (dispatch, getState) => {
    const listeners = getState().question.listeners;
    Object.keys(listeners).forEach((name) => {
      // turn off each listeners
      listeners[name].off();
    });
    dispatch(voidListener());
  };
};

export const actions = {
  joinAsync,
  questionAsync,
  vote,
};

export const listeners = {
  participantsListener,
  votesListener,
  voidQuestionListener,
};

// ------------------------------------
// Reducer
// ------------------------------------
const calculateScore = (structure, votes) => {
  // [[0, 0, 0], [0, 1, 2]] => avg => [0, 0.5, 1]
  const avg = (arr) => {
    // const result = [];
    // for (let j = 0; j < arr[0].length; ++j) {
    //   let sum = 0;
    //   for (let i = 0; i < arr.length; ++i) {
    //     sum += arr[i][j];
    //   }
    //   result.push(sum / arr.length);
    // }
    // return result;

    const acc = [];
    for (let i = 0; i < arr[0].length; ++i) {
      acc.push(0);
    }

    const sum = arr.reduce(
      (acc, row) => acc.map(
        (acc, i) => acc + row[i])
      , acc
    );

    const avg = sum.map(x => x / arr.length);
    return avg;
  };
  const allDates = Object.keys(structure);
  const result = {};
  for (let date of allDates) {
    result[date] = {
      date,
      periods: avg(
        Object.keys(votes)
          .map((uid) => votes[uid][date].periods))
    };
  }
  return result;
};

export default handleActions({
  [REG_QUESTION_LISTENER]: (state, {payload}) => {
    return {
      ...state,
      listeners: {
        ...state.listeners,
        [payload.name]: payload.ref,
      },
    };
  },

  [VOID_QUESTION_LISTENER]: (state, {payload}) => {
    return {
      ...state,
      listeners: {},
    };
  },

  [JOIN_REQUEST]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: true,
        name: payload,
      }
    };
  },

  [JOIN_SUCCESS]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: false,
      },
    };
  },

  [JOIN_ERROR]: (state, { payload }) => {
    return {
      ...state,
      _join: {
        ...state._join,
        loading: false,
        error: payload
      },
    };
  },

  [QUESTION_REQUEST]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: true,
        name: payload
      },
    };
  },

  [QUESTION_SUCCESS]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: false,
      },
      questionData: payload,
    };
  },

  [QUESTION_ERROR]: (state, { payload }) => {
    return {
      ...state,
      _question: {
        ...state._question,
        loading: false,
        error: payload,
      },
      questionData: null,
    };
  },

  [PARTICIPANTS_UPDATE]: (state, { payload }) => {
    return {
      ...state,
      questionData: {
        ...state.questionData,
        participants: payload
      },
    };
  },

  [VOTES_UPDATE]: (state, {payload}) => {
    const q = state.questionData;
    const {votes, myVotes} = payload;
    return {
      ...state,
      questionData: {
        ...state.questionData,
        votes: votes,
      },
      votes: myVotes,
      score: calculateScore(q.structure, votes),
    };
  },

}, {
  // default state
  listeners: {},

  _join: {
    loading: false,
    error: null,
    name: null,
  },

  _question: {
    loading: false,
    error: null,
    name: null,
  },

  questionData: undefined,
  votes: null,
  score: null,
});
<|endoftext|>