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

/**
 * Faucet with a pinball machine 'shooter'.
 * Pulling out the shooter changes the flow rate.
 * Releasing the shooter sets the flow rate to zero.
 * When the faucet is disabled, the flow rate is set to zero and the shooter is disabled.
 * Origin is at the bottom-center of the spout.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Timer = require( 'JOIST/Timer' );

  // images
  var knobImage = require( 'image!SCENERY_PHET/faucet_knob.png' );
  var knobDisabledImage = require( 'image!SCENERY_PHET/faucet_knob_disabled.png' );
  var shaftImage = require( 'image!SCENERY_PHET/faucet_shaft.png' );
  var flangeImage = require( 'image!SCENERY_PHET/faucet_flange.png' );
  var flangeDisabledImage = require( 'image!SCENERY_PHET/faucet_flange_disabled.png' );
  var stopImage = require( 'image!SCENERY_PHET/faucet_stop.png' );
  var horizontalPipeImage = require( 'image!SCENERY_PHET/faucet_horizontal_pipe.png' );
  var verticalPipeImage = require( 'image!SCENERY_PHET/faucet_vertical_pipe.png' );
  var bodyImage = require( 'image!SCENERY_PHET/faucet_body.png' );
  var spoutImage = require( 'image!SCENERY_PHET/faucet_spout.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SPOUT_OUTPUT_CENTER_X = 112; // center of spout in bodyImage
  var HORIZONTAL_PIPE_Y_OFFSET = 30; // y-offset of horizontal pipe in bodyImage
  var HORIZONTAL_PIPE_X_OVERLAP = 1; // overlap between horizontal pipe and faucet body, so vertical seam is not visible
  var VERTICAL_PIPE_Y_OVERLAP = 1; // overlap between vertical pipe and faucet body/spout, so horizontal seam is not visible
  var SHOOTER_MIN_X_OFFSET = 4; // x-offset of shooter's off position in bodyImage
  var SHOOTER_MAX_X_OFFSET = 66; // x-offset of shooter's full-on position in bodyImage
  var SHOOTER_Y_OFFSET = 15; // y-offset of shooter's centerY in bodyImage
  var SHOOTER_WINDOW_BOUNDS = new Bounds2( 10, 10, 90, 25 ); // bounds of the window in bodyImage, through which you see the shooter handle

  /**
   * The 'shooter' is the interactive part of the faucet.
   * It's a relatively complicated node, so it's encapsulated in this nested type.
   *
   * @param {Property<boolean>} enabledProperty
   * @constructor
   */
  function ShooterNode( enabledProperty, options ) {

    options = _.extend( {
      knobScale: 1
    }, options );

    // knob
    var knobNode = new Image( knobImage );
    var dx = 0.5 * knobNode.width;
    var dy = 0.5 * knobNode.height;
    knobNode.touchArea = Shape.rectangle( -dx, -dy, knobNode.width + dx + dx, knobNode.height + dy + dy ); // before scaling!
    knobNode.scale( options.knobScale );
    var knobDisabledNode = new Image( knobDisabledImage );
    knobDisabledNode.scale( knobNode.getScaleVector() );

    // shaft
    var shaftNode = new Image( shaftImage );

    // flange
    var flangeNode = new Image( flangeImage );
    var flangeDisabledNode = new Image( flangeDisabledImage );

    // stop
    var stopNode = new Image( stopImage );

    Node.call( this, { children: [ shaftNode, stopNode, flangeNode, flangeDisabledNode, knobNode, knobDisabledNode ] } );

    // layout, relative to shaft
    stopNode.x = shaftNode.x + 12;
    stopNode.centerY = shaftNode.centerY;
    flangeNode.left = shaftNode.right;
    flangeNode.centerY = shaftNode.centerY;
    flangeDisabledNode.x = flangeNode.x;
    flangeDisabledNode.y = flangeNode.y;
    knobNode.left = flangeNode.right - 8; // a bit of overlap makes this look better
    knobNode.centerY = flangeNode.centerY;
    knobDisabledNode.x = knobNode.x;
    knobDisabledNode.y = knobNode.y;

    enabledProperty.link( function( enabled ) {
      // the entire shooter is draggable, but encourage dragging by the knob by changing its cursor
      knobNode.cursor = flangeNode.cursor = enabled ? 'pointer' : 'default';
      knobNode.visible = enabled;
      knobDisabledNode.visible = !enabled;
      flangeNode.visible = enabled;
      flangeDisabledNode.visible = !enabled;
    } );
  }

  inherit( Node, ShooterNode );

  /**
   * @param {Number} maxFlowRate
   * @param {Property<Number>} flowRateProperty
   * @param {Property<Boolean>} enabledProperty
   * @param {*} options
   * @constructor
   */
  function FaucetNode( maxFlowRate, flowRateProperty, enabledProperty, options ) {

    options = _.extend( {
      scale: 1,
      knobScale: 0.6, // values in the range 0.6 - 1.0 look decent
      horizontalPipeLength: 50, // distance between left edge of horizontal pipe and spout's center
      verticalPipeLength: 43, // length of the vertical pipe that connects the faucet body to the spout
      tapToDispenseEnabled: true, // tap-to-dispense feature: tapping the shooter dispenses some fluid
      tapToDispenseAmount: 0.25 * maxFlowRate, // tap-to-dispense feature: amount to dispense, in L
      tapToDispenseInterval: 500, // tap-to-dispense feature: amount of time that fluid is dispensed, in milliseconds
      closeOnRelease: true // when the shooter is released, close the faucet
    }, options );
    assert && assert( ( 1000 * options.tapToDispenseAmount / options.tapToDispenseInterval ) <= maxFlowRate );

    var thisNode = this;
    Node.call( thisNode );

    // shooter
    var shooterNode = new ShooterNode( enabledProperty, { knobScale: options.knobScale } );

    // horizontal pipe, tiled horizontally
    var horizontalPipeNode = new Image( horizontalPipeImage );
    var horizontalPipeWidth = options.horizontalPipeLength - SPOUT_OUTPUT_CENTER_X + HORIZONTAL_PIPE_X_OVERLAP;
    assert && assert( horizontalPipeWidth > 0 );
    horizontalPipeNode.setScaleMagnitude( horizontalPipeWidth / horizontalPipeImage.width, 1 );

    // vertical pipe
    var verticalPipeNode = new Image( verticalPipeImage );
    var verticalPipeNodeHeight = options.verticalPipeLength + ( 2 * VERTICAL_PIPE_Y_OVERLAP );
    assert && assert( verticalPipeNodeHeight > 0 );
    verticalPipeNode.setScaleMagnitude( 1, verticalPipeNodeHeight / verticalPipeNode.height );

    // other nodes
    var spoutNode = new Image( spoutImage );
    var bodyNode = new Image( bodyImage );
    var shooterWindowNode = new Rectangle( SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.minY,
      SHOOTER_WINDOW_BOUNDS.maxX - SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.maxY - SHOOTER_WINDOW_BOUNDS.minY,
      { fill: 'rgb(107,107,107)' } );

    // rendering order
    thisNode.addChild( shooterWindowNode );
    thisNode.addChild( shooterNode );
    thisNode.addChild( horizontalPipeNode );
    thisNode.addChild( verticalPipeNode );
    thisNode.addChild( spoutNode );
    thisNode.addChild( bodyNode );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // spout's origin is at bottom-center
      spoutNode.centerX = 0;
      spoutNode.bottom = 0;
      // vertical pipe above spout
      verticalPipeNode.centerX = spoutNode.centerX;
      verticalPipeNode.bottom = spoutNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // body above vertical pipe
      bodyNode.right = verticalPipeNode.right;
      bodyNode.bottom = verticalPipeNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // shooter window is in the body's coordinate frame
      shooterWindowNode.x = bodyNode.x;
      shooterWindowNode.y = bodyNode.y;
      // horizontal pipe connects to left edge of body
      horizontalPipeNode.right = bodyNode.left + HORIZONTAL_PIPE_X_OVERLAP;
      horizontalPipeNode.top = bodyNode.top + HORIZONTAL_PIPE_Y_OFFSET;
      // shooter at top of body
      shooterNode.left = bodyNode.left + SHOOTER_MIN_X_OFFSET;
      shooterNode.centerY = bodyNode.top + SHOOTER_Y_OFFSET;
    }

    // x-offset relative to left edge of bodyNode
    var offsetToFlowRate = new LinearFunction( SHOOTER_MIN_X_OFFSET, SHOOTER_MAX_X_OFFSET, 0, maxFlowRate, true /* clamp */ );

    // tap-to-dispense feature
    var tapToDispenseIsArmed = false; // should we do tap-to-dispense when the pointer is released?
    var tapToDispenseIsRunning = false; // is tap-to-dispense in progress?
    var timeoutID = null;
    var intervalID = null;
    var startTapToDispense = function() {
      if ( enabledProperty.get() && tapToDispenseIsArmed ) { // redundant guard
        tapToDispenseIsArmed = false;
        tapToDispenseIsRunning = true;
        flowRateProperty.set( ( options.tapToDispenseAmount / options.tapToDispenseInterval ) * 1000 ); // L/ms -> L/sec
        timeoutID = Timer.setTimeout( function() {
          intervalID = Timer.setInterval( function() {
            endTapToDispense();
          }, options.tapToDispenseInterval );
        }, 0 );
      }
    };
    var endTapToDispense = function() {
      flowRateProperty.set( 0 );
      if ( timeoutID !== null ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID !== null ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
      tapToDispenseIsRunning = false;
    };

    var shooterHandler = new SimpleDragHandler( {

      startXOffset: 0, // where the drag started, relative to the target node's origin, in parent view coordinates

      allowTouchSnag: true,

      start: function( event ) {
        // prepare to do tap-to-dispense, will be canceled if the user drags before releasing the pointer
        tapToDispenseIsArmed = options.tapToDispenseEnabled;
        this.startXOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x - event.currentTarget.left;
      },

      // adjust the flow
      drag: function( event ) {

        // dragging is the cue that we're not doing tap-to-dispense
        tapToDispenseIsArmed = false;
        if ( tapToDispenseIsRunning ) { endTapToDispense(); }

        // compute the new flow rate
        if ( enabledProperty.get() ) {
          // offsetToFlowRate is relative to bodyNode.left, so account for it
          var xOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x - this.startXOffset - bodyNode.left;
          var flowRate = offsetToFlowRate( xOffset );
          flowRateProperty.set( flowRate );
        }
      },

      end: function() {
        if ( enabledProperty.get() ) {
          if ( tapToDispenseIsArmed ) {
            // tapping toggles the tap-to-dispense state
            ( tapToDispenseIsRunning || flowRateProperty.get() !== 0 ) ? endTapToDispense() : startTapToDispense();
          }
          else if ( options.closeOnRelease ) {
            // the shooter was dragged and released, so turn off the faucet
            flowRateProperty.set( 0 );
          }
        }
      }
    } );
    shooterNode.addInputListener( shooterHandler );

    flowRateProperty.link( function( flowRate ) {
      shooterNode.left = bodyNode.left + offsetToFlowRate.inverse( flowRate );
    } );

    enabledProperty.link( function( enabled ) {
      if ( !enabled && shooterHandler.dragging ) {
        shooterHandler.endDrag();
      }
      if ( !enabled && tapToDispenseIsRunning ) {
        endTapToDispense();
      }
    } );

    thisNode.mutate( options );
  }

  return inherit( Node, FaucetNode );
} );
<commit_msg>#51 update doc to describe behavior options for the shooter<commit_after>// Copyright 2002-2014, University of Colorado Boulder

/**
 * Faucet with a pinball machine 'shooter'.
 * When the faucet is disabled, the flow rate is set to zero and the shooter is disabled.
 * Origin is at the bottom-center of the spout.
 *
 * The shooter is interactive, with the following features:
 *
 * (1) Close-on-release mode: When the user drags the slider, releasing it sets the flow to zero.
 * See options.closeToRelease: true.
 *
 * (2) Slider mode: When the user drags the slider, releasing it will leave the shooter wherever it is
 * released, and (if in the on position) the flow will continue. See options.closeToRelease: false.
 *
 * (3) Tap-to-dispense: When the user taps on the shooter without dragging, it's on/off state toggles.
 * If the shooter was in the off state when tapped, it opens and dispenses a configurable amount of fluid.
 * This feature can be enabled simultaneously with (1) and (2) above. See the various tapToDispense* options.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var Bounds2 = require( 'DOT/Bounds2' );
  var Circle = require( 'SCENERY/nodes/Circle' );
  var Image = require( 'SCENERY/nodes/Image' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var Timer = require( 'JOIST/Timer' );

  // images
  var knobImage = require( 'image!SCENERY_PHET/faucet_knob.png' );
  var knobDisabledImage = require( 'image!SCENERY_PHET/faucet_knob_disabled.png' );
  var shaftImage = require( 'image!SCENERY_PHET/faucet_shaft.png' );
  var flangeImage = require( 'image!SCENERY_PHET/faucet_flange.png' );
  var flangeDisabledImage = require( 'image!SCENERY_PHET/faucet_flange_disabled.png' );
  var stopImage = require( 'image!SCENERY_PHET/faucet_stop.png' );
  var horizontalPipeImage = require( 'image!SCENERY_PHET/faucet_horizontal_pipe.png' );
  var verticalPipeImage = require( 'image!SCENERY_PHET/faucet_vertical_pipe.png' );
  var bodyImage = require( 'image!SCENERY_PHET/faucet_body.png' );
  var spoutImage = require( 'image!SCENERY_PHET/faucet_spout.png' );

  // constants
  var DEBUG_ORIGIN = false;
  var SPOUT_OUTPUT_CENTER_X = 112; // center of spout in bodyImage
  var HORIZONTAL_PIPE_Y_OFFSET = 30; // y-offset of horizontal pipe in bodyImage
  var HORIZONTAL_PIPE_X_OVERLAP = 1; // overlap between horizontal pipe and faucet body, so vertical seam is not visible
  var VERTICAL_PIPE_Y_OVERLAP = 1; // overlap between vertical pipe and faucet body/spout, so horizontal seam is not visible
  var SHOOTER_MIN_X_OFFSET = 4; // x-offset of shooter's off position in bodyImage
  var SHOOTER_MAX_X_OFFSET = 66; // x-offset of shooter's full-on position in bodyImage
  var SHOOTER_Y_OFFSET = 15; // y-offset of shooter's centerY in bodyImage
  var SHOOTER_WINDOW_BOUNDS = new Bounds2( 10, 10, 90, 25 ); // bounds of the window in bodyImage, through which you see the shooter handle

  /**
   * The 'shooter' is the interactive part of the faucet.
   * It's a relatively complicated node, so it's encapsulated in this nested type.
   *
   * @param {Property<boolean>} enabledProperty
   * @constructor
   */
  function ShooterNode( enabledProperty, options ) {

    options = _.extend( {
      knobScale: 1
    }, options );

    // knob
    var knobNode = new Image( knobImage );
    var dx = 0.5 * knobNode.width;
    var dy = 0.5 * knobNode.height;
    knobNode.touchArea = Shape.rectangle( -dx, -dy, knobNode.width + dx + dx, knobNode.height + dy + dy ); // before scaling!
    knobNode.scale( options.knobScale );
    var knobDisabledNode = new Image( knobDisabledImage );
    knobDisabledNode.scale( knobNode.getScaleVector() );

    // shaft
    var shaftNode = new Image( shaftImage );

    // flange
    var flangeNode = new Image( flangeImage );
    var flangeDisabledNode = new Image( flangeDisabledImage );

    // stop
    var stopNode = new Image( stopImage );

    Node.call( this, { children: [ shaftNode, stopNode, flangeNode, flangeDisabledNode, knobNode, knobDisabledNode ] } );

    // layout, relative to shaft
    stopNode.x = shaftNode.x + 12;
    stopNode.centerY = shaftNode.centerY;
    flangeNode.left = shaftNode.right;
    flangeNode.centerY = shaftNode.centerY;
    flangeDisabledNode.x = flangeNode.x;
    flangeDisabledNode.y = flangeNode.y;
    knobNode.left = flangeNode.right - 8; // a bit of overlap makes this look better
    knobNode.centerY = flangeNode.centerY;
    knobDisabledNode.x = knobNode.x;
    knobDisabledNode.y = knobNode.y;

    enabledProperty.link( function( enabled ) {
      // the entire shooter is draggable, but encourage dragging by the knob by changing its cursor
      knobNode.cursor = flangeNode.cursor = enabled ? 'pointer' : 'default';
      knobNode.visible = enabled;
      knobDisabledNode.visible = !enabled;
      flangeNode.visible = enabled;
      flangeDisabledNode.visible = !enabled;
    } );
  }

  inherit( Node, ShooterNode );

  /**
   * @param {Number} maxFlowRate
   * @param {Property<Number>} flowRateProperty
   * @param {Property<Boolean>} enabledProperty
   * @param {*} options
   * @constructor
   */
  function FaucetNode( maxFlowRate, flowRateProperty, enabledProperty, options ) {

    options = _.extend( {
      scale: 1,
      knobScale: 0.6, // values in the range 0.6 - 1.0 look decent
      horizontalPipeLength: 50, // distance between left edge of horizontal pipe and spout's center
      verticalPipeLength: 43, // length of the vertical pipe that connects the faucet body to the spout
      tapToDispenseEnabled: true, // tap-to-dispense feature: tapping the shooter dispenses some fluid
      tapToDispenseAmount: 0.25 * maxFlowRate, // tap-to-dispense feature: amount to dispense, in L
      tapToDispenseInterval: 500, // tap-to-dispense feature: amount of time that fluid is dispensed, in milliseconds
      closeOnRelease: true // when the shooter is released, close the faucet
    }, options );
    assert && assert( ( 1000 * options.tapToDispenseAmount / options.tapToDispenseInterval ) <= maxFlowRate );

    var thisNode = this;
    Node.call( thisNode );

    // shooter
    var shooterNode = new ShooterNode( enabledProperty, { knobScale: options.knobScale } );

    // horizontal pipe, tiled horizontally
    var horizontalPipeNode = new Image( horizontalPipeImage );
    var horizontalPipeWidth = options.horizontalPipeLength - SPOUT_OUTPUT_CENTER_X + HORIZONTAL_PIPE_X_OVERLAP;
    assert && assert( horizontalPipeWidth > 0 );
    horizontalPipeNode.setScaleMagnitude( horizontalPipeWidth / horizontalPipeImage.width, 1 );

    // vertical pipe
    var verticalPipeNode = new Image( verticalPipeImage );
    var verticalPipeNodeHeight = options.verticalPipeLength + ( 2 * VERTICAL_PIPE_Y_OVERLAP );
    assert && assert( verticalPipeNodeHeight > 0 );
    verticalPipeNode.setScaleMagnitude( 1, verticalPipeNodeHeight / verticalPipeNode.height );

    // other nodes
    var spoutNode = new Image( spoutImage );
    var bodyNode = new Image( bodyImage );
    var shooterWindowNode = new Rectangle( SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.minY,
        SHOOTER_WINDOW_BOUNDS.maxX - SHOOTER_WINDOW_BOUNDS.minX, SHOOTER_WINDOW_BOUNDS.maxY - SHOOTER_WINDOW_BOUNDS.minY,
      { fill: 'rgb(107,107,107)' } );

    // rendering order
    thisNode.addChild( shooterWindowNode );
    thisNode.addChild( shooterNode );
    thisNode.addChild( horizontalPipeNode );
    thisNode.addChild( verticalPipeNode );
    thisNode.addChild( spoutNode );
    thisNode.addChild( bodyNode );

    // origin
    if ( DEBUG_ORIGIN ) {
      thisNode.addChild( new Circle( { radius: 3, fill: 'red' } ) );
    }

    // layout
    {
      // spout's origin is at bottom-center
      spoutNode.centerX = 0;
      spoutNode.bottom = 0;
      // vertical pipe above spout
      verticalPipeNode.centerX = spoutNode.centerX;
      verticalPipeNode.bottom = spoutNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // body above vertical pipe
      bodyNode.right = verticalPipeNode.right;
      bodyNode.bottom = verticalPipeNode.top + VERTICAL_PIPE_Y_OVERLAP;
      // shooter window is in the body's coordinate frame
      shooterWindowNode.x = bodyNode.x;
      shooterWindowNode.y = bodyNode.y;
      // horizontal pipe connects to left edge of body
      horizontalPipeNode.right = bodyNode.left + HORIZONTAL_PIPE_X_OVERLAP;
      horizontalPipeNode.top = bodyNode.top + HORIZONTAL_PIPE_Y_OFFSET;
      // shooter at top of body
      shooterNode.left = bodyNode.left + SHOOTER_MIN_X_OFFSET;
      shooterNode.centerY = bodyNode.top + SHOOTER_Y_OFFSET;
    }

    // x-offset relative to left edge of bodyNode
    var offsetToFlowRate = new LinearFunction( SHOOTER_MIN_X_OFFSET, SHOOTER_MAX_X_OFFSET, 0, maxFlowRate, true /* clamp */ );

    // tap-to-dispense feature
    var tapToDispenseIsArmed = false; // should we do tap-to-dispense when the pointer is released?
    var tapToDispenseIsRunning = false; // is tap-to-dispense in progress?
    var timeoutID = null;
    var intervalID = null;
    var startTapToDispense = function() {
      if ( enabledProperty.get() && tapToDispenseIsArmed ) { // redundant guard
        tapToDispenseIsArmed = false;
        tapToDispenseIsRunning = true;
        flowRateProperty.set( ( options.tapToDispenseAmount / options.tapToDispenseInterval ) * 1000 ); // L/ms -> L/sec
        timeoutID = Timer.setTimeout( function() {
          intervalID = Timer.setInterval( function() {
            endTapToDispense();
          }, options.tapToDispenseInterval );
        }, 0 );
      }
    };
    var endTapToDispense = function() {
      flowRateProperty.set( 0 );
      if ( timeoutID !== null ) {
        Timer.clearTimeout( timeoutID );
        timeoutID = null;
      }
      if ( intervalID !== null ) {
        Timer.clearInterval( intervalID );
        intervalID = null;
      }
      tapToDispenseIsRunning = false;
    };

    var shooterHandler = new SimpleDragHandler( {

      startXOffset: 0, // where the drag started, relative to the target node's origin, in parent view coordinates

      allowTouchSnag: true,

      start: function( event ) {
        // prepare to do tap-to-dispense, will be canceled if the user drags before releasing the pointer
        tapToDispenseIsArmed = options.tapToDispenseEnabled;
        this.startXOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x - event.currentTarget.left;
      },

      // adjust the flow
      drag: function( event ) {

        // dragging is the cue that we're not doing tap-to-dispense
        tapToDispenseIsArmed = false;
        if ( tapToDispenseIsRunning ) { endTapToDispense(); }

        // compute the new flow rate
        if ( enabledProperty.get() ) {
          // offsetToFlowRate is relative to bodyNode.left, so account for it
          var xOffset = event.currentTarget.globalToParentPoint( event.pointer.point ).x - this.startXOffset - bodyNode.left;
          var flowRate = offsetToFlowRate( xOffset );
          flowRateProperty.set( flowRate );
        }
      },

      end: function() {
        if ( enabledProperty.get() ) {
          if ( tapToDispenseIsArmed ) {
            // tapping toggles the tap-to-dispense state
            ( tapToDispenseIsRunning || flowRateProperty.get() !== 0 ) ? endTapToDispense() : startTapToDispense();
          }
          else if ( options.closeOnRelease ) {
            // the shooter was dragged and released, so turn off the faucet
            flowRateProperty.set( 0 );
          }
        }
      }
    } );
    shooterNode.addInputListener( shooterHandler );

    flowRateProperty.link( function( flowRate ) {
      shooterNode.left = bodyNode.left + offsetToFlowRate.inverse( flowRate );
    } );

    enabledProperty.link( function( enabled ) {
      if ( !enabled && shooterHandler.dragging ) {
        shooterHandler.endDrag();
      }
      if ( !enabled && tapToDispenseIsRunning ) {
        endTapToDispense();
      }
    } );

    thisNode.mutate( options );
  }

  return inherit( Node, FaucetNode );
} );
<|endoftext|>


<commit_before>// Copyright 2015, EMC, Inc.

'use strict';

describe('Subscription', function () {
    var Subscription;

    helper.before();

    before(function () {
        Subscription = helper.injector.get('Subscription');

        this.options = {
            consumerTag: 'fake'
        };
    });

    beforeEach(function() {
        this.queue = {
            state: 'open',
            unsubscribe: sinon.stub().resolves(),
            destroy: sinon.stub().resolves(),
            close: sinon.stub().resolves()
        };
        this.subject = new Subscription(
            this.queue,
            this.options
        );
    });

    helper.after();

    describe('constructor', function () {
        it('assigns queue to queue', function () {
            this.subject.queue.should.deep.equal(this.queue);
        });

        it('assigns options to options', function () {
            this.subject.options.should.deep.equal(this.options);
        });
    });

    describe('dispose', function () {
        it('should unsubscribe the queue when the state is open', function () {
            var self = this;

            return this.subject.dispose().then(function () {
                self.queue.unsubscribe.should.have.been.calledWith('fake');
                self.queue.destroy.should.have.been.called;
                self.queue.close.should.have.been.called;
            });
        });

        it('should prevent closing the queue multiple times', function() {
            var self = this;

            return self.subject.dispose()
            .then(function() {
                return self.subject.dispose();
            })
            .then(function() {
                self.queue.unsubscribe.should.have.been.calledOnce;
                self.queue.destroy.should.have.been.calledOnce;
                self.queue.close.should.have.been.calledOnce;
            });
        });

        it('should retry closing the queue silently if it fails for the caller', function(done) {
            var self = this;
            sinon.spy(self.subject, 'dispose');
            self.subject.retryDelay = 0;
            self.subject.MAX_DISPOSE_RETRIES = 1;
            self.queue.unsubscribe.rejects(new Error('test error'));

            return expect(self.subject.dispose()).to.be.rejectedWith(/test error/)
            .then(function() {
                setImmediate(function() {
                    try {
                        expect(self.subject.dispose).to.have.been.calledTwice;
                        done();
                    } catch (e) {
                        done(e);
                    }
                });
            })
            .catch(function(err) {
                done(err);
            });
        });

        it('should reject with an error if the queue is already unsubscribed', function () {
            this.subject.queue.state = 'closing';

            return this.subject.dispose().should.be.rejectedWith(
                Error,
                'Attempted to dispose a subscription whose queue state is not open'
            );
        });
    });
});
<commit_msg>Make test pass with 50ms setTimeout<commit_after>// Copyright 2015, EMC, Inc.

'use strict';

describe('Subscription', function () {
    var Subscription;

    helper.before();

    before(function () {
        Subscription = helper.injector.get('Subscription');

        this.options = {
            consumerTag: 'fake'
        };
    });

    beforeEach(function() {
        this.queue = {
            state: 'open',
            unsubscribe: sinon.stub().resolves(),
            destroy: sinon.stub().resolves(),
            close: sinon.stub().resolves()
        };
        this.subject = new Subscription(
            this.queue,
            this.options
        );
    });

    helper.after();

    describe('constructor', function () {
        it('assigns queue to queue', function () {
            this.subject.queue.should.deep.equal(this.queue);
        });

        it('assigns options to options', function () {
            this.subject.options.should.deep.equal(this.options);
        });
    });

    describe('dispose', function () {
        it('should unsubscribe the queue when the state is open', function () {
            var self = this;

            return this.subject.dispose().then(function () {
                self.queue.unsubscribe.should.have.been.calledWith('fake');
                self.queue.destroy.should.have.been.called;
                self.queue.close.should.have.been.called;
            });
        });

        it('should prevent closing the queue multiple times', function() {
            var self = this;

            return self.subject.dispose()
            .then(function() {
                return self.subject.dispose();
            })
            .then(function() {
                self.queue.unsubscribe.should.have.been.calledOnce;
                self.queue.destroy.should.have.been.calledOnce;
                self.queue.close.should.have.been.calledOnce;
            });
        });

        it('should retry closing the queue silently if it fails for the caller', function(done) {
            var self = this;
            sinon.spy(self.subject, 'dispose');
            self.subject.retryDelay = 0;
            self.subject.MAX_DISPOSE_RETRIES = 1;
            self.queue.unsubscribe.rejects(new Error('test error'));

            return expect(self.subject.dispose()).to.be.rejectedWith(/test error/)
            .then(function() {
                setTimeout(function() {
                    try {
                        expect(self.subject.dispose).to.have.been.calledTwice;
                        done();
                    } catch (e) {
                        done(e);
                    }
                }, 50);
            })
            .catch(function(err) {
                done(err);
            });
        });

        it('should reject with an error if the queue is already unsubscribed', function () {
            this.subject.queue.state = 'closing';

            return this.subject.dispose().should.be.rejectedWith(
                Error,
                'Attempted to dispose a subscription whose queue state is not open'
            );
        });
    });
});
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';

import isNullOrUndefined from './utils/isNullOrUndefined';
import { LAYER_WIDTH } from './constants';
import decorate from './utils/decorate';

const propTypes = {
  align: PropTypes.oneOf(['left', 'center', 'right']),
  className: PropTypes.string,
  dataKey: PropTypes.string,
  isHeaderCell: PropTypes.bool,

  width: PropTypes.number,
  height: PropTypes.number,
  left: PropTypes.number,
  headerHeight: PropTypes.number,

  rowKey: PropTypes.string,

  /* eslint-disable */
  rowData: PropTypes.object,
  rowIndex: PropTypes.number,
  layer: PropTypes.number,  // for tree table

  style: PropTypes.object,
  firstColumn: PropTypes.bool,
  lastColumn: PropTypes.bool,
  hasChildren: PropTypes.bool,

  onTreeToggle: PropTypes.func,
  renderTreeToggle: PropTypes.func,
  cellRenderer: PropTypes.func,
  sortable: PropTypes.bool,
  wordWrap: PropTypes.bool
};

const defaultProps = {
  align: 'left',
  headerHeight: 36,
  height: 36,
  width: 0,
  layer: 0,
  left: 0
};

class Cell extends React.Component {
  shouldComponentUpdate(nextProps) {
    return !isEqual(this.props, nextProps);
  }
  renderExpandIcon() {
    const {
      hasChildren,
      firstColumn,
      onTreeToggle,
      rowKey,
      rowIndex,
      rowData,
      renderTreeToggle
    } = this.props;

    const handleExpandClick = (event) => {
      onTreeToggle && onTreeToggle(rowKey, rowIndex, rowData, event);
    };

    const expandButton = (
       {
          event.stopPropagation();
          handleExpandClick(event);
        }}
      />
    );

    /**
     * 如果用子节点，同时是第一列,则创建一个 icon 用于展开节点
     */
    if (hasChildren && firstColumn) {
      return renderTreeToggle ?
        (

            {renderTreeToggle(expandButton, rowData)}

        ) :
        expandButton;
    }

    return null;
  }

  render() {

    let {
      width,
      left,
      height,
      style,
      className,
      firstColumn,
      lastColumn,
      isHeaderCell,
      headerHeight,
      layer,
      align,
      sortable,
      children,
      rowData,
      dataKey,
      cellRenderer,
      removed,
      wordWrap,
      ...props
    } = this.props;

    if (removed) {
      return null;
    }

    const classes = classNames(this.prefix('cell'), {
      sortable: sortable && isHeaderCell,
      first: firstColumn,
      last: lastColumn
    }, className);

    const layerWidth = layer * LAYER_WIDTH;
    const nextWidth = (!isHeaderCell && firstColumn) ? width - layerWidth : width;
    const nextHeight = isHeaderCell ? headerHeight : height;

    const styles = {
      width: nextWidth,
      height: nextHeight,
      zIndex: layer,
      left: (!isHeaderCell && firstColumn) ? left + layerWidth : left,
      ...style
    };

    const contentStyles = {
      width: nextWidth,
      height: nextHeight,
      textAlign: align
    };

    if (sortable) {
      contentStyles.paddingRight = 28;
    }

    const contentChildren = isNullOrUndefined(children) && rowData ? get(rowData, dataKey) : children;
    const elementProps = omit(props, [
      'index',
      'fixed',
      'resizable',
      'flexGrow',
      'sortColumn',
      'sortType',
      'onSortColumn',
      'onColumnResizeEnd',
      'onColumnResizeStart',
      'onColumnResizeMove',
      'colSpan',
      ...Object.keys(propTypes)
    ]);

    return (

              {wordWrap ? (

                    {this.renderExpandIcon()}
                    {cellRenderer ? cellRenderer(contentChildren) : contentChildren}

              ) : (

                    {this.renderExpandIcon()}
                    {cellRenderer ? cellRenderer(contentChildren) : contentChildren}

                )
              }

    );
  }
}

Cell.propTypes = propTypes;
Cell.defaultProps = defaultProps;

export default decorate()(Cell);
<commit_msg>ok Merge branch 'master' of github.com:rsuite/rsuite-table<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';

import isNullOrUndefined from './utils/isNullOrUndefined';
import { LAYER_WIDTH } from './constants';
import decorate from './utils/decorate';

const propTypes = {
  align: PropTypes.oneOf(['left', 'center', 'right']),
  className: PropTypes.string,
  dataKey: PropTypes.string,
  isHeaderCell: PropTypes.bool,

  width: PropTypes.number,
  height: PropTypes.number,
  left: PropTypes.number,
  headerHeight: PropTypes.number,

  rowKey: PropTypes.string,

  /* eslint-disable */
  rowData: PropTypes.object,
  rowIndex: PropTypes.number,
  layer: PropTypes.number,  // for tree table

  style: PropTypes.object,
  firstColumn: PropTypes.bool,
  lastColumn: PropTypes.bool,
  hasChildren: PropTypes.bool,

  onTreeToggle: PropTypes.func,
  renderTreeToggle: PropTypes.func,
  cellRenderer: PropTypes.func,
  sortable: PropTypes.bool,
  wordWrap: PropTypes.bool
};

const defaultProps = {
  align: 'left',
  headerHeight: 36,
  height: 36,
  width: 0,
  layer: 0,
  left: 0
};

class Cell extends React.Component {
  shouldComponentUpdate(nextProps) {
    return !isEqual(this.props, nextProps);
  }
  renderExpandIcon() {
    const {
      hasChildren,
      firstColumn,
      onTreeToggle,
      rowKey,
      rowIndex,
      rowData,
      renderTreeToggle
    } = this.props;

    const handleExpandClick = (event) => {
      onTreeToggle && onTreeToggle(rowKey, rowIndex, rowData, event);
    };

    const expandButton = (
       {
          event.stopPropagation();
          handleExpandClick(event);
        }}
      />
    );

    /**
     * 如果用子节点，同时是第一列,则创建一个 icon 用于展开节点
     */
    if (hasChildren && firstColumn) {
      return renderTreeToggle ?
        (

            {renderTreeToggle(expandButton, rowData)}

        ) :
        expandButton;
    }

    return null;
  }

  render() {

    let {
      width,
      left,
      height,
      style,
      className,
      firstColumn,
      lastColumn,
      isHeaderCell,
      headerHeight,
      layer,
      align,
      sortable,
      children,
      rowData,
      dataKey,
      cellRenderer,
      removed,
      wordWrap,
      ...props
    } = this.props;

    if (removed) {
      return null;
    }

    const classes = classNames(this.prefix('cell'), {
      sortable: sortable && isHeaderCell,
      first: firstColumn,
      last: lastColumn
    }, className);

    const layerWidth = layer * LAYER_WIDTH;
    const nextWidth = (!isHeaderCell && firstColumn) ? width - layerWidth : width;
    const nextHeight = isHeaderCell ? headerHeight : height;

    const styles = {
      width: nextWidth,
      height: nextHeight,
      zIndex: layer,
      left: (!isHeaderCell && firstColumn) ? left + layerWidth : left,
      ...style
    };

    const contentStyles = {
      width: nextWidth,
      height: nextHeight,
      textAlign: align
    };

    if (sortable) {
      contentStyles.paddingRight = 28;
    }

    const contentChildren = isNullOrUndefined(children) && rowData ? get(rowData, dataKey) : children;
    const elementProps = omit(props, [
      'index',
      'fixed',
      'resizable',
      'flexGrow',
      'minWidth',
      'sortColumn',
      'sortType',
      'onSortColumn',
      'onColumnResizeEnd',
      'onColumnResizeStart',
      'onColumnResizeMove',
      'colSpan',
      ...Object.keys(propTypes)
    ]);

    return (

              {wordWrap ? (

                    {this.renderExpandIcon()}
                    {cellRenderer ? cellRenderer(contentChildren) : contentChildren}

              ) : (

                    {this.renderExpandIcon()}
                    {cellRenderer ? cellRenderer(contentChildren) : contentChildren}

                )
              }

    );
  }
}

Cell.propTypes = propTypes;
Cell.defaultProps = defaultProps;

export default decorate()(Cell);
<|endoftext|>


<commit_before>'use strict';

const config = require('config');
const plog = require(config.get('plog'));
const Schema = require('../schema.js');
const ResponseMessages = require('../../responseMessages');
const Utils = require('../utils');
const Database = require('better-sqlite3');
const db = new Database(config.get('data.treatments'));
const fs = require('fs');
//const treatmentStatus = require('../lib/treatmentsStatus');

const uriZenodeo = config.get('uri.zenodeo');
const cacheOn = config.get('v2.cache.on');

const queryMaker = require('../lib/query-maker');

const plugins = {
    _resource: 'treatment',
    _resources: 'treatments',
    _resourceId: 'treatmentId',
    _name: 'treatments2',
    _segment: 'treatments2',
    _path: '/treatments',
    _order: 2
};

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

module.exports = {
    plugin: {
        name: plugins._name,
        register: function(server, options) {

            const cache = Utils.makeCache({
                server: server,
                options: options,
                query: getRecords,
                segment: plugins._segment
            });

            // binds the cache to every route registered
            // **within this plugin** after this line
            server.bind({ cache });

            server.route([{
                path: plugins._path,
                method: 'GET',

                config: {
                    description: `Fetch ${plugins._resources} from Zenodeo`,
                    tags: [plugins._resources, 'api'],
                    plugins: {
                        'hapi-swagger': {
                            order: plugins._order,
                            responseMessages: ResponseMessages
                        }
                    },
                    validate: Schema[plugins._resources],
                    notes: [
                        `This is the main route for fetching ${plugins._resources} from Zenodeo matching the provided query parameters.`
                    ]
                },

                handler
            }]);
        },
    },
};

const handler = function(request, h) {

    //plog.info('request.query', request.query);

    // if xml is being requested, send it back and be done with it
    if (request.query.format && request.query.format === 'xml') {

        return h.response(getXml(request.query.treatmentId))
            .type('text/xml')
            .header('Content-Type', 'application/xml');

    }

    // cacheKey is the URL query without the refreshCache param.
    // The default params, if any, are used in making the cacheKey.
    // The default params are also used in queryObject to actually
    // perform the query. However, the default params are not used
    // to determine what kind of query to perform.
    const cacheKey = Utils.makeCacheKey(request);
    //plog.info('cacheKey', cacheKey);
<<<<<<< HEAD
    plog.log({header: 'WEB QUERY', messages: [
        {l: 'request.query', p: request.query},
        {l: 'cacheKey', p: cacheKey}
    ]});
=======

    plog.log({
        header: 'WEB QUERY',
        messages: [
            {label: 'request.query', params: request.query},
            {label: 'cacheKey', params: cacheKey}
        ]
    });
>>>>>>> fix-plog

    if (cacheOn) {
        if (request.query.refreshCache === 'true') {
            plog.info('forcing refreshCache');
            this.cache.drop(cacheKey);
        }

        return this.cache.get(cacheKey);
    }
    else {
        return getRecords(cacheKey);
    }
};

const getRecords = function(cacheKey) {

    const queryObject = Utils.makeQueryObject(cacheKey);
    queryObject.resource = plugins._resources;
    //plog.info('queryObject', queryObject);

    // A resourceId is present. The query is for a specific
    // record. All other query params are ignored
    if (queryObject[plugins._resouceId]) {
        return getOneRecord(queryObject);
    }

    // More complicated queries with search parameters
    else {
        return getManyRecords(queryObject)
    }
};

const getOneRecord = function(queryObject) {

    let data;
    const queries = queryMaker(queryObject);

    try {
        plog.info('ONE seldata', queries.seldata);

        // if the query is unsuccessful, add 'num-of-records' = 0
        data = db.prepare(queries.seldata).get(queryObject) || { 'num-of-records': 0 };
    }
    catch (error) {
        plog.log.error(error);
    }

    data['search-criteria'] = queryObject;
    data._links = Utils.makeSelfLink({
        uri: uriZenodeo,
        resource: plugins._resources,
        queryString: Object.entries(queryObject)
            .map(e => e[0] + '=' + e[1])
            .sort()
            .join('&')
    });

    plog.info('num-of-records', data['num-of-records']);

    // if 'num-of-records' is undefined, that means the query
    // was successful and a match was found
    if (typeof data['num-of-records'] === 'undefined') {

        data['num-of-records'] = 1;

        // more data from beyond the database
        data.xml = getXml(queryObject.treatmentId);
        data.taxonStats = getTaxonStats(data);
        data['related-records'] = getRelatedRecords(queries, queryObject);
    }

    return data;
};

const getManyRecords = function(queryObject) {

    const queries = queryMaker(queryObject);
    const messages = [{label: 'queryObject', params: queryObject}];

    const data = {};

    // first find total number of matches
    try {
        messages.push({label: 'selcount', params: queries.selcount});
        data['num-of-records'] = db.prepare(queries.selcount)
            .get(queryObject)
            .numOfRecords;
    }
    catch (error) {
        plog.error(error);
    }

    data['search-criteria'] = queryObject;
    data._links = Utils.makeSelfLink({
        uri: uriZenodeo,
        resource: plugins._resources,
        queryString: Object.entries(queryObject)
            .map(e => e[0] + '=' + e[1])
            .sort()
            .join('&')
    });

    // We are done if no records found
    if (!data['num-of-records']) {
        return data;
    }

    // now, get the records
    const id = queryObject.id ? parseInt(queryObject.id) : 0;
    const page = queryObject.page ? parseInt(queryObject.page) : 1;
    const offset = (page - 1) * 30;
    const limit = 30;

    // get the records
    try {
        queryObject.limit = limit;
        queryObject.offset = offset;
        messages.push({label: 'seldata', params: queries.seldata});
        data.records = db.prepare(queries.seldata).all(queryObject) || [];
    }
    catch (error) {
        plog.error(error);
    }

    plog.log({
        header: 'MANY QUERIES',
        messages: messages
    });

    if (data.records.length > 0) {
        data.records.forEach(rec => {
            rec._links = Utils.makeSelfLink({
                uri: uriZenodeo,
                resource: plugins._resources,
                queryString: Object.entries({treatmentId: rec[plugins._resourceId]})
                    .map(e => e[0] + '=' + e[1])
                    .sort()
                    .join('&')
            });
        });

        const lastrec = data.records[data.records.length - 1];
        data.nextid = lastrec.id;
    }
    else {
        data.nextid = '';
    }

    // set some records-specific from and to for the formatted
    // search criteria string
    data.from = ((page - 1) * 30) + 1;
    data.to = data.records.length < limit ?
        data.from + data.records.length - 1 :
        data.from + limit - 1;

    data.previd = id;

    data.prevpage = page >= 1 ? page - 1 : '';
    data.nextpage = data.records.length < limit ? '' : parseInt(page) + 1;

    // finally, get facets and stats, if requested
    if ('facets' in queryObject && queryObject.facets === 'true') {
        data.facets = getFacets(queries, queryObject);
    }

    if ('stats' in queryObject && queryObject.stats === 'true') {
        data.stats = getStats(queries, queryObject);
    }

    // all done
    return data;

};

const getFacets = function(queries, queryObject) {

    const facets = {};
    const messages = [];

    if (queries.selfacets) {
        for (let q in queries.selfacets) {
            try {
                //plog.info(`MANY FACETS ${q}`, queries.selfacets[q]);
                messages.push({label: q, params: queries.selfacets[q]});
                facets[q] = db.prepare(queries.selfacets[q]).all(queryObject);
            }
            catch (error) {
                plog.error(error);
            }
        }
    }

    plog.log({
        header: 'MANY FACETS',
        messages: messages
    });

    return facets;
}

const getStats = function(queries, queryObject) {

    const stats = {};
    const messages = [];

    if (queries.selstats) {
        for (let q in queries.selstats) {
            try {
                //plog.info(`MANY STATS ${q}`, queries.selstats[q]);
                messages.push({label: q, params: queries.selstats[q]});
                stats[q] = db.prepare(queries.selstats[q]).all(queryObject);
            }
            catch (error) {
                plog.error(error);
            }
        }
    }

    plog.log({
        header: 'MANY STATS',
        messages: messages
    });

    return stats;
};

const getRelatedRecords = function(queries, queryObject) {
    const rr = {};

    const relatedRecords = queries.selrelated;
    plog.info(queries);

    for (let relatedResource in relatedRecords) {

        try {
            const select = relatedRecords[relatedResource];
            plog.info(`ONE RELATED ${relatedResource}`, select);
            const data = db.prepare(select).all(queryObject);

            rr[relatedResource] = Utils.halify({
                records: data,
                uri: uriZenodeo,
                resource: relatedResource,
                id: `${relatedResource.substr(0, relatedResource.length - 1)}Id`
            })
        }
        catch(error) {
            plog.info(error);
        }
    }

    return rr;
};

const getTaxonStats = function(data) {
    const taxonStats = [
        { name: 'kingdom', value: data.kingdom, num: 0 },
        { name: 'phylum',  value: data.phylum,  num: 0 },
        { name: '"order"', value: data.order,   num: 0 },
        { name: 'family',  value: data.family,  num: 0 },
        { name: 'genus',   value: data.genus,   num: 0 },
        { name: 'species', value: data.species, num: 0 }
    ];

    taxonStats.forEach((taxon, index) => {
        const select = `SELECT Count(treatmentId) AS num FROM treatments WHERE deleted = 0 AND ${taxon.name} = '${taxon.value}'`;

        try {
            plog.info('ONE TAXONSTATS', select);
            taxonStats[index].num = db.prepare(select).get().num;
        }
        catch (error) {
            plog.error(error);
        }
    })

    return taxonStats;
};

const getXml = function(treatmentId) {
    const one = treatmentId.substr(0, 1);
    const two = treatmentId.substr(0, 2);
    const thr = treatmentId.substr(0, 3);

    plog.info(`getting the xml for ${treatmentId}`);

    return fs.readFileSync(
        `data/treatments/${one}/${two}/${thr}/${treatmentId}.xml`,
        'utf8'
    )
};

const formatAuthors = function(authors) {
    let authorsArr = authors.map(a => { return a.author });
    const numOfAuthors = authorsArr.length;

    let authorsList = '';
    if (numOfAuthors === 1) {
        authorsList = authorsArr[0];
    }
    else if (numOfAuthors === 2) {
        authorsList = authorsArr.join(' and ');
    }
    else if (numOfAuthors > 2) {
        authorsList = authorsArr.slice(0, 2).join(', ');
        authorsList += ' and ' + authorsArr[numOfAuthors - 1]
    }

    return authorsList;
};<commit_msg>bunched plog in treatments<commit_after>'use strict';

const config = require('config');
const plog = require(config.get('plog'));
const Schema = require('../schema.js');
const ResponseMessages = require('../../responseMessages');
const Utils = require('../utils');
const Database = require('better-sqlite3');
const db = new Database(config.get('data.treatments'));
const fs = require('fs');
//const treatmentStatus = require('../lib/treatmentsStatus');

const uriZenodeo = config.get('uri.zenodeo');
const cacheOn = config.get('v2.cache.on');

const queryMaker = require('../lib/query-maker');

const plugins = {
    _resource: 'treatment',
    _resources: 'treatments',
    _resourceId: 'treatmentId',
    _name: 'treatments2',
    _segment: 'treatments2',
    _path: '/treatments',
    _order: 2
};

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

module.exports = {
    plugin: {
        name: plugins._name,
        register: function(server, options) {

            const cache = Utils.makeCache({
                server: server,
                options: options,
                query: getRecords,
                segment: plugins._segment
            });

            // binds the cache to every route registered
            // **within this plugin** after this line
            server.bind({ cache });

            server.route([{
                path: plugins._path,
                method: 'GET',

                config: {
                    description: `Fetch ${plugins._resources} from Zenodeo`,
                    tags: [plugins._resources, 'api'],
                    plugins: {
                        'hapi-swagger': {
                            order: plugins._order,
                            responseMessages: ResponseMessages
                        }
                    },
                    validate: Schema[plugins._resources],
                    notes: [
                        `This is the main route for fetching ${plugins._resources} from Zenodeo matching the provided query parameters.`
                    ]
                },

                handler
            }]);
        },
    },
};

const handler = function(request, h) {

    //plog.info('request.query', request.query);

    // if xml is being requested, send it back and be done with it
    if (request.query.format && request.query.format === 'xml') {

        return h.response(getXml(request.query.treatmentId))
            .type('text/xml')
            .header('Content-Type', 'application/xml');

    }

    // cacheKey is the URL query without the refreshCache param.
    // The default params, if any, are used in making the cacheKey.
    // The default params are also used in queryObject to actually
    // perform the query. However, the default params are not used
    // to determine what kind of query to perform.
    const cacheKey = Utils.makeCacheKey(request);
    //plog.info('cacheKey', cacheKey);

    plog.log({
        header: 'WEB QUERY',
        messages: [
            {label: 'request.query', params: request.query},
            {label: 'cacheKey', params: cacheKey}
        ]
    });

    if (cacheOn) {
        if (request.query.refreshCache === 'true') {
            plog.info('forcing refreshCache');
            this.cache.drop(cacheKey);
        }

        return this.cache.get(cacheKey);
    }
    else {
        return getRecords(cacheKey);
    }
};

const getRecords = function(cacheKey) {

    const queryObject = Utils.makeQueryObject(cacheKey);
    queryObject.resource = plugins._resources;
    //plog.info('queryObject', queryObject);

    // A resourceId is present. The query is for a specific
    // record. All other query params are ignored
    if (queryObject[plugins._resouceId]) {
        return getOneRecord(queryObject);
    }

    // More complicated queries with search parameters
    else {
        return getManyRecords(queryObject)
    }
};

const getOneRecord = function(queryObject) {

    let data;
    const queries = queryMaker(queryObject);

    try {
        plog.info('ONE seldata', queries.seldata);

        // if the query is unsuccessful, add 'num-of-records' = 0
        data = db.prepare(queries.seldata).get(queryObject) || { 'num-of-records': 0 };
    }
    catch (error) {
        plog.log.error(error);
    }

    data['search-criteria'] = queryObject;
    data._links = Utils.makeSelfLink({
        uri: uriZenodeo,
        resource: plugins._resources,
        queryString: Object.entries(queryObject)
            .map(e => e[0] + '=' + e[1])
            .sort()
            .join('&')
    });

    plog.info('num-of-records', data['num-of-records']);

    // if 'num-of-records' is undefined, that means the query
    // was successful and a match was found
    if (typeof data['num-of-records'] === 'undefined') {

        data['num-of-records'] = 1;

        // more data from beyond the database
        data.xml = getXml(queryObject.treatmentId);
        data.taxonStats = getTaxonStats(data);
        data['related-records'] = getRelatedRecords(queries, queryObject);
    }

    return data;
};

const getManyRecords = function(queryObject) {

    const queries = queryMaker(queryObject);
    const messages = [{label: 'queryObject', params: queryObject}];

    const data = {};

    // first find total number of matches
    try {
        messages.push({label: 'selcount', params: queries.selcount});
        data['num-of-records'] = db.prepare(queries.selcount)
            .get(queryObject)
            .numOfRecords;
    }
    catch (error) {
        plog.error(error);
    }

    data['search-criteria'] = queryObject;
    data._links = Utils.makeSelfLink({
        uri: uriZenodeo,
        resource: plugins._resources,
        queryString: Object.entries(queryObject)
            .map(e => e[0] + '=' + e[1])
            .sort()
            .join('&')
    });

    // We are done if no records found
    if (!data['num-of-records']) {
        return data;
    }

    // now, get the records
    const id = queryObject.id ? parseInt(queryObject.id) : 0;
    const page = queryObject.page ? parseInt(queryObject.page) : 1;
    const offset = (page - 1) * 30;
    const limit = 30;

    // get the records
    try {
        queryObject.limit = limit;
        queryObject.offset = offset;
        messages.push({label: 'seldata', params: queries.seldata});
        data.records = db.prepare(queries.seldata).all(queryObject) || [];
    }
    catch (error) {
        plog.error(error);
    }

    plog.log({
        header: 'MANY QUERIES',
        messages: messages
    });

    if (data.records.length > 0) {
        data.records.forEach(rec => {
            rec._links = Utils.makeSelfLink({
                uri: uriZenodeo,
                resource: plugins._resources,
                queryString: Object.entries({treatmentId: rec[plugins._resourceId]})
                    .map(e => e[0] + '=' + e[1])
                    .sort()
                    .join('&')
            });
        });

        const lastrec = data.records[data.records.length - 1];
        data.nextid = lastrec.id;
    }
    else {
        data.nextid = '';
    }

    // set some records-specific from and to for the formatted
    // search criteria string
    data.from = ((page - 1) * 30) + 1;
    data.to = data.records.length < limit ?
        data.from + data.records.length - 1 :
        data.from + limit - 1;

    data.previd = id;

    data.prevpage = page >= 1 ? page - 1 : '';
    data.nextpage = data.records.length < limit ? '' : parseInt(page) + 1;

    // finally, get facets and stats, if requested
    if ('facets' in queryObject && queryObject.facets === 'true') {
        data.facets = getFacets(queries, queryObject);
    }

    if ('stats' in queryObject && queryObject.stats === 'true') {
        data.stats = getStats(queries, queryObject);
    }

    // all done
    return data;

};

const getFacets = function(queries, queryObject) {

    const facets = {};
    const messages = [];

    if (queries.selfacets) {
        for (let q in queries.selfacets) {
            try {
                //plog.info(`MANY FACETS ${q}`, queries.selfacets[q]);
                messages.push({label: q, params: queries.selfacets[q]});
                facets[q] = db.prepare(queries.selfacets[q]).all(queryObject);
            }
            catch (error) {
                plog.error(error);
            }
        }
    }

    plog.log({
        header: 'MANY FACETS',
        messages: messages
    });

    return facets;
}

const getStats = function(queries, queryObject) {

    const stats = {};
    const messages = [];

    if (queries.selstats) {
        for (let q in queries.selstats) {
            try {
                //plog.info(`MANY STATS ${q}`, queries.selstats[q]);
                messages.push({label: q, params: queries.selstats[q]});
                stats[q] = db.prepare(queries.selstats[q]).all(queryObject);
            }
            catch (error) {
                plog.error(error);
            }
        }
    }

    plog.log({
        header: 'MANY STATS',
        messages: messages
    });

    return stats;
};

const getRelatedRecords = function(queries, queryObject) {
    const rr = {};

    const relatedRecords = queries.selrelated;
    plog.info(queries);

    for (let relatedResource in relatedRecords) {

        try {
            const select = relatedRecords[relatedResource];
            plog.info(`ONE RELATED ${relatedResource}`, select);
            const data = db.prepare(select).all(queryObject);

            rr[relatedResource] = Utils.halify({
                records: data,
                uri: uriZenodeo,
                resource: relatedResource,
                id: `${relatedResource.substr(0, relatedResource.length - 1)}Id`
            })
        }
        catch(error) {
            plog.info(error);
        }
    }

    return rr;
};

const getTaxonStats = function(data) {
    const taxonStats = [
        { name: 'kingdom', value: data.kingdom, num: 0 },
        { name: 'phylum',  value: data.phylum,  num: 0 },
        { name: '"order"', value: data.order,   num: 0 },
        { name: 'family',  value: data.family,  num: 0 },
        { name: 'genus',   value: data.genus,   num: 0 },
        { name: 'species', value: data.species, num: 0 }
    ];

    taxonStats.forEach((taxon, index) => {
        const select = `SELECT Count(treatmentId) AS num FROM treatments WHERE deleted = 0 AND ${taxon.name} = '${taxon.value}'`;

        try {
            plog.info('ONE TAXONSTATS', select);
            taxonStats[index].num = db.prepare(select).get().num;
        }
        catch (error) {
            plog.error(error);
        }
    })

    return taxonStats;
};

const getXml = function(treatmentId) {
    const one = treatmentId.substr(0, 1);
    const two = treatmentId.substr(0, 2);
    const thr = treatmentId.substr(0, 3);

    plog.info(`getting the xml for ${treatmentId}`);

    return fs.readFileSync(
        `data/treatments/${one}/${two}/${thr}/${treatmentId}.xml`,
        'utf8'
    )
};

const formatAuthors = function(authors) {
    let authorsArr = authors.map(a => { return a.author });
    const numOfAuthors = authorsArr.length;

    let authorsList = '';
    if (numOfAuthors === 1) {
        authorsList = authorsArr[0];
    }
    else if (numOfAuthors === 2) {
        authorsList = authorsArr.join(' and ');
    }
    else if (numOfAuthors > 2) {
        authorsList = authorsArr.slice(0, 2).join(', ');
        authorsList += ' and ' + authorsArr[numOfAuthors - 1]
    }

    return authorsList;
};<|endoftext|>


<commit_before>(function (TankJS, undefined)
{

TankJS.GameObject = function(id)
{
  // Name of the game object
  this.name = "GameObject " + id;

  // ID of the game object
  this.id = id;

  // Map of components by component name
  this._components = {};

  // Map of components by component tag
  this._taggedComponents = {};
}

TankJS.GameObject.prototype.remove = function()
{
  TankJS.removeObject(this.id);
}

TankJS.GameObject.prototype.addComponent = function(componentName)
{
  // Check if we have this component already
  if (this.getComponent(componentName))
    return this;

  // Get the component definition object
  var componentDef = TankJS._components[componentName];
  if (!componentDef)
  {
    console.log("GameObject.addComponent: No component registered with name " + componentName);
    return this;
  }

  // Temporarily add a fake component just to mark this one as added
  // for the upcoming recursive calls
  this._components[componentName] = "Placeholder";

  // Add all the included components
  for (var i in componentDef._includes)
  {
    this.addComponent(componentDef._includes[i]);
  }

  // Clone the component into our list of components
  var c = componentDef.clone();
  this._components[componentName] = c;

  // Track this component by its tags
  for (var i in componentDef._tags)
  {
    // Get the list of components with this tag
    var componentList = TankJS._taggedComponents[componentDef._tags[i]];
    var componentListLocal = this._taggedComponents[componentDef._tags[i]];
    if (!componentList)
    {
      componentList = {};
      TankJS._taggedComponents[componentDef._tags[i]] = componentList;
    }
    if (!componentListLocal)
    {
      componentListLocal = {};
      this._taggedComponents[componentDef._tags[i]] = componentListLocal;
    }

    componentList[this.name + "." + componentDef.name] = c;
    componentListLocal[componentDef.name] = c;
  }

  // Set some attributes of the component instance
  c.parent = this;

  // Initialize the component
  c.init.apply(c);

  // Inform the engine this component was initialized
  TankJS.dispatchEvent("OnComponentInitialized", c);

  return this;
}

TankJS.GameObject.prototype.addComponents = function(componentNames)
{
  // Get array of component names
  componentNames = componentNames.replace(/\s/g, "");
  var components = componentNames.split(",");

  // Add components to object
  for (var i in components)
  {
    this.addComponent(components[i]);
  }

  return this;
}

TankJS.GameObject.prototype.removeComponent = function(componentName)
{
  // If we don't have the component then just return
  var c = this.getComponent(componentName);
  if (!c)
    return;

  // Inform the engine this component was uninitialized
  TankJS.dispatchEvent("OnComponentUninitialized", c);

  // Uninitialize the component
  c.uninit.apply(c);

  // Stop tracking this component by its tags
  var componentDef = TankJS._components[componentName];
  for (var i in componentDef._tags)
  {
    // Get the list of components with this tag
    var componentList = TankJS._taggedComponents[componentDef._tags[i]];
    var componentListLocal = this._taggedComponents[componentDef._tags[i]];

    if (componentList)
      delete componentList[this.name + "." + componentDef.name];
    if (componentListLocal)
      delete componentListLocal[componentDef.name];
  }

  // Remove component
  delete this._components[componentName];
}

TankJS.GameObject.prototype.getComponent = function(componentName)
{
  return this._components[componentName];
}

TankJS.GameObject.prototype.getComponentsWithTag = function(tagName)
{
  return this._taggedComponents[tagName];
}

TankJS.GameObject.prototype.invoke = function(funcName, args)
{
  // Construct arguments
  var message_args = [];
  for (var i = 1; i < arguments.length; ++i)
    message_args.push(arguments[i]);

  // Invoke on each component
  for (var i in this._components)
  {
    if (this._components[i][funcName])
      this._components[i][funcName].apply(this._components[i], message_args);
  }
}

TankJS.GameObject.prototype.attr = function(componentName, attrs)
{
  var c = this.getComponent(componentName);
  if (!c)
    return;

  for (var i in attrs)
    c[i] = attrs[i];

  return this;
}

TankJS.GameObject.prototype.uninit = function()
{
  // Remove all components
  for (var i in this._components)
  {
    this.removeComponent(i);
  }
}

} (window.TankJS = window.TankJS || {}));<commit_msg>Add error checking to .attr in gameObject<commit_after>(function (TankJS, undefined)
{

TankJS.GameObject = function(id)
{
  // Name of the game object
  this.name = "GameObject " + id;

  // ID of the game object
  this.id = id;

  // Map of components by component name
  this._components = {};

  // Map of components by component tag
  this._taggedComponents = {};
}

TankJS.GameObject.prototype.remove = function()
{
  TankJS.removeObject(this.id);
}

TankJS.GameObject.prototype.addComponent = function(componentName)
{
  // Check if we have this component already
  if (this.getComponent(componentName))
    return this;

  // Get the component definition object
  var componentDef = TankJS._components[componentName];
  if (!componentDef)
  {
    console.log("GameObject.addComponent: No component registered with name " + componentName);
    return this;
  }

  // Temporarily add a fake component just to mark this one as added
  // for the upcoming recursive calls
  this._components[componentName] = "Placeholder";

  // Add all the included components
  for (var i in componentDef._includes)
  {
    this.addComponent(componentDef._includes[i]);
  }

  // Clone the component into our list of components
  var c = componentDef.clone();
  this._components[componentName] = c;

  // Track this component by its tags
  for (var i in componentDef._tags)
  {
    // Get the list of components with this tag
    var componentList = TankJS._taggedComponents[componentDef._tags[i]];
    var componentListLocal = this._taggedComponents[componentDef._tags[i]];
    if (!componentList)
    {
      componentList = {};
      TankJS._taggedComponents[componentDef._tags[i]] = componentList;
    }
    if (!componentListLocal)
    {
      componentListLocal = {};
      this._taggedComponents[componentDef._tags[i]] = componentListLocal;
    }

    componentList[this.name + "." + componentDef.name] = c;
    componentListLocal[componentDef.name] = c;
  }

  // Set some attributes of the component instance
  c.parent = this;

  // Initialize the component
  c.init.apply(c);

  // Inform the engine this component was initialized
  TankJS.dispatchEvent("OnComponentInitialized", c);

  return this;
}

TankJS.GameObject.prototype.addComponents = function(componentNames)
{
  // Get array of component names
  componentNames = componentNames.replace(/\s/g, "");
  var components = componentNames.split(",");

  // Add components to object
  for (var i in components)
  {
    this.addComponent(components[i]);
  }

  return this;
}

TankJS.GameObject.prototype.removeComponent = function(componentName)
{
  // If we don't have the component then just return
  var c = this.getComponent(componentName);
  if (!c)
    return;

  // Inform the engine this component was uninitialized
  TankJS.dispatchEvent("OnComponentUninitialized", c);

  // Uninitialize the component
  c.uninit.apply(c);

  // Stop tracking this component by its tags
  var componentDef = TankJS._components[componentName];
  for (var i in componentDef._tags)
  {
    // Get the list of components with this tag
    var componentList = TankJS._taggedComponents[componentDef._tags[i]];
    var componentListLocal = this._taggedComponents[componentDef._tags[i]];

    if (componentList)
      delete componentList[this.name + "." + componentDef.name];
    if (componentListLocal)
      delete componentListLocal[componentDef.name];
  }

  // Remove component
  delete this._components[componentName];
}

TankJS.GameObject.prototype.getComponent = function(componentName)
{
  return this._components[componentName];
}

TankJS.GameObject.prototype.getComponentsWithTag = function(tagName)
{
  return this._taggedComponents[tagName];
}

TankJS.GameObject.prototype.invoke = function(funcName, args)
{
  // Construct arguments
  var message_args = [];
  for (var i = 1; i < arguments.length; ++i)
    message_args.push(arguments[i]);

  // Invoke on each component
  for (var i in this._components)
  {
    if (this._components[i][funcName])
      this._components[i][funcName].apply(this._components[i], message_args);
  }
}

TankJS.GameObject.prototype.attr = function(componentName, attrs)
{
  var c = this.getComponent(componentName);
  if (!c)
  {
    console.log("GameObject.attr: Could not find component with name " + componentName);
    return;
  }

  for (var i in attrs)
    c[i] = attrs[i];

  return this;
}

TankJS.GameObject.prototype.uninit = function()
{
  // Remove all components
  for (var i in this._components)
  {
    this.removeComponent(i);
  }
}

} (window.TankJS = window.TankJS || {}));<|endoftext|>


<commit_before>import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import {Model, SK} from 'sk-js';
import Reacts from './Reacts';

export default class Comp extends React.Component {
  static KS_PROPS_PREFIX = 'ks';//applay for all sub specify element
  static SK_COMP_NAME = 'Comp';
  static SK_COMP_STATE_ID_PREFIX = 'skCompStateUid';
  static SK_PROPS_PREFIX = 'sk';//applay for all sub element
  static SK_PROPS_SYS = 'sys';
  static SK_PROPS = {
    //sk
    COMP_TAG: 'compTag',
    MODEL: 'model',
    MODEL_ID: 'modelId',//a.b.c or property in array like a.b[2].c
    //ui state, are bfo (boolean, function, object{deps:,func:})
    //deps to monitor
    ACTIVE: 'active',
    DISABLED: 'disabled',
    HIDDEN: 'hidden',
    PREVIEW: 'preview',
    READ_ONLY: 'readOnly',
    REQUIRED: 'required', //just paint required icon
    //component monitor list, can be string[reg], string array or object
    MONITOR: 'monitor',
  };
  static defaultProps = {};
  static propTypes = {
    compTag: PropTypes.any,
    skModel: Comp.IS_PROP_TYPES_MODEL, //Business Model(page), PlainObject
    skSysModel: Comp.IS_PROP_TYPES_MODEL, //System Model(whole of page), PlainObject
    model: Comp.IS_PROP_TYPES_MODEL, //Business Model(part of page), PlainObject
    modelId: PropTypes.string,
    monitor: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.array,
      PropTypes.object,
    ]), //monitor is string[reg], array[string] or object of modelId

    active: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skActive: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    disabled: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skDisabled: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    hidden: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skHidden: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    preview: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skPreview: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    readOnly: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skReadOnly: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    required: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skRequired: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),

    m2eConvertor: PropTypes.func, //Model 2 Editing
    e2mConvertor: PropTypes.func, //Editing 2 Model
    m2vConvertor: PropTypes.func, //Model 2 View, like Date Comp, moment format to view: YYYY-MM-DDTHH:mm:ss.SSSZ -> YYYY-MM-DD
  };

  constructor(...args) {
    super(...args);
    this.SK_COMP_NAME = Comp.SK_COMP_NAME;
    this.monitors = [];
    this.updateUI = (evt) => {
      this.setState({stateUid: _.uniqueId(Comp.SK_COMP_STATE_ID_PREFIX)});
    };
  }

  static IS_PROP_TYPES_MODEL(props, propName, componentName) {
    if (props[propName] && !(props[propName] instanceof Model)) {
      return new Error(`The [${componentName}]'s [${propName}] is not a model!`);
    }
  }

  static ksPropsFilter(k) {
    return _.startsWith(SK.s4s(k), Comp.KS_PROPS_PREFIX);
  }

  static skPropsFilter(k) {
    return _.startsWith(SK.s4s(k), Comp.SK_PROPS_PREFIX);
  }

  // react
  componentDidMount() {
    //super.componentDidMount();
    this.addAllChangedMonitor();
    this.addExtendChangedMonitor();
  }

  componentWillUpdate() {
    this.rmvAllChangedMonitor();
    this.rmvExtendChangedMonitor();
    //super.componentWillUpdate();
  }

  componentDidUpdate() {
    //super.componentDidUpdate();
    this.addAllChangedMonitor();
    this.addExtendChangedMonitor();
  }

  componentWillUnmount() {
    this.rmvAllChangedMonitor();
    this.rmvExtendChangedMonitor();
    //super.componentWillUnmount();
  }

  render() {
    const {compTag: CompTag} = this.props;

    if (this.skBfo(Comp.SK_PROPS.PREVIEW)) {
      return this.renderPreview();
    } else {
      return this.renderComp();
    }
  }

  // monitor
  addAllChangedMonitor() {
    Model.parseSao(this.props.monitor).forEach((item) => {
      this.addChangedMonitor(item);
    });
    //Self value monitor
    if (this.getModelId()) {
      this.addChangedMonitor(this.getModelId());
    }
  }

  addChangedMonitor(idOrReg) {
    if (!_.isNil(idOrReg)) {
      if (this.monitors.indexOf(idOrReg) < 0) {
        this.monitors.push(idOrReg);
      }
      if (_.isRegExp(idOrReg)) {
        this.skModel().addRegChangedListener(idOrReg, this.updateUI);
      } else {
        this.skModel().addIdChangedListener(idOrReg, this.updateUI);
      }
    }
  }

  addExtendChangedMonitor() {

  }

  rmvAllChangedMonitor() {
    this.monitors.forEach((item) => {
      this.rmvChangedMonitor(item);
    });
  }

  rmvChangedMonitor(idOrReg) {
    if (_.isRegExp(idOrReg)) {
      this.skModel().rmvRegChangedListener(idOrReg, this.updateUI);
    } else {
      this.skModel().rmvIdChangedListener(idOrReg, this.updateUI);
    }
    this.monitors.skRmv(idOrReg);
  }

  rmvExtendChangedMonitor() {

  }

  // other
  allowTransProps2Child(child) {
    return [];
  }

  allowTransProps2Self(comp = this.props.compTag, prop = this.props) {
    return [];
  }

  clearAllErrors() {
    if (this.skModel()) {
      this.skModel().setErrors();
    }
  }

  denyTransProps2Child(child) {
    return [];
  }

  denyTransProps2Self(comp = this.props.compTag, prop = this.props) {
    return [];
  }

  e2m(val) {
    return this.skVal(_.isFunction(this.props.e2mConvertor) ? this.props.e2mConvertor(this, this.skModel(), val) : this.e2mConvertor(val));
  }

  e2mConvertor(val) {
    return val;
  }

  /**
   * Returns boolean value of boolean, function or object with skWhen properties
   *
   * @param {boolean|function|object} bfo
   * @param {boolean} defaultValue
   * @returns {boolean}
   */
  execBfo(bfo, defaultValue = false) {
    if (_.isBoolean(bfo)) {
      return bfo;
    } else if (_.isFunction(bfo)) {
      return bfo.call(this);
    } else if (_.isObject(bfo) && _.isFunction(bfo.func)) {
      return bfo.func.call(this);
    } else {
      return defaultValue;
    }
  }

  getErrors() {
    return this.skModel().getErrors(this.getModelId());
  }

  /**
   * Get modelId: x.xx.xxx
   *
   * @returns {string}
   */
  getModelId() {
    return this.props.modelId;
  }

  getSysModel() {
    return this.props.skSysModel;
  }

  /**
   * Get prop value: prop -> skProp -> skSysProp
   *
   * @param {string}prop
   * @param {*} defaultValue
   * @returns {*} Maybe undefined
   */
  ksProp(prop, defaultValue = undefined) {
    let rtn = this.props[prop];
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + this.SK_COMP_NAME + prop];
    }
    return rtn === undefined ? defaultValue : rtn;
  }

  m2e() {
    return _.isFunction(this.props.m2eConvertor) ? this.props.m2eConvertor(this, this.skModel(), this.skVal()) : this.m2eConvertor();
  }

  m2eConvertor() {
    return this.skVal();
  }

  m2v() {
    return _.isFunction(this.props.m2vConvertor) ? this.props.m2vConvertor(this, this.skModel(), this.skVal()) : this.m2vConvertor();
  }

  m2vConvertor() {
    return this.skVal();
  }

  renderComp() {
    const {compTag: CompTag} = this.props;

    return (
      <CompTag {...this.skTransProps2Self(CompTag)}>
        {this.skTransProps2Child()}
      </CompTag>
    );
  }

  renderPreview() {
    return this.m2v();
  }

  /**
   * @see Comp.execBfo
   */
  skBfo(prop, defaultValue = false) {
    return this.execBfo(this.skProp(prop, defaultValue));
  }

  /**
   * Get model if exist, else skModel
   *
   * @returns {Model}
   */
  skModel() {
    return this.skProp(Comp.SK_PROPS.MODEL);
  }

  /**
   * Get prop value: prop -> skProp -> skSysProp
   *
   * @param {string}prop
   * @param {*} defaultValue
   * @returns {*} Maybe undefined
   */
  skProp(prop, defaultValue = undefined) {
    let rtn = this.props[prop];
    if (rtn === undefined) {
      rtn = this.ksProp(prop);
    }
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + SK.upperWordsFirstChar(prop)];
    }
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + SK.upperWordsFirstChar(Comp.SK_PROPS_SYS) + SK.upperWordsFirstChar(prop)];
    }
    return rtn === undefined ? defaultValue : rtn;
  }

  skTmpVal(id = this.getModelId(), value) {
    return arguments.length > 1 ? this.skModel().skVal(`tmp.${id}`, value) : this.skModel().skVal(`tmp.${id}`);
  }

  skTransProps2Child(children = this.props.children) {
    const skProps = Object.keys(this.props.skFilter(false, Comp.skPropsFilter));
    const ksProps = Object.keys(this.props.skFilter(false, Comp.ksPropsFilter));

    let rtn = React.Children.map(children, child => {
      if (React.isValidElement(child)) {
        let allowProps = skProps;
        allowProps = Reacts.TAG[child.type] ? [] : allowProps.concat(ksProps);//if html node, nothing to do
        allowProps = allowProps.concat(this.allowTransProps2Child(child));

        let denyProps = [Comp.SK_PROPS.COMP_TAG, Comp.SK_PROPS.MODEL_ID, 'children', 'className'];//force ignore props
        denyProps = child.props ? denyProps.concat(Object.keys(child.props)) : denyProps;
        denyProps = denyProps.concat(this.denyTransProps2Child(child));

        return React.cloneElement(child, _.omit(_.pick(this.props, allowProps), denyProps));
      }
      return child;
    });

    if (!_.isArray(children) && _.isArray(rtn)) {
      rtn = rtn[0];
    }
    return rtn;
  }

  /**
   * Valid props for this comp
   *
   * @param {Comp|string} comp not div, just have 'div' and Div or React Component etc...
   * @param {object} prop
   * @param {boolean} htmlProps @Deprecated at antd 3.8.1 used in FormComp.render at antd 3.5.4
   * @returns {object}
   */
  skTransProps2Self(comp = this.props.compTag, prop = this.props) {
    const skProps = Object.keys(prop.skFilter(false, Comp.skPropsFilter));
    const ksProps = Object.keys(prop.skFilter(false, Comp.ksPropsFilter));

    let allowProps = skProps;
    if (this.SK_COMP_NAME) {
      if (comp.NON_SK_COMP_NAME && (Comp.SK_PROPS_PREFIX.toUpperCase() + comp.NON_SK_COMP_NAME) === this.SK_COMP_NAME) {
        //SK COMP to AntD
        allowProps = allowProps.concat(Reacts.P.skVals());
      }
      if (this.SK_EXTEND_COMP_NAME && comp.SK_COMP_NAME === this.SK_COMP_NAME) {
        //SKFormInput to SKInput
        allowProps = allowProps.concat(Reacts.P.skVals());
      }
    }
    allowProps = Reacts.TAG[comp] ? SK.s4a(Reacts.TP[comp]) : allowProps.concat(ksProps);//if html node, nothing to do
    allowProps = comp.propTypes ? allowProps.concat(Object.keys(comp.propTypes)) : allowProps;
    allowProps = allowProps.concat(this.allowTransProps2Self(comp, prop));

    let denyProps = [Comp.SK_PROPS.COMP_TAG, Comp.SK_PROPS.MODEL_ID];//force ignore props
    denyProps = denyProps.concat(this.denyTransProps2Self(comp, prop));

    return _.omit(_.pick(prop, allowProps), denyProps);
  }

  /**
   * set/get value for modelId
   *
   * @param {*} val
   * @returns {*}
   */
  skVal(val) {
    if (arguments.length > 0) {
      this.skModel().skVal(this.getModelId(), val);
      return this;
    } else {
      return this.skModel().skVal(this.getModelId());
    }
  }
}
<commit_msg>fixed #2 skTransProps2Self&skTransProps2Child just trans sk.../ks... props<commit_after>import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import {Model, SK} from 'sk-js';
import Reacts from './Reacts';

export default class Comp extends React.Component {
  static KS_PROPS_PREFIX = 'ks';//applay for all sub specify element
  static SK_COMP_NAME = 'Comp';
  static SK_COMP_STATE_ID_PREFIX = 'skCompStateUid';
  static SK_PROPS_PREFIX = 'sk';//applay for all sub element
  static SK_PROPS_SYS = 'sys';
  static SK_PROPS = {
    //sk
    COMP_TAG: 'compTag',
    MODEL: 'model',
    MODEL_ID: 'modelId',//a.b.c or property in array like a.b[2].c
    //ui state, are bfo (boolean, function, object{deps:,func:})
    //deps to monitor
    ACTIVE: 'active',
    DISABLED: 'disabled',
    HIDDEN: 'hidden',
    PREVIEW: 'preview',
    READ_ONLY: 'readOnly',
    REQUIRED: 'required', //just paint required icon
    //component monitor list, can be string[reg], string array or object
    MONITOR: 'monitor',
  };
  static defaultProps = {};
  static propTypes = {
    compTag: PropTypes.any,
    skModel: Comp.IS_PROP_TYPES_MODEL, //Business Model(page), PlainObject
    skSysModel: Comp.IS_PROP_TYPES_MODEL, //System Model(whole of page), PlainObject
    model: Comp.IS_PROP_TYPES_MODEL, //Business Model(part of page), PlainObject
    modelId: PropTypes.string,
    monitor: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.array,
      PropTypes.object,
    ]), //monitor is string[reg], array[string] or object of modelId

    active: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skActive: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    disabled: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skDisabled: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    hidden: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skHidden: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    preview: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skPreview: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    readOnly: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skReadOnly: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    required: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),
    skRequired: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.func,
      PropTypes.object,
    ]),

    m2eConvertor: PropTypes.func, //Model 2 Editing
    e2mConvertor: PropTypes.func, //Editing 2 Model
    m2vConvertor: PropTypes.func, //Model 2 View, like Date Comp, moment format to view: YYYY-MM-DDTHH:mm:ss.SSSZ -> YYYY-MM-DD
  };

  constructor(...args) {
    super(...args);
    this.SK_COMP_NAME = Comp.SK_COMP_NAME;
    this.monitors = [];
    this.updateUI = (evt) => {
      this.setState({stateUid: _.uniqueId(Comp.SK_COMP_STATE_ID_PREFIX)});
    };
  }

  static IS_PROP_TYPES_MODEL(props, propName, componentName) {
    if (props[propName] && !(props[propName] instanceof Model)) {
      return new Error(`The [${componentName}]'s [${propName}] is not a model!`);
    }
  }

  static ksPropsFilter(k) {
    return _.startsWith(SK.s4s(k), Comp.KS_PROPS_PREFIX);
  }

  static skPropsFilter(k) {
    return _.startsWith(SK.s4s(k), Comp.SK_PROPS_PREFIX);
  }

  // react
  componentDidMount() {
    //super.componentDidMount();
    this.addAllChangedMonitor();
    this.addExtendChangedMonitor();
  }

  componentWillUpdate() {
    this.rmvAllChangedMonitor();
    this.rmvExtendChangedMonitor();
    //super.componentWillUpdate();
  }

  componentDidUpdate() {
    //super.componentDidUpdate();
    this.addAllChangedMonitor();
    this.addExtendChangedMonitor();
  }

  componentWillUnmount() {
    this.rmvAllChangedMonitor();
    this.rmvExtendChangedMonitor();
    //super.componentWillUnmount();
  }

  render() {
    const {compTag: CompTag} = this.props;

    if (this.skBfo(Comp.SK_PROPS.PREVIEW)) {
      return this.renderPreview();
    } else {
      return this.renderComp();
    }
  }

  // monitor
  addAllChangedMonitor() {
    Model.parseSao(this.props.monitor).forEach((item) => {
      this.addChangedMonitor(item);
    });
    //Self value monitor
    if (this.getModelId()) {
      this.addChangedMonitor(this.getModelId());
    }
  }

  addChangedMonitor(idOrReg) {
    if (!_.isNil(idOrReg)) {
      if (this.monitors.indexOf(idOrReg) < 0) {
        this.monitors.push(idOrReg);
      }
      if (_.isRegExp(idOrReg)) {
        this.skModel().addRegChangedListener(idOrReg, this.updateUI);
      } else {
        this.skModel().addIdChangedListener(idOrReg, this.updateUI);
      }
    }
  }

  addExtendChangedMonitor() {

  }

  rmvAllChangedMonitor() {
    this.monitors.forEach((item) => {
      this.rmvChangedMonitor(item);
    });
  }

  rmvChangedMonitor(idOrReg) {
    if (_.isRegExp(idOrReg)) {
      this.skModel().rmvRegChangedListener(idOrReg, this.updateUI);
    } else {
      this.skModel().rmvIdChangedListener(idOrReg, this.updateUI);
    }
    this.monitors.skRmv(idOrReg);
  }

  rmvExtendChangedMonitor() {

  }

  // other
  allowTransProps2Child(child) {
    return [];
  }

  allowTransProps2Self(comp = this.props.compTag, prop = this.props) {
    return [];
  }

  clearAllErrors() {
    if (this.skModel()) {
      this.skModel().setErrors();
    }
  }

  denyTransProps2Child(child) {
    return [];
  }

  denyTransProps2Self(comp = this.props.compTag, prop = this.props) {
    return [];
  }

  e2m(val) {
    return this.skVal(_.isFunction(this.props.e2mConvertor) ? this.props.e2mConvertor(this, this.skModel(), val) : this.e2mConvertor(val));
  }

  e2mConvertor(val) {
    return val;
  }

  /**
   * Returns boolean value of boolean, function or object with skWhen properties
   *
   * @param {boolean|function|object} bfo
   * @param {boolean} defaultValue
   * @returns {boolean}
   */
  execBfo(bfo, defaultValue = false) {
    if (_.isBoolean(bfo)) {
      return bfo;
    } else if (_.isFunction(bfo)) {
      return bfo.call(this);
    } else if (_.isObject(bfo) && _.isFunction(bfo.func)) {
      return bfo.func.call(this);
    } else {
      return defaultValue;
    }
  }

  getErrors() {
    return this.skModel().getErrors(this.getModelId());
  }

  /**
   * Get modelId: x.xx.xxx
   *
   * @returns {string}
   */
  getModelId() {
    return this.props.modelId;
  }

  getSysModel() {
    return this.props.skSysModel;
  }

  /**
   * Get prop value: prop -> skProp -> skSysProp
   *
   * @param {string}prop
   * @param {*} defaultValue
   * @returns {*} Maybe undefined
   */
  ksProp(prop, defaultValue = undefined) {
    let rtn = this.props[prop];
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + this.SK_COMP_NAME + prop];
    }
    return rtn === undefined ? defaultValue : rtn;
  }

  m2e() {
    return _.isFunction(this.props.m2eConvertor) ? this.props.m2eConvertor(this, this.skModel(), this.skVal()) : this.m2eConvertor();
  }

  m2eConvertor() {
    return this.skVal();
  }

  m2v() {
    return _.isFunction(this.props.m2vConvertor) ? this.props.m2vConvertor(this, this.skModel(), this.skVal()) : this.m2vConvertor();
  }

  m2vConvertor() {
    return this.skVal();
  }

  renderComp() {
    const {compTag: CompTag} = this.props;

    return (
      <CompTag {...this.skTransProps2Self(CompTag)}>
        {this.skTransProps2Child()}
      </CompTag>
    );
  }

  renderPreview() {
    return this.m2v();
  }

  /**
   * @see Comp.execBfo
   */
  skBfo(prop, defaultValue = false) {
    return this.execBfo(this.skProp(prop, defaultValue));
  }

  /**
   * Get model if exist, else skModel
   *
   * @returns {Model}
   */
  skModel() {
    return this.skProp(Comp.SK_PROPS.MODEL);
  }

  /**
   * Get prop value: prop -> skProp -> skSysProp
   *
   * @param {string}prop
   * @param {*} defaultValue
   * @returns {*} Maybe undefined
   */
  skProp(prop, defaultValue = undefined) {
    let rtn = this.props[prop];
    if (rtn === undefined) {
      rtn = this.ksProp(prop);
    }
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + SK.upperWordsFirstChar(prop)];
    }
    if (rtn === undefined) {
      rtn = this.props[Comp.SK_PROPS_PREFIX + SK.upperWordsFirstChar(Comp.SK_PROPS_SYS) + SK.upperWordsFirstChar(prop)];
    }
    return rtn === undefined ? defaultValue : rtn;
  }

  skTmpVal(id = this.getModelId(), value) {
    return arguments.length > 1 ? this.skModel().skVal(`tmp.${id}`, value) : this.skModel().skVal(`tmp.${id}`);
  }

  skTransProps2Child(children = this.props.children) {
    const skProps = Object.keys(this.props.skFilter(false, Comp.skPropsFilter));
    const ksProps = Object.keys(this.props.skFilter(false, Comp.ksPropsFilter));

    let rtn = React.Children.map(children, child => {
      if (React.isValidElement(child)) {
        let allowProps = skProps;
        allowProps = Reacts.TAG[child.type] ? [] : allowProps.concat(ksProps);//if html node, nothing to do
        allowProps = allowProps.concat(this.allowTransProps2Child(child));

        let denyProps = [Comp.SK_PROPS.COMP_TAG, Comp.SK_PROPS.MODEL_ID, 'children', 'className'];//force ignore props
        denyProps = child.props ? denyProps.concat(Object.keys(child.props)) : denyProps;
        denyProps = denyProps.concat(this.denyTransProps2Child(child));

        return React.cloneElement(child, _.omit(_.pick(this.props, allowProps), denyProps));
      }
      return child;
    });

    if (!_.isArray(children) && _.isArray(rtn)) {
      rtn = rtn[0];
    }
    return rtn;
  }

  /**
   * 1. trans prop start with sk
   * 2. trans all prop to "self"(like SKInput for Input, SKFormInput to SKInput)
   */
  skTransProps2Self(comp = this.props.compTag, prop = this.props) {
    const skProps = Object.keys(prop.skFilter(false, Comp.skPropsFilter));
    const ksProps = Object.keys(prop.skFilter(false, Comp.ksPropsFilter));

    let allowProps = skProps;
    allowProps = Reacts.TAG[comp] ? SK.s4a(Reacts.TP[comp]) : allowProps.concat(ksProps);//if html node, nothing to do
    if (this.SK_COMP_NAME
      && ((comp.NON_SK_COMP_NAME && (Comp.SK_PROPS_PREFIX.toUpperCase() + comp.NON_SK_COMP_NAME) === this.SK_COMP_NAME)
        || (this.SK_EXTEND_COMP_NAME && comp.SK_COMP_NAME === this.SK_COMP_NAME))) {
      allowProps = allowProps.concat(Reacts.P.skVals());
      allowProps = comp.propTypes ? allowProps.concat(Object.keys(comp.propTypes)) : allowProps;
    }
    allowProps = allowProps.concat(this.allowTransProps2Self(comp, prop));

    let denyProps = [Comp.SK_PROPS.COMP_TAG, Comp.SK_PROPS.MODEL_ID];//force ignore props
    denyProps = denyProps.concat(this.denyTransProps2Self(comp, prop));

    return _.omit(_.pick(prop, allowProps), denyProps);
  }

  /**
   * set/get value for modelId
   *
   * @param {*} val
   * @returns {*}
   */
  skVal(val) {
    if (arguments.length > 0) {
      this.skModel().skVal(this.getModelId(), val);
      return this;
    } else {
      return this.skModel().skVal(this.getModelId());
    }
  }
}
<|endoftext|>