<commit_before>/*
The canvas renderer was written by Yue Dong.

Modifications tracked on Github.
*/

import * as util from '../../../util';
import * as is from '../../../is';
import { makeBoundingBox } from '../../../math';
import ElementTextureCache from './ele-texture-cache';
import LayeredTextureCache from './layered-texture-cache';

import arrowShapes from './arrow-shapes';
import drawingElements from './drawing-elements';
import drawingEdges from './drawing-edges';
import drawingImages from './drawing-images';
import drawingLabelText from './drawing-label-text';
import drawingNodes from './drawing-nodes';
import drawingRedraw from './drawing-redraw';
import drawingShapes from './drawing-shapes';
import exportImage from './export-image';
import nodeShapes from './node-shapes';

var CR = CanvasRenderer;
var CRp = CanvasRenderer.prototype;

CRp.CANVAS_LAYERS = 3;
//
CRp.SELECT_BOX = 0;
CRp.DRAG = 1;
CRp.NODE = 2;

CRp.BUFFER_COUNT = 3;
//
CRp.TEXTURE_BUFFER = 0;
CRp.MOTIONBLUR_BUFFER_NODE = 1;
CRp.MOTIONBLUR_BUFFER_DRAG = 2;

function CanvasRenderer( options ){
  var r = this;

  r.data = {
    canvases: new Array( CRp.CANVAS_LAYERS ),
    contexts: new Array( CRp.CANVAS_LAYERS ),
    canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ),

    bufferCanvases: new Array( CRp.BUFFER_COUNT ),
    bufferContexts: new Array( CRp.CANVAS_LAYERS ),
  };

  var tapHlOff = '-webkit-tap-highlight-color: rgba(0,0,0,0);';

  r.data.canvasContainer = document.createElement( 'div' ); // eslint-disable-line no-undef
  var containerStyle = r.data.canvasContainer.style;
  r.data.canvasContainer.setAttribute( 'style', tapHlOff );
  containerStyle.position = 'relative';
  containerStyle.zIndex = '0';
  containerStyle.overflow = 'hidden';

  var container = options.cy.container();
  container.appendChild( r.data.canvasContainer );

  if( (container.getAttribute('style') || '').indexOf(tapHlOff) < 0 ){
    container.setAttribute( 'style', ( container.getAttribute( 'style' ) || '' ) + tapHlOff );
  }

  for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){
    var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' );  // eslint-disable-line no-undef
    r.data.contexts[ i ] = canvas.getContext( '2d' );
    canvas.setAttribute( 'style', '-webkit-user-select: none; -moz-user-select: -moz-none; user-select: none; -webkit-tap-highlight-color: rgba(0,0,0,0); outline-style: none;' + ( is.ms() ? ' -ms-touch-action: none; touch-action: none; ' : '' ) );
    canvas.style.position = 'absolute';
    canvas.setAttribute( 'data-id', 'layer' + i );
    canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i );
    r.data.canvasContainer.appendChild( canvas );

    r.data.canvasNeedsRedraw[ i ] = false;
  }
  r.data.topCanvas = r.data.canvases[0];

  r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' );
  r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' );
  r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' );

  for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){
    r.data.bufferCanvases[ i ] = document.createElement( 'canvas' );  // eslint-disable-line no-undef
    r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' );
    r.data.bufferCanvases[ i ].style.position = 'absolute';
    r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i );
    r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 );
    r.data.bufferCanvases[ i ].style.visibility = 'hidden';
    //r.data.canvasContainer.appendChild(r.data.bufferCanvases[i]);
  }

  r.pathsEnabled = true;

  let emptyBb = makeBoundingBox();

  let getStyleKey = ele => ele[0]._private.nodeKey;
  let drawElement = (context, ele, bb, scaledLabelShown) => r.drawElement( context, ele, bb, false );
  let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.overlayBounds; };

  let getLabelKey = ele => ele[0]._private.labelStyleKey;
  let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey;
  let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey;
  let drawLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main' );
  let drawSourceLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source' );
  let drawTargetLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target' );
  let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; };
  let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; };
  let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; };
  let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown;

  let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, {
    getKey: getStyleKey,
    drawElement: drawElement,
    getBoundingBox: getElementBox,
    allowEdgeTxrCaching: false,
    allowParentTxrCaching: false
  } );

  let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, {
    getKey: getLabelKey,
    drawElement: drawLabel,
    getBoundingBox: getLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, {
    getKey: getSourceLabelKey,
    drawElement: drawSourceLabel,
    getBoundingBox: getSourceLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, {
    getKey: getTargetLabelKey,
    drawElement: drawTargetLabel,
    getBoundingBox: getTargetLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r );

  r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){
    // each cache should check for sub-key diff to see that the update affects that cache particularly
    eleTxrCache.invalidateElements( eles );
    lblTxrCache.invalidateElements( eles );
    slbTxrCache.invalidateElements( eles );
    tlbTxrCache.invalidateElements( eles );

    // any change invalidates the layers
    lyrTxrCache.invalidateElements( eles );
  });

  let refineInLayers = reqs => {
    for( var i = 0; i < reqs.length; i++ ){
      lyrTxrCache.enqueueElementRefinement( reqs[i].ele );
    }
  };

  eleTxrCache.onDequeue(refineInLayers);
  lblTxrCache.onDequeue(refineInLayers);
  slbTxrCache.onDequeue(refineInLayers);
  tlbTxrCache.onDequeue(refineInLayers);
}

CRp.redrawHint = function( group, bool ){
  var r = this;

  switch( group ){
    case 'eles':
      r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
      break;
    case 'drag':
      r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool;
      break;
    case 'select':
      r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
      break;
  }
};

// whether to use Path2D caching for drawing
var pathsImpld = typeof Path2D !== 'undefined';

CRp.path2dEnabled = function( on ){
  if( on === undefined ){
    return this.pathsEnabled;
  }

  this.pathsEnabled = on ? true : false;
};

CRp.usePaths = function(){
  return pathsImpld && this.pathsEnabled;
};

CRp.setImgSmoothing = function( context, bool ){
  if( context.imageSmoothingEnabled != null ){
    context.imageSmoothingEnabled = bool;
  } else {
    context.webkitImageSmoothingEnabled = bool;
    context.mozImageSmoothingEnabled = bool;
    context.msImageSmoothingEnabled = bool;
  }
};

CRp.getImgSmoothing = function( context ){
  if( context.imageSmoothingEnabled != null ){
    return context.imageSmoothingEnabled;
  } else {
    return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled;
  }
};

CRp.makeOffscreenCanvas = function(width, height){
  let canvas;

  if( typeof OffscreenCanvas !== typeof undefined ){
    canvas = new OffscreenCanvas(width, height);
  } else {
    canvas = document.createElement('canvas'); // eslint-disable-line no-undef
    canvas.width = width;
    canvas.height = height;
  }

  return canvas;
};

[
  arrowShapes,
  drawingElements,
  drawingEdges,
  drawingImages,
  drawingLabelText,
  drawingNodes,
  drawingRedraw,
  drawingShapes,
  exportImage,
  nodeShapes
].forEach( function( props ){
  util.extend( CRp, props );
} );

export default CR;
<commit_msg>Fix linting error<commit_after>/*
The canvas renderer was written by Yue Dong.

Modifications tracked on Github.
*/

/* global OffscreenCanvas */

import * as util from '../../../util';
import * as is from '../../../is';
import { makeBoundingBox } from '../../../math';
import ElementTextureCache from './ele-texture-cache';
import LayeredTextureCache from './layered-texture-cache';

import arrowShapes from './arrow-shapes';
import drawingElements from './drawing-elements';
import drawingEdges from './drawing-edges';
import drawingImages from './drawing-images';
import drawingLabelText from './drawing-label-text';
import drawingNodes from './drawing-nodes';
import drawingRedraw from './drawing-redraw';
import drawingShapes from './drawing-shapes';
import exportImage from './export-image';
import nodeShapes from './node-shapes';

var CR = CanvasRenderer;
var CRp = CanvasRenderer.prototype;

CRp.CANVAS_LAYERS = 3;
//
CRp.SELECT_BOX = 0;
CRp.DRAG = 1;
CRp.NODE = 2;

CRp.BUFFER_COUNT = 3;
//
CRp.TEXTURE_BUFFER = 0;
CRp.MOTIONBLUR_BUFFER_NODE = 1;
CRp.MOTIONBLUR_BUFFER_DRAG = 2;

function CanvasRenderer( options ){
  var r = this;

  r.data = {
    canvases: new Array( CRp.CANVAS_LAYERS ),
    contexts: new Array( CRp.CANVAS_LAYERS ),
    canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ),

    bufferCanvases: new Array( CRp.BUFFER_COUNT ),
    bufferContexts: new Array( CRp.CANVAS_LAYERS ),
  };

  var tapHlOff = '-webkit-tap-highlight-color: rgba(0,0,0,0);';

  r.data.canvasContainer = document.createElement( 'div' ); // eslint-disable-line no-undef
  var containerStyle = r.data.canvasContainer.style;
  r.data.canvasContainer.setAttribute( 'style', tapHlOff );
  containerStyle.position = 'relative';
  containerStyle.zIndex = '0';
  containerStyle.overflow = 'hidden';

  var container = options.cy.container();
  container.appendChild( r.data.canvasContainer );

  if( (container.getAttribute('style') || '').indexOf(tapHlOff) < 0 ){
    container.setAttribute( 'style', ( container.getAttribute( 'style' ) || '' ) + tapHlOff );
  }

  for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){
    var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' );  // eslint-disable-line no-undef
    r.data.contexts[ i ] = canvas.getContext( '2d' );
    canvas.setAttribute( 'style', '-webkit-user-select: none; -moz-user-select: -moz-none; user-select: none; -webkit-tap-highlight-color: rgba(0,0,0,0); outline-style: none;' + ( is.ms() ? ' -ms-touch-action: none; touch-action: none; ' : '' ) );
    canvas.style.position = 'absolute';
    canvas.setAttribute( 'data-id', 'layer' + i );
    canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i );
    r.data.canvasContainer.appendChild( canvas );

    r.data.canvasNeedsRedraw[ i ] = false;
  }
  r.data.topCanvas = r.data.canvases[0];

  r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' );
  r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' );
  r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' );

  for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){
    r.data.bufferCanvases[ i ] = document.createElement( 'canvas' );  // eslint-disable-line no-undef
    r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' );
    r.data.bufferCanvases[ i ].style.position = 'absolute';
    r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i );
    r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 );
    r.data.bufferCanvases[ i ].style.visibility = 'hidden';
    //r.data.canvasContainer.appendChild(r.data.bufferCanvases[i]);
  }

  r.pathsEnabled = true;

  let emptyBb = makeBoundingBox();

  let getStyleKey = ele => ele[0]._private.nodeKey;
  let drawElement = (context, ele, bb, scaledLabelShown) => r.drawElement( context, ele, bb, false );
  let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.overlayBounds; };

  let getLabelKey = ele => ele[0]._private.labelStyleKey;
  let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey;
  let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey;
  let drawLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main' );
  let drawSourceLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source' );
  let drawTargetLabel = (context, ele, bb, scaledLabelShown) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target' );
  let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; };
  let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; };
  let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; };
  let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown;

  let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, {
    getKey: getStyleKey,
    drawElement: drawElement,
    getBoundingBox: getElementBox,
    allowEdgeTxrCaching: false,
    allowParentTxrCaching: false
  } );

  let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, {
    getKey: getLabelKey,
    drawElement: drawLabel,
    getBoundingBox: getLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, {
    getKey: getSourceLabelKey,
    drawElement: drawSourceLabel,
    getBoundingBox: getSourceLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, {
    getKey: getTargetLabelKey,
    drawElement: drawTargetLabel,
    getBoundingBox: getTargetLabelBox,
    isVisible: isLabelVisibleAtScale
  } );

  let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r );

  r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){
    // each cache should check for sub-key diff to see that the update affects that cache particularly
    eleTxrCache.invalidateElements( eles );
    lblTxrCache.invalidateElements( eles );
    slbTxrCache.invalidateElements( eles );
    tlbTxrCache.invalidateElements( eles );

    // any change invalidates the layers
    lyrTxrCache.invalidateElements( eles );
  });

  let refineInLayers = reqs => {
    for( var i = 0; i < reqs.length; i++ ){
      lyrTxrCache.enqueueElementRefinement( reqs[i].ele );
    }
  };

  eleTxrCache.onDequeue(refineInLayers);
  lblTxrCache.onDequeue(refineInLayers);
  slbTxrCache.onDequeue(refineInLayers);
  tlbTxrCache.onDequeue(refineInLayers);
}

CRp.redrawHint = function( group, bool ){
  var r = this;

  switch( group ){
    case 'eles':
      r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
      break;
    case 'drag':
      r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool;
      break;
    case 'select':
      r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
      break;
  }
};

// whether to use Path2D caching for drawing
var pathsImpld = typeof Path2D !== 'undefined';

CRp.path2dEnabled = function( on ){
  if( on === undefined ){
    return this.pathsEnabled;
  }

  this.pathsEnabled = on ? true : false;
};

CRp.usePaths = function(){
  return pathsImpld && this.pathsEnabled;
};

CRp.setImgSmoothing = function( context, bool ){
  if( context.imageSmoothingEnabled != null ){
    context.imageSmoothingEnabled = bool;
  } else {
    context.webkitImageSmoothingEnabled = bool;
    context.mozImageSmoothingEnabled = bool;
    context.msImageSmoothingEnabled = bool;
  }
};

CRp.getImgSmoothing = function( context ){
  if( context.imageSmoothingEnabled != null ){
    return context.imageSmoothingEnabled;
  } else {
    return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled;
  }
};

CRp.makeOffscreenCanvas = function(width, height){
  let canvas;

  if( typeof OffscreenCanvas !== typeof undefined ){
    canvas = new OffscreenCanvas(width, height);
  } else {
    canvas = document.createElement('canvas'); // eslint-disable-line no-undef
    canvas.width = width;
    canvas.height = height;
  }

  return canvas;
};

[
  arrowShapes,
  drawingElements,
  drawingEdges,
  drawingImages,
  drawingLabelText,
  drawingNodes,
  drawingRedraw,
  drawingShapes,
  exportImage,
  nodeShapes
].forEach( function( props ){
  util.extend( CRp, props );
} );

export default CR;
<|endoftext|>


<commit_before>
forEach(
  'onHold onTap onTouch onRelease onDrag onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeBottom onSwipeLeft'.split(' '),
  function(name) {
    IonicModule.directive(name, gestureDirective(name));
  }
);

/**
 * @ngdoc directive
 * @name onHold
 * @module ionic
 * @restrict A
 *
 * @description
 * Touch stays at the same location for 500ms.
 *
 * @usage
 * ```html
 * <button on-hold="onHold()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onTap
 * @module ionic
 * @restrict A
 *
 * @description
 * Quick touch at a location. If the duration of the touch goes
 * longer than 250ms it is no longer a tap gesture.
 *
 * @usage
 * ```html
 * <button on-tap="onTap()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onTouch
 * @module ionic
 * @restrict A
 *
 * @description
 * Called immediately when the user first begins a touch. This
 * gesture does not wait for a touchend/mouseup.
 *
 * @usage
 * ```html
 * <button on-touch="onTouch()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onRelease
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the user ends a touch.
 *
 * @usage
 * ```html
 * <button on-release="onRelease()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDrag
 * @module ionic
 * @restrict A
 *
 * @description
 * Move with one touch around on the page. Blocking the scrolling when
 * moving left and right is a good practice. When all the drag events are
 * blocking you disable scrolling on that area.
 *
 * @usage
 * ```html
 * <button on-drag="onDrag()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragUp
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged up.
 *
 * @usage
 * ```html
 * <button on-drag-up="onDragUp()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragRight
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged to the right.
 *
 * @usage
 * ```html
 * <button on-drag-right="onDragRight()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragDown
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged down.
 *
 * @usage
 * ```html
 * <button on-drag-down="onDragDown()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragLeft
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged to the left.
 *
 * @usage
 * ```html
 * <button on-drag-left="onDragLeft()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipe
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity in any direction.
 *
 * @usage
 * ```html
 * <button on-swipe="onSwipe()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeUp
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving up.
 *
 * @usage
 * ```html
 * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeRight
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving to the right.
 *
 * @usage
 * ```html
 * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeDown
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving down.
 *
 * @usage
 * ```html
 * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeLeft
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving to the left.
 *
 * @usage
 * ```html
 * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
 * ```
 */

function gestureDirective(directiveName) {
  return ['$ionicGesture', '$parse', function($ionicGesture, $parse) {
    var eventType = directiveName.substr(2).toLowerCase();

    return function(scope, element, attr) {
      var fn = $parse( attr[directiveName] );

      var listener = function(ev) {
        scope.$apply(function() {
          fn(scope, {
            $event: ev
          });
        });
      };

      var gesture = $ionicGesture.on(eventType, listener, element);

      scope.$on('$destroy', function() {
        $ionicGesture.off(gesture, eventType, listener);
      });
    };
  }];
}
<commit_msg>fix(gesture): fix onSwipeDown<commit_after>
forEach(
  'onHold onTap onTouch onRelease onDrag onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' '),
  function(name) {
    IonicModule.directive(name, gestureDirective(name));
  }
);

/**
 * @ngdoc directive
 * @name onHold
 * @module ionic
 * @restrict A
 *
 * @description
 * Touch stays at the same location for 500ms.
 *
 * @usage
 * ```html
 * <button on-hold="onHold()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onTap
 * @module ionic
 * @restrict A
 *
 * @description
 * Quick touch at a location. If the duration of the touch goes
 * longer than 250ms it is no longer a tap gesture.
 *
 * @usage
 * ```html
 * <button on-tap="onTap()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onTouch
 * @module ionic
 * @restrict A
 *
 * @description
 * Called immediately when the user first begins a touch. This
 * gesture does not wait for a touchend/mouseup.
 *
 * @usage
 * ```html
 * <button on-touch="onTouch()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onRelease
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the user ends a touch.
 *
 * @usage
 * ```html
 * <button on-release="onRelease()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDrag
 * @module ionic
 * @restrict A
 *
 * @description
 * Move with one touch around on the page. Blocking the scrolling when
 * moving left and right is a good practice. When all the drag events are
 * blocking you disable scrolling on that area.
 *
 * @usage
 * ```html
 * <button on-drag="onDrag()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragUp
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged up.
 *
 * @usage
 * ```html
 * <button on-drag-up="onDragUp()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragRight
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged to the right.
 *
 * @usage
 * ```html
 * <button on-drag-right="onDragRight()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragDown
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged down.
 *
 * @usage
 * ```html
 * <button on-drag-down="onDragDown()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onDragLeft
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when the element is dragged to the left.
 *
 * @usage
 * ```html
 * <button on-drag-left="onDragLeft()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipe
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity in any direction.
 *
 * @usage
 * ```html
 * <button on-swipe="onSwipe()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeUp
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving up.
 *
 * @usage
 * ```html
 * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeRight
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving to the right.
 *
 * @usage
 * ```html
 * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeDown
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving down.
 *
 * @usage
 * ```html
 * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
 * ```
 */

/**
 * @ngdoc directive
 * @name onSwipeLeft
 * @module ionic
 * @restrict A
 *
 * @description
 * Called when a moving touch has a high velocity moving to the left.
 *
 * @usage
 * ```html
 * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
 * ```
 */

function gestureDirective(directiveName) {
  return ['$ionicGesture', '$parse', function($ionicGesture, $parse) {
    var eventType = directiveName.substr(2).toLowerCase();

    return function(scope, element, attr) {
      var fn = $parse( attr[directiveName] );

      var listener = function(ev) {
        scope.$apply(function() {
          fn(scope, {
            $event: ev
          });
        });
      };

      var gesture = $ionicGesture.on(eventType, listener, element);

      scope.$on('$destroy', function() {
        $ionicGesture.off(gesture, eventType, listener);
      });
    };
  }];
}
<|endoftext|>


<commit_before>// @flow

import { threadPermissions } from 'lib/types/thread-types';
import { threadSubscriptions } from 'lib/types/subscription-types';

import apn from '@parse/node-apn';
import invariant from 'invariant';

import { promiseAll } from 'lib/utils/promises';

import { dbQuery, SQL, SQLStatement } from '../database/database';
import { apnPush, fcmPush } from './utils';
import createIDs from '../creators/id-creator';

async function rescindPushNotifs(
  notifCondition: SQLStatement,
  inputCountCondition?: SQLStatement,
) {
  const notificationExtractString = `$.${threadSubscriptions.home}`;
  const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
  const fetchQuery = SQL`
    SELECT n.id, n.user, n.thread, n.message, n.delivery, n.collapse_key, COUNT(
  `;
  fetchQuery.append(inputCountCondition ? inputCountCondition : SQL`m.thread`);
  fetchQuery.append(SQL`
      ) AS unread_count
    FROM notifications n
    LEFT JOIN memberships m ON m.user = n.user
      AND m.last_message > m.last_read_message
      AND m.role > 0
      AND JSON_EXTRACT(subscription, ${notificationExtractString})
      AND JSON_EXTRACT(permissions, ${visPermissionExtractString})
    WHERE n.rescinded = 0 AND
  `);
  fetchQuery.append(notifCondition);
  fetchQuery.append(SQL` GROUP BY n.id, m.user`);
  const [fetchResult] = await dbQuery(fetchQuery);

  const deliveryPromises = {};
  const notifInfo = {};
  const rescindedIDs = [];
  for (let row of fetchResult) {
    const deliveries = Array.isArray(row.delivery)
      ? row.delivery
      : [row.delivery];
    const id = row.id.toString();
    const threadID = row.thread.toString();
    notifInfo[id] = {
      userID: row.user.toString(),
      threadID,
      messageID: row.message.toString(),
    };
    for (let delivery of deliveries) {
      if (delivery.iosID && delivery.iosDeviceTokens) {
        // Old iOS
        const notification = prepareIOSNotification(
          delivery.iosID,
          row.unread_count,
        );
        deliveryPromises[id] = apnPush(notification, delivery.iosDeviceTokens);
      } else if (delivery.androidID) {
        // Old Android
        const notification = prepareAndroidNotification(
          row.collapse_key ? row.collapse_key : id,
          row.unread_count,
          threadID,
          null,
        );
        deliveryPromises[id] = fcmPush(
          notification,
          delivery.androidDeviceTokens,
          null,
        );
      } else if (delivery.deviceType === 'ios') {
        // New iOS
        const { iosID, deviceTokens } = delivery;
        const notification = prepareIOSNotification(iosID, row.unread_count);
        deliveryPromises[id] = apnPush(notification, deviceTokens);
      } else if (delivery.deviceType === 'android') {
        // New Android
        const { deviceTokens, codeVersion } = delivery;
        const notification = prepareAndroidNotification(
          row.collapse_key ? row.collapse_key : id,
          row.unread_count,
          threadID,
          codeVersion,
        );
        deliveryPromises[id] = fcmPush(notification, deviceTokens, null);
      }
    }
    rescindedIDs.push(row.id);
  }

  const numRescinds = Object.keys(deliveryPromises).length;
  const promises = [promiseAll(deliveryPromises)];
  if (numRescinds > 0) {
    promises.push(createIDs('notifications', numRescinds));
  }
  if (rescindedIDs.length > 0) {
    const rescindQuery = SQL`
      UPDATE notifications SET rescinded = 1 WHERE id IN (${rescindedIDs})
    `;
    promises.push(dbQuery(rescindQuery));
  }

  const [deliveryResults, dbIDs] = await Promise.all(promises);
  const newNotifRows = [];
  if (numRescinds > 0) {
    invariant(dbIDs, 'dbIDs should be set');
    for (const rescindedID in deliveryResults) {
      const delivery = {};
      delivery.type = 'rescind';
      delivery.rescindedID = rescindedID;
      const { errors } = deliveryResults[rescindedID];
      if (errors) {
        delivery.errors = errors;
      }
      const dbID = dbIDs.shift();
      const { userID, threadID, messageID } = notifInfo[rescindedID];
      newNotifRows.push([
        dbID,
        userID,
        threadID,
        messageID,
        null,
        JSON.stringify([delivery]),
        1,
      ]);
    }
  }
  if (newNotifRows.length > 0) {
    const insertQuery = SQL`
      INSERT INTO notifications
        (id, user, thread, message, collapse_key, delivery, rescinded)
      VALUES ${newNotifRows}
    `;
    await dbQuery(insertQuery);
  }
}

function prepareIOSNotification(
  iosID: string,
  unreadCount: number,
): apn.Notification {
  const notification = new apn.Notification();
  notification.contentAvailable = true;
  notification.badge = unreadCount;
  notification.topic = 'org.squadcal.app';
  notification.payload = {
    managedAps: {
      action: 'CLEAR',
      notificationId: iosID,
    },
  };
  return notification;
}

function prepareAndroidNotification(
  notifID: string,
  unreadCount: number,
  threadID: string,
  codeVersion: ?number,
): Object {
  if (!codeVersion || codeVersion < 31) {
    return {
      data: {
        badge: unreadCount.toString(),
        custom_notification: JSON.stringify({
          rescind: 'true',
          notifID,
        }),
      },
    };
  }
  return {
    data: {
      badge: unreadCount.toString(),
      rescind: 'true',
      rescindID: notifID,
      threadID,
    },
  };
}

export { rescindPushNotifs };
<commit_msg>[server] Include source key in all delivery objects<commit_after>// @flow

import { threadPermissions } from 'lib/types/thread-types';
import { threadSubscriptions } from 'lib/types/subscription-types';

import apn from '@parse/node-apn';
import invariant from 'invariant';

import { promiseAll } from 'lib/utils/promises';

import { dbQuery, SQL, SQLStatement } from '../database/database';
import { apnPush, fcmPush } from './utils';
import createIDs from '../creators/id-creator';

async function rescindPushNotifs(
  notifCondition: SQLStatement,
  inputCountCondition?: SQLStatement,
) {
  const notificationExtractString = `$.${threadSubscriptions.home}`;
  const visPermissionExtractString = `$.${threadPermissions.VISIBLE}.value`;
  const fetchQuery = SQL`
    SELECT n.id, n.user, n.thread, n.message, n.delivery, n.collapse_key, COUNT(
  `;
  fetchQuery.append(inputCountCondition ? inputCountCondition : SQL`m.thread`);
  fetchQuery.append(SQL`
      ) AS unread_count
    FROM notifications n
    LEFT JOIN memberships m ON m.user = n.user
      AND m.last_message > m.last_read_message
      AND m.role > 0
      AND JSON_EXTRACT(subscription, ${notificationExtractString})
      AND JSON_EXTRACT(permissions, ${visPermissionExtractString})
    WHERE n.rescinded = 0 AND
  `);
  fetchQuery.append(notifCondition);
  fetchQuery.append(SQL` GROUP BY n.id, m.user`);
  const [fetchResult] = await dbQuery(fetchQuery);

  const deliveryPromises = {};
  const notifInfo = {};
  const rescindedIDs = [];
  for (let row of fetchResult) {
    const deliveries = Array.isArray(row.delivery)
      ? row.delivery
      : [row.delivery];
    const id = row.id.toString();
    const threadID = row.thread.toString();
    notifInfo[id] = {
      userID: row.user.toString(),
      threadID,
      messageID: row.message.toString(),
    };
    for (let delivery of deliveries) {
      if (delivery.iosID && delivery.iosDeviceTokens) {
        // Old iOS
        const notification = prepareIOSNotification(
          delivery.iosID,
          row.unread_count,
        );
        deliveryPromises[id] = apnPush(notification, delivery.iosDeviceTokens);
      } else if (delivery.androidID) {
        // Old Android
        const notification = prepareAndroidNotification(
          row.collapse_key ? row.collapse_key : id,
          row.unread_count,
          threadID,
          null,
        );
        deliveryPromises[id] = fcmPush(
          notification,
          delivery.androidDeviceTokens,
          null,
        );
      } else if (delivery.deviceType === 'ios') {
        // New iOS
        const { iosID, deviceTokens } = delivery;
        const notification = prepareIOSNotification(iosID, row.unread_count);
        deliveryPromises[id] = apnPush(notification, deviceTokens);
      } else if (delivery.deviceType === 'android') {
        // New Android
        const { deviceTokens, codeVersion } = delivery;
        const notification = prepareAndroidNotification(
          row.collapse_key ? row.collapse_key : id,
          row.unread_count,
          threadID,
          codeVersion,
        );
        deliveryPromises[id] = fcmPush(notification, deviceTokens, null);
      }
    }
    rescindedIDs.push(row.id);
  }

  const numRescinds = Object.keys(deliveryPromises).length;
  const promises = [promiseAll(deliveryPromises)];
  if (numRescinds > 0) {
    promises.push(createIDs('notifications', numRescinds));
  }
  if (rescindedIDs.length > 0) {
    const rescindQuery = SQL`
      UPDATE notifications SET rescinded = 1 WHERE id IN (${rescindedIDs})
    `;
    promises.push(dbQuery(rescindQuery));
  }

  const [deliveryResults, dbIDs] = await Promise.all(promises);
  const newNotifRows = [];
  if (numRescinds > 0) {
    invariant(dbIDs, 'dbIDs should be set');
    for (const rescindedID in deliveryResults) {
      const delivery = {};
      delivery.source = 'rescind';
      delivery.rescindedID = rescindedID;
      const { errors } = deliveryResults[rescindedID];
      if (errors) {
        delivery.errors = errors;
      }
      const dbID = dbIDs.shift();
      const { userID, threadID, messageID } = notifInfo[rescindedID];
      newNotifRows.push([
        dbID,
        userID,
        threadID,
        messageID,
        null,
        JSON.stringify([delivery]),
        1,
      ]);
    }
  }
  if (newNotifRows.length > 0) {
    const insertQuery = SQL`
      INSERT INTO notifications
        (id, user, thread, message, collapse_key, delivery, rescinded)
      VALUES ${newNotifRows}
    `;
    await dbQuery(insertQuery);
  }
}

function prepareIOSNotification(
  iosID: string,
  unreadCount: number,
): apn.Notification {
  const notification = new apn.Notification();
  notification.contentAvailable = true;
  notification.badge = unreadCount;
  notification.topic = 'org.squadcal.app';
  notification.payload = {
    managedAps: {
      action: 'CLEAR',
      notificationId: iosID,
    },
  };
  return notification;
}

function prepareAndroidNotification(
  notifID: string,
  unreadCount: number,
  threadID: string,
  codeVersion: ?number,
): Object {
  if (!codeVersion || codeVersion < 31) {
    return {
      data: {
        badge: unreadCount.toString(),
        custom_notification: JSON.stringify({
          rescind: 'true',
          notifID,
        }),
      },
    };
  }
  return {
    data: {
      badge: unreadCount.toString(),
      rescind: 'true',
      rescindID: notifID,
      threadID,
    },
  };
}

export { rescindPushNotifs };
<|endoftext|>


<commit_before>import Tone from "../core/Tone";
import "../component/Volume";
import "../core/AudioNode";

/**
 *  @class  Tone.UserMedia uses MediaDevices.getUserMedia to open up
 *          and external microphone or audio input. Check
 *          [MediaDevices API Support](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
 *          to see which browsers are supported. Access to an external input
 *          is limited to secure (HTTPS) connections.
 *
 *  @constructor
 *  @extends {Tone.AudioNode}
 *  @param {Decibels=} volume The level of the input
 *  @example
 * //list the inputs and open the third one
 * var motu = new Tone.UserMedia();
 *
 * //opening the input asks the user to activate their mic
 * motu.open().then(function(){
 * 	//promise resolves when input is available
 * });
 */

Tone.UserMedia = function(){

	var options = Tone.defaults(arguments, ["volume"], Tone.UserMedia);
	Tone.AudioNode.call(this);

	/**
	 *  The MediaStreamNode
	 *  @type {MediaStreamAudioSourceNode}
	 *  @private
	 */
	this._mediaStream = null;

	/**
	 *  The media stream created by getUserMedia.
	 *  @type {LocalMediaStream}
	 *  @private
	 */
	this._stream = null;

	/**
	 *  The open device
	 *  @type  {MediaDeviceInfo}
	 *  @private
	 */
	this._device = null;

	/**
	 *  The output volume node
	 *  @type  {Tone.Volume}
	 *  @private
	 */
	this._volume = this.output = new Tone.Volume(options.volume);

	/**
	 * The volume of the output in decibels.
	 * @type {Decibels}
	 * @signal
	 * @example
	 * input.volume.value = -6;
	 */
	this.volume = this._volume.volume;
	this._readOnly("volume");

	this.mute = options.mute;
};

Tone.extend(Tone.UserMedia, Tone.AudioNode);

/**
 * the default parameters
 * @type {Object}
 */
Tone.UserMedia.defaults = {
	"volume" : 0,
	"mute" : false
};

/**
 *  Open the media stream. If a string is passed in, it is assumed
 *  to be the label or id of the stream, if a number is passed in,
 *  it is the input number of the stream.
 *  @param  {String|Number} [labelOrId="default"] The label or id of the audio input media device.
 *                                                With no argument, the default stream is opened.
 *  @return {Promise} The promise is resolved when the stream is open.
 */
Tone.UserMedia.prototype.open = function(labelOrId){
	//close the previous stream
	if (this.state === Tone.State.Started){
		this.close();
	}
	return Tone.UserMedia.enumerateDevices().then(function(devices){
		var device;
		if (Tone.isNumber(labelOrId)){
			device = devices[labelOrId];
		} else {
			device = devices.find(function(device){
				return device.label === labelOrId || device.deviceId === labelOrId;
			});
			//didn't find a matching device
			if (!device && devices.length > 0){
				device = devices[0];
			} else if (!device && Tone.isDefined(labelOrId)){
				throw new Error("Tone.UserMedia: no matching device: "+labelOrId);
			}
		}
		this._device = device;
		//do getUserMedia
		var constraints = {
			audio : {
				"echoCancellation" : false,
				"sampleRate" : this.context.sampleRate,
				"noiseSuppression" : false,
				"mozNoiseSuppression" : false,
			}
		};
		if (device){
			constraints.audio.deviceId = device.deviceId;
		}
		return navigator.mediaDevices.getUserMedia(constraints).then(function(stream){
			//start a new source only if the previous one is closed
			if (!this._stream){
				this._stream = stream;
				//Wrap a MediaStreamSourceNode around the live input stream.
				this._mediaStream = this.context.createMediaStreamSource(stream);
				//Connect the MediaStreamSourceNode to a gate gain node
				this._mediaStream.connect(this.output);
			}
			return this;
		}.bind(this));
	}.bind(this));
};

/**
 *  Close the media stream
 *  @return {Tone.UserMedia} this
 */
Tone.UserMedia.prototype.close = function(){
	if (this._stream){
		this._stream.getAudioTracks().forEach(function(track){
			track.stop();
		});
		this._stream = null;
		//remove the old media stream
		this._mediaStream.disconnect();
		this._mediaStream = null;
	}
	this._device = null;
	return this;
};

/**
 *  Returns a promise which resolves with the list of audio input devices available.
 *  @return {Promise} The promise that is resolved with the devices
 *  @static
 *  @example
 * Tone.UserMedia.enumerateDevices().then(function(devices){
 * 	console.log(devices)
 * })
 */
Tone.UserMedia.enumerateDevices = function(){
	return navigator.mediaDevices.enumerateDevices().then(function(devices){
		return devices.filter(function(device){
			return device.kind === "audioinput";
		});
	});
};

/**
 *  Returns the playback state of the source, "started" when the microphone is open
 *  and "stopped" when the mic is closed.
 *  @type {Tone.State}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name state
 */
Object.defineProperty(Tone.UserMedia.prototype, "state", {
	get : function(){
		return this._stream && this._stream.active ? Tone.State.Started : Tone.State.Stopped;
	}
});

/**
 * 	Returns an identifier for the represented device that is
 * 	persisted across sessions. It is un-guessable by other applications and
 * 	unique to the origin of the calling application. It is reset when the
 * 	user clears cookies (for Private Browsing, a different identifier is
 * 	used that is not persisted across sessions). Returns undefined when the
 * 	device is not open.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name deviceId
 */
Object.defineProperty(Tone.UserMedia.prototype, "deviceId", {
	get : function(){
		if (this._device){
			return this._device.deviceId;
		}
	}
});

/**
 * 	Returns a group identifier. Two devices have the
 * 	same group identifier if they belong to the same physical device.
 * 	Returns undefined when the device is not open.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name groupId
 */
Object.defineProperty(Tone.UserMedia.prototype, "groupId", {
	get : function(){
		if (this._device){
			return this._device.groupId;
		}
	}
});

/**
 * 	Returns a label describing this device (for example "Built-in Microphone").
 * 	Returns undefined when the device is not open or label is not available
 * 	because of permissions.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name groupId
 */
Object.defineProperty(Tone.UserMedia.prototype, "label", {
	get : function(){
		if (this._device){
			return this._device.label;
		}
	}
});

/**
 * Mute the output.
 * @memberOf Tone.UserMedia#
 * @type {boolean}
 * @name mute
 * @example
 * //mute the output
 * userMedia.mute = true;
 */
Object.defineProperty(Tone.UserMedia.prototype, "mute", {
	get : function(){
		return this._volume.mute;
	},
	set : function(mute){
		this._volume.mute = mute;
	}
});

/**
 * Clean up.
 * @return {Tone.UserMedia} this
 */
Tone.UserMedia.prototype.dispose = function(){
	Tone.AudioNode.prototype.dispose.call(this);
	this.close();
	this._writable("volume");
	this._volume.dispose();
	this._volume = null;
	this.volume = null;
	return this;
};

/**
 *  If getUserMedia is supported by the browser.
 *  @type  {Boolean}
 *  @memberOf Tone.UserMedia#
 *  @name supported
 *  @static
 *  @readOnly
 */
Object.defineProperty(Tone.UserMedia, "supported", {
	get : function(){
		return Tone.isDefined(navigator.mediaDevices) && Tone.isFunction(navigator.mediaDevices.getUserMedia);
	}
});

export default Tone.UserMedia;

<commit_msg>using Tone.connect for native nodes<commit_after>import Tone from "../core/Tone";
import "../component/Volume";
import "../core/AudioNode";

/**
 *  @class  Tone.UserMedia uses MediaDevices.getUserMedia to open up
 *          and external microphone or audio input. Check
 *          [MediaDevices API Support](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
 *          to see which browsers are supported. Access to an external input
 *          is limited to secure (HTTPS) connections.
 *
 *  @constructor
 *  @extends {Tone.AudioNode}
 *  @param {Decibels=} volume The level of the input
 *  @example
 * //list the inputs and open the third one
 * var motu = new Tone.UserMedia();
 *
 * //opening the input asks the user to activate their mic
 * motu.open().then(function(){
 * 	//promise resolves when input is available
 * });
 */

Tone.UserMedia = function(){

	var options = Tone.defaults(arguments, ["volume"], Tone.UserMedia);
	Tone.AudioNode.call(this);

	/**
	 *  The MediaStreamNode
	 *  @type {MediaStreamAudioSourceNode}
	 *  @private
	 */
	this._mediaStream = null;

	/**
	 *  The media stream created by getUserMedia.
	 *  @type {LocalMediaStream}
	 *  @private
	 */
	this._stream = null;

	/**
	 *  The open device
	 *  @type  {MediaDeviceInfo}
	 *  @private
	 */
	this._device = null;

	/**
	 *  The output volume node
	 *  @type  {Tone.Volume}
	 *  @private
	 */
	this._volume = this.output = new Tone.Volume(options.volume);

	/**
	 * The volume of the output in decibels.
	 * @type {Decibels}
	 * @signal
	 * @example
	 * input.volume.value = -6;
	 */
	this.volume = this._volume.volume;
	this._readOnly("volume");

	this.mute = options.mute;
};

Tone.extend(Tone.UserMedia, Tone.AudioNode);

/**
 * the default parameters
 * @type {Object}
 */
Tone.UserMedia.defaults = {
	"volume" : 0,
	"mute" : false
};

/**
 *  Open the media stream. If a string is passed in, it is assumed
 *  to be the label or id of the stream, if a number is passed in,
 *  it is the input number of the stream.
 *  @param  {String|Number} [labelOrId="default"] The label or id of the audio input media device.
 *                                                With no argument, the default stream is opened.
 *  @return {Promise} The promise is resolved when the stream is open.
 */
Tone.UserMedia.prototype.open = function(labelOrId){
	//close the previous stream
	if (this.state === Tone.State.Started){
		this.close();
	}
	return Tone.UserMedia.enumerateDevices().then(function(devices){
		var device;
		if (Tone.isNumber(labelOrId)){
			device = devices[labelOrId];
		} else {
			device = devices.find(function(device){
				return device.label === labelOrId || device.deviceId === labelOrId;
			});
			//didn't find a matching device
			if (!device && devices.length > 0){
				device = devices[0];
			} else if (!device && Tone.isDefined(labelOrId)){
				throw new Error("Tone.UserMedia: no matching device: "+labelOrId);
			}
		}
		this._device = device;
		//do getUserMedia
		var constraints = {
			audio : {
				"echoCancellation" : false,
				"sampleRate" : this.context.sampleRate,
				"noiseSuppression" : false,
				"mozNoiseSuppression" : false,
			}
		};
		if (device){
			constraints.audio.deviceId = device.deviceId;
		}
		return navigator.mediaDevices.getUserMedia(constraints).then(function(stream){
			//start a new source only if the previous one is closed
			if (!this._stream){
				this._stream = stream;
				//Wrap a MediaStreamSourceNode around the live input stream.
				this._mediaStream = this.context.createMediaStreamSource(stream);
				//Connect the MediaStreamSourceNode to a gate gain node
				Tone.connect(this._mediaStream, this.output);
			}
			return this;
		}.bind(this));
	}.bind(this));
};

/**
 *  Close the media stream
 *  @return {Tone.UserMedia} this
 */
Tone.UserMedia.prototype.close = function(){
	if (this._stream){
		this._stream.getAudioTracks().forEach(function(track){
			track.stop();
		});
		this._stream = null;
		//remove the old media stream
		this._mediaStream.disconnect();
		this._mediaStream = null;
	}
	this._device = null;
	return this;
};

/**
 *  Returns a promise which resolves with the list of audio input devices available.
 *  @return {Promise} The promise that is resolved with the devices
 *  @static
 *  @example
 * Tone.UserMedia.enumerateDevices().then(function(devices){
 * 	console.log(devices)
 * })
 */
Tone.UserMedia.enumerateDevices = function(){
	return navigator.mediaDevices.enumerateDevices().then(function(devices){
		return devices.filter(function(device){
			return device.kind === "audioinput";
		});
	});
};

/**
 *  Returns the playback state of the source, "started" when the microphone is open
 *  and "stopped" when the mic is closed.
 *  @type {Tone.State}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name state
 */
Object.defineProperty(Tone.UserMedia.prototype, "state", {
	get : function(){
		return this._stream && this._stream.active ? Tone.State.Started : Tone.State.Stopped;
	}
});

/**
 * 	Returns an identifier for the represented device that is
 * 	persisted across sessions. It is un-guessable by other applications and
 * 	unique to the origin of the calling application. It is reset when the
 * 	user clears cookies (for Private Browsing, a different identifier is
 * 	used that is not persisted across sessions). Returns undefined when the
 * 	device is not open.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name deviceId
 */
Object.defineProperty(Tone.UserMedia.prototype, "deviceId", {
	get : function(){
		if (this._device){
			return this._device.deviceId;
		}
	}
});

/**
 * 	Returns a group identifier. Two devices have the
 * 	same group identifier if they belong to the same physical device.
 * 	Returns undefined when the device is not open.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name groupId
 */
Object.defineProperty(Tone.UserMedia.prototype, "groupId", {
	get : function(){
		if (this._device){
			return this._device.groupId;
		}
	}
});

/**
 * 	Returns a label describing this device (for example "Built-in Microphone").
 * 	Returns undefined when the device is not open or label is not available
 * 	because of permissions.
 *  @type {String}
 *  @readOnly
 *  @memberOf Tone.UserMedia#
 *  @name groupId
 */
Object.defineProperty(Tone.UserMedia.prototype, "label", {
	get : function(){
		if (this._device){
			return this._device.label;
		}
	}
});

/**
 * Mute the output.
 * @memberOf Tone.UserMedia#
 * @type {boolean}
 * @name mute
 * @example
 * //mute the output
 * userMedia.mute = true;
 */
Object.defineProperty(Tone.UserMedia.prototype, "mute", {
	get : function(){
		return this._volume.mute;
	},
	set : function(mute){
		this._volume.mute = mute;
	}
});

/**
 * Clean up.
 * @return {Tone.UserMedia} this
 */
Tone.UserMedia.prototype.dispose = function(){
	Tone.AudioNode.prototype.dispose.call(this);
	this.close();
	this._writable("volume");
	this._volume.dispose();
	this._volume = null;
	this.volume = null;
	return this;
};

/**
 *  If getUserMedia is supported by the browser.
 *  @type  {Boolean}
 *  @memberOf Tone.UserMedia#
 *  @name supported
 *  @static
 *  @readOnly
 */
Object.defineProperty(Tone.UserMedia, "supported", {
	get : function(){
		return Tone.isDefined(navigator.mediaDevices) && Tone.isFunction(navigator.mediaDevices.getUserMedia);
	}
});

export default Tone.UserMedia;

<|endoftext|>


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

/**
 * Draws the curve that is described by a quadratic equation.
 * The curve is labeled with the equation.
 * Performance is optimized to update only what's visible.
 *
 * @author Andrea Lin
 * @author Chris Malley (PixelZoom, Inc.)
 */

import Range from '../../../../dot/js/Range.js';
import Shape from '../../../../kite/js/Shape.js';
import merge from '../../../../phet-core/js/merge.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Path from '../../../../scenery/js/nodes/Path.js';
import RichText from '../../../../scenery/js/nodes/RichText.js';
import graphingQuadratics from '../../graphingQuadratics.js';
import GQConstants from '../GQConstants.js';
import GQBackgroundNode from './GQBackgroundNode.js';
import GQEquationFactory from './GQEquationFactory.js';

class QuadraticNode extends Node {

  /**
   * @param {Property.<Quadratic>} quadraticProperty - the quadratic to be rendered
   * @param {Range} xRange - range of the graph's x axis
   * @param {Range} yRange - range of the graph's y axis
   * @param {ModelViewTransform2} modelViewTransform
   * @param {string} equationForm - form of the equation displayed on the curve, see GQConstants.EQUATION_FORMS
   * @param {BooleanProperty} equationsVisibleProperty
   * @param {Object} [options]
   */
  constructor( quadraticProperty, xRange, yRange, modelViewTransform, equationForm,
               equationsVisibleProperty, options ) {

    assert && assert( _.includes( GQConstants.EQUATION_FORMS, equationForm ),
      'invalid equationForm: ' + equationForm );

    options = merge( {

      // prevent a parabola's vertex and equation from overlapping
      preventVertexAndEquationOverlap: true,

      // Path options
      lineWidth: 1
    }, options );

    super( options );

    // @private
    this.quadraticProperty = quadraticProperty;
    this.xRange = xRange;
    this.yRange = yRange;
    this.modelViewTransform = modelViewTransform;
    this.equationForm = equationForm;
    this.preventVertexAndEquationOverlap = options.preventVertexAndEquationOverlap;

    // @protected quadratic curve, y = ax^2 + bx + c
    this.quadraticPath = new Path( null, {
      lineWidth: options.lineWidth
    } );
    this.addChild( this.quadraticPath );

    // @private the equation's text
    this.equationText = new RichText( '', {
      font: GQConstants.GRAPHED_EQUATION_FONT
    } );

    // @private equation text on a translucent background
    this.equationNode = new GQBackgroundNode( this.equationText, {
      maxWidth: 200 // determined empirically
    } );

    // @private Makes positioning and rotating the equation a little easier to grok.
    // equationParent will be rotated and translated, equationNode will be translated to adjust spacing
    // between the equation and its associated curve.
    this.equationParent = new Node( { children: [ this.equationNode ] } );
    this.addChild( this.equationParent );

    // @private ranges for equation placement, just inside the edges of the graph
    this.xEquationRange =
      new Range( xRange.min + GQConstants.EQUATION_X_MARGIN, xRange.max - GQConstants.EQUATION_X_MARGIN );
    this.yEquationRange =
      new Range( yRange.min + GQConstants.EQUATION_Y_MARGIN, yRange.max - GQConstants.EQUATION_Y_MARGIN );

    // updates the equation, but only when it's visible
    const quadraticListener = quadratic => {
      if ( this.visible ) {
        this.update( quadratic );
      }
    };
    quadraticProperty.link( quadraticListener ); // unlink required in dispose

    // updates the equation when it is made visible
    const equationsVisibleListener = visible => {
      this.equationParent.visible = visible;
      if ( visible ) {
        this.updateEquation( this.quadraticProperty.value );
      }
    };
    equationsVisibleProperty.link( equationsVisibleListener ); // unlink required in dispose

    // @private
    this.disposeQuadraticNode = () => {
      if ( quadraticProperty.hasListener( quadraticListener ) ) {
        quadraticProperty.unlink( quadraticListener );
      }
      if ( equationsVisibleProperty.hasListener( equationsVisibleListener ) ) {
        equationsVisibleProperty.unlink( equationsVisibleListener );
      }
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    super.dispose();
    this.disposeQuadraticNode();
  }

  /**
   * Sets the visibility of this Node.  Update is deferred until this Node becomes visible.
   * @param {boolean} visible
   * @public
   * @override
   */
  setVisible( visible ) {
    super.setVisible( visible );
    if ( visible ) {
      this.update( this.quadraticProperty.value );
    }
  }

  /**
   * Updates this Node to display the specified quadratic.
   * @param {Quadratic} quadratic
   * @private
   */
  update( quadratic ) {

    // update shape
    const bezierControlPoints = quadratic.getControlPoints( this.xRange );
    this.quadraticPath.shape = new Shape()
      .moveToPoint( bezierControlPoints.startPoint )
      .quadraticCurveToPoint( bezierControlPoints.controlPoint, bezierControlPoints.endPoint )
      .transformed( this.modelViewTransform.getMatrix() );

    // update color
    this.quadraticPath.stroke = quadratic.color;

    // update equation
    this.updateEquation( quadratic );
  }

  /**
   * Updates the equation displayed on the quadratic.
   * @param {Quadratic} quadratic
   * @private
   */
  updateEquation( quadratic ) {

    // update the equation text
    if ( this.equationForm === 'standard' ) {
      this.equationText.text = GQEquationFactory.createStandardForm( quadratic );
    }
    else {
      this.equationText.text = GQEquationFactory.createVertexForm( quadratic );
    }

    // update the equation color
    this.equationText.fill = quadratic.color;

    // reset the origin for the equation + background
    this.equationNode.x = 0;
    this.equationNode.y = 0;

    // Position the equation.
    if ( quadratic.a === 0 ) {

      // straight line: equation above left end of line
      const x = this.xEquationRange.min;
      const p = quadratic.getClosestPointInRange( x, this.xEquationRange, this.yEquationRange );
      assert && assert( this.xRange.contains( p.x ) && this.yRange.contains( p.y ), 'p is off the graph: ' + p );

      // rotate to match line's slope
      this.equationParent.rotation = -Math.atan( quadratic.b );

      // move equation to (x,y)
      this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );

      // space between line and equation
      this.equationNode.bottom = -GQConstants.EQUATION_CURVE_SPACING;
    }
    else {

      // parabola: pick a point on the parabola, at the edge of the graph
      const x = ( quadratic.vertex.x >= 0 ) ? this.xEquationRange.min : this.xEquationRange.max;
      const p = quadratic.getClosestPointInRange( x, this.xEquationRange, this.yEquationRange );
      assert && assert( this.xRange.contains( p.x ) && this.yRange.contains( p.y ), 'p is off the graph: ' + p );

      // Width of the equation in model coordinates
      const equationModelWidth = Math.abs( this.modelViewTransform.viewToModelDeltaX( this.equationNode.width ) );

      if ( this.preventVertexAndEquationOverlap &&
           Math.abs( quadratic.a ) >= 0.75 && // a narrow parabola
           p.distance( quadratic.vertex ) <= equationModelWidth ) {

        // When the equation and vertex are liable to overlap, place the equation (not rotated) to the left or right
        // of the parabola. See https://github.com/phetsims/graphing-quadratics/issues/39#issuecomment-426688827
        this.equationParent.rotation = 0;
        this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );
        if ( p.x < quadratic.vertex.x ) {
          this.equationNode.right = -GQConstants.EQUATION_CURVE_SPACING;
        }
        else {
          this.equationNode.left = GQConstants.EQUATION_CURVE_SPACING;
        }
        if ( p.y < 0 ) {
          this.equationNode.bottom = 0;
        }
      }
      else {

        // Place the equation on outside of the parabola, parallel to tangent line, at the edge of the graph.
        // rotate to match tangent's slope
        this.equationParent.rotation = -Math.atan( quadratic.getTangentSlope( p.x ) );

        // move equation to (x,y)
        this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );

        // when equation is on the right side of parabola, move it's origin to the right end of the equation
        if ( p.x > quadratic.vertex.x ) {
          this.equationNode.right = 0;
        }

        // space between tangent line and equation
        if ( quadratic.a >= 0 ) {
          this.equationNode.top = GQConstants.EQUATION_CURVE_SPACING;
        }
        else {
          this.equationNode.bottom = -GQConstants.EQUATION_CURVE_SPACING;
        }
      }
    }
  }
}

graphingQuadratics.register( 'QuadraticNode', QuadraticNode );
export default QuadraticNode;<commit_msg>convert setVisible override to visibleProperty listener, https://github.com/phetsims/scenery/issues/1170<commit_after>// Copyright 2018-2020, University of Colorado Boulder

/**
 * Draws the curve that is described by a quadratic equation.
 * The curve is labeled with the equation.
 * Performance is optimized to update only what's visible.
 *
 * @author Andrea Lin
 * @author Chris Malley (PixelZoom, Inc.)
 */

import Range from '../../../../dot/js/Range.js';
import Shape from '../../../../kite/js/Shape.js';
import merge from '../../../../phet-core/js/merge.js';
import Node from '../../../../scenery/js/nodes/Node.js';
import Path from '../../../../scenery/js/nodes/Path.js';
import RichText from '../../../../scenery/js/nodes/RichText.js';
import graphingQuadratics from '../../graphingQuadratics.js';
import GQConstants from '../GQConstants.js';
import GQBackgroundNode from './GQBackgroundNode.js';
import GQEquationFactory from './GQEquationFactory.js';

class QuadraticNode extends Node {

  /**
   * @param {Property.<Quadratic>} quadraticProperty - the quadratic to be rendered
   * @param {Range} xRange - range of the graph's x axis
   * @param {Range} yRange - range of the graph's y axis
   * @param {ModelViewTransform2} modelViewTransform
   * @param {string} equationForm - form of the equation displayed on the curve, see GQConstants.EQUATION_FORMS
   * @param {BooleanProperty} equationsVisibleProperty
   * @param {Object} [options]
   */
  constructor( quadraticProperty, xRange, yRange, modelViewTransform, equationForm,
               equationsVisibleProperty, options ) {

    assert && assert( _.includes( GQConstants.EQUATION_FORMS, equationForm ),
      'invalid equationForm: ' + equationForm );

    options = merge( {

      // prevent a parabola's vertex and equation from overlapping
      preventVertexAndEquationOverlap: true,

      // Path options
      lineWidth: 1
    }, options );

    super( options );

    // @private
    this.quadraticProperty = quadraticProperty;
    this.xRange = xRange;
    this.yRange = yRange;
    this.modelViewTransform = modelViewTransform;
    this.equationForm = equationForm;
    this.preventVertexAndEquationOverlap = options.preventVertexAndEquationOverlap;

    // @protected quadratic curve, y = ax^2 + bx + c
    this.quadraticPath = new Path( null, {
      lineWidth: options.lineWidth
    } );
    this.addChild( this.quadraticPath );

    // @private the equation's text
    this.equationText = new RichText( '', {
      font: GQConstants.GRAPHED_EQUATION_FONT
    } );

    // @private equation text on a translucent background
    this.equationNode = new GQBackgroundNode( this.equationText, {
      maxWidth: 200 // determined empirically
    } );

    // @private Makes positioning and rotating the equation a little easier to grok.
    // equationParent will be rotated and translated, equationNode will be translated to adjust spacing
    // between the equation and its associated curve.
    this.equationParent = new Node( { children: [ this.equationNode ] } );
    this.addChild( this.equationParent );

    // @private ranges for equation placement, just inside the edges of the graph
    this.xEquationRange =
      new Range( xRange.min + GQConstants.EQUATION_X_MARGIN, xRange.max - GQConstants.EQUATION_X_MARGIN );
    this.yEquationRange =
      new Range( yRange.min + GQConstants.EQUATION_Y_MARGIN, yRange.max - GQConstants.EQUATION_Y_MARGIN );

    // updates the equation, but only when it's visible
    const quadraticListener = quadratic => {
      if ( this.visible ) {
        this.update( quadratic );
      }
    };
    quadraticProperty.link( quadraticListener ); // unlink required in dispose

    // updates the equation when it is made visible
    const equationsVisibleListener = visible => {
      this.equationParent.visible = visible;
      if ( visible ) {
        this.updateEquation( this.quadraticProperty.value );
      }
    };
    equationsVisibleProperty.link( equationsVisibleListener ); // unlink required in dispose

    // Update when this Node becomes visible.
    this.visibleProperty.link( visible => visible && this.update( this.quadraticProperty.value ) );

    // @private
    this.disposeQuadraticNode = () => {
      if ( quadraticProperty.hasListener( quadraticListener ) ) {
        quadraticProperty.unlink( quadraticListener );
      }
      if ( equationsVisibleProperty.hasListener( equationsVisibleListener ) ) {
        equationsVisibleProperty.unlink( equationsVisibleListener );
      }
    };
  }

  /**
   * @public
   * @override
   */
  dispose() {
    super.dispose();
    this.disposeQuadraticNode();
  }

  /**
   * Updates this Node to display the specified quadratic.
   * @param {Quadratic} quadratic
   * @private
   */
  update( quadratic ) {

    // update shape
    const bezierControlPoints = quadratic.getControlPoints( this.xRange );
    this.quadraticPath.shape = new Shape()
      .moveToPoint( bezierControlPoints.startPoint )
      .quadraticCurveToPoint( bezierControlPoints.controlPoint, bezierControlPoints.endPoint )
      .transformed( this.modelViewTransform.getMatrix() );

    // update color
    this.quadraticPath.stroke = quadratic.color;

    // update equation
    this.updateEquation( quadratic );
  }

  /**
   * Updates the equation displayed on the quadratic.
   * @param {Quadratic} quadratic
   * @private
   */
  updateEquation( quadratic ) {

    // update the equation text
    if ( this.equationForm === 'standard' ) {
      this.equationText.text = GQEquationFactory.createStandardForm( quadratic );
    }
    else {
      this.equationText.text = GQEquationFactory.createVertexForm( quadratic );
    }

    // update the equation color
    this.equationText.fill = quadratic.color;

    // reset the origin for the equation + background
    this.equationNode.x = 0;
    this.equationNode.y = 0;

    // Position the equation.
    if ( quadratic.a === 0 ) {

      // straight line: equation above left end of line
      const x = this.xEquationRange.min;
      const p = quadratic.getClosestPointInRange( x, this.xEquationRange, this.yEquationRange );
      assert && assert( this.xRange.contains( p.x ) && this.yRange.contains( p.y ), 'p is off the graph: ' + p );

      // rotate to match line's slope
      this.equationParent.rotation = -Math.atan( quadratic.b );

      // move equation to (x,y)
      this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );

      // space between line and equation
      this.equationNode.bottom = -GQConstants.EQUATION_CURVE_SPACING;
    }
    else {

      // parabola: pick a point on the parabola, at the edge of the graph
      const x = ( quadratic.vertex.x >= 0 ) ? this.xEquationRange.min : this.xEquationRange.max;
      const p = quadratic.getClosestPointInRange( x, this.xEquationRange, this.yEquationRange );
      assert && assert( this.xRange.contains( p.x ) && this.yRange.contains( p.y ), 'p is off the graph: ' + p );

      // Width of the equation in model coordinates
      const equationModelWidth = Math.abs( this.modelViewTransform.viewToModelDeltaX( this.equationNode.width ) );

      if ( this.preventVertexAndEquationOverlap &&
           Math.abs( quadratic.a ) >= 0.75 && // a narrow parabola
           p.distance( quadratic.vertex ) <= equationModelWidth ) {

        // When the equation and vertex are liable to overlap, place the equation (not rotated) to the left or right
        // of the parabola. See https://github.com/phetsims/graphing-quadratics/issues/39#issuecomment-426688827
        this.equationParent.rotation = 0;
        this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );
        if ( p.x < quadratic.vertex.x ) {
          this.equationNode.right = -GQConstants.EQUATION_CURVE_SPACING;
        }
        else {
          this.equationNode.left = GQConstants.EQUATION_CURVE_SPACING;
        }
        if ( p.y < 0 ) {
          this.equationNode.bottom = 0;
        }
      }
      else {

        // Place the equation on outside of the parabola, parallel to tangent line, at the edge of the graph.
        // rotate to match tangent's slope
        this.equationParent.rotation = -Math.atan( quadratic.getTangentSlope( p.x ) );

        // move equation to (x,y)
        this.equationParent.translation = this.modelViewTransform.modelToViewPosition( p );

        // when equation is on the right side of parabola, move it's origin to the right end of the equation
        if ( p.x > quadratic.vertex.x ) {
          this.equationNode.right = 0;
        }

        // space between tangent line and equation
        if ( quadratic.a >= 0 ) {
          this.equationNode.top = GQConstants.EQUATION_CURVE_SPACING;
        }
        else {
          this.equationNode.bottom = -GQConstants.EQUATION_CURVE_SPACING;
        }
      }
    }
  }
}

graphingQuadratics.register( 'QuadraticNode', QuadraticNode );
export default QuadraticNode;<|endoftext|>


<commit_before>/**
 * Developer: Stepan Burguchev
 * Date: 10/13/2014
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 * Published under the MIT license
 */

"use strict";

import { keypress } from '../../libApi';
import dropdown from '../../dropdown/dropdownApi';
import template from './templates/referenceEditor.hbs';
import BaseLayoutEditorView from './base/BaseLayoutEditorView';
import ReferenceButtonView from './impl/reference/views/ReferenceButtonView';
import ReferencePanelView from './impl/reference/views/ReferencePanelView';
import BaseReferenceCollection from './impl/reference/collections/BaseReferenceCollection';
import DefaultReferenceModel from './impl/reference/models/DefaultReferenceModel';

const classes = {
};

const defaultOptions = {
    'controller': null,
    'showAddNewButton': false,
    'buttonView': ReferenceButtonView,
    'panelView': ReferencePanelView
};

/**
 * @name ReferenceEditorView
 * @memberof module:core.form.editors
 * @class Редактор для выбора объекта в формате { id, text } из асинхронно подружаемых с сервера массива вариантов.
 * @extends module:core.form.editors.base.BaseEditorView
 * @param {Object} options Options object. All the properties of {@link module:core.form.editors.base.BaseEditorView BaseEditorView} class are also supported.
 * @param {BaseReferenceEditorController} options.controller Провайдер данных, наследник
 * {@link module:core.form.editors.reference.controllers.BaseReferenceEditorController BaseReferenceEditorController}.
 * */
Backbone.Form.editors.Reference = BaseLayoutEditorView.extend(/** @lends module:core.form.editors.ReferenceEditorView.prototype */{
    initialize: function (options) {
        if (options.schema) {
            _.extend(this.options, defaultOptions, _.pick(options.schema, _.keys(defaultOptions)));
        } else {
            _.extend(this.options, defaultOptions, _.pick(options || {}, _.keys(defaultOptions)));
        }

        this.reqres = new Backbone.Wreqr.RequestResponse();

        this.controller = this.options.controller;

        this.value = this.__adjustValue(this.value);

        this.showAddNewButton = this.options.showAddNewButton;

        this.reqres.setHandler('panel:open', this.onPanelOpenRequest, this);
        this.reqres.setHandler('value:clear', this.onValueClear, this);
        this.reqres.setHandler('value:set', this.onValueSet, this);
        this.reqres.setHandler('value:navigate', this.onValueNavigate, this);
        this.reqres.setHandler('filter:text', this.onFilterText, this);
        this.reqres.setHandler('add:new:item',this.onAddNewItem, this );

        this.viewModel = new Backbone.Model({
            button: new Backbone.Model({
                value: this.getValue(),
                state: 'view',
                enabled: this.getEnabled(),
                readonly: this.getReadonly()

            }),
            panel: new Backbone.Model({
                value: this.getValue(),
                collection: this.controller.collection,
                totalCount: this.controller.totalCount || 0
            })
        });
    },

    focusElement: null,

    attributes: {
        tabindex: 0
    },

    regions: {
        dropdownRegion: '.js-dropdown-region'
    },

    className: 'editor editor_reference',

    template: template,

    setValue: function (value) {
        value = this.__adjustValue(value);
        this.__value(value, false);
    },

    onRender: function () {
        // dropdown
        this.dropdownView = dropdown.factory.createDropdown({
            buttonView: this.options.buttonView,
            buttonViewOptions: {
                model: this.viewModel.get('button'),
                reqres: this.reqres
            },
            panelView: this.options.panelView,
            panelViewOptions: {
                model: this.viewModel.get('panel'),
                reqres: this.reqres,
                showAddNewButton: this.showAddNewButton
            },
            panelPosition: 'down-over',
            autoOpen: false
        });
        this.listenTo(this.dropdownView, 'open', this.onFocus);
        this.listenTo(this.dropdownView, 'close', this.onBlur);
        this.dropdownRegion.show(this.dropdownView);

        // hotkeys
        if (this.keyListener) {
            this.keyListener.reset();
        }
        this.keyListener = new keypress.Listener(this.el);
        _.each('down,enter,num_enter'.split(','), function (key) {
            this.keyListener.simple_combo(key, function () {
                if (this.getEnabled() && !this.getReadonly()) {
                    this.dropdownView.open();
                }
            }.bind(this));
        }, this);
    },

    __adjustValue: function (value) {
        if (!value || !value.id) {
            return null;
        }
        if (value instanceof DefaultReferenceModel) {
            return value;
        }
        if (value instanceof Backbone.Model) {
            value = value.attributes;
        }

        return new DefaultReferenceModel(value);
    },

    __value: function (value, triggerChange) {
        if (this.value === value) {
            return;
        }
        this.value = value;
        this.viewModel.get('button').set('value', value);
        this.viewModel.get('panel').set('value', value);
        if (triggerChange) {
            this.__triggerChange();
        }
    },

    isEmptyValue: function () {
        let value = this.getValue();
        return !value || _.isEmpty(value);
    },

    onValueClear: function () {
        this.__value(null, true);
    },

    onValueSet: function (model) {
        this.__value(model, true);
        this.dropdownView.close();
        this.$el.focus();
    },

    onValueNavigate: function () {
       return this.controller.navigate(this.getValue());
    },

    onFilterText: function (options) {
        var deferred = $.Deferred();
        this.controller.fetch(options).then(function () {
            this.viewModel.get('panel').set('totalCount', this.controller.totalCount);
            deferred.resolve();
        }.bind(this));
        return deferred.promise();
    },

    onPanelOpenRequest: function () {
        if (this.getEnabled() && !this.getReadonly()) {
            this.dropdownView.open();
        }
    },

    onAddNewItem:function(){
        this.controller.addNewItem();
    },

    setReadonly: function (readonly) {
        BaseLayoutEditorView.prototype.__setReadonly.call(this, readonly);
        this.viewModel.get('button').set('readonly', this.getReadonly());
    },

    setEnabled: function (enabled) {
        BaseLayoutEditorView.prototype.__setEnabled.call(this, enabled);
        this.viewModel.get('button').set('enabled', this.getEnabled());
    },

    focus: function () {
        this.dropdownView.open();
    },

    blur: function () {
        this.dropdownView.close();
    }
});

export default Backbone.Form.editors.Reference;
<commit_msg>Added button & list items views options.<commit_after>/**
 * Developer: Stepan Burguchev
 * Date: 10/13/2014
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 * Published under the MIT license
 */

"use strict";

import { keypress } from '../../libApi';
import dropdown from '../../dropdown/dropdownApi';
import template from './templates/referenceEditor.hbs';
import BaseLayoutEditorView from './base/BaseLayoutEditorView';
import ReferenceButtonView from './impl/reference/views/ReferenceButtonView';
import ReferencePanelView from './impl/reference/views/ReferencePanelView';
import BaseReferenceCollection from './impl/reference/collections/BaseReferenceCollection';
import DefaultReferenceModel from './impl/reference/models/DefaultReferenceModel';

const classes = {
};

const defaultOptions = {
    'controller': null,
    'showAddNewButton': false,
    'buttonView': ReferenceButtonView,
    'listItemView': null
};

/**
 * @name ReferenceEditorView
 * @memberof module:core.form.editors
 * @class Редактор для выбора объекта в формате { id, text } из асинхронно подружаемых с сервера массива вариантов.
 * @extends module:core.form.editors.base.BaseEditorView
 * @param {Object} options Options object. All the properties of {@link module:core.form.editors.base.BaseEditorView BaseEditorView} class are also supported.
 * @param {BaseReferenceEditorController} options.controller Провайдер данных, наследник
 * {@link module:core.form.editors.reference.controllers.BaseReferenceEditorController BaseReferenceEditorController}.
 * */
Backbone.Form.editors.Reference = BaseLayoutEditorView.extend(/** @lends module:core.form.editors.ReferenceEditorView.prototype */{
    initialize: function (options) {
        if (options.schema) {
            _.extend(this.options, defaultOptions, _.pick(options.schema, _.keys(defaultOptions)));
        } else {
            _.extend(this.options, defaultOptions, _.pick(options || {}, _.keys(defaultOptions)));
        }

        this.reqres = new Backbone.Wreqr.RequestResponse();

        this.controller = this.options.controller;

        this.value = this.__adjustValue(this.value);

        this.showAddNewButton = this.options.showAddNewButton;

        this.reqres.setHandler('panel:open', this.onPanelOpenRequest, this);
        this.reqres.setHandler('value:clear', this.onValueClear, this);
        this.reqres.setHandler('value:set', this.onValueSet, this);
        this.reqres.setHandler('value:navigate', this.onValueNavigate, this);
        this.reqres.setHandler('filter:text', this.onFilterText, this);
        this.reqres.setHandler('add:new:item',this.onAddNewItem, this );

        this.viewModel = new Backbone.Model({
            button: new Backbone.Model({
                value: this.getValue(),
                state: 'view',
                enabled: this.getEnabled(),
                readonly: this.getReadonly()

            }),
            panel: new Backbone.Model({
                value: this.getValue(),
                collection: this.controller.collection,
                totalCount: this.controller.totalCount || 0
            })
        });
    },

    focusElement: null,

    attributes: {
        tabindex: 0
    },

    regions: {
        dropdownRegion: '.js-dropdown-region'
    },

    className: 'editor editor_reference',

    template: template,

    setValue: function (value) {
        value = this.__adjustValue(value);
        this.__value(value, false);
    },

    onRender: function () {
        // dropdown
        this.dropdownView = dropdown.factory.createDropdown({
            buttonView: this.options.buttonView,
            buttonViewOptions: {
                model: this.viewModel.get('button'),
                reqres: this.reqres
            },
            panelView: ReferencePanelView,
            panelViewOptions: {
                model: this.viewModel.get('panel'),
                reqres: this.reqres,
                showAddNewButton: this.showAddNewButton,
                listItemView: this.options.listItemView
            },
            panelPosition: 'down-over',
            autoOpen: false
        });
        this.listenTo(this.dropdownView, 'open', this.onFocus);
        this.listenTo(this.dropdownView, 'close', this.onBlur);
        this.dropdownRegion.show(this.dropdownView);

        // hotkeys
        if (this.keyListener) {
            this.keyListener.reset();
        }
        this.keyListener = new keypress.Listener(this.el);
        _.each('down,enter,num_enter'.split(','), function (key) {
            this.keyListener.simple_combo(key, function () {
                if (this.getEnabled() && !this.getReadonly()) {
                    this.dropdownView.open();
                }
            }.bind(this));
        }, this);
    },

    __adjustValue: function (value) {
        if (!value || !value.id) {
            return null;
        }
        if (value instanceof DefaultReferenceModel) {
            return value;
        }
        if (value instanceof Backbone.Model) {
            value = value.attributes;
        }

        return new DefaultReferenceModel(value);
    },

    __value: function (value, triggerChange) {
        if (this.value === value) {
            return;
        }
        this.value = value;
        this.viewModel.get('button').set('value', value);
        this.viewModel.get('panel').set('value', value);
        if (triggerChange) {
            this.__triggerChange();
        }
    },

    isEmptyValue: function () {
        let value = this.getValue();
        return !value || _.isEmpty(value);
    },

    onValueClear: function () {
        this.__value(null, true);
    },

    onValueSet: function (model) {
        this.__value(model, true);
        this.dropdownView.close();
        this.$el.focus();
    },

    onValueNavigate: function () {
       return this.controller.navigate(this.getValue());
    },

    onFilterText: function (options) {
        var deferred = $.Deferred();
        this.controller.fetch(options).then(function () {
            this.viewModel.get('panel').set('totalCount', this.controller.totalCount);
            deferred.resolve();
        }.bind(this));
        return deferred.promise();
    },

    onPanelOpenRequest: function () {
        if (this.getEnabled() && !this.getReadonly()) {
            this.dropdownView.open();
        }
    },

    onAddNewItem:function(){
        this.controller.addNewItem();
    },

    setReadonly: function (readonly) {
        BaseLayoutEditorView.prototype.__setReadonly.call(this, readonly);
        this.viewModel.get('button').set('readonly', this.getReadonly());
    },

    setEnabled: function (enabled) {
        BaseLayoutEditorView.prototype.__setEnabled.call(this, enabled);
        this.viewModel.get('button').set('enabled', this.getEnabled());
    },

    focus: function () {
        this.dropdownView.open();
    },

    blur: function () {
        this.dropdownView.close();
    }
});

export default Backbone.Form.editors.Reference;
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react'
import ReactDom from 'react-dom'

import Button from '../components/button'
import FeedbackFilter from '../components/feedback-filter'
import FeedbackOverview from '../components/feedback-overview'
import FeedbackRow from '../components/feedback-row'
import FeedbackButton from '../components/feedback-button'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'

import { connect } from 'react-redux'
import { updateFeedback, enableFeedback} from '../actions'

import '../../css/feedback-panel.less'

class FeedbackPanel extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      showOnlyNeedReview: true,
      showFeedbackPanel: false
    }

    this.makeOnlyNeedReview = this.makeOnlyNeedReview.bind(this)
    this.makeShowAll  = this.makeShowAll.bind(this)
    this.showFeedback = this.showFeedback.bind(this)
    this.hideFeedback = this.hideFeedback.bind(this)
    this.answerIsMarkedComplete = this.answerIsMarkedComplete.bind(this)
    this.enableText  = this.enableText.bind(this)
    this.enableScore = this.enableScore.bind(this)
    this.setMaxScore = this.setMaxScore.bind(this)
    this.scrollStudentIntoView = this.scrollStudentIntoView.bind(this)
    this.studentRowRef = this.studentRowRef.bind(this)
  }

  makeOnlyNeedReview() {
    this.setState({showOnlyNeedReview: true})
  }

  makeShowAll() {
    this.setState({showOnlyNeedReview: false})
  }

  showFeedback() {
    this.setState({
      showFeedbackPanel: true
    })
  }

  hideFeedback() {
    this.setState({
      showFeedbackPanel: false
    })
  }

  answerIsMarkedComplete(answer) {
    const feedbackId = answer.get('feedbacks') && answer.get('feedbacks').last()
    const feedback = this.props.feedbacks.get(feedbackId)
    return feedback && feedback.get("hasBeenReviewed")
  }

  enableText(event) {
    this.props.enableFeedback(this.props.question.get('key'), {feedbackEnabled: event.target.checked})
  }

  enableScore(event) {
    this.props.enableFeedback(this.props.question.get('key'), {scoreEnabled: event.target.checked})
  }

  setMaxScore(event) {
    const value = parseInt(event.target.value) || null
    this.props.enableFeedback(this.props.question.get('key'), {maxScore: value})
  }

  studentRowRef(index){
    return `student-row-${index}`
  }

  scrollStudentIntoView(eventProxy) {
    const index = eventProxy.target.value
    const ref = this.studentRowRef(index-1)
    const itemComponent = this.refs[ref]
    if (itemComponent) {
      const domNode = ReactDom.findDOMNode(itemComponent)
      domNode.scrollIntoView()
    }
  }

  renderGettingStarted() {
    return(

          To start, choose the type of feedback you want to leave in the Feedback Type settings above.

⤴

    )
  }

  render() {
    const question      = this.props.question
    const showing       = this.state.showFeedbackPanel
    const prompt        = question.get('prompt')
    const number        = question.get('questionNumber')
    const answers       = this.props.answers

    const realAnswers     = answers.filter(a => a.get('type') != 'NoAnswer')
    const needingFeedback = realAnswers.filter( a => ! this.answerIsMarkedComplete(a) )

    const filteredAnswers = this.state.showOnlyNeedReview ? needingFeedback : answers
    const numAnswers       = realAnswers.count()
    const numNoAnswers     = answers.count() - realAnswers.count()
    let   numNeedsFeedback = needingFeedback.count()
    let   numFeedbackGiven = numAnswers - numNeedsFeedback

    const scoreEnabled = question.get('scoreEnabled') || false
    const feedbackEnabled = question.get('feedbackEnabled') || false
    const maxScore = question.get('maxScore') || 0

    const showGettingStarted = (!scoreEnabled) && (!feedbackEnabled)
    const studentsPulldown = filteredAnswers.map( (a) => {
      return {
        realName: a.getIn(['student', 'realName']),
        id: a.getIn(['student','id']),
        answer: a,
      }
    })

    if((!scoreEnabled) && (!feedbackEnabled)) {
      numNeedsFeedback = 0
      numFeedbackGiven = 0
    }

    if (!showing) { return (

        <FeedbackButton
          feedbackEnabled={feedbackEnabled || scoreEnabled}
          needsReviewCount={numNeedsFeedback}
          disabled={numAnswers < 1}
          showFeedback={this.showFeedback}/>

    )}
    return (

Feedback: Question {number}

              <FeedbackOverview
                numNoAnswers={numNoAnswers}
                numFeedbackGiven={numFeedbackGiven}
                numNeedsFeedback={numNeedsFeedback}
              />

Feedback Type

              <input id="feedbackEnabled" type="checkbox" checked={feedbackEnabled} onChange={this.enableText}/>
               Give Written Feedback

              <input id="scoreEnabled" checked={scoreEnabled} type="checkbox" onChange={this.enableScore}/>
              Give Score

              { scoreEnabled ?

                  Max. Score
                  <input className="max-score-input" value={maxScore} onChange={this.setMaxScore}/>

 : ""
              }

            <FeedbackFilter
              showOnlyNeedReview={this.state.showOnlyNeedReview}
              studentSelected={this.scrollStudentIntoView}
              makeOnlyNeedReview={this.makeOnlyNeedReview}
              students={studentsPulldown}
              makeShowAll={this.makeShowAll}
              disable={showGettingStarted}
            />

              { showGettingStarted ?  this.renderGettingStarted() : ""}

                <ReactCSSTransitionGroup transitionName="answer" transitionEnterTimeout={400} transitionLeaveTimeout={300}>
                { filteredAnswers.map((answer, i) =>
                    <FeedbackRow
                      answer={answer}
                      ref={this.studentRowRef(i)}
                      key={answer.get('key')}
                      scoreEnabled={scoreEnabled}
                      feedbackEnabled={feedbackEnabled}
                      maxScore={maxScore}
                      feedbacks={this.props.feedbacks}
                      updateFeedback={this.props.updateFeedback}
                      showOnlyNeedsRiew={this.state.showOnlyNeedReview}
                    />
                )}
                </ReactCSSTransitionGroup>

            <Button onClick = {this.hideFeedback}>Done</Button>

    )
  }
}

function mapStateToProps(state) {
  return { feedbacks: state.get('feedbacks')}
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    updateFeedback: (answerKey, feedback) => dispatch(updateFeedback(answerKey, feedback)),
    enableFeedback: (embeddableKey, feedbackFlags)  => dispatch(enableFeedback(embeddableKey, feedbackFlags)),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(FeedbackPanel)
<commit_msg>Fix spacing after Feedback Type checkbox.<commit_after>import React, { PureComponent } from 'react'
import ReactDom from 'react-dom'

import Button from '../components/button'
import FeedbackFilter from '../components/feedback-filter'
import FeedbackOverview from '../components/feedback-overview'
import FeedbackRow from '../components/feedback-row'
import FeedbackButton from '../components/feedback-button'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'

import { connect } from 'react-redux'
import { updateFeedback, enableFeedback} from '../actions'

import '../../css/feedback-panel.less'

class FeedbackPanel extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      showOnlyNeedReview: true,
      showFeedbackPanel: false
    }

    this.makeOnlyNeedReview = this.makeOnlyNeedReview.bind(this)
    this.makeShowAll  = this.makeShowAll.bind(this)
    this.showFeedback = this.showFeedback.bind(this)
    this.hideFeedback = this.hideFeedback.bind(this)
    this.answerIsMarkedComplete = this.answerIsMarkedComplete.bind(this)
    this.enableText  = this.enableText.bind(this)
    this.enableScore = this.enableScore.bind(this)
    this.setMaxScore = this.setMaxScore.bind(this)
    this.scrollStudentIntoView = this.scrollStudentIntoView.bind(this)
    this.studentRowRef = this.studentRowRef.bind(this)
  }

  makeOnlyNeedReview() {
    this.setState({showOnlyNeedReview: true})
  }

  makeShowAll() {
    this.setState({showOnlyNeedReview: false})
  }

  showFeedback() {
    this.setState({
      showFeedbackPanel: true
    })
  }

  hideFeedback() {
    this.setState({
      showFeedbackPanel: false
    })
  }

  answerIsMarkedComplete(answer) {
    const feedbackId = answer.get('feedbacks') && answer.get('feedbacks').last()
    const feedback = this.props.feedbacks.get(feedbackId)
    return feedback && feedback.get("hasBeenReviewed")
  }

  enableText(event) {
    this.props.enableFeedback(this.props.question.get('key'), {feedbackEnabled: event.target.checked})
  }

  enableScore(event) {
    this.props.enableFeedback(this.props.question.get('key'), {scoreEnabled: event.target.checked})
  }

  setMaxScore(event) {
    const value = parseInt(event.target.value) || null
    this.props.enableFeedback(this.props.question.get('key'), {maxScore: value})
  }

  studentRowRef(index){
    return `student-row-${index}`
  }

  scrollStudentIntoView(eventProxy) {
    const index = eventProxy.target.value
    const ref = this.studentRowRef(index-1)
    const itemComponent = this.refs[ref]
    if (itemComponent) {
      const domNode = ReactDom.findDOMNode(itemComponent)
      domNode.scrollIntoView()
    }
  }

  renderGettingStarted() {
    return(

          To start, choose the type of feedback you want to leave in the Feedback Type settings above.

⤴

    )
  }

  render() {
    const question      = this.props.question
    const showing       = this.state.showFeedbackPanel
    const prompt        = question.get('prompt')
    const number        = question.get('questionNumber')
    const answers       = this.props.answers

    const realAnswers     = answers.filter(a => a.get('type') != 'NoAnswer')
    const needingFeedback = realAnswers.filter( a => ! this.answerIsMarkedComplete(a) )

    const filteredAnswers = this.state.showOnlyNeedReview ? needingFeedback : answers
    const numAnswers       = realAnswers.count()
    const numNoAnswers     = answers.count() - realAnswers.count()
    let   numNeedsFeedback = needingFeedback.count()
    let   numFeedbackGiven = numAnswers - numNeedsFeedback

    const scoreEnabled = question.get('scoreEnabled') || false
    const feedbackEnabled = question.get('feedbackEnabled') || false
    const maxScore = question.get('maxScore') || 0

    const showGettingStarted = (!scoreEnabled) && (!feedbackEnabled)
    const studentsPulldown = filteredAnswers.map( (a) => {
      return {
        realName: a.getIn(['student', 'realName']),
        id: a.getIn(['student','id']),
        answer: a,
      }
    })

    if((!scoreEnabled) && (!feedbackEnabled)) {
      numNeedsFeedback = 0
      numFeedbackGiven = 0
    }

    if (!showing) { return (

        <FeedbackButton
          feedbackEnabled={feedbackEnabled || scoreEnabled}
          needsReviewCount={numNeedsFeedback}
          disabled={numAnswers < 1}
          showFeedback={this.showFeedback}/>

    )}
    return (

Feedback: Question {number}

              <FeedbackOverview
                numNoAnswers={numNoAnswers}
                numFeedbackGiven={numFeedbackGiven}
                numNeedsFeedback={numNeedsFeedback}
              />

Feedback Type

              <input id="feedbackEnabled" type="checkbox" checked={feedbackEnabled} onChange={this.enableText}/>
               Give Written Feedback

              <input id="scoreEnabled" checked={scoreEnabled} type="checkbox" onChange={this.enableScore}/>
              Give Score

              { scoreEnabled ?

                  Max. Score
                  <input className="max-score-input" value={maxScore} onChange={this.setMaxScore}/>

 : ""
              }

            <FeedbackFilter
              showOnlyNeedReview={this.state.showOnlyNeedReview}
              studentSelected={this.scrollStudentIntoView}
              makeOnlyNeedReview={this.makeOnlyNeedReview}
              students={studentsPulldown}
              makeShowAll={this.makeShowAll}
              disable={showGettingStarted}
            />

              { showGettingStarted ?  this.renderGettingStarted() : ""}

                <ReactCSSTransitionGroup transitionName="answer" transitionEnterTimeout={400} transitionLeaveTimeout={300}>
                { filteredAnswers.map((answer, i) =>
                    <FeedbackRow
                      answer={answer}
                      ref={this.studentRowRef(i)}
                      key={answer.get('key')}
                      scoreEnabled={scoreEnabled}
                      feedbackEnabled={feedbackEnabled}
                      maxScore={maxScore}
                      feedbacks={this.props.feedbacks}
                      updateFeedback={this.props.updateFeedback}
                      showOnlyNeedsRiew={this.state.showOnlyNeedReview}
                    />
                )}
                </ReactCSSTransitionGroup>

            <Button onClick = {this.hideFeedback}>Done</Button>

    )
  }
}

function mapStateToProps(state) {
  return { feedbacks: state.get('feedbacks')}
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    updateFeedback: (answerKey, feedback) => dispatch(updateFeedback(answerKey, feedback)),
    enableFeedback: (embeddableKey, feedbackFlags)  => dispatch(enableFeedback(embeddableKey, feedbackFlags)),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(FeedbackPanel)
<|endoftext|>