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

/**
 * An ScrollingChartNode that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */

import Utils from '../../dot/js/Utils.js';
import merge from '../../phet-core/js/merge.js';
import ArrowNode from '../../scenery-phet/js/ArrowNode.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import ScrollingChartNode from './ScrollingChartNode.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;
const ARROW_CUE_FILL_COLOR = new Color( 180, 180, 230 );
const ARROW_CUE_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorPlot extends ScrollingChartNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {
    options = merge( {

      // options passed on to the chart cursor, see ChartCursor
      cursorOptions: null,

      graphPanelOptions: {

        // so that the cursor is draggable
        pickable: true
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @private - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedXValue = 0;
    this.maxRecordedXValue = 0;

    // @private - Keep track of listeners for each series so the listeners can be removed when the series is removed
    this.dynamicSeriesListenerMap = new Map();

    // @protected (read-only)
    this.dynamicSeriesList = [];

    // @private {ChartCursor} - draggable Node that shows the cursor value
    this.chartCursor = new ChartCursor( this, this.modelViewTransformProperty, options.cursorOptions );
    this.graphPanel.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateChartCursor();
  }

  /**
   * Set the cursor value. The value of the cursor is constrained to be within plot bounds.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    const modelViewTransform = this.modelViewTransformProperty.get();
    const minX = modelViewTransform.viewToModelX( 0 );
    const maxX = modelViewTransform.viewToModelX( this.plotWidth + this.chartCursor.width / 2 );
    this.cursorValue = Utils.clamp( value, minX, maxX );
    this.updateChartCursor();
  }

  /**
   * Add a Dynamic series to the XYCursorPlot.
   * @override
   * @public
   * @param dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    super.addDynamicSeries( dynamicSeries );

    this.dynamicSeriesList.push( dynamicSeries );

    // when a point is added, update the min and max recorded values
    const seriesListener = () => {

      // update the
      this.updateMinMaxRecordedValues();

      // if all data has been removed from the plot, update cursor visibility
      this.updateChartCursor();
    };

    // save to map so that listener can be found again for disposal
    this.dynamicSeriesListenerMap.set( dynamicSeries, seriesListener );
    dynamicSeries.addDynamicSeriesListener( seriesListener );
  }

  /**
   * Remove a series from the plot and dispose of the plot specific series listener.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries - to remove
   */
  removeDynamicSeries( dynamicSeries ) {
    super.removeDynamicSeries( dynamicSeries );
    const seriesIndex = this.dynamicSeriesList.indexOf( dynamicSeries );
    this.dynamicSeriesList.splice( seriesIndex, 1 );

    dynamicSeries.removeDynamicSeriesListener( this.dynamicSeriesListenerMap.get( dynamicSeries ) );
    this.dynamicSeriesListenerMap.delete( dynamicSeries );
  }

  /**
   * Reset the ChartCursor on this XYCursorPlot.
   * @public
   */
  resetCursor() {
    this.chartCursor.reset();
  }

  /**
   * Get the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   * @public
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateChartCursorVisibility();
  }

  /**
   * Update the chart cursor visibility and position.
   * @private
   */
  updateChartCursor() {
    this.updateChartCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateChartCursorPos();
    }
  }

  /**
   * Update the chart cursor position.
   * @private
   */
  updateChartCursorPos() {
    this.moveChartCursorToValue( this.cursorValue );
  }

  /**
   * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
   * the recorded value range.
   * @private
   */
  updateChartCursorVisibility() {

    const wasVisible = this.chartCursor.visible;
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {

      const maxX = this.modelViewTransformProperty.get().viewToModelX( this.plotWidth + this.chartCursor.width / 2 );
      const minX = this.modelViewTransformProperty.get().viewToModelX( 0 );

      const isCurrentValueOnChart = ( this.cursorValue >= minX ) && ( this.cursorValue <= maxX );
      const dataExists = this.getDataExists();
      const chartCursorVisible = isCurrentValueOnChart && dataExists;

      this.chartCursor.setVisible( chartCursorVisible );
    }

    // if the chart cursor just became invisible, interrupt any active dragging
    if ( !this.chartCursor.visible && wasVisible ) {
      this.chartCursor.interruptDrag();
    }
  }

  /**
   * Returns true if any data is attached to this plot.
   * @returns {boolean}
   * @public
   */
  getDataExists() {
    let dataExists = false;
    this.dynamicSeriesListenerMap.forEach( ( listener, dataSeries, map ) => {
      dataExists = dataSeries.hasData();

      // break early
      if ( dataExists ) {
        return true;
      }
    } );

    return dataExists;
  }

  /**
   * Move the chart cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveChartCursorToValue( value ) {
    const viewPosition = this.modelViewTransformProperty.get().modelToViewX( value );

    // keep the cursor within the grid bounds
    this.chartCursor.centerX = Utils.clamp( viewPosition, 0, this.plotWidth );
    this.chartCursor.centerY = this.gridNode.centerY;
  }

  /**
   * From the existing data, update the min and max recorded X values from all of the dynamicSeries of this plot,
   * so that the cursor can be limited to the recorded data.
   * @private
   */
  updateMinMaxRecordedValues() {
    let minValue = Number.POSITIVE_INFINITY;
    let maxValue = Number.NEGATIVE_INFINITY;
    this.dynamicSeriesList.forEach( dynamicSeries => {
      if ( dynamicSeries.getLength() > 0 ) {
        const seriesMinValue = dynamicSeries.getDataPoint( 0 ).x;
        const seriesMaxValue = dynamicSeries.getDataPoint( dynamicSeries.getLength() - 1 ).x;
        if ( seriesMinValue < minValue ) {
          minValue = seriesMinValue;
        }
        if ( seriesMaxValue > maxValue ) {
          maxValue = seriesMaxValue;
        }
      }
    } );

    this.minRecordedXValue = minValue;
    this.maxRecordedXValue = maxValue;
  }
}

/**
 * Rectangular cursor that indicates a current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {ScrollingChartNode} plot
   * @param {Property.<ModelViewTransform2>} modelViewTransformProperty
   * @param {Object} [options]
   */
  constructor( plot, modelViewTransformProperty, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // {boolean} - if true, a double headed arrow will be shown to indicate
      // that the cursor is draggable - becomes invisible after first
      // drag
      includeDragCue: false,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = plot.plotWidth * WIDTH_PROPORTION;
    const height = plot.plotHeight;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'ew-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    this.plot = plot;
    this.modelViewTransformProperty = modelViewTransformProperty;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    this.includeDragCue = options.includeDragCue;

    if ( this.includeDragCue ) {

      // @private - indicates to the user that the cursor is draggable, only created
      // and added if necessary
      this.dragCueArrowNode = new ArrowNode( -width * 2, 0, width * 2, 0, {
        doubleHead: true,
        headWidth: 12,
        headHeight: 10,
        fill: ARROW_CUE_FILL_COLOR,
        stroke: ARROW_CUE_STROKE_COLOR,
        center: this.center.plusXY( 0, height * 0.4 )
      } );

      this.addChild( this.dragCueArrowNode );
    }

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    // @private - so that we can interrupt the DragListener if necessary
    this.dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.modelViewTransformProperty.get().viewToModelX( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, this.plot.minRecordedXValue, this.plot.maxRecordedXValue );
        this.plot.setCursorValue( newValue );

        options.drag();
      },
      end: ( event, listener ) => {
        options.endDrag();

        // no need to show arrow after user successfully drags the cursor
        if ( this.includeDragCue ) {
          this.dragCueArrowNode.visible = false;
        }
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( this.dragListener );
  }

  /**
   * Interrupt dragging of the cursor, useful when ChartCursor visibility changes.
   * @public
   */
  interruptDrag() {
    this.dragListener.interrupt();
  }

  /**
   * Reset the ChartCursor to its initial state. Note that this does not modify data
   * or the cursor position (cursorValue), only aspects of the view for the cursor itself.
   * @public
   */
  reset() {
    if ( this.includeDragCue ) {
      this.dragCueArrowNode.visible = true;
    }
  }
}

/**
 * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
 * grab.  This is meant to look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorPlot', XYCursorPlot );
export default XYCursorPlot;<commit_msg>doc and type expression for dynamicSeriesListenerMap, https://github.com/phetsims/griddle/issues/70<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * An ScrollingChartNode that includes a draggable cursor that allows the user to scrub or play back through the data.
 *
 * @author Jesse Greenberg
 */

import Utils from '../../dot/js/Utils.js';
import merge from '../../phet-core/js/merge.js';
import ArrowNode from '../../scenery-phet/js/ArrowNode.js';
import DragListener from '../../scenery/js/listeners/DragListener.js';
import Circle from '../../scenery/js/nodes/Circle.js';
import Node from '../../scenery/js/nodes/Node.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import griddle from './griddle.js';
import ScrollingChartNode from './ScrollingChartNode.js';

// constants
const WIDTH_PROPORTION = 0.013; // empirically determined
const CURSOR_FILL_COLOR = new Color( 50, 50, 200, 0.2 );
const CURSOR_STROKE_COLOR = Color.DARK_GRAY;
const ARROW_CUE_FILL_COLOR = new Color( 180, 180, 230 );
const ARROW_CUE_STROKE_COLOR = Color.DARK_GRAY;

class XYCursorPlot extends ScrollingChartNode {

  /**
   * @param {Object} [options]
   */
  constructor( options ) {
    options = merge( {

      // options passed on to the chart cursor, see ChartCursor
      cursorOptions: null,

      graphPanelOptions: {

        // so that the cursor is draggable
        pickable: true
      },

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    super( options );

    // @private {boolean|null} - if set with setCursorVisible, then this will indicate visibility of the cursor
    this._cursorVisibleOverride = null;

    // @private {number} - value for the cursor, determines cursor positioning
    this.cursorValue = 0;

    // @private - minimum and maximum recorded value, required by the cursor to limit dragging
    this.minRecordedXValue = 0;
    this.maxRecordedXValue = 0;

    // @private Map.<DynamicSeries,function> Keep track of the listener for each series, so the listener can be removed
    // when a series is removed.
    this.dynamicSeriesListenerMap = new Map();

    // @protected (read-only)
    this.dynamicSeriesList = [];

    // @private {ChartCursor} - draggable Node that shows the cursor value
    this.chartCursor = new ChartCursor( this, this.modelViewTransformProperty, options.cursorOptions );
    this.graphPanel.addChild( this.chartCursor );

    // initialize position and visibility of the cursor
    this.updateChartCursor();
  }

  /**
   * Add a Dynamic series to the XYCursorPlot.
   * @override
   * @public
   * @param dynamicSeries
   */
  addDynamicSeries( dynamicSeries ) {
    super.addDynamicSeries( dynamicSeries );

    this.dynamicSeriesList.push( dynamicSeries );

    // when a point is added, update the min and max recorded values
    const seriesListener = () => {

      // update the
      this.updateMinMaxRecordedValues();

      // if all data has been removed from the plot, update cursor visibility
      this.updateChartCursor();
    };

    // save to map so that listener can be found again for disposal
    this.dynamicSeriesListenerMap.set( dynamicSeries, seriesListener );
    dynamicSeries.addDynamicSeriesListener( seriesListener );
  }

  /**
   * Remove a series from the plot and dispose of the plot specific series listener.
   * @public
   *
   * @param {DynamicSeries} dynamicSeries - to remove
   */
  removeDynamicSeries( dynamicSeries ) {
    super.removeDynamicSeries( dynamicSeries );
    const seriesIndex = this.dynamicSeriesList.indexOf( dynamicSeries );
    this.dynamicSeriesList.splice( seriesIndex, 1 );

    dynamicSeries.removeDynamicSeriesListener( this.dynamicSeriesListenerMap.get( dynamicSeries ) );
    this.dynamicSeriesListenerMap.delete( dynamicSeries );
  }

  /**
   * Set the cursor value. The value of the cursor is constrained to be within plot bounds.
   * @public
   *
   * @param {number} value
   */
  setCursorValue( value ) {
    const modelViewTransform = this.modelViewTransformProperty.get();
    const minX = modelViewTransform.viewToModelX( 0 );
    const maxX = modelViewTransform.viewToModelX( this.plotWidth + this.chartCursor.width / 2 );
    this.cursorValue = Utils.clamp( value, minX, maxX );
    this.updateChartCursor();
  }

  /**
   * Get the value currently under the cursor.
   * @public
   *
   * @returns {number}
   */
  getCursorValue() {
    return this.cursorValue;
  }

  /**
   * Reset the ChartCursor on this XYCursorPlot.
   * @public
   */
  resetCursor() {
    this.chartCursor.reset();
  }

  /**
   * Override the default behavior for setting cursor visibility. If set to null, cursor visibility will behave as
   * described in updateChartCursorVisibility. Otherwise, visibility will equal the boolean value set here.
   * @param {boolean|null} visible
   * @public
   */
  setCursorVisibleOverride( visible ) {
    assert && assert( typeof visible === 'boolean' || visible === null, 'visible must be boolean or null' );
    this._cursorVisibleOverride = visible;
    this.updateChartCursorVisibility();
  }

  /**
   * Update the chart cursor visibility and position.
   * @private
   */
  updateChartCursor() {
    this.updateChartCursorVisibility();
    if ( this.chartCursor.isVisible() ) {
      this.updateChartCursorPos();
    }
  }

  /**
   * Update the chart cursor position.
   * @private
   */
  updateChartCursorPos() {
    this.moveChartCursorToValue( this.cursorValue );
  }

  /**
   * Update the chart cursor visibility. The chart cursor should be visible any time the cursor value is within
   * the recorded value range.
   * @private
   */
  updateChartCursorVisibility() {

    const wasVisible = this.chartCursor.visible;
    if ( typeof this._cursorVisibleOverride === 'boolean' ) {
      this.chartCursor.setVisible( this._cursorVisibleOverride );
    }
    else {

      const maxX = this.modelViewTransformProperty.get().viewToModelX( this.plotWidth + this.chartCursor.width / 2 );
      const minX = this.modelViewTransformProperty.get().viewToModelX( 0 );

      const isCurrentValueOnChart = ( this.cursorValue >= minX ) && ( this.cursorValue <= maxX );
      const dataExists = this.getDataExists();
      const chartCursorVisible = isCurrentValueOnChart && dataExists;

      this.chartCursor.setVisible( chartCursorVisible );
    }

    // if the chart cursor just became invisible, interrupt any active dragging
    if ( !this.chartCursor.visible && wasVisible ) {
      this.chartCursor.interruptDrag();
    }
  }

  /**
   * Returns true if any data is attached to this plot.
   * @returns {boolean}
   * @public
   */
  getDataExists() {
    let dataExists = false;
    this.dynamicSeriesListenerMap.forEach( ( listener, dataSeries, map ) => {
      dataExists = dataSeries.hasData();

      // break early
      if ( dataExists ) {
        return true;
      }
    } );

    return dataExists;
  }

  /**
   * Move the chart cursor to the specified value.
   * @private
   *
   * @param {number} value
   */
  moveChartCursorToValue( value ) {
    const viewPosition = this.modelViewTransformProperty.get().modelToViewX( value );

    // keep the cursor within the grid bounds
    this.chartCursor.centerX = Utils.clamp( viewPosition, 0, this.plotWidth );
    this.chartCursor.centerY = this.gridNode.centerY;
  }

  /**
   * From the existing data, update the min and max recorded X values from all of the dynamicSeries of this plot,
   * so that the cursor can be limited to the recorded data.
   * @private
   */
  updateMinMaxRecordedValues() {
    let minValue = Number.POSITIVE_INFINITY;
    let maxValue = Number.NEGATIVE_INFINITY;
    this.dynamicSeriesList.forEach( dynamicSeries => {
      if ( dynamicSeries.getLength() > 0 ) {
        const seriesMinValue = dynamicSeries.getDataPoint( 0 ).x;
        const seriesMaxValue = dynamicSeries.getDataPoint( dynamicSeries.getLength() - 1 ).x;
        if ( seriesMinValue < minValue ) {
          minValue = seriesMinValue;
        }
        if ( seriesMaxValue > maxValue ) {
          maxValue = seriesMaxValue;
        }
      }
    } );

    this.minRecordedXValue = minValue;
    this.maxRecordedXValue = maxValue;
  }
}

/**
 * Rectangular cursor that indicates a current or selected value on the chart.
 */
class ChartCursor extends Rectangle {

  /**
   * @param {ScrollingChartNode} plot
   * @param {Property.<ModelViewTransform2>} modelViewTransformProperty
   * @param {Object} [options]
   */
  constructor( plot, modelViewTransformProperty, options ) {

    options = merge( {
      startDrag: () => {},
      endDrag: () => {},
      drag: () => {},

      // {boolean} - if true, a double headed arrow will be shown to indicate
      // that the cursor is draggable - becomes invisible after first
      // drag
      includeDragCue: false,

      // phet-io
      tandem: Tandem.OPTIONAL
    }, options );

    const width = plot.plotWidth * WIDTH_PROPORTION;
    const height = plot.plotHeight;

    // Set the shape. Origin is at the center top of the rectangle.
    super( 0, -height, width, height, 0, 0, {
      cursor: 'ew-resize',
      fill: CURSOR_FILL_COLOR,
      stroke: CURSOR_STROKE_COLOR,
      lineWidth: 0.4,
      lineDash: [ 4, 4 ]
    } );

    this.plot = plot;
    this.modelViewTransformProperty = modelViewTransformProperty;

    // Make it easier to grab this cursor by giving it expanded mouse and touch areas.
    this.mouseArea = this.localBounds.dilatedX( 12 );
    this.touchArea = this.localBounds.dilatedX( 12 );

    this.includeDragCue = options.includeDragCue;

    if ( this.includeDragCue ) {

      // @private - indicates to the user that the cursor is draggable, only created
      // and added if necessary
      this.dragCueArrowNode = new ArrowNode( -width * 2, 0, width * 2, 0, {
        doubleHead: true,
        headWidth: 12,
        headHeight: 10,
        fill: ARROW_CUE_FILL_COLOR,
        stroke: ARROW_CUE_STROKE_COLOR,
        center: this.center.plusXY( 0, height * 0.4 )
      } );

      this.addChild( this.dragCueArrowNode );
    }

    // Add the indentations that are intended to convey the idea of "gripability".
    const grippyNode = new Node();
    const indentSpacing = height * 0.05;
    for ( let i = 0; i < 3; i++ ) {
      const indentNode = new GrippyIndentNode( width / 2, CURSOR_FILL_COLOR );
      indentNode.top = i * ( indentNode.height + indentSpacing );
      grippyNode.addChild( indentNode );
    }
    grippyNode.center = this.center;
    this.addChild( grippyNode );

    // @private - so that we can interrupt the DragListener if necessary
    this.dragListener = new DragListener( {
      start: ( event, listener ) => {
        assert && assert( this.plot.getDataExists(), 'data should exist for the cursor to be draggable' );
        options.startDrag();
      },
      drag: ( event, listener ) => {
        const parentX = listener.parentPoint.x;
        let newValue = this.modelViewTransformProperty.get().viewToModelX( parentX );

        // limit cursor to the domain of recorded values
        newValue = Utils.clamp( newValue, this.plot.minRecordedXValue, this.plot.maxRecordedXValue );
        this.plot.setCursorValue( newValue );

        options.drag();
      },
      end: ( event, listener ) => {
        options.endDrag();

        // no need to show arrow after user successfully drags the cursor
        if ( this.includeDragCue ) {
          this.dragCueArrowNode.visible = false;
        }
      },
      tandem: options.tandem.createTandem( 'dragListener' )
    } );
    this.addInputListener( this.dragListener );
  }

  /**
   * Interrupt dragging of the cursor, useful when ChartCursor visibility changes.
   * @public
   */
  interruptDrag() {
    this.dragListener.interrupt();
  }

  /**
   * Reset the ChartCursor to its initial state. Note that this does not modify data
   * or the cursor position (cursorValue), only aspects of the view for the cursor itself.
   * @public
   */
  reset() {
    if ( this.includeDragCue ) {
      this.dragCueArrowNode.visible = true;
    }
  }
}

/**
 * This node is meant to portray a small round indentation on a surface.  This is a modern user interface paradigm that
 * is intended to convey the concept of "gripability" (sp?), i.e. something that the user can click on and subsequently
 * grab.  This is meant to look somewhat 3D, much like etched borders do.
 */
class GrippyIndentNode extends Circle {

  /**
   * @param {number} diameter
   * @param {Color} baseColor
   * @param {Object} [options]
   */
  constructor( diameter, baseColor, options ) {

    options = merge( {
      lineWidth: 0.5
    }, options );

    const baseDarkerColor = baseColor.darkerColor( 0.9 );
    const translucentDarkerColor = new Color( baseDarkerColor.getRed(), baseDarkerColor.getGreen(),
      baseDarkerColor.getBlue(), baseColor.getAlpha() );
    const baseLighterColor = baseColor.brighterColor( 0.9 );
    const translucentBrighterColor = new Color( baseLighterColor.getRed(), baseLighterColor.getGreen(),
      baseLighterColor.getBlue(), baseColor.getAlpha() );
    const radius = diameter / 2 - options.lineWidth;

    super( radius, {
      fill: translucentDarkerColor,
      stroke: translucentBrighterColor,
      lineWidth: options.lineWidth
    } );
  }
}

griddle.register( 'XYCursorPlot', XYCursorPlot );
export default XYCursorPlot;<|endoftext|>


<commit_before>require('./ai');

describe( 'ai tests', () => {
   it( 'Qué hora es', () => {
     var textResult = textRequest('Qué hora es');
     assert.typeOf( textResult, 'object' );
   });
});
<commit_msg>testing<commit_after>require('./ai');

describe( 'ai tests', () => {
  it( 'm', () => {
    var textResult = textRequest('m');
    assert.typeOf( textResult, 'null' );
  });
  it( 'Qué hora es', () => {
    var textResult = textRequest('Qué hora es');
    assert.typeOf( textResult, 'object' );
    assert.equal( textResult.intention, 'know-date-time' );
    assert.typeOf( textResult.data, 'object' );
  });
  it( 'A qué día estamos', () => {
    var textResult = textRequest('Qué hora es');
    assert.typeOf( textResult, 'object' );
    assert.equal( textResult.intention, 'know-date-time' );
    assert.typeOf( textResult.data, 'object' );
  });
  it( 'Buscar fotos de coches en Google', () => {
    var textResult = textRequest('Buscar fotos de coches en Google', true);
    assert.typeOf( textResult, 'object' );
    assert.equal( textResult.intention, 'internet-search' );
    assert.typeOf( textResult.data, 'object' );
    assert.equal( textResult.data['internet-search-sources'], 'google' );
    assert.equal( textResult.match, 'fotos de coches' );
  });
  it( 'Quiero ver una pelicula', () => {
    var textResult = textRequest('Quiero ver una pelicula');
    assert.typeOf( textResult, 'object' );
    assert.typeOf( textResult.data, 'object' );
    assert.equal( textResult.intention, 'mmedia-netflix' );
    assert.equal( textResult.data['mmedia-video-type'], 'film' );
  });
  it( 'Quiero ver Nombre Aleatorio', () => {
    var textResult = textRequest('Quiero ver Nombre Aleatorio');
    assert.typeOf( textResult, 'object' );
    assert.isUndefined( textResult.data );
    assert.equal( textResult.intention, 'mmedia-netflix' );
    assert.equal( textResult.match, 'Nombre Aleatorio' );
  });
});
<|endoftext|>


<commit_before>(function (angular) {

  'use strict';

  angular.module('birdyard.utilities')

  .factory('stashService', ['$window', function ($window) {

    // Private

    function isObject(input) {
      if (input) {
        return typeof input === 'object';
      } else {
        return false;
      }
    }

    function isSerializedObject(input) {
      if (input) {
        try {
          var deserialized = JSON.parse(input);
          return isObject(deserialized);
        } catch(ex) {
          return false;
        }
      } else {
        return false;
      }
    }

    // Public

    var _stashService = {};

    function _set(key, value) {
      if (key && value) {
        if (isObject(value)) {
          value = JSON.stringify(value);
        }
        $window.sessionStorage.setItem(key, value);
      } else {
        throw new Error('A Key and Value must be provided.');
      }
    }

    function _get(key) {
      if (key) {
        var value = $window.sessionStorage.getItem(key);
        if (isSerializedObject(value)) {
          return JSON.parse(value);
        } else {
          return value;
        }
      } else {
        throw new Error('A Key must be provided.');
      }
    }

    function _exists(key) {
      if (key) {
        return !!$window.localStorage.getItem(key);
      } else {
        throw new Error('A Key must be provided.');
      }
    }

    function _setLongTerm(key, value) {
      if (key && value) {
        if (isObject(value)) {
          value = JSON.stringify(value);
        }
        $window.localStorage.setItem(key, value);
      } else {
        throw new Error('A Key and Value must be provided.');
      }
    }

    _stashService.set = _set;
    _stashService.setLongTerm = _setLongTerm;
    _stashService.get = _get;
    _stashService.exists = _get;

    return _stashService;

  }]);

})(angular);
<commit_msg>Refactor stash service<commit_after>(function (angular) {

  'use strict';

  angular.module('birdyard.utilities')

  .factory('stashService', ['$window', function ($window) {

    // Private

    function isObject(input) {
      if (input) {
        return typeof input === 'object';
      } else {
        return false;
      }
    }

    function isSerializedObject(input) {
      if (input) {
        try {
          var deserialized = JSON.parse(input);
          return isObject(deserialized);
        } catch(ex) {
          return false;
        }
      } else {
        return false;
      }
    }

    function get(key, storageType) {
      if (key) {
        var value = $window[storageType].getItem(key);
        if (isSerializedObject(value)) {
          return JSON.parse(value);
        } else {
          return value;
        }
      } else {
        throw new Error('A Key must be provided.');
      }
    }

    function set(key, value, storageType) {
      if (key && value) {
        if (isObject(value)) {
          value = JSON.stringify(value);
        }
        $window[storageType].setItem(key, value);
      } else {
        throw new Error('A Key and Value must be provided.');
      }
    }

    // Public

    var _stashService = {};

    function _exists(key) {
      if (key) {
        return !!$window.localStorage.getItem(key);
      } else {
        throw new Error('A Key must be provided.');
      }
    }

    function _get(key) {
      get(key, 'sessionStorage');
    }

    function _getLongTerm(key) {
      get(key, 'localStorage');
    }

    function _set(key, value) {
      set(key, value, 'sessionStorage');
    }

    function _setLongTerm(key, value) {
      set(key, value, 'localStorage');
    }

    _stashService.set = _set;
    _stashService.setLongTerm = _setLongTerm;
    _stashService.get = _get;
    _stashService.exists = _get;

    return _stashService;

  }]);

})(angular);
<|endoftext|>


<commit_before>var expect = require('chai').expect
var Hacktoberfest = require('../main.js')

it('Check if Hacktoberfeststats work', function(done) {
  Hacktoberfest('MatejMecka', '2017', function(hacktoberfestStats, error) {
    expect(hacktoberfestStats.mainStats.Name).to.equal('Matej Plavevski')
    done()
  })
})
<commit_msg>Update tests<commit_after>const expect = require('chai').expect
const { getHacktoberfestStats } = require('../main.js')

it('Check if Hacktoberfeststats (callback) work', function(done) {
  getHacktoberfestStats('MatejMecka', '2017', stats => {
    expect(stats.current).to.equal(15)
    done()
  })
})

it('Check if Hacktoberfeststats (promise) work', function(done) {
  getHacktoberfestStats('MatejMecka', '2017')
    .then(stats => {
      expect(stats.current).to.equal(15)
      done()
    })
})<|endoftext|>


<commit_before>/*
	Copyright (c) 2010-2012 Filip Moberg, (filip@mcsquare.se)

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

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

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.

	Author:		Filip Moberg
	Name:		inception.js
	Version:	1.2
	Codename:	Di Caprio
	Released:	2012-01-03
	Comments:	This version was so awesome, it deserved TWO new version numbers. Totally rebuilt from scratch - inception revisited.
*/

// Sandbox the environment
/** @param {...undefined} undefined */
(function(window, undefined) {

	// Declare environment variables
	var document = window.document, navigator = window.navigator, location = window.location;

	// Create function in window object
	/** @constructor */
	window.inception = (function inception(oo) {

		var env = this;
		var self = this;
		var core = env.__core__;

		// Make sure core function is instantiated
		if (this === window) {
			return new inception(arguments[0]);
		}

		// Declare core object (with core functions)
		this.__core__ = {

			settings: {
				jQuery: {
					ready: "document"
				},
				error: {
					show: true
				}
			},

			onload: [],

			instance: arguments[0],

			// Extend a primary object with a secondary
			extend: function(o) {

				// If set to true, the returned object will be a copy instead of reference
				if (o.clone === true) {
					o.from = this.clone(o.from);
				}

				for (var n in o.from) {
					if (o.construct === true && typeof o.from[n] === "function" && n !== "wrapper") {

						// Make a clone so the use of multiple selectors won't construct the same function twice
						o.from = this.clone(o.from);
						var core = env.__core__, node = [core.instance].concat(o.node.replace(/@/g, "").split(".")), wrapper = node[0], cache;
						o.wrapper = [];

						cache = core.wrapper.cache[core.instance];

						if (typeof cache === "function") {
							o.wrapper.push(cache);
						}

						for (var i=1; i < node.length; ++i) {
							wrapper = wrapper + "__" + node[i];
							cache = core.wrapper.cache[wrapper];
							if (typeof cache === "function") {
								o.wrapper.push(cache);
							}
						}

						for (var i=o.wrapper.length; i >= 0; --i) {

							// Construct the function
							o.from[n] = this.functionConstructor({method: o.from[n], func: n, node: o.node, loop: o.loop, wrapper: o.wrapper[i]});
						}
					}

					if (typeof o.from[n] === "object" && n !== "selector" && n !== "$") {

						if (typeof o.to[n] === "undefined") {
							if (o.from[n] === null) {
								o.to[n] = null;
							} else if (o.from[n].constructor.toString().indexOf("Array") !== -1) {
								o.to[n] = [];
							} else {
								o.to[n] = {};
							}
						}

						this.extend({to: o.to[n], from: o.from[n], construct: o.construct, node: o.node, loop: o.loop});
					} else {
						o.to[n] = o.from[n];
					}
				}

				return o.to;
			},

			// Create deep copy of target object
			clone: function() {
				return this.extend({to: {}, from: arguments[0]});
			},

			// Count child objects
			count : function(o) {

				var c = 0, i;

				for (i in o) {
					if (o.hasOwnProperty(o)) {
						++c;
					}
				}

				return c;
			},

			// Create object from node string
			stringToObject: function() {

				var selector = arguments[0].replace(/ /g, "").split("@").slice(1), a = [], s, o, i, node, shell;

				for (s in selector) {
					node = selector[s].split(".");

					if (node[0] !== "") {
						o = {};
						shell = o;

						for (i = 0; i < node.length-1; ++i) {
							o = o[node[i]] = {};
						}

						if (arguments[1]) {
							o = o[node[i]] = arguments[1];
						} else {
							o = o[node[i]] = {};
						}

						a.push(shell);
					}
				}

				return a;
			},

			// Create selector from node string
			stringToSelector: function() {
				var a = [], n = this.stringToArray(arguments[0]), i, x;

				for (i in n) {
					var s = n[i].split(".");
					var node = env;

					for (x in s) {
						if (typeof node[s[x]] !== "undefined") {
							node = node[s[x]];
						}
					}

					if (node !== env) {
						a.push(node);
					}
				}

				return a;
			},

			// Create array from node string
			stringToArray: function() {
				return arguments[0].replace(/ /g, "").split("@").slice(1);
			},

			// Wrapper engine. Used for fetching stored wrappers in runtime.
			wrapper: {
				cache: {
				},

				get: function(o) {
					var wrapper, core = env.__core__, node = [core.instance].concat(o.node.replace(/@/g, "").split(".")), n, i;

					for (i in node) {
						if (!n) {
							n = node[i];
						} else {
							n = n + "__" + node[i];
						}
						if (typeof core.wrapper.cache[n] === "function") {
							wrapper = core.wrapper.cache[n];
						}
					}
					return wrapper;
				}
			},

			// Construct user function(s)
			functionConstructor: function(o) {

				var self = this;
				var core = env.__core__;

				switch(o.func) {

					// If function is an onReady function and it's the first node selector, load it into jQuery ready - else, load it straight after the window object.
					case "onReady":
						if (o.loop === "0" && typeof o.method !== "undefined") {

							if (typeof window.jQuery === "function") {

								window.jQuery(window[core.settings.jQuery.ready]).ready(function() {

									o.nodeKeys = o.node.split(".");
									o.parentNode = window[self.instance];
									for (var i in o.nodeKeys) {
										o.parentNode = o.parentNode[o.nodeKeys[i]];
									}

									// Call method with its parent object as reference
									o.method.call(window[self.instance][o.node]);
								});

							} else {

								this.onload.push(o.method);

								window.onload = function() {
									for (o in self.onload) {

										o.nodeKeys = o.node.split(".");
										o.parentNode = window[self.instance];
										for (var i in o.nodeKeys) {
											o.parentNode = o.parentNode[o.nodeKeys[i]];
										}

										// Call method with its parent object as reference
										self.onload[o].call(window[self.instance][o.node]);
									}
								}
							}
						}
						break;
				}

				// Construct function as ordinary
				return function() {

					if (typeof this[o.func] === "function") {

						// Set up selector
						this[o.func].selector = this[o.func].$ = core.selector;

						// Set up selector (for new methods, if the current method is used as a constructor)
						this[o.func].prototype.selector = this[o.func].prototype.$ = core.selector;

						// Set up node
						this[o.func].node = core.clone(core.node);

						// This line is for legacy purposes only (where the selector is called from the global object)
						window[core.instance].$ = window[core.instance].selector = core.selector;
					}

					// Execute user function in correct environment
					if (typeof o.wrapper === "function") {
						var self = this;
						var args = arguments;

						// Execute user function inside wrappers (if available)
						return o.wrapper.call({
							call: function() {

								// Apply method with its parent object as reference
								return o.method.apply(self, args);
							}
						});
					} else {
						// Execute user function as ordinary (if there are no wrappers defined for this object)
						return o.method.apply(this, arguments);
					}
				};
			},

			// Error handling
			error: function() {
				var core = env.__core__;

				if (core.settings.error.show === true) {
					throw new Error(arguments[0]);
				}
				return
			}
		};

		// Make the global object a reference to the instance function and return it
		/** @constructor */
		return new (window[arguments[0]] = function() {

			var extended, core = env.__core__;

			// Extend core object with instance object internally
			// Declare user functions and variables
			extended = core.extend({to: env, from: {

				// Default node and selector values
				__core__: {
					node: {
						getString: "",
						getArray: []
					},
					selector: []
				},

				// Extend selected node(s) with given object/function
				extend: function() {
					var shell, s = [], node;

					// Error on invalid node selector
					if (typeof core.node.getArray[0] === "undefined") {
						return core.error("inception.js: Trying to extend with invalid node selector.");
					}

					// Error on false input object
					if (typeof arguments[0] === "undefined") {
						return core.error("inception.js: Trying to extend with invalid input object.");
					}

					// Create shell object of selector node(s)
					shell = core.stringToObject(core.node.getString, arguments[0]);

					// Match shell with node array
					node = core.node.getArray;

					// Iterate over shell and extend each node
					for (var i in shell) {
						core.extend({to: this, from: shell[i], construct: true, node: node[i], loop: i});
						s.push(shell[i]);
					}

					// Extend up to the global object
					window[core.instance] = core.extend({to: window[core.instance], from: this});

					// Return inception object
					return env;
				},

				// Create a new instance and extend with a copy of selected objects
				clone: function() {

					var name = [], arg = arguments[0], i;

					if (arg) {
						name.push(arg);
					} else {
						name = core.node.getArray;
					}

					for (i in name) {
						// Create a new instance
						new inception(name[i]);

						// Extend with core object
						core.extend({to: window[name[i]], from: core.clone(env)});

						// Extend with inner object
						core.extend({to: window[name[i]](), from: core.clone(env)});
					}

					return env;
				},

				// Crate a wrapper that sticks to a specified node, and then wraps the calling function with itself.
				wrap: function() {

					var a, i;

					if (typeof arguments[0] === "function") {

						a = core.node.getString.replace(/\./g, "__").replace(/ /g, "").split("@").slice(1);

						if (a.length === 0) {

							// Set wrapper cache
							core.wrapper.cache[core.instance] = arguments[0];
						}

						for (i in a) {

							// Set wrapper cache
							core.wrapper.cache[core.instance + "__" + a[i]] = arguments[0];

							// Extend to core object
							core.extend({to: window[core.instance].__core__.wrapper, from: core.wrapper});
						}
					}

					return env;
				},

				// Used to read and set new environment settings
				settings: function() {
					var core = env.__core__, settings = arguments[0];

					return core.extend({to: core.settings, from: settings});
				},

				// a DOM helper, to easily create new elements through jQuery. This actually is more of a jQuery plugin, but it's often useful - so I included it.
				append: function() {
					var core = env.__core__, o = arguments[0], jQuery = window.jQuery;

					// Faster than jQueries append, but lets you pass through css, bind and attribute objects to jQuery.
					if (typeof window.jQuery === "function") {
						return core.$.each(function() {

							if (o.element) {
								var obj = document.createElement(o.element);
								this.appendChild(obj);
							}

							if (o.css) {
								jQuery(obj).css(o.css);
							}

							if (o.attr) {
								jQuery(obj).attr(o.attr);
							}

							if (o.bind) {
								for (var k in o.bind) {
									jQuery(obj).bind(k, o.bind[k]);
								}
							}
						});
					}

					return env;
				}
			}});

			// Extend up to the global object
			window[core.instance] = core.extend({to: window[core.instance], from: extended});

			// Append correct selector
			if (typeof arguments[0] === "string" && arguments[0].indexOf("@") !== -1) {

				// Set selector to an inception selector
				core.$ = core.selector = core.stringToSelector(arguments[0]);

				// Save original selector string
				core.node.getString = arguments[0];
				core.node.getArray = core.stringToArray(arguments[0]);

			} else if (typeof window.jQuery === "function") {

				// Set selector to a jQuery selector (if it's not an inception selector and if jQuery is present)
				core.$ = core.selector = window.jQuery(arguments[0]);
			} else if (arguments[0]) {

				// Use native javascript CSS selector
				core.$ = core.selector = document.querySelectorAll(arguments[0]);
			} else {

				// Clear all old selectors
				core.node.getString = "";
				core.node.getArray = "";
				core.$ = core.selector = [];
			}

			// Return extended core object
			return extended;
		});
	});

})(window);
<commit_msg>Updated version.<commit_after><|endoftext|>


<commit_before>/* eslint-env browser */

window.mathquill4quill = function(dependencies) {
  dependencies = dependencies || {};

  var Quill = dependencies.Quill || window.Quill;
  var MathQuill = dependencies.MathQuill || window.MathQuill;
  var katex = dependencies.katex || window.katex;

  function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }

  function areAllDependenciesMet(quill) {
    if (!Quill) {
      console.log("Quill.js not loaded"); // eslint-disable-line no-console
      return false;
    }

    if (!MathQuill) {
      console.log("MathQuill.js not loaded"); // eslint-disable-line no-console
      return false;
    }

    if (!katex) {
      console.log("katex.js not loaded"); // eslint-disable-line no-console
      return false;
    }

    if (!quill.options.modules.formula) {
      console.log("Formula module not enabled"); // eslint-disable-line no-console
      return false;
    }

    return true;
  }

  function createMathquillInput(latexInput) {
    function applyInputStyles(mqInput) {
      mqInput.style.border = "1px solid #ccc";
      mqInput.style.fontSize = "13px";
      mqInput.style.minHeight = "26px";
      mqInput.style.margin = "0px";
      mqInput.style.padding = "3px 5px";
      mqInput.style.width = "170px";
    }

    var mqInput = document.createElement("span");
    applyInputStyles(mqInput);
    insertAfter(mqInput, latexInput);
    latexInput.setAttribute(
      "style",
      "visibility:hidden;padding:0px;border:0px;width:0px;"
    );

    var mqField = MathQuill.getInterface(2).MathField(mqInput, {
      handlers: {
        edit: function() {
          latexInput.value = mqField.latex();
        }
      }
    });

    return {
      field: mqField
    };
  }

  function createOperatorButtons(operators, tooltip, mqField) {
    function applyOperatorButtonStyles(button) {
      button.style.margin = "5px";
      button.style.width = "50px";
      button.style.height = "50px";
      button.style.backgroundColor = "#ffffff";
      button.style.borderColor = "#000000";
      button.style.borderRadius = "7px";
      button.style.borderWidth = "2px";
    }

    function createOperatorButton(element) {
      var displayOperator = element[0];
      var operator = element[1];

      var button = document.createElement("button");
      katex.render(displayOperator, button, {
        throwOnError: false
      });
      button.onclick = function() {
        mqField.cmd(operator);
        mqField.focus();
      };
      applyOperatorButtonStyles(button);
      return button;
    }

    var container = document.createElement("div");
    operators.forEach(function(element) {
      container.appendChild(createOperatorButton(element));
    });

    if (operators.length > 0) {
      tooltip.appendChild(container);
    }

    return {
      display: function(show) {
        if (show) {
          container.style.display = "flex";
          container.style.alignItems = "center";
        } else {
          container.style.display = "none";
        }
      }
    };
  }

  function enableMathQuillFormulaAuthoring(quill, options) {
    if (!areAllDependenciesMet(quill)) {
      return;
    }

    options = options || {};
    options.operators = options.operators || [];

    var tooltip = quill.container.getElementsByClassName("ql-tooltip")[0];
    var latexInput = tooltip.getElementsByTagName("input")[0];
    var formulaButton = document.getElementsByClassName("ql-formula")[0];

    // replace LaTeX formula input with MathQuill input
    var mqInput = createMathquillInput(latexInput);

    var operatorButtons = createOperatorButtons(
      options.operators,
      tooltip,
      mqInput.field
    );

    var observer = new MutationObserver(function() {
      var isFormula = tooltip.attributes["data-mode"].value === "formula";

      // hide operator buttons on non-formula tooltips
      operatorButtons.display(isFormula);

      // hide math on non-formula tooltips
      if (!isFormula) {
        mqInput.field.latex("");
      }
    });

    observer.observe(tooltip, {
      attributes: true,
      attributeFilter: ["data-mode"]
    });

    formulaButton.onclick = function() {
      // set focus to formula editor when it is opened
      window.setTimeout(function() {
        mqInput.field.focus();
      }, 1);
    };
  }

  return enableMathQuillFormulaAuthoring;
};

// for backwards compatibility with prototype-based API
if (window.Quill) {
  window.Quill.prototype.enableMathQuillFormulaAuthoring = function(options) {
    window.mathquill4quill()(this, options);
  };
}
<commit_msg>Refactor to prevent leaking latex to other popups<commit_after>/* eslint-env browser */

window.mathquill4quill = function(dependencies) {
  dependencies = dependencies || {};

  var Quill = dependencies.Quill || window.Quill;
  var MathQuill = dependencies.MathQuill || window.MathQuill;
  var katex = dependencies.katex || window.katex;

  function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }

  function enableMathQuillFormulaAuthoring(quill, options) {
    options = options || {};

    function areAllDependenciesMet() {
      if (!Quill) {
        console.log("Quill.js not loaded"); // eslint-disable-line no-console
        return false;
      }

      if (!MathQuill) {
        console.log("MathQuill.js not loaded"); // eslint-disable-line no-console
        return false;
      }

      if (!katex) {
        console.log("katex.js not loaded"); // eslint-disable-line no-console
        return false;
      }

      if (!quill.options.modules.formula) {
        console.log("Formula module not enabled"); // eslint-disable-line no-console
        return false;
      }

      if (!MutationObserver) {
        console.log("MutationObserver not defined"); // eslint-disable-line no-console
        return false;
      }

      return true;
    }

    function getTooltip() {
      return quill.container.getElementsByClassName("ql-tooltip")[0];
    }

    function getLatexInput() {
      var tooltip = getTooltip();
      return tooltip.getElementsByTagName("input")[0];
    }

    function newMathquillInput() {
      var autofocus = options.autofocus || true;
      var mqInput = null;
      var mqField = null;
      var latexInputStyle = null;

      function applyMathquillInputStyles(mqInput) {
        mqInput.style.border = "1px solid #ccc";
        mqInput.style.fontSize = "13px";
        mqInput.style.minHeight = "26px";
        mqInput.style.margin = "0px";
        mqInput.style.padding = "3px 5px";
        mqInput.style.width = "170px";
      }

      function applyLatexInputStyles(latexInput) {
        latexInput.setAttribute(
          "style",
          "visibility:hidden;padding:0px;border:0px;width:0px;"
        );
      }

      function syncMathquillToQuill(latexInput) {
        var mqField = MathQuill.getInterface(2).MathField(mqInput, {
          handlers: {
            edit: function() {
              latexInput.value = mqField.latex();
            }
          }
        });

        return mqField;
      }

      function autofocusFormulaField(mqField) {
        if (!autofocus) {
          return;
        }

        window.setTimeout(function() {
          mqField.focus();
        }, 1);
      }

      return {
        render: function() {
          if (mqInput != null) {
            return;
          }

          var latexInput = getLatexInput();

          mqInput = document.createElement("span");
          applyMathquillInputStyles(mqInput);

          latexInputStyle = latexInput.style.all;
          applyLatexInputStyles(latexInput);

          mqField = syncMathquillToQuill(latexInput);
          autofocusFormulaField(mqField);

          insertAfter(mqInput, latexInput);
          return mqField;
        },
        destroy: function() {
          if (mqInput == null) {
            return;
          }

          var latexInput = getLatexInput();

          latexInput.setAttribute("style", latexInputStyle);

          mqInput.remove();
          mqInput = null;
        }
      };
    }

    function newOperatorButtons() {
      var operators = options.operators || [];
      var container = null;

      function applyOperatorButtonStyles(button) {
        button.style.margin = "5px";
        button.style.width = "50px";
        button.style.height = "50px";
        button.style.backgroundColor = "#ffffff";
        button.style.borderColor = "#000000";
        button.style.borderRadius = "7px";
        button.style.borderWidth = "2px";
      }

      function applyOperatorContainerStyles(container) {
        container.style.display = "flex";
        container.style.alignItems = "center";
      }

      function createOperatorButton(element, mqField) {
        var displayOperator = element[0];
        var operator = element[1];

        var button = document.createElement("button");

        katex.render(displayOperator, button, {
          throwOnError: false
        });
        button.onclick = function() {
          mqField.cmd(operator);
          mqField.focus();
        };

        return button;
      }

      return {
        render: function(mqField) {
          if (container != null || operators.length === 0) {
            return;
          }

          var tooltip = getTooltip();

          container = document.createElement("div");
          applyOperatorContainerStyles(container);

          operators.forEach(function(element) {
            var button = createOperatorButton(element, mqField);
            applyOperatorButtonStyles(button);
            container.appendChild(button);
          });

          tooltip.appendChild(container);
        },
        destroy: function() {
          if (container == null) {
            return;
          }

          container.remove();
          container = null;
        }
      };
    }

    if (!areAllDependenciesMet()) {
      return;
    }

    var tooltip = getTooltip();

    var mqInput = newMathquillInput();
    var operatorButtons = newOperatorButtons();

    var observer = new MutationObserver(function() {
      var isFormulaTooltipActive =
        !tooltip.classList.contains("ql-hidden") &&
        tooltip.attributes["data-mode"] &&
        tooltip.attributes["data-mode"].value === "formula";

      if (isFormulaTooltipActive) {
        var mqField = mqInput.render();
        operatorButtons.render(mqField);
      } else {
        mqInput.destroy();
        operatorButtons.destroy();
      }
    });

    observer.observe(tooltip, {
      attributes: true,
      attributeFilter: ["class", "data-mode"]
    });
  }

  return enableMathQuillFormulaAuthoring;
};

// for backwards compatibility with prototype-based API
if (window.Quill) {
  window.Quill.prototype.enableMathQuillFormulaAuthoring = function(options) {
    window.mathquill4quill()(this, options);
  };
}
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

define(
  ["designer/utils", "ceci/ceci-designer", "l10n"],
  function(Util, Ceci, L10n) {
    "use strict";

    var knownComponents = [];

    function addComponentsFromRegistry () {
      var trayComponentContainer = document.getElementById('components');

      Ceci.forEachComponent(function (name, component) {
        var urlComponent = window.CustomElements.registry[name].prototype.resolvePath('locale/' + L10n.getCurrentLang() + '.json');
        L10n.ready({url: urlComponent});

        // Avoid adding components that are already in the tray
        if (trayComponentContainer.querySelector('designer-component-tray-item[name="' + name + '"]')) return;

        var item = document.createElement('designer-component-tray-item');
        var meta;

        // This part is ugly. Reach into CustomElements and pull out a <template>.

        var ceciDefinitionScript = Ceci.getCeciDefinitionScript(name);

        try {
          meta = JSON.parse(ceciDefinitionScript.innerHTML);
        }
        catch (e) {
          throw new TypeError("Ceci component, \"" + name + "\" is lacking ceci definitions. Likely it shouldn't be returned from ceci-designer.");
        }

        item.setAttribute('name', name);
        item.setAttribute('thumbnail', meta.thumbnail);
        item.setAttribute('label', Util.prettyName(name));

        item.setAttribute('description', meta.description);
        item.setAttribute('author', meta.author);
        item.setAttribute('updatedat', meta.updatedAt);

        item.addEventListener('click', function (e) {
          var card = document.querySelector('ceci-card[visible]');

          if (card) {
            var newElement = document.createElement(name);
            card.appendChild(newElement);
          }
        }, false);

        trayComponentContainer.appendChild(item);
      });
    }

    // Load elements that might exist already, but also wait for WebComponentsReady in case
    // we load this module early.
    window.addEventListener('WebComponentsReady', addComponentsFromRegistry, false);
    addComponentsFromRegistry();
  }
);

<commit_msg>Add meta.name<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

define(
  ["designer/utils", "ceci/ceci-designer", "l10n"],
  function(Util, Ceci, L10n) {
    "use strict";

    var knownComponents = [];

    function addComponentsFromRegistry () {
      var trayComponentContainer = document.getElementById('components');

      Ceci.forEachComponent(function (name, component) {
        var urlComponent = window.CustomElements.registry[name].prototype.resolvePath('locale/' + L10n.getCurrentLang() + '.json');
        L10n.ready({url: urlComponent});

        // Avoid adding components that are already in the tray
        if (trayComponentContainer.querySelector('designer-component-tray-item[name="' + name + '"]')) return;

        var item = document.createElement('designer-component-tray-item');
        var meta;

        // This part is ugly. Reach into CustomElements and pull out a <template>.

        var ceciDefinitionScript = Ceci.getCeciDefinitionScript(name);

        try {
          meta = JSON.parse(ceciDefinitionScript.innerHTML);
        }
        catch (e) {
          throw new TypeError("Ceci component, \"" + name + "\" is lacking ceci definitions. Likely it shouldn't be returned from ceci-designer.");
        }

        item.setAttribute('name', name);
        item.setAttribute('thumbnail', meta.thumbnail);
        item.setAttribute('label', meta.name || Util.prettyName(name));

        item.setAttribute('description', meta.description);
        item.setAttribute('author', meta.author);
        item.setAttribute('updatedat', meta.updatedAt);

        item.addEventListener('click', function (e) {
          var card = document.querySelector('ceci-card[visible]');

          if (card) {
            var newElement = document.createElement(name);
            card.appendChild(newElement);
          }
        }, false);

        trayComponentContainer.appendChild(item);
      });
    }

    // Load elements that might exist already, but also wait for WebComponentsReady in case
    // we load this module early.
    window.addEventListener('WebComponentsReady', addComponentsFromRegistry, false);
    addComponentsFromRegistry();
  }
);

<|endoftext|>


<commit_before>test("Initialization", function() {
	var scroller1 = new Scroller();
	equal(typeof scroller1, "object");

	var scroller2 = new Scroller(function(left, top, zoom) {});
	equal(typeof scroller2, "object");

	var scroller3 = new Scroller(null, {
		scrollingY: false
	});
	equal(typeof scroller3, "object");

	var scroller4 = new Scroller(function(left, top, zoom) {}, {
		scrollingY: false
	});
	equal(typeof scroller4, "object");
});

test("Setting up dimensions", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);

});

test("Query values", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 0);
	equal(values.top, 0);
	equal(values.zoom, 1);

});

test("Scroll", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 300);
	equal(values.top, 500);
	equal(values.zoom, 1);

});

test("Zoom", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2.45);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.zoom, 2.45);

});

test("Zoom + Scroll", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(1.7);
	scroller.scrollTo(300, 500);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 300);
	equal(values.top, 500);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7, false, 0, 0); // origin is top/left corner

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left / 1.7, 300);
	equal(values.top / 1.7, 500);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom (Auto Origin)", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7);

	var originLeft = 1000 / 2;
	var originTop = 600 / 2;

	// Compute center zooming
	var newLeft = ((originLeft + 300) * 1.7 / 1) - originLeft;
	var newTop = ((originTop + 500) * 1.7 / 1) - originTop;

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, newLeft);
	equal(values.top, newTop);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom + Scroll", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7);
	scroller.scrollTo(500, 700);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 500);
	equal(values.top, 700);
	equal(values.zoom, 1.7);

});

test("Snapping", function() {

	var scroller = new Scroller(null, {
		snapping: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.setSnapSize(50, 100);

	scroller.scrollTo(200, 400);
	var values = scroller.getValues();
	equal(values.left, 200);
	equal(values.top, 400);

	scroller.scrollTo(237, 124);
	var values = scroller.getValues();
	equal(values.left, 250);
	equal(values.top, 100);

});

test("Paging", function() {

	var scroller = new Scroller(null, {
		paging: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.scrollTo(1000, 600);
	var values = scroller.getValues();
	equal(values.left, 1000);
	equal(values.top, 600);

	scroller.scrollTo(1400, 1100);
	var values = scroller.getValues();
	equal(values.left, 1000);
	equal(values.top, 1200);

});

test("ZoomBy", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	scroller.zoomBy(1.5);
	var values = scroller.getValues();
	equal(values.zoom, 1.5);

	scroller.zoomBy(1.2);
	var values = scroller.getValues();
	equal(values.zoom, 1.5 * 1.2);

});

test("ScrollBy", function() {

	var scroller = new Scroller();
	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.scrollBy(200, 300);
	var values = scroller.getValues();
	equal(values.left, 200);
	equal(values.top, 300);

	scroller.scrollBy(300, 400);
	var values = scroller.getValues();
	equal(values.left, 500);
	equal(values.top, 700);

});

test("Out Of Boundaries", function() {

	var scroller = new Scroller();

	// Scroll without dimensions
	scroller.scrollTo(200, 300);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 0);

	// Setup
	scroller.setDimensions(1000, 600, 5000, 5000);

	// Scroll out of max
	scroller.scrollTo(10000, 10000);
	var values = scroller.getValues();
	equal(values.left, 4000);
	equal(values.top, 4400);

	// Scroll out of min
	scroller.scrollTo(-30, -100);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 0);

});

test("Scroll Axis Limited", function() {

	var scroller = new Scroller(null, {
		scrollingX: false
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 400);

	var scroller = new Scroller(null, {
		scrollingY: false
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400);
	var values = scroller.getValues();
	equal(values.left, 300);
	equal(values.top, 0);

});

test("Zoom Limits", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.zoomTo(2);
	var values = scroller.getValues();
	equal(values.zoom, 2);

	scroller.zoomTo(20);
	var values = scroller.getValues();
	equal(values.zoom, 3);

	scroller.zoomTo(0.1);
	var values = scroller.getValues();
	equal(values.zoom, 0.5);

});

asyncTest("Scroll Animated", 4, function() {

	var scroller = new Scroller();

	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	window.setTimeout(function() {
		var values = scroller.getValues();
		equal(values.left, 300);
		equal(values.top, 400);
		equal(values.zoom, 1);
		start();
	}, 500);

});

asyncTest("Zoom Animated", 4, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	window.setTimeout(function() {
		var values = scroller.getValues();

		// zooming is centered automatically
		equal(values.left, 500);
		equal(values.top, 300);
		equal(values.zoom, 2);
		start();
	}, 500);

});

asyncTest("Scroll + Zoom Animated", 8, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);

	var max = scroller.getScrollMax();
	equal(max.left, 5000-1000);
	equal(max.top, 5000-600);

	scroller.scrollTo(300, 400, true, 2);

	window.setTimeout(function() {
		var values = scroller.getValues();
		equal(values.left, 600);
		equal(values.top, 800);
		equal(values.zoom, 2);

		var max = scroller.getScrollMax();
		equal(max.left, (5000*2)-1000);
		equal(max.top, (5000*2)-600);

		start();
	}, 500);

});

<commit_msg>Structured tests with modules. Improved titles.<commit_after>module("Basics");

test("Initialization", function() {
	var scroller1 = new Scroller();
	equal(typeof scroller1, "object");

	var scroller2 = new Scroller(function(left, top, zoom) {});
	equal(typeof scroller2, "object");

	var scroller3 = new Scroller(null, {
		scrollingY: false
	});
	equal(typeof scroller3, "object");

	var scroller4 = new Scroller(function(left, top, zoom) {}, {
		scrollingY: false
	});
	equal(typeof scroller4, "object");
});

test("Setting up dimensions", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);

});

test("Query values", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 0);
	equal(values.top, 0);
	equal(values.zoom, 1);

});

module("Scrolling/Zooming");

test("Scroll", function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 300);
	equal(values.top, 500);
	equal(values.zoom, 1);

});

test("Zoom", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2.45);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.zoom, 2.45);

});

test("Zoom + Scroll", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(1.7);
	scroller.scrollTo(300, 500);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 300);
	equal(values.top, 500);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7, false, 0, 0); // origin is top/left corner

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left / 1.7, 300);
	equal(values.top / 1.7, 500);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom (Auto Origin)", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7);

	var originLeft = 1000 / 2;
	var originTop = 600 / 2;

	// Compute center zooming
	var newLeft = ((originLeft + 300) * 1.7 / 1) - originLeft;
	var newTop = ((originTop + 500) * 1.7 / 1) - originTop;

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, newLeft);
	equal(values.top, newTop);
	equal(values.zoom, 1.7);

});

test("Scroll + Zoom + Scroll", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");
	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 500);
	scroller.zoomTo(1.7);
	scroller.scrollTo(500, 700);

	var values = scroller.getValues();
	equal(typeof values, "object");
	equal(values.left, 500);
	equal(values.top, 700);
	equal(values.zoom, 1.7);

});

module("Snapping/Paging");

test("Snapping", function() {

	var scroller = new Scroller(null, {
		snapping: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.setSnapSize(50, 100);

	scroller.scrollTo(200, 400);
	var values = scroller.getValues();
	equal(values.left, 200);
	equal(values.top, 400);

	scroller.scrollTo(237, 124);
	var values = scroller.getValues();
	equal(values.left, 250);
	equal(values.top, 100);

});

test("Paging", function() {

	var scroller = new Scroller(null, {
		paging: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.scrollTo(1000, 600);
	var values = scroller.getValues();
	equal(values.left, 1000);
	equal(values.top, 600);

	scroller.scrollTo(1400, 1100);
	var values = scroller.getValues();
	equal(values.left, 1000);
	equal(values.top, 1200);

});

module("Scroll/Zoom Relative");

test("ZoomBy", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	scroller.zoomBy(1.5);
	var values = scroller.getValues();
	equal(values.zoom, 1.5);

	scroller.zoomBy(1.2);
	var values = scroller.getValues();
	equal(values.zoom, 1.5 * 1.2);

});

test("ScrollBy", function() {

	var scroller = new Scroller();
	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.scrollBy(200, 300);
	var values = scroller.getValues();
	equal(values.left, 200);
	equal(values.top, 300);

	scroller.scrollBy(300, 400);
	var values = scroller.getValues();
	equal(values.left, 500);
	equal(values.top, 700);

});

module("Boundaries");

test("Out Of Boundaries", function() {

	var scroller = new Scroller();

	// Scroll without dimensions
	scroller.scrollTo(200, 300);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 0);

	// Setup
	scroller.setDimensions(1000, 600, 5000, 5000);

	// Scroll out of max
	scroller.scrollTo(10000, 10000);
	var values = scroller.getValues();
	equal(values.left, 4000);
	equal(values.top, 4400);

	// Scroll out of min
	scroller.scrollTo(-30, -100);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 0);

});

test("Scroll Axis Limited", function() {

	var scroller = new Scroller(null, {
		scrollingX: false
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400);
	var values = scroller.getValues();
	equal(values.left, 0);
	equal(values.top, 400);

	var scroller = new Scroller(null, {
		scrollingY: false
	});

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400);
	var values = scroller.getValues();
	equal(values.left, 300);
	equal(values.top, 0);

});

test("Zoom Limits", function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	scroller.setDimensions(1000, 600, 5000, 5000);

	scroller.zoomTo(2);
	var values = scroller.getValues();
	equal(values.zoom, 2);

	scroller.zoomTo(20);
	var values = scroller.getValues();
	equal(values.zoom, 3);

	scroller.zoomTo(0.1);
	var values = scroller.getValues();
	equal(values.zoom, 0.5);

});

module("Animations");

asyncTest("Scroll Animated", 4, function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	setTimeout(function() {
		var values = scroller.getValues();
		equal(values.left, 300);
		equal(values.top, 400);
		equal(values.zoom, 1);
		start();
	}, 500);

});

asyncTest("Zoom Animated", 4, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	setTimeout(function() {
		var values = scroller.getValues();

		// zooming is centered automatically
		equal(values.left, 500);
		equal(values.top, 300);
		equal(values.zoom, 2);
		start();
	}, 500);

});

asyncTest("Scroll + Zoom Animated", 8, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});

	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);

	var max = scroller.getScrollMax();
	equal(max.left, 5000-1000);
	equal(max.top, 5000-600);

	scroller.scrollTo(300, 400, true, 2);

	setTimeout(function() {
		var values = scroller.getValues();
		equal(values.left, 600);
		equal(values.top, 800);
		equal(values.zoom, 2);

		var max = scroller.getScrollMax();
		equal(max.left, (5000*2)-1000);
		equal(max.top, (5000*2)-600);

		start();
	}, 500);

});

module("Animation Interrupted");

asyncTest("Scroll Animated + Scroll Animated", 4, function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	setTimeout(function() {
		scroller.scrollTo(500, 800, true);
	}, 150);

	setTimeout(function() {
		var values = scroller.getValues();
		equal(values.left, 500);
		equal(values.top, 800);
		equal(values.zoom, 1);
		start();
	}, 1000);

});

asyncTest("Scroll Animated + Zoom Animated", 6, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	setTimeout(function() {
		scroller.zoomTo(2, true);
	}, 150);

	setTimeout(function() {
		var values = scroller.getValues();

		// Scroll position can not have reached final position yet
		notEqual(values.left, 300);
		notEqual(values.top, 400);
		equal(values.zoom, 2);

		// Scroll max need have values based on final zoom
		var max = scroller.getScrollMax();
		equal(max.left, (5000*2)-1000);
		equal(max.top, (5000*2)-600);

		start();
	}, 1000);

});

asyncTest("Zoom Animated + Zoom Animated", 2, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	setTimeout(function() {
		scroller.zoomTo(3, true);
	}, 150);

	setTimeout(function() {
		var values = scroller.getValues();
		equal(values.zoom, 3);
		start();
	}, 1000);

});

asyncTest("Zoom Animated +  Scroll Animated", 6, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	setTimeout(function() {
		scroller.scrollTo(300, 400, true);
	}, 150);

	setTimeout(function() {
		var values = scroller.getValues();

		// Zoom level can not have reached final position yet
		equal(values.left, 300);
		equal(values.top, 400);
		notEqual(values.zoom, 2);

		// Scroll max need have values different as
		// they must not be based of final zoom, but current zoom
		var max = scroller.getScrollMax();
		notEqual(max.left, (5000*2)-1000);
		notEqual(max.top, (5000*2)-600);

		start();
	}, 1000);

});

asyncTest("Scroll Animated + Scroll Static", 4, function() {

	var scroller = new Scroller();
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	setTimeout(function() {
		scroller.scrollTo(500, 800);

		var values = scroller.getValues();
		equal(values.left, 500);
		equal(values.top, 800);
		equal(values.zoom, 1);
		start();
	}, 150);

});

asyncTest("Scroll Animated + Zoom Static", 6, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.scrollTo(300, 400, true);

	setTimeout(function() {
		scroller.zoomTo(2);

		var values = scroller.getValues();

		// Scroll position can not have reached final position yet
		notEqual(values.left, 300);
		notEqual(values.top, 400);
		equal(values.zoom, 2);

		// Scroll max need have values based on final zoom
		var max = scroller.getScrollMax();
		equal(max.left, (5000*2)-1000);
		equal(max.top, (5000*2)-600);

		start();
	}, 150);

});

asyncTest("Zoom Animated + Zoom Static", 2, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	setTimeout(function() {
		scroller.zoomTo(3);

		var values = scroller.getValues();
		equal(values.zoom, 3);
		start();
	}, 150);

});

asyncTest("Zoom Animated + Scroll Static", 6, function() {

	var scroller = new Scroller(null, {
		zooming: true
	});
	equal(typeof scroller, "object");

	scroller.setDimensions(1000, 600, 5000, 5000);
	scroller.zoomTo(2, true);

	setTimeout(function() {
		scroller.scrollTo(300, 400);

		var values = scroller.getValues();

		// Zoom level can not have reached final position yet
		equal(values.left, 300);
		equal(values.top, 400);
		notEqual(values.zoom, 2);

		// Scroll max need have values different as
		// they must not be based of final zoom, but current zoom
		var max = scroller.getScrollMax();
		notEqual(max.left, (5000*2)-1000);
		notEqual(max.top, (5000*2)-600);

		start();
	}, 150);

});

<|endoftext|>


<commit_before>module.exports = {
  siteTitle: 'maxpou.fr',
  siteDescription: "👋 Hey I'm Max",
  authorName: 'Maxence Poutord',
  twitterUsername: '_maxpou',
  authorAvatar: '/images/avatar.jpeg',
  multilangPosts: true, // enable/disable flags in post lists
  authorDescription: `
  For the last decade, Maxence Poutord has worked with a variety of web technologies. He is currently focused on front-end development.
  On his day to day job, he is working as a senior front-end engineer at VSware. He is also a frequent tech speaker and a mentor.
  As a new digital nomad, he is living where the WIFI and sun is 😎

  Do you want to know more? Read this!
  `,
  siteUrl: 'https://www.maxpou.fr/',
  disqusSiteUrl: 'https://www.maxpou.fr/',
  // Prefixes all links. For cases when deployed to maxpou.fr/gatsby-starter-morning-dew/
  pathPrefix: '/', // Note: it must *not* have a trailing slash.
  siteCover: '/images/cover.jpg',
  googleAnalyticsId: 'UA-67868977-1',
  background_color: '#ffffff',
  theme_color: '#222222',
  display: 'minimal-ui',
  icon: 'src/assets/icon.jpeg',
  postsPerPage: 10,
  disqusShortname: 'maxpou',
  headerLinks: [
    {
      label: '🏡',
      url: '/',
    },
    {
      label: 'Blog',
      url: '/',
    },
    {
      label: 'About',
      url: '/about',
    },
    {
      label: 'Speaking',
      url: '/speaking',
    },
  ],
  // Footer information (ex: Github, Netlify...)
  websiteHost: {
    name: 'GitHub',
    url: 'https://github.com',
  },
  footerLinks: [
    [
      'Explore',
      {
        label: 'Blog',
        url: '/',
      },
      {
        label: 'About',
        url: '/about',
      },
      {
        label: 'Speaking',
        url: '/speaking',
      },
      {
        label: 'Portfolio',
        url: '/portfolio',
      },
    ],
    [
      'Follow me',
      {
        label: 'GitHub',
        url: 'https://github.com/maxpou',
        iconClassName: 'fa fa-github',
      },
      {
        label: 'LinkedIn',
        url: 'https://www.linkedin.com/in/maxpou/',
        iconClassName: 'fa fa-linkedin',
      },
      {
        label: 'Twitter',
        url: 'https://twitter.com/_maxpou',
        iconClassName: 'fa fa-twitter',
      },
    ],
  ],
}
<commit_msg>feat: add dev.to + nomadlist<commit_after>module.exports = {
  siteTitle: 'maxpou.fr',
  siteDescription: "👋 Hey I'm Max",
  authorName: 'Maxence Poutord',
  twitterUsername: '_maxpou',
  authorAvatar: '/images/avatar.jpeg',
  multilangPosts: true, // enable/disable flags in post lists
  authorDescription: `
  For the last decade, Maxence Poutord has worked with a variety of web technologies. He is currently focused on front-end development.
  On his day to day job, he is working as a senior front-end engineer at VSware. He is also a frequent tech speaker and a mentor.
  As a new digital nomad, he is living where the WIFI and sun is 😎

  Do you want to know more? Read this!
  `,
  siteUrl: 'https://www.maxpou.fr/',
  disqusSiteUrl: 'https://www.maxpou.fr/',
  // Prefixes all links. For cases when deployed to maxpou.fr/gatsby-starter-morning-dew/
  pathPrefix: '/', // Note: it must *not* have a trailing slash.
  siteCover: '/images/cover.jpg',
  googleAnalyticsId: 'UA-67868977-1',
  background_color: '#ffffff',
  theme_color: '#222222',
  display: 'minimal-ui',
  icon: 'src/assets/icon.jpeg',
  postsPerPage: 10,
  disqusShortname: 'maxpou',
  headerLinks: [
    {
      label: '🏡',
      url: '/',
    },
    {
      label: 'Blog',
      url: '/',
    },
    {
      label: 'About',
      url: '/about',
    },
    {
      label: 'Speaking',
      url: '/speaking',
    },
  ],
  // Footer information (ex: Github, Netlify...)
  websiteHost: {
    name: 'GitHub',
    url: 'https://github.com',
  },
  footerLinks: [
    [
      'Explore',
      {
        label: 'Blog',
        url: '/',
      },
      {
        label: 'About',
        url: '/about',
      },
      {
        label: 'Speaking',
        url: '/speaking',
      },
      {
        label: 'Portfolio',
        url: '/portfolio',
      },
    ],
    [
      'Follow me',
      {
        label: 'GitHub',
        url: 'https://github.com/maxpou',
        iconClassName: 'fa fa-github',
      },
      {
        label: 'LinkedIn',
        url: 'https://www.linkedin.com/in/maxpou/',
        iconClassName: 'fa fa-linkedin',
      },
      {
        label: 'Twitter',
        url: 'https://twitter.com/_maxpou',
        iconClassName: 'fa fa-twitter',
      },
      {
        label: 'Nomad List',
        url: 'https://nomadlist.com/@maxpou',
      },
      {
        label: 'Dev',
        url: 'https://dev.to/maxpou',
      },
    ],
  ],
}
<|endoftext|>