<commit_before>/**
 * @author Eberhard Graether / http://egraether.com/
 */

THREE.TrackballControls = function ( object, domElement ) {

	THREE.EventDispatcher.call( this );

	var _this = this;
	var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5 };

	this.object = object;
	this.domElement = ( domElement !== undefined ) ? domElement : document;

	// API

	this.enabled = true;

	this.screen = { width: 0, height: 0, offsetLeft: 0, offsetTop: 0 };
	this.radius = ( this.screen.width + this.screen.height ) / 4;

	this.rotateSpeed = 1.0;
	this.zoomSpeed = 1.2;
	this.panSpeed = 0.3;

	this.noRotate = false;
	this.noZoom = false;
	this.noPan = false;

	this.staticMoving = false;
	this.dynamicDampingFactor = 0.2;

	this.minDistance = 0;
	this.maxDistance = Infinity;

	this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];

	// internals

	this.target = new THREE.Vector3();

	var lastPosition = new THREE.Vector3();

	var _state = STATE.NONE,
	_prevState = STATE.NONE,

	_eye = new THREE.Vector3(),

	_rotateStart = new THREE.Vector3(),
	_rotateEnd = new THREE.Vector3(),

	_zoomStart = new THREE.Vector2(),
	_zoomEnd = new THREE.Vector2(),

	_touchZoomDistanceStart = 0,
	_touchZoomDistanceEnd = 0,

	_panStart = new THREE.Vector2(),
	_panEnd = new THREE.Vector2();

	// for reset

	this.target0 = this.target.clone();
	this.position0 = this.object.position.clone();
	this.up0 = this.object.up.clone();

	// events

	var changeEvent = { type: 'change' };

	// methods

	this.handleResize = function () {

		this.screen.width = window.innerWidth;
		this.screen.height = window.innerHeight;

		this.screen.offsetLeft = 0;
		this.screen.offsetTop = 0;

		this.radius = ( this.screen.width + this.screen.height ) / 4;

	};

	this.handleEvent = function ( event ) {

		if ( typeof this[ event.type ] == 'function' ) {

			this[ event.type ]( event );

		}

	};

	this.getMouseOnScreen = function ( clientX, clientY ) {

		return new THREE.Vector2(
			( clientX - _this.screen.offsetLeft ) / _this.radius * 0.5,
			( clientY - _this.screen.offsetTop ) / _this.radius * 0.5
		);

	};

	this.getMouseProjectionOnBall = function ( clientX, clientY ) {

		var mouseOnBall = new THREE.Vector3(
			( clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft ) / _this.radius,
			( _this.screen.height * 0.5 + _this.screen.offsetTop - clientY ) / _this.radius,
			0.0
		);

		var length = mouseOnBall.length();

		if ( length > 1.0 ) {

			mouseOnBall.normalize();

		} else {

			mouseOnBall.z = Math.sqrt( 1.0 - length * length );

		}

		_eye.copy( _this.object.position ).sub( _this.target );

		var projection = _this.object.up.clone().setLength( mouseOnBall.y );
		projection.add( _this.object.up.clone().cross( _eye ).setLength( mouseOnBall.x ) );
		projection.add( _eye.setLength( mouseOnBall.z ) );

		return projection;

	};

	this.rotateCamera = function () {

		var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() );

		if ( angle ) {

			var axis = ( new THREE.Vector3() ).crossVectors( _rotateStart, _rotateEnd ).normalize(),
				quaternion = new THREE.Quaternion();

			angle *= _this.rotateSpeed;

			quaternion.setFromAxisAngle( axis, -angle );

			_eye.applyQuaternion( quaternion );
			_this.object.up.applyQuaternion( quaternion );

			_rotateEnd.applyQuaternion( quaternion );

			if ( _this.staticMoving ) {

				_rotateStart.copy( _rotateEnd );

			} else {

				quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) );
				_rotateStart.applyQuaternion( quaternion );

			}

		}

	};

	this.zoomCamera = function () {

		if ( _state === STATE.TOUCH_ZOOM ) {

			var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
			_touchZoomDistanceStart = _touchZoomDistanceEnd;
			_eye.multiplyScalar( factor );

		} else {

			var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;

			if ( factor !== 1.0 && factor > 0.0 ) {

				_eye.multiplyScalar( factor );

				if ( _this.staticMoving ) {

					_zoomStart.copy( _zoomEnd );

				} else {

					_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;

				}

			}

		}

	};

	this.panCamera = function () {

		var mouseChange = _panEnd.clone().sub( _panStart );

		if ( mouseChange.lengthSq() ) {

			mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );

			var pan = _eye.clone().cross( _this.object.up ).setLength( mouseChange.x );
			pan.add( _this.object.up.clone().setLength( mouseChange.y ) );

			_this.object.position.add( pan );
			_this.target.add( pan );

			if ( _this.staticMoving ) {

				_panStart = _panEnd;

			} else {

				_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );

			}

		}

	};

	this.checkDistances = function () {

		if ( !_this.noZoom || !_this.noPan ) {

			if ( _this.object.position.lengthSq() > _this.maxDistance * _this.maxDistance ) {

				_this.object.position.setLength( _this.maxDistance );

			}

			if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {

				_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );

			}

		}

	};

	this.update = function () {

		_eye.subVectors( _this.object.position, _this.target );

		if ( !_this.noRotate ) {

			_this.rotateCamera();

		}

		if ( !_this.noZoom ) {

			_this.zoomCamera();

		}

		if ( !_this.noPan ) {

			_this.panCamera();

		}

		_this.object.position.addVectors( _this.target, _eye );

		_this.checkDistances();

		_this.object.lookAt( _this.target );

		if ( lastPosition.distanceToSquared( _this.object.position ) > 0 ) {

			_this.dispatchEvent( changeEvent );

			lastPosition.copy( _this.object.position );

		}

	};

	this.reset = function () {

		_state = STATE.NONE;
		_prevState = STATE.NONE;

		_this.target.copy( _this.target0 );
		_this.object.position.copy( _this.position0 );
		_this.object.up.copy( _this.up0 );

		_eye.subVectors( _this.object.position, _this.target );

		_this.object.lookAt( _this.target );

		//_this.dispatchEvent( changeEvent ); // not sure what this is for

		lastPosition.copy( _this.object.position );

	};

	// listeners

	function keydown( event ) {

		if ( _this.enabled === false ) return;

		window.removeEventListener( 'keydown', keydown );

		_prevState = _state;

		if ( _state !== STATE.NONE ) {

			return;

		} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) {

			_state = STATE.ROTATE;

		} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) {

			_state = STATE.ZOOM;

		} else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) {

			_state = STATE.PAN;

		}

	}

	function keyup( event ) {

		if ( _this.enabled === false ) return;

		_state = _prevState;

		window.addEventListener( 'keydown', keydown, false );

	}

	function mousedown( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.NONE ) {

			_state = event.button;

		}

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

		document.addEventListener( 'mousemove', mousemove, false );
		document.addEventListener( 'mouseup', mouseup, false );

	}

	function mousemove( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			_rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

	}

	function mouseup( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		_state = STATE.NONE;

		document.removeEventListener( 'mousemove', mousemove );
		document.removeEventListener( 'mouseup', mouseup );

	}

	function mousewheel( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		var delta = 0;

		if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9

			delta = event.wheelDelta / 40;

		} else if ( event.detail ) { // Firefox

			delta = - event.detail / 3;

		}

		_zoomStart.y += ( 1 / delta ) * 0.05;

	}

	function touchstart( event ) {

		if ( _this.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				_state = STATE.TOUCH_ROTATE;
				_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				_state = STATE.TOUCH_ZOOM;
				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
				_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
				break;

			case 3:
				_state = STATE.TOUCH_PAN;
				_panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			default:
				_state = STATE.NONE;

		}

	}

	function touchmove( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		switch ( event.touches.length ) {

			case 1:
				_rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy )
				break;

			case 3:
				_panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			default:
				_state = STATE.NONE;

		}

	}

	function touchend( event ) {

		if ( _this.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
				break;

			case 3:
				_panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

		}

		_state = STATE.NONE;

	}

	this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );

	this.domElement.addEventListener( 'mousedown', mousedown, false );

	this.domElement.addEventListener( 'mousewheel', mousewheel, false );
	this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox

	this.domElement.addEventListener( 'touchstart', touchstart, false );
	this.domElement.addEventListener( 'touchend', touchend, false );
	this.domElement.addEventListener( 'touchmove', touchmove, false );

	window.addEventListener( 'keydown', keydown, false );
	window.addEventListener( 'keyup', keyup, false );

	this.handleResize();

};
<commit_msg>TrackballControls: Uncommented change event dispatch.<commit_after>/**
 * @author Eberhard Graether / http://egraether.com/
 */

THREE.TrackballControls = function ( object, domElement ) {

	THREE.EventDispatcher.call( this );

	var _this = this;
	var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5 };

	this.object = object;
	this.domElement = ( domElement !== undefined ) ? domElement : document;

	// API

	this.enabled = true;

	this.screen = { width: 0, height: 0, offsetLeft: 0, offsetTop: 0 };
	this.radius = ( this.screen.width + this.screen.height ) / 4;

	this.rotateSpeed = 1.0;
	this.zoomSpeed = 1.2;
	this.panSpeed = 0.3;

	this.noRotate = false;
	this.noZoom = false;
	this.noPan = false;

	this.staticMoving = false;
	this.dynamicDampingFactor = 0.2;

	this.minDistance = 0;
	this.maxDistance = Infinity;

	this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];

	// internals

	this.target = new THREE.Vector3();

	var lastPosition = new THREE.Vector3();

	var _state = STATE.NONE,
	_prevState = STATE.NONE,

	_eye = new THREE.Vector3(),

	_rotateStart = new THREE.Vector3(),
	_rotateEnd = new THREE.Vector3(),

	_zoomStart = new THREE.Vector2(),
	_zoomEnd = new THREE.Vector2(),

	_touchZoomDistanceStart = 0,
	_touchZoomDistanceEnd = 0,

	_panStart = new THREE.Vector2(),
	_panEnd = new THREE.Vector2();

	// for reset

	this.target0 = this.target.clone();
	this.position0 = this.object.position.clone();
	this.up0 = this.object.up.clone();

	// events

	var changeEvent = { type: 'change' };

	// methods

	this.handleResize = function () {

		this.screen.width = window.innerWidth;
		this.screen.height = window.innerHeight;

		this.screen.offsetLeft = 0;
		this.screen.offsetTop = 0;

		this.radius = ( this.screen.width + this.screen.height ) / 4;

	};

	this.handleEvent = function ( event ) {

		if ( typeof this[ event.type ] == 'function' ) {

			this[ event.type ]( event );

		}

	};

	this.getMouseOnScreen = function ( clientX, clientY ) {

		return new THREE.Vector2(
			( clientX - _this.screen.offsetLeft ) / _this.radius * 0.5,
			( clientY - _this.screen.offsetTop ) / _this.radius * 0.5
		);

	};

	this.getMouseProjectionOnBall = function ( clientX, clientY ) {

		var mouseOnBall = new THREE.Vector3(
			( clientX - _this.screen.width * 0.5 - _this.screen.offsetLeft ) / _this.radius,
			( _this.screen.height * 0.5 + _this.screen.offsetTop - clientY ) / _this.radius,
			0.0
		);

		var length = mouseOnBall.length();

		if ( length > 1.0 ) {

			mouseOnBall.normalize();

		} else {

			mouseOnBall.z = Math.sqrt( 1.0 - length * length );

		}

		_eye.copy( _this.object.position ).sub( _this.target );

		var projection = _this.object.up.clone().setLength( mouseOnBall.y );
		projection.add( _this.object.up.clone().cross( _eye ).setLength( mouseOnBall.x ) );
		projection.add( _eye.setLength( mouseOnBall.z ) );

		return projection;

	};

	this.rotateCamera = function () {

		var angle = Math.acos( _rotateStart.dot( _rotateEnd ) / _rotateStart.length() / _rotateEnd.length() );

		if ( angle ) {

			var axis = ( new THREE.Vector3() ).crossVectors( _rotateStart, _rotateEnd ).normalize(),
				quaternion = new THREE.Quaternion();

			angle *= _this.rotateSpeed;

			quaternion.setFromAxisAngle( axis, -angle );

			_eye.applyQuaternion( quaternion );
			_this.object.up.applyQuaternion( quaternion );

			_rotateEnd.applyQuaternion( quaternion );

			if ( _this.staticMoving ) {

				_rotateStart.copy( _rotateEnd );

			} else {

				quaternion.setFromAxisAngle( axis, angle * ( _this.dynamicDampingFactor - 1.0 ) );
				_rotateStart.applyQuaternion( quaternion );

			}

		}

	};

	this.zoomCamera = function () {

		if ( _state === STATE.TOUCH_ZOOM ) {

			var factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
			_touchZoomDistanceStart = _touchZoomDistanceEnd;
			_eye.multiplyScalar( factor );

		} else {

			var factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;

			if ( factor !== 1.0 && factor > 0.0 ) {

				_eye.multiplyScalar( factor );

				if ( _this.staticMoving ) {

					_zoomStart.copy( _zoomEnd );

				} else {

					_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;

				}

			}

		}

	};

	this.panCamera = function () {

		var mouseChange = _panEnd.clone().sub( _panStart );

		if ( mouseChange.lengthSq() ) {

			mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );

			var pan = _eye.clone().cross( _this.object.up ).setLength( mouseChange.x );
			pan.add( _this.object.up.clone().setLength( mouseChange.y ) );

			_this.object.position.add( pan );
			_this.target.add( pan );

			if ( _this.staticMoving ) {

				_panStart = _panEnd;

			} else {

				_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );

			}

		}

	};

	this.checkDistances = function () {

		if ( !_this.noZoom || !_this.noPan ) {

			if ( _this.object.position.lengthSq() > _this.maxDistance * _this.maxDistance ) {

				_this.object.position.setLength( _this.maxDistance );

			}

			if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {

				_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );

			}

		}

	};

	this.update = function () {

		_eye.subVectors( _this.object.position, _this.target );

		if ( !_this.noRotate ) {

			_this.rotateCamera();

		}

		if ( !_this.noZoom ) {

			_this.zoomCamera();

		}

		if ( !_this.noPan ) {

			_this.panCamera();

		}

		_this.object.position.addVectors( _this.target, _eye );

		_this.checkDistances();

		_this.object.lookAt( _this.target );

		if ( lastPosition.distanceToSquared( _this.object.position ) > 0 ) {

			_this.dispatchEvent( changeEvent );

			lastPosition.copy( _this.object.position );

		}

	};

	this.reset = function () {

		_state = STATE.NONE;
		_prevState = STATE.NONE;

		_this.target.copy( _this.target0 );
		_this.object.position.copy( _this.position0 );
		_this.object.up.copy( _this.up0 );

		_eye.subVectors( _this.object.position, _this.target );

		_this.object.lookAt( _this.target );

		_this.dispatchEvent( changeEvent );

		lastPosition.copy( _this.object.position );

	};

	// listeners

	function keydown( event ) {

		if ( _this.enabled === false ) return;

		window.removeEventListener( 'keydown', keydown );

		_prevState = _state;

		if ( _state !== STATE.NONE ) {

			return;

		} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && !_this.noRotate ) {

			_state = STATE.ROTATE;

		} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && !_this.noZoom ) {

			_state = STATE.ZOOM;

		} else if ( event.keyCode === _this.keys[ STATE.PAN ] && !_this.noPan ) {

			_state = STATE.PAN;

		}

	}

	function keyup( event ) {

		if ( _this.enabled === false ) return;

		_state = _prevState;

		window.addEventListener( 'keydown', keydown, false );

	}

	function mousedown( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.NONE ) {

			_state = event.button;

		}

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomStart = _zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panStart = _panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

		document.addEventListener( 'mousemove', mousemove, false );
		document.addEventListener( 'mouseup', mouseup, false );

	}

	function mousemove( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		if ( _state === STATE.ROTATE && !_this.noRotate ) {

			_rotateEnd = _this.getMouseProjectionOnBall( event.clientX, event.clientY );

		} else if ( _state === STATE.ZOOM && !_this.noZoom ) {

			_zoomEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		} else if ( _state === STATE.PAN && !_this.noPan ) {

			_panEnd = _this.getMouseOnScreen( event.clientX, event.clientY );

		}

	}

	function mouseup( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		_state = STATE.NONE;

		document.removeEventListener( 'mousemove', mousemove );
		document.removeEventListener( 'mouseup', mouseup );

	}

	function mousewheel( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		var delta = 0;

		if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9

			delta = event.wheelDelta / 40;

		} else if ( event.detail ) { // Firefox

			delta = - event.detail / 3;

		}

		_zoomStart.y += ( 1 / delta ) * 0.05;

	}

	function touchstart( event ) {

		if ( _this.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				_state = STATE.TOUCH_ROTATE;
				_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				_state = STATE.TOUCH_ZOOM;
				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
				_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
				break;

			case 3:
				_state = STATE.TOUCH_PAN;
				_panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			default:
				_state = STATE.NONE;

		}

	}

	function touchmove( event ) {

		if ( _this.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		switch ( event.touches.length ) {

			case 1:
				_rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy )
				break;

			case 3:
				_panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			default:
				_state = STATE.NONE;

		}

	}

	function touchend( event ) {

		if ( _this.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				_rotateStart = _rotateEnd = _this.getMouseProjectionOnBall( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

			case 2:
				_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
				break;

			case 3:
				_panStart = _panEnd = _this.getMouseOnScreen( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
				break;

		}

		_state = STATE.NONE;

	}

	this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );

	this.domElement.addEventListener( 'mousedown', mousedown, false );

	this.domElement.addEventListener( 'mousewheel', mousewheel, false );
	this.domElement.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox

	this.domElement.addEventListener( 'touchstart', touchstart, false );
	this.domElement.addEventListener( 'touchend', touchend, false );
	this.domElement.addEventListener( 'touchmove', touchmove, false );

	window.addEventListener( 'keydown', keydown, false );
	window.addEventListener( 'keyup', keyup, false );

	this.handleResize();

};
<|endoftext|>


<commit_before>(function() {
  /**
   * shows a dialog to choose another base map
   * new BaseMapChooser({})
   *
   */
  cdb.admin.BaseMapAdder = cdb.admin.BaseDialog.extend({

    MAPBOX_HTTPS: 'https://dnv9my2eseobd.cloudfront.net',

    _WAITING_INPUT_TIME: 1000,

    events: {
      "keydown input":    "_checkEnter",
      "click .ok.button": "ok",
      "click .cancel":    "_cancel",
      "click .close":     "_cancel"
    },

    initialize: function() {

      _.bindAll(this, "_successChooser", "_errorChooser", "_showLoader", "_hideLoader");

      var self = this;

      _.extend(this.options, {
        title: _t("Add your basemap"),
        description: _t(""),
        clean_on_hide: true,
        cancel_button_classes: "margin15",
        ok_button_classes: "button grey",
        ok_title: _t("Add basemap"),
        modal_type: "compressed",
        width: 512,
        modal_class: 'basemap_chooser_dialog'
      });

      this.constructor.__super__.initialize.apply(this);

      this.enable = true;
      this.tilejson = null;
    },

    render_content: function() {
      return this.getTemplate('table/views/basemap_chooser_dialog')();
    },

    /**
     * Check enter keydown
     */
    _checkEnter: function(ev) {
      // If it is a enter... nothing
      var code = (ev.keyCode ? ev.keyCode : ev.which);
      if(code == 13) {
        this.killEvent(ev);
        this.ok();
      }
    },

    /**
     * If the url is not valid
     */
    _errorChooser: function(e) {
      // End loader
      this._hideLoader();

      // Show error
      this.$el.find("input").addClass("error");
      this.$el.find("div.error").addClass("active");

      // Enable dialog? nop!
      this.$el.find("a.button.ok").removeClass("disabled");
      this.enable = true;
    },

    /**
     * If the url is valid
     */
    _successChooser: function(data) {
      // End loader
      this._hideLoader();

      var layer = new cdb.admin.TileLayer({
        urlTemplate:  data.tiles[0],
        attribution:  data.attribution || null,
        maxZoom:      data.maxzoom || 21,
        minZoom:      data.minzoom || 0,
        name:         data.name || ''
      });
      // do not save before add because the layer collection
      // has the correct url
      this.options.baseLayers.add(layer);
      layer.save();

      // Remove error
      this.$el.find("input").removeClass("error");
      this.$el.find(".error").removeClass("active");

      this.hide();
      this.options.ok && this.options.ok(layer);
    },

    /**
     * Show loader
     */
    _showLoader: function() {
      // Get spin target
      this.$el.find("div.loader").show();
    },

    /**
     * Hide loader
     */
    _hideLoader: function() {
      this.$el.find("div.loader").hide();
    },

    /**
     * return a https url if the current application is loaded form https
     */
    _fixHttps: function(url, loc) {
      loc = loc || location;

      // fix the url to https or http
     if(url.search('https') !== 0 && loc.protocol === 'https:') {
        // search for mapping
        var i = url.indexOf('mapbox.com');
        if(i != -1) {
          return this.MAPBOX_HTTPS + url.substr(i + 'mapbox.com'.length);
        }
        return url.replace(/http/, 'https');
      }
      return url;
    },

    /**
     * this function has to check the url is correct and try to get
     * the tilejson
     */
    _checkTileJson: function(ev) {

      var $input =  this.$el.find('input')
        , url =     $input.val()
        , self =    this;

      // Remove error
      $input.removeClass("error");
      this.$el.find(".error").removeClass("active");
      // Start loader
      this._showLoader();

      // Check if it is xyz
      if ( url.search("{x}") != -1) {
        url = url.replace(/\{x\}/g,"1").replace(/\{y\}/g,"1").replace(/\{z\}/g,"1")
      }

      url = this._fixHttps(url);

      $.ajax({
        type: "GET",
        url: url,
        dataType: 'jsonp',
        success: this._successChooser,
        error: this._errorChooser
      });
    },

    /**
     * Click on OK button
     */
    ok: function(ev) {
      if (ev && ev.preventDefault) ev.preventDefault();

      if (this.enable) {
        this.enable = false;
        this._checkTileJson();
      }
    }
  });
})();
<commit_msg>fixed possibility of adding XYZ basemaps<commit_after>(function() {
  /**
   * shows a dialog to choose another base map
   * new BaseMapChooser({})
   *
   */
  cdb.admin.BaseMapAdder = cdb.admin.BaseDialog.extend({

    MAPBOX_HTTPS: 'https://dnv9my2eseobd.cloudfront.net',

    _WAITING_INPUT_TIME: 1000,

    events: {
      "keydown input":    "_checkEnter",
      "click .ok.button": "ok",
      "click .cancel":    "_cancel",
      "click .close":     "_cancel"
    },

    initialize: function() {

      _.bindAll(this, "_successChooser", "_errorChooser", "_showLoader", "_hideLoader");

      var self = this;

      _.extend(this.options, {
        title: _t("Add your basemap"),
        description: _t(""),
        clean_on_hide: true,
        cancel_button_classes: "margin15",
        ok_button_classes: "button grey",
        ok_title: _t("Add basemap"),
        modal_type: "compressed",
        width: 512,
        modal_class: 'basemap_chooser_dialog'
      });

      this.constructor.__super__.initialize.apply(this);

      this.enable = true;
      this.tilejson = null;
    },

    render_content: function() {
      return this.getTemplate('table/views/basemap_chooser_dialog')();
    },

    /**
     * Check enter keydown
     */
    _checkEnter: function(ev) {
      // If it is a enter... nothing
      var code = (ev.keyCode ? ev.keyCode : ev.which);
      if(code == 13) {
        this.killEvent(ev);
        this.ok();
      }
    },

    /**
     * If the url is not valid
     */
    _errorChooser: function(e) {
      var $input = this.$el.find("input");

      // End loader
      this._hideLoader();

      // Show error
      $input.addClass("error");
      this.$el.find("div.error").addClass("active");
      // Enable input
      $input.attr("disabled");

      // Enable dialog? nop!
      this.$el.find("a.button.ok").removeClass("disabled");
      this.enable = true;
    },

    /**
     * If the url is valid
     */
    _successChooser: function(data) {
      // End loader
      this._hideLoader();

      var layer = new cdb.admin.TileLayer({
        urlTemplate:  data.tiles[0],
        attribution:  data.attribution || null,
        maxZoom:      data.maxzoom || 21,
        minZoom:      data.minzoom || 0,
        name:         data.name || ''
      });
      // do not save before add because the layer collection
      // has the correct url
      this.options.baseLayers.add(layer);
      layer.save();

      // Remove error
      this.$el.find("input").removeClass("error");
      this.$el.find(".error").removeClass("active");

      this.hide();
      this.options.ok && this.options.ok(layer);
    },

    /**
     * Show loader
     */
    _showLoader: function() {
      // Get spin target
      this.$el.find("div.loader").show();
    },

    /**
     * Hide loader
     */
    _hideLoader: function() {
      this.$el.find("div.loader").hide();
    },

    /**
     * return a https url if the current application is loaded form https
     */
    _fixHttps: function(url, loc) {
      loc = loc || location;

      // fix the url to https or http
     if(url.search('https') !== 0 && loc.protocol === 'https:') {
        // search for mapping
        var i = url.indexOf('mapbox.com');
        if(i != -1) {
          return this.MAPBOX_HTTPS + url.substr(i + 'mapbox.com'.length);
        }
        return url.replace(/http/, 'https');
      }
      return url;
    },

    /**
     * this function has to check the url is correct and try to get
     * the tilejson
     */
    _checkTileJson: function(ev) {

      var $input =  this.$el.find('input')
        , url =     $input.val()
        , self =    this
        , type =    'json';

      // Remove error
      $input.removeClass("error");
      this.$el.find(".error").removeClass("active");
      // Start loader
      this._showLoader();
      // Disable input
      $input.attr("disabled");

      // Check if it is xyz
      if ( url.search("{x}") != -1) {
        url = url.replace(/\{x\}/g,"0").replace(/\{y\}/g,"0").replace(/\{z\}/g,"0")
        type = 'xyz';
      }

      url = this._fixHttps(url);

      if (type == "xyz") {
        // Type xyz
        var image = new Image();
        image.onload = function(e) {
          self._successChooser({
            tiles: [$input.val()]
          })
        }
        image.onerror = this._errorChooser;
        image.src = url;
      } else {
        // Type json
        $.ajax({
          type: "GET",
          url: url,
          dataType: 'jsonp',
          success: this._successChooser,
          error: this._errorChooser
        });
      }
    },

    /**
     * Click on OK button
     */
    ok: function(ev) {
      if (ev && ev.preventDefault) ev.preventDefault();

      if (this.enable) {
        this.enable = false;
        this._checkTileJson();
      }
    }
  });
})();
<|endoftext|>


<commit_before>/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var isFunction = require( '@stdlib/assert/is-function' );
var RE_NATIVE_FUNCTION = require( '@stdlib/regexp/native-function' );

// VARIABLES //

// Using `Function#toString` bypasses a value's own `toString` method to provide an extra, but not security proof, precaution to prevent a provided function from impersonating a native function:
var fcn2str = Function.prototype.toString;

// MAIN //

/**
* Tests if a value is a native function.
*
* @param {*} value - value to test
* @returns {boolean} boolean indicating whether value is a native function
*
* @example
* function beep() {
*     console.log( 'boop' );
* }
*
* var bool = isNativeFunction( beep );
* // returns false
*
* bool = isNativeFunction( Date );
* // returns true
*/
function isNativeFunction( value ) {
	return (
		isFunction( value ) &&
		RE_NATIVE_FUNCTION.test( fcn2str.call( value ) )
	);
}

// EXPORTS //

module.exports = isNativeFunction;
<commit_msg>Fix regular expression usage<commit_after>/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var isFunction = require( '@stdlib/assert/is-function' );
var reNativeFunction = require( '@stdlib/regexp/native-function' );

// VARIABLES //

// Using `Function#toString` bypasses a value's own `toString` method to provide an extra, but not security proof, precaution to prevent a provided function from impersonating a native function:
var fcn2str = Function.prototype.toString;

// MAIN //

/**
* Tests if a value is a native function.
*
* @param {*} value - value to test
* @returns {boolean} boolean indicating whether value is a native function
*
* @example
* function beep() {
*     console.log( 'boop' );
* }
*
* var bool = isNativeFunction( beep );
* // returns false
*
* bool = isNativeFunction( Date );
* // returns true
*/
function isNativeFunction( value ) {
	return (
		isFunction( value ) &&
		reNativeFunction.REGEXP.test( fcn2str.call( value ) )
	);
}

// EXPORTS //

module.exports = isNativeFunction;
<|endoftext|>


<commit_before>'use strict';

var round = require( '@stdlib/math/base/special/round' );
var randu = require( '@stdlib/math/base/random/randu' );
var daxpy = require( './../lib' ).wasm();

var xbytes;
var ybytes;
var x;
var y;
var i;

// Allocate space on the heap:
xbytes = daxpy.malloc( 100 * 8 ); // 8 bytes per double
ybytes = daxpy.malloc( 100 * 8 );

// Create Float64Array views:
x = new Float64Array( xbytes.buffer, xbytes.byteOffset, 100 );
y = new Float64Array( ybytes.buffer, ybytes.byteOffset, 100 );

// Set data on the heap...
for ( i = 0; i < x.length; i++ ) {
	x[ i ] = round( randu()*100.0 );
	y[ i ] = round( randu()*10.0 );
}
console.log( daxpy( x.length, 5.0, xbytes, 1, 0, ybytes, -1, y.length-1 ) );

// Free allocated memory:
daxpy.free( xbytes );
daxpy.free( ybytes );
<commit_msg>Update example<commit_after>'use strict';

var round = require( '@stdlib/math/base/special/round' );
var randu = require( '@stdlib/math/base/random/randu' );
var daxpy = require( './../lib' ).wasm();

var xbytes;
var ybytes;
var x;
var y;
var i;

// Allocate space on the heap:
xbytes = daxpy.malloc( 100 * 8 ); // 8 bytes per double
ybytes = daxpy.malloc( 100 * 8 );

// Create Float64Array views:
x = new Float64Array( xbytes.buffer, xbytes.byteOffset, 100 );
y = new Float64Array( ybytes.buffer, ybytes.byteOffset, 100 );

// Set data on the heap...
for ( i = 0; i < x.length; i++ ) {
	x[ i ] = round( randu()*100.0 );
	y[ i ] = round( randu()*10.0 );
}
console.log( x );
console.log( y );

daxpy( x.length, 5.0, xbytes, 1, 0, ybytes, -1, y.length-1 );
console.log( y );

// Free allocated memory:
daxpy.free( xbytes );
daxpy.free( ybytes );
<|endoftext|>


<commit_before>import React, {Component} from 'react'
import t from 't-component'
import Timeago from 'lib/site/timeago'
import userConnector from 'lib/site/connectors/user'
import CommentReplies from './replies/component'

export default function CommentsList (props) {
  const comments = props.comments || []
  return (

      {
        comments.map((item) => {
          const handlers = {
            onUnvote: () => props.onUnvote(item.id),
            onUpvote: () => props.onUpvote(item.id),
            onDownvote: () => props.onDownvote(item.id)
          }

          return (
            <Comment
              key={item.id}
              comment={item}
              onReply={props.onReply}
              commentsReplying={props.commentsReplying}
              onDelete={props.onDelete}
              commentDeleting={props.commentDeleting}
              {...handlers}
              forum={props.forum} />
          )
        })
      }

  )
}

const Comment = userConnector(class extends Component {
  constructor (props) {
    super(props)
    this.state = {
      repliesVisibility: false,
      deleteConfirmationVisibility: false,
      showTooltip: false
    }
  }

  handleUnvote = (evt) => {
    evt.currentTarget.classList.remove('active')
    this.props.onUnvote()
  }

  handleUpvote = (evt) => {
    evt.currentTarget.classList.add('active')
    this.props.onUpvote()
  }

  handleDownvote = (evt) => {
    evt.currentTarget.classList.add('active')
    this.props.onDownvote()
  }

  handleToggleReplies = () => {
    this.setState({repliesVisibility: !this.state.repliesVisibility})
  }

  handleToggleTooltip = () => {
    this.setState({showTooltip: !this.state.showTooltip})
  }

  handleToggleDeleteConfirmation = () => {
    this.setState({deleteConfirmationVisibility: !this.state.deleteConfirmationVisibility})
  }

  handleDelete = () => {
    this.props.onDelete({id: this.props.comment.id})
  }

  render () {
    const {
      comment,
      user,
      forum
    } = this.props

    if (user.state.pending) return null

    const {upvoted, downvoted} = (comment.currentUser || {})
    const userAttrs = user.state.value || {}
    const isOwner = userAttrs.id === comment.author.id

    return (
      <article className='comments-list-item' id={`comment-${comment.id}`}>

{comment.author.displayName}

            <Timeago date={comment.createdAt} />

          {
            (isOwner || forum.privileges.canEdit) &&
            (

                <button className='arrow'>

                </button>
                {
                  this.state.showTooltip &&
                  (

                      <button onClick={this.handleToggleDeleteConfirmation}>
                        {t('comment-card.remove-argument')}
                      </button>

                  )
                }

            )
          }

              {comment.score}
              {' '}
              {t('comments.votes')}

            {!isOwner && (
              <button
                className={`upvote ${upvoted ? 'active' : ''}`}
                onClick={upvoted ? this.handleUnvote : this.handleUpvote}>

              </button>
            )}
            {!isOwner && (
              <button
                className={`downvote ${downvoted ? 'active' : ''}`}
                onClick={downvoted ? this.handleUnvote : this.handleDownvote}>

              </button>
            )}

            {!!comment.repliesCount && (

                {comment.repliesCount}
                {' '}

                  {
                    comment.repliesCount === 1 ?
                      t('comments.reply') :
                      t('comments.replies')
                  }

            )}
            <button
              className='reply'
              title={t('comments.arguments.reply')}
              onClick={this.handleToggleReplies}>

            </button>

        <CommentReplies
          commentId={comment.id}
          replies={comment.replies}
          forum={forum}
          user={user}
          repliesVisibility={this.state.repliesVisibility}
          onReply={this.props.onReply}
          commentsReplying={this.props.commentsReplying} />
        {
          this.state.deleteConfirmationVisibility && (

{t('comments.arguments.confirm-remove')}

                <button
                  onClick={this.handleToggleDeleteConfirmation}
                  className='btn btn-sm btn-default'>
                  {t('common.cancel')}
                </button>
                <button
                  onClick={this.handleDelete}
                  className='btn btn-sm btn-danger'>
                  {t('common.ok')}
                </button>

          )
        }

    )
  }
})
<commit_msg>update comments voting label<commit_after>import React, {Component} from 'react'
import t from 't-component'
import Timeago from 'lib/site/timeago'
import userConnector from 'lib/site/connectors/user'
import CommentReplies from './replies/component'

export default function CommentsList (props) {
  const comments = props.comments || []
  return (

      {
        comments.map((item) => {
          const handlers = {
            onUnvote: () => props.onUnvote(item.id),
            onUpvote: () => props.onUpvote(item.id),
            onDownvote: () => props.onDownvote(item.id)
          }

          return (
            <Comment
              key={item.id}
              comment={item}
              onReply={props.onReply}
              commentsReplying={props.commentsReplying}
              onDelete={props.onDelete}
              commentDeleting={props.commentDeleting}
              {...handlers}
              forum={props.forum} />
          )
        })
      }

  )
}

const Comment = userConnector(class extends Component {
  constructor (props) {
    super(props)
    this.state = {
      repliesVisibility: false,
      deleteConfirmationVisibility: false,
      showTooltip: false
    }
  }

  handleUnvote = (evt) => {
    evt.currentTarget.classList.remove('active')
    this.props.onUnvote()
  }

  handleUpvote = (evt) => {
    evt.currentTarget.classList.add('active')
    this.props.onUpvote()
  }

  handleDownvote = (evt) => {
    evt.currentTarget.classList.add('active')
    this.props.onDownvote()
  }

  handleToggleReplies = () => {
    this.setState({repliesVisibility: !this.state.repliesVisibility})
  }

  handleToggleTooltip = () => {
    this.setState({showTooltip: !this.state.showTooltip})
  }

  handleToggleDeleteConfirmation = () => {
    this.setState({deleteConfirmationVisibility: !this.state.deleteConfirmationVisibility})
  }

  handleDelete = () => {
    this.props.onDelete({id: this.props.comment.id})
  }

  render () {
    const {
      comment,
      user,
      forum
    } = this.props

    if (user.state.pending) return null

    const {upvoted, downvoted} = (comment.currentUser || {})
    const userAttrs = user.state.value || {}
    const isOwner = userAttrs.id === comment.author.id

    return (
      <article className='comments-list-item' id={`comment-${comment.id}`}>

{comment.author.displayName}

            <Timeago date={comment.createdAt} />

          {
            (isOwner || forum.privileges.canEdit) &&
            (

                <button className='arrow'>

                </button>
                {
                  this.state.showTooltip &&
                  (

                      <button onClick={this.handleToggleDeleteConfirmation}>
                        {t('comment-card.remove-argument')}
                      </button>

                  )
                }

            )
          }

              {comment.score}
              {' '}

            {!isOwner && (
              <button
                className={`upvote ${upvoted ? 'active' : ''}`}
                onClick={upvoted ? this.handleUnvote : this.handleUpvote}>

              </button>
            )}
            {!isOwner && (
              <button
                className={`downvote ${downvoted ? 'active' : ''}`}
                onClick={downvoted ? this.handleUnvote : this.handleDownvote}>

              </button>
            )}

            {!!comment.repliesCount && (

                {comment.repliesCount}
                {' '}

                  {
                    comment.repliesCount === 1 ?
                      t('comments.reply') :
                      t('comments.replies')
                  }

            )}
            <button
              className='reply'
              title={t('comments.arguments.reply')}
              onClick={this.handleToggleReplies}>

            </button>

        <CommentReplies
          commentId={comment.id}
          replies={comment.replies}
          forum={forum}
          user={user}
          repliesVisibility={this.state.repliesVisibility}
          onReply={this.props.onReply}
          commentsReplying={this.props.commentsReplying} />
        {
          this.state.deleteConfirmationVisibility && (

{t('comments.arguments.confirm-remove')}

                <button
                  onClick={this.handleToggleDeleteConfirmation}
                  className='btn btn-sm btn-default'>
                  {t('common.cancel')}
                </button>
                <button
                  onClick={this.handleDelete}
                  className='btn btn-sm btn-danger'>
                  {t('common.ok')}
                </button>

          )
        }

    )
  }
})
<|endoftext|>


<commit_before>import debounce from './Debounce';

class Search {
  constructor() {
    this.retrieve = debounce(this.retrieve.bind(this));
  }

  async retrieve(query) {
    if (!query) return [];
    const response = await fetch(`https://api.npms.io/v2/search?from=0&size=25&q=${query}`);
    const { results, total } = await response.json();
    return results;
  }
}

export default new Search();
<commit_msg>search api: throw response as an error<commit_after>import debounce from './Debounce';

class Search {
  constructor() {
    this.retrieve = debounce(this.retrieve.bind(this));
  }

  async retrieve(query) {
    const response = await fetch(`https://api.npms.io/v2/search?from=0&size=25&q=${query}`);

    if (response.ok) {
      return response.json();
    }

    throw new Error(await response.text());
  }
}

export default new Search();
<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Wizard, Button, Icon } from 'patternfly-react'
import merge from 'lodash/merge'
import { List } from 'immutable'

import * as Actions from '_/actions'
import { generateUnique } from '_/helpers'
import { msg } from '_/intl'
import { createTemplateList } from '_/components/utils'

import BasicSettings from './steps/BasicSettings'
import Networking from './steps/Networking'
import Storage from './steps/Storage'
import SummaryReview from './steps/SummaryReview'

const DEFAULT_STATE = {
  activeStepIndex: 0,
  steps: {
    basic: {
      operatingSystemId: '0', // "Other OS"
      memory: 1024, // MiB
      cpus: 1,
      startOnCreation: false,
      initEnabled: false,
      optimizedFor: 'desktop',
    },
    network: {
      nics: [],
      updated: 0,
    },
    storage: {
      disks: [],
      updated: 0,
    },
  },

  // Creation status tracking to drive how the Review screen looks
  correlationId: null,
}

/**
 * Given the set of clusters and VM templates available to the user, build the initial
 * new VM state for the create wizard.
 */
function getInitialState (clusters, templates, blankTemplateId) {
  // 1 cluster available? select it by default
  let clusterId
  if (clusters.size === 1) {
    clusterId = clusters.first().get('id')
  }

  // 1 template available (Blank only) on the default cluster? set the source to 'iso'
  let provisionSource = 'template'
  let templateId
  if (clusterId) {
    const templateList = createTemplateList(templates, clusterId)
    if (templateList.length === 1 && templateList[0].id === blankTemplateId) {
      provisionSource = 'iso'
    } else {
      templateId = blankTemplateId
    }
  }

  const state = merge(
    {},
    DEFAULT_STATE,
    {
      steps: {
        basic: {
          clusterId,
          provisionSource,
          templateId,
        },
      },
    })
  return state
}

/**
 * Wizard to create a new VM.  Each step in the wizard is a controlled component.  All
 * of the new VM data is held in this component's state until the creation is submitted
 * or the wizard is canceled or closed.
 *
 * Create a new VM from the following sources:
 *   1. ISO (from "scratch")
 *     - Blank Template
 *     - Pick an ISO from the selected Data Center and load as the VM's CD
 *     - Initial boot from CD
 *     - Need to create a disk (unless it is a live CD?, maybe warn on review)
 *     - NIC isn't strictly needed
 *
 *   2. Template
 *     - Pick a template available from the selected Data Center
 *     - Default to __thin__/dependent template use, use __clone__/independent based on
 *       the computer type (desktop == thin, server == clone)
 *         Note: on webadmin, the option is on 'Resource Allocation' advanced options tab
 *         See: https://github.com/oVirt/ovirt-web-ui/issues/882
 *     - Clone permissions? (true if clone is true)
 *     - Boot based on template settings
 *     - Use thin or cloned disks from template (allow adding more and change storage domains)
 *     - NICs default to those from the template (allow adding more)
 */
class CreateVmWizard extends React.Component {
  constructor (props) {
    super(props)

    this.state = getInitialState(props.clusters, props.templates, props.blankTemplateId)
    this.hideAndResetState = this.hideAndResetState.bind(this)
    this.hideAndNavigate = this.hideAndNavigate.bind(this)
    this.handleBasicOnUpdate = this.handleBasicOnUpdate.bind(this)
    this.handleBasicOnExit = this.handleBasicOnExit.bind(this)
    this.handleListOnUpdate = this.handleListOnUpdate.bind(this)
    this.handleCreateVm = this.handleCreateVm.bind(this)
    this.wizardGoToStep = this.wizardGoToStep.bind(this)
    this.wizardClickBack = this.wizardClickBack.bind(this)
    this.wizardClickNext = this.wizardClickNext.bind(this)

    this.wizardStepsMap = {
      basic: {
        title: msg.createVmWizardStepTitleBasic(),
        preventEnter: false,
        preventExit: true,

        render: () => (
          <BasicSettings
            id='create-vm-wizard-basic'
            data={this.state.steps.basic}

            onUpdate={({ valid = false, partialUpdate = {} }) => {
              this.handleBasicOnUpdate(partialUpdate)
              this.wizardStepsMap.basic.preventExit = !valid
            }}
          />
        ),
        onExit: this.handleBasicOnExit,
      },

      network: {
        title: msg.createVmWizardStepTitleNetwork(),
        preventEnter: false,
        preventExit: false,

        render: () => (
          <Networking
            id='create-vm-wizards-net'
            dataCenterId={this.state.steps.basic.dataCenterId}
            clusterId={this.state.steps.basic.clusterId}
            nics={this.state.steps.network.nics}

            onUpdate={({ valid = true, ...updatePayload }) => {
              this.handleListOnUpdate('network', 'nics', updatePayload)
              this.wizardStepsMap.network.preventExit = !valid
            }}
          />
        ),
      },

      storage: {
        title: msg.createVmWizardStepTitleStorage(),
        preventEnter: false,
        preventExit: false,

        render: () => (
          <Storage
            id='create-vm-wizards-storage'
            vmName={this.state.steps.basic.name}
            clusterId={this.state.steps.basic.clusterId}
            dataCenterId={this.state.steps.basic.dataCenterId}
            optimizedFor={this.state.steps.basic.optimizedFor}
            disks={this.state.steps.storage.disks}

            onUpdate={({ valid = true, ...updatePayload }) => {
              this.handleListOnUpdate('storage', 'disks', updatePayload)
              this.wizardStepsMap.storage.preventExit = !valid
            }}
          />
        ),
      },

      review: {
        title: msg.createVmWizardStepTitleReview(),
        preventEnter: false,
        preventExit: false,

        render: () => {
          const { correlationId } = this.state
          const inProgress = correlationId !== null && !this.props.actionResults.has(correlationId)

          const correlatedResult = correlationId === null || inProgress
            ? undefined
            : this.props.actionResults.get(correlationId) ? 'success' : 'error'

          // Look for any `userMessages` that correlate to the last create action executed
          const correlatedMessages = correlationId === null
            ? null
            : this.props.userMessages
              .get('records')
              .filter(
                record => record.getIn([ 'failedAction', 'meta', 'correlationId' ]) === correlationId
              )
              .map(record => record.get('message'))
              .toJS()

          return <SummaryReview
            id='create-vm-wizard-review'
            basic={this.state.steps.basic}
            network={this.state.steps.network.nics}
            storage={this.state.steps.storage.disks}
            progress={{
              inProgress,
              result: correlatedResult, // undefined (no result yet) | 'success' | 'error'
              messages: correlatedMessages,
            }}
          />
        },
        onExit: () => {
          this.setState({ correlationId: null })
        },
      },
    }
    this.wizardSteps = [
      this.wizardStepsMap.basic,
      this.wizardStepsMap.network,
      this.wizardStepsMap.storage,
      this.wizardStepsMap.review,
    ]
  }

  hideAndResetState () {
    const { onHide, clusters, templates, blankTemplateId } = this.props

    this.setState(getInitialState(clusters, templates, blankTemplateId))
    merge(this.wizardStepsMap, {
      basic: {
        preventEnter: false,
        preventExit: true,
      },
      network: {
        preventEnter: false,
        preventExit: false,
      },
      storage: {
        preventEnter: false,
        preventExit: false,
      },
      review: {
        preventEnter: false,
        preventExit: false,
      },
    })

    onHide()
  }

  hideAndNavigate () {
    const vmId = this.props.actionResults.get(this.state.correlationId)
    this.hideAndResetState()
    this.props.navigateToVm(vmId)
  }

  handleBasicOnUpdate (partialUpdate) {
    const { provisionSource, templateId } = this.state.steps.basic
    const { provisionSource_, templateId_ } = partialUpdate

    this.setState(state => {
      const extraUpdates = {}
      if (provisionSource !== provisionSource_ || templateId !== templateId_) {
        // reset network and storage updates if the provision source or selected template changes
        extraUpdates.network = merge({}, state.steps.network, { updated: 0 })
        extraUpdates.storage = merge({}, state.steps.storage, { updated: 0 })
      }

      return {
        steps: {
          ...state.steps,
          basic: {
            ...state.steps.basic,
            ...partialUpdate,
          },
          ...extraUpdates,
        },
      }
    })
  }

  /**
   * Before transitioning off the Basic page, setup the NICs and Disks as appropriate
   * based on what has been selected/changed.
   */
  handleBasicOnExit () {
    const resetNics = this.state.steps.network.updated === 0
    const resetDisks = this.state.steps.storage.updated === 0

    if (resetNics || resetDisks) {
      this.setState(state => {
        const template = this.props.templates.get(this.state.steps.basic.templateId)
        const update = { steps: { ...state.steps } }

        if (resetNics) {
          update.steps.network = {
            updated: (state.steps.network.updated + 1),
            nics: !template
              ? []
              : template.get('nics', List())
                .map(nic => ({
                  id: nic.get('id'),
                  name: nic.get('name'),
                  vnicProfileId: nic.getIn(['vnicProfile', 'id']),
                  deviceType: nic.get('interface'),
                  isFromTemplate: true,
                }))
                .toJS(),
          }
        }

        if (resetDisks) {
          update.steps.storage = {
            updated: (state.steps.storage.updated + 1),
            disks: !template
              ? []
              : template.get('disks', List())
                .map(disk => ({
                  id: disk.get('attachmentId'),
                  name: disk.get('name'),

                  diskId: disk.get('id'),
                  storageDomainId: disk.get('storageDomainId'),

                  bootable: disk.get('bootable'),
                  iface: disk.get('iface'),
                  type: disk.get('type'), // [ image | lun | cinder ]
                  format: disk.get('format'), // [ cow | raw ]
                  size: disk.get('provisionedSize'), // bytes
                  isFromTemplate: true,
                }))
                .toJS(),
          }
        }

        return update
      })
    }
  }

  handleListOnUpdate (stepName, listName, { remove, update, create }, setStateCallback) {
    this.setState(state => {
      let list = this.state.steps[stepName][listName].slice(0)

      if (remove) {
        list = list.filter(item => item.id !== remove)
      }

      if (update) {
        const toUpdate = list.findIndex(item => item.id === update.id)
        if (toUpdate >= 0) {
          list[toUpdate] = { ...list[toUpdate], ...update }
        }
      }

      if (create) {
        list.push(create)
      }

      const newState = {
        steps: {
          ...state.steps,
          [stepName]: {
            ...state.steps[stepName],
            [listName]: list,
            updated: (state.steps[stepName].updated + 1),
          },
        },
      }
      return newState
    },
    setStateCallback)
  }

  handleCreateVm () {
    const correlationId = generateUnique('CreateVmWizard_')
    const { basic, network: { nics }, storage: { disks } } = this.state.steps

    this.setState({ correlationId })
    this.props.onCreate(basic, nics, disks, correlationId)
  }

  wizardGoToStep (newStepIndex) {
    const { activeStepIndex } = this.state
    const activeStep = this.wizardSteps[activeStepIndex]
    const newStep = this.wizardSteps[newStepIndex]

    // make sure we can leave the current step and enter the new step
    if (activeStep.preventExit || newStep.preventEnter) return

    // run and the current step's `onExit()`
    if (activeStep.onExit) {
      activeStep.onExit()
    }

    this.setState({ activeStepIndex: newStepIndex })
  }

  wizardClickBack () {
    this.wizardGoToStep(Math.max(this.state.activeStepIndex - 1, 0))
  }

  wizardClickNext () {
    this.wizardGoToStep(Math.min(this.state.activeStepIndex + 1, this.wizardSteps.length - 1))
  }

  render () {
    const { activeStepIndex, correlationId } = this.state
    const activeStep = this.wizardSteps[activeStepIndex]
    const vmCreateWorking = correlationId !== null && !this.props.actionResults.has(correlationId)
    const vmCreateStarted = correlationId !== null && !!this.props.actionResults.get(correlationId)

    const isReviewStep = this.wizardSteps[activeStepIndex] === this.wizardStepsMap.review
    const isPrimaryNext = !isReviewStep
    const isPrimaryCreate = isReviewStep && !vmCreateStarted
    const isPrimaryClose = isReviewStep && vmCreateStarted

    return <Wizard
      dialogClassName='modal-lg wizard-pf'
      show={this.props.show}
      onHide={this.hideAndResetState}
    >
      <Wizard.Header onClose={this.hideAndResetState} title={msg.addNewVm()} />
      <Wizard.Body>
        <Wizard.Pattern.Body
          steps={this.wizardSteps}
          activeStepIndex={activeStepIndex}
          activeStepStr={(activeStepIndex + 1).toString()}
          goToStep={this.wizardGoToStep}
        />
      </Wizard.Body>
      <Wizard.Footer>
        <Button bsStyle='default' onClick={this.hideAndResetState}>
          { msg.createVmWizardButtonCancel() }
        </Button>
        <Button bsStyle='default' onClick={this.wizardClickBack} disabled={activeStepIndex === 0 || isPrimaryClose}>
          <Icon type='fa' name='angle-left' />
          { msg.createVmWizardButtonBack() }
        </Button>

        { isPrimaryClose &&
          <Button onClick={this.hideAndResetState}>
            { msg.createVmWizardButtonClose() }
          </Button>
        }
        <Button
          bsStyle='primary'
          onClick={
            isPrimaryNext ? this.wizardClickNext
              : isPrimaryCreate ? this.handleCreateVm
                : this.hideAndNavigate
          }
          disabled={activeStep.preventExit || vmCreateWorking}
        >
          { isPrimaryNext && msg.createVmWizardButtonNext() }
          { isPrimaryCreate && msg.createVmWizardButtonCreate() }
          { isPrimaryClose && msg.createVmWizardButtonCloseAndNavigate() }
          <Icon type='fa' name='angle-right' />
        </Button>
      </Wizard.Footer>
    </Wizard>
  }
}
CreateVmWizard.propTypes = {
  show: PropTypes.bool,
  onHide: PropTypes.func,

  clusters: PropTypes.object.isRequired,
  templates: PropTypes.object.isRequired,
  blankTemplateId: PropTypes.string.isRequired,
  userMessages: PropTypes.object.isRequired,
  actionResults: PropTypes.object.isRequired,

  onCreate: PropTypes.func,
  navigateToVm: PropTypes.func,
}

export default connect(
  (state) => ({
    clusters: state.clusters,
    templates: state.templates,
    blankTemplateId: state.config.get('blankTemplateId'),
    userMessages: state.userMessages,
    actionResults: state.vms.get('correlationResult'),
  }),
  (dispatch) => ({
    onCreate: (basic, nics, disks, correlationId) => dispatch(
      Actions.composeAndCreateVm({ basic, nics, disks }, { correlationId })
    ),
    navigateToVm: (vmId) => dispatch(Actions.navigateToVmDetails(vmId)),
  })
)(CreateVmWizard)
<commit_msg>Fix error in Create Wizard with handleBasicOnUpdate<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Wizard, Button, Icon } from 'patternfly-react'
import merge from 'lodash/merge'
import { List } from 'immutable'

import * as Actions from '_/actions'
import { generateUnique } from '_/helpers'
import { msg } from '_/intl'
import { createTemplateList } from '_/components/utils'

import BasicSettings from './steps/BasicSettings'
import Networking from './steps/Networking'
import Storage from './steps/Storage'
import SummaryReview from './steps/SummaryReview'

const DEFAULT_STATE = {
  activeStepIndex: 0,
  steps: {
    basic: {
      operatingSystemId: '0', // "Other OS"
      memory: 1024, // MiB
      cpus: 1,
      startOnCreation: false,
      initEnabled: false,
      optimizedFor: 'desktop',
    },
    network: {
      nics: [],
      updated: 0,
    },
    storage: {
      disks: [],
      updated: 0,
    },
  },

  // Creation status tracking to drive how the Review screen looks
  correlationId: null,
}

/**
 * Given the set of clusters and VM templates available to the user, build the initial
 * new VM state for the create wizard.
 */
function getInitialState (clusters, templates, blankTemplateId) {
  // 1 cluster available? select it by default
  let clusterId
  if (clusters.size === 1) {
    clusterId = clusters.first().get('id')
  }

  // 1 template available (Blank only) on the default cluster? set the source to 'iso'
  let provisionSource = 'template'
  let templateId
  if (clusterId) {
    const templateList = createTemplateList(templates, clusterId)
    if (templateList.length === 1 && templateList[0].id === blankTemplateId) {
      provisionSource = 'iso'
    } else {
      templateId = blankTemplateId
    }
  }

  const state = merge(
    {},
    DEFAULT_STATE,
    {
      steps: {
        basic: {
          clusterId,
          provisionSource,
          templateId,
        },
      },
    })
  return state
}

/**
 * Wizard to create a new VM.  Each step in the wizard is a controlled component.  All
 * of the new VM data is held in this component's state until the creation is submitted
 * or the wizard is canceled or closed.
 *
 * Create a new VM from the following sources:
 *   1. ISO (from "scratch")
 *     - Blank Template
 *     - Pick an ISO from the selected Data Center and load as the VM's CD
 *     - Initial boot from CD
 *     - Need to create a disk (unless it is a live CD?, maybe warn on review)
 *     - NIC isn't strictly needed
 *
 *   2. Template
 *     - Pick a template available from the selected Data Center
 *     - Default to __thin__/dependent template use, use __clone__/independent based on
 *       the computer type (desktop == thin, server == clone)
 *         Note: on webadmin, the option is on 'Resource Allocation' advanced options tab
 *         See: https://github.com/oVirt/ovirt-web-ui/issues/882
 *     - Clone permissions? (true if clone is true)
 *     - Boot based on template settings
 *     - Use thin or cloned disks from template (allow adding more and change storage domains)
 *     - NICs default to those from the template (allow adding more)
 */
class CreateVmWizard extends React.Component {
  constructor (props) {
    super(props)

    this.state = getInitialState(props.clusters, props.templates, props.blankTemplateId)
    this.hideAndResetState = this.hideAndResetState.bind(this)
    this.hideAndNavigate = this.hideAndNavigate.bind(this)
    this.handleBasicOnUpdate = this.handleBasicOnUpdate.bind(this)
    this.handleBasicOnExit = this.handleBasicOnExit.bind(this)
    this.handleListOnUpdate = this.handleListOnUpdate.bind(this)
    this.handleCreateVm = this.handleCreateVm.bind(this)
    this.wizardGoToStep = this.wizardGoToStep.bind(this)
    this.wizardClickBack = this.wizardClickBack.bind(this)
    this.wizardClickNext = this.wizardClickNext.bind(this)

    this.wizardStepsMap = {
      basic: {
        title: msg.createVmWizardStepTitleBasic(),
        preventEnter: false,
        preventExit: true,

        render: () => (
          <BasicSettings
            id='create-vm-wizard-basic'
            data={this.state.steps.basic}

            onUpdate={({ valid = false, partialUpdate = {} }) => {
              this.handleBasicOnUpdate(partialUpdate)
              this.wizardStepsMap.basic.preventExit = !valid
            }}
          />
        ),
        onExit: this.handleBasicOnExit,
      },

      network: {
        title: msg.createVmWizardStepTitleNetwork(),
        preventEnter: false,
        preventExit: false,

        render: () => (
          <Networking
            id='create-vm-wizards-net'
            dataCenterId={this.state.steps.basic.dataCenterId}
            clusterId={this.state.steps.basic.clusterId}
            nics={this.state.steps.network.nics}

            onUpdate={({ valid = true, ...updatePayload }) => {
              this.handleListOnUpdate('network', 'nics', updatePayload)
              this.wizardStepsMap.network.preventExit = !valid
            }}
          />
        ),
      },

      storage: {
        title: msg.createVmWizardStepTitleStorage(),
        preventEnter: false,
        preventExit: false,

        render: () => (
          <Storage
            id='create-vm-wizards-storage'
            vmName={this.state.steps.basic.name}
            clusterId={this.state.steps.basic.clusterId}
            dataCenterId={this.state.steps.basic.dataCenterId}
            optimizedFor={this.state.steps.basic.optimizedFor}
            disks={this.state.steps.storage.disks}

            onUpdate={({ valid = true, ...updatePayload }) => {
              this.handleListOnUpdate('storage', 'disks', updatePayload)
              this.wizardStepsMap.storage.preventExit = !valid
            }}
          />
        ),
      },

      review: {
        title: msg.createVmWizardStepTitleReview(),
        preventEnter: false,
        preventExit: false,

        render: () => {
          const { correlationId } = this.state
          const inProgress = correlationId !== null && !this.props.actionResults.has(correlationId)

          const correlatedResult = correlationId === null || inProgress
            ? undefined
            : this.props.actionResults.get(correlationId) ? 'success' : 'error'

          // Look for any `userMessages` that correlate to the last create action executed
          const correlatedMessages = correlationId === null
            ? null
            : this.props.userMessages
              .get('records')
              .filter(
                record => record.getIn([ 'failedAction', 'meta', 'correlationId' ]) === correlationId
              )
              .map(record => record.get('message'))
              .toJS()

          return <SummaryReview
            id='create-vm-wizard-review'
            basic={this.state.steps.basic}
            network={this.state.steps.network.nics}
            storage={this.state.steps.storage.disks}
            progress={{
              inProgress,
              result: correlatedResult, // undefined (no result yet) | 'success' | 'error'
              messages: correlatedMessages,
            }}
          />
        },
        onExit: () => {
          this.setState({ correlationId: null })
        },
      },
    }
    this.wizardSteps = [
      this.wizardStepsMap.basic,
      this.wizardStepsMap.network,
      this.wizardStepsMap.storage,
      this.wizardStepsMap.review,
    ]
  }

  hideAndResetState () {
    const { onHide, clusters, templates, blankTemplateId } = this.props

    this.setState(getInitialState(clusters, templates, blankTemplateId))
    merge(this.wizardStepsMap, {
      basic: {
        preventEnter: false,
        preventExit: true,
      },
      network: {
        preventEnter: false,
        preventExit: false,
      },
      storage: {
        preventEnter: false,
        preventExit: false,
      },
      review: {
        preventEnter: false,
        preventExit: false,
      },
    })

    onHide()
  }

  hideAndNavigate () {
    const vmId = this.props.actionResults.get(this.state.correlationId)
    this.hideAndResetState()
    this.props.navigateToVm(vmId)
  }

  handleBasicOnUpdate (partialUpdate) {
    const { provisionSource, templateId } = this.state.steps.basic
    const { provisionSource: provisionSource_, templateId: templateId_ } = partialUpdate

    this.setState(state => {
      const extraUpdates = {}
      if (provisionSource !== provisionSource_ || templateId !== templateId_) {
        // reset network and storage updates if the provision source or selected template changes
        extraUpdates.network = merge({}, state.steps.network, { updated: 0 })
        extraUpdates.storage = merge({}, state.steps.storage, { updated: 0 })
      }

      return {
        steps: {
          ...state.steps,
          basic: {
            ...state.steps.basic,
            ...partialUpdate,
          },
          ...extraUpdates,
        },
      }
    })
  }

  /**
   * Before transitioning off the Basic page, setup the NICs and Disks as appropriate
   * based on what has been selected/changed.
   */
  handleBasicOnExit () {
    const resetNics = this.state.steps.network.updated === 0
    const resetDisks = this.state.steps.storage.updated === 0

    if (resetNics || resetDisks) {
      this.setState(state => {
        const template = this.props.templates.get(this.state.steps.basic.templateId)
        const update = { steps: { ...state.steps } }

        if (resetNics) {
          update.steps.network = {
            updated: (state.steps.network.updated + 1),
            nics: !template
              ? []
              : template.get('nics', List())
                .map(nic => ({
                  id: nic.get('id'),
                  name: nic.get('name'),
                  vnicProfileId: nic.getIn(['vnicProfile', 'id']),
                  deviceType: nic.get('interface'),
                  isFromTemplate: true,
                }))
                .toJS(),
          }
        }

        if (resetDisks) {
          update.steps.storage = {
            updated: (state.steps.storage.updated + 1),
            disks: !template
              ? []
              : template.get('disks', List())
                .map(disk => ({
                  id: disk.get('attachmentId'),
                  name: disk.get('name'),

                  diskId: disk.get('id'),
                  storageDomainId: disk.get('storageDomainId'),

                  bootable: disk.get('bootable'),
                  iface: disk.get('iface'),
                  type: disk.get('type'), // [ image | lun | cinder ]
                  format: disk.get('format'), // [ cow | raw ]
                  size: disk.get('provisionedSize'), // bytes
                  isFromTemplate: true,
                }))
                .toJS(),
          }
        }

        return update
      })
    }
  }

  handleListOnUpdate (stepName, listName, { remove, update, create }, setStateCallback) {
    this.setState(state => {
      let list = this.state.steps[stepName][listName].slice(0)

      if (remove) {
        list = list.filter(item => item.id !== remove)
      }

      if (update) {
        const toUpdate = list.findIndex(item => item.id === update.id)
        if (toUpdate >= 0) {
          list[toUpdate] = { ...list[toUpdate], ...update }
        }
      }

      if (create) {
        list.push(create)
      }

      const newState = {
        steps: {
          ...state.steps,
          [stepName]: {
            ...state.steps[stepName],
            [listName]: list,
            updated: (state.steps[stepName].updated + 1),
          },
        },
      }
      return newState
    },
    setStateCallback)
  }

  handleCreateVm () {
    const correlationId = generateUnique('CreateVmWizard_')
    const { basic, network: { nics }, storage: { disks } } = this.state.steps

    this.setState({ correlationId })
    this.props.onCreate(basic, nics, disks, correlationId)
  }

  wizardGoToStep (newStepIndex) {
    const { activeStepIndex } = this.state
    const activeStep = this.wizardSteps[activeStepIndex]
    const newStep = this.wizardSteps[newStepIndex]

    // make sure we can leave the current step and enter the new step
    if (activeStep.preventExit || newStep.preventEnter) return

    // run and the current step's `onExit()`
    if (activeStep.onExit) {
      activeStep.onExit()
    }

    this.setState({ activeStepIndex: newStepIndex })
  }

  wizardClickBack () {
    this.wizardGoToStep(Math.max(this.state.activeStepIndex - 1, 0))
  }

  wizardClickNext () {
    this.wizardGoToStep(Math.min(this.state.activeStepIndex + 1, this.wizardSteps.length - 1))
  }

  render () {
    const { activeStepIndex, correlationId } = this.state
    const activeStep = this.wizardSteps[activeStepIndex]
    const vmCreateWorking = correlationId !== null && !this.props.actionResults.has(correlationId)
    const vmCreateStarted = correlationId !== null && !!this.props.actionResults.get(correlationId)

    const isReviewStep = this.wizardSteps[activeStepIndex] === this.wizardStepsMap.review
    const isPrimaryNext = !isReviewStep
    const isPrimaryCreate = isReviewStep && !vmCreateStarted
    const isPrimaryClose = isReviewStep && vmCreateStarted

    return <Wizard
      dialogClassName='modal-lg wizard-pf'
      show={this.props.show}
      onHide={this.hideAndResetState}
    >
      <Wizard.Header onClose={this.hideAndResetState} title={msg.addNewVm()} />
      <Wizard.Body>
        <Wizard.Pattern.Body
          steps={this.wizardSteps}
          activeStepIndex={activeStepIndex}
          activeStepStr={(activeStepIndex + 1).toString()}
          goToStep={this.wizardGoToStep}
        />
      </Wizard.Body>
      <Wizard.Footer>
        <Button bsStyle='default' onClick={this.hideAndResetState}>
          { msg.createVmWizardButtonCancel() }
        </Button>
        <Button bsStyle='default' onClick={this.wizardClickBack} disabled={activeStepIndex === 0 || isPrimaryClose}>
          <Icon type='fa' name='angle-left' />
          { msg.createVmWizardButtonBack() }
        </Button>

        { isPrimaryClose &&
          <Button onClick={this.hideAndResetState}>
            { msg.createVmWizardButtonClose() }
          </Button>
        }
        <Button
          bsStyle='primary'
          onClick={
            isPrimaryNext ? this.wizardClickNext
              : isPrimaryCreate ? this.handleCreateVm
                : this.hideAndNavigate
          }
          disabled={activeStep.preventExit || vmCreateWorking}
        >
          { isPrimaryNext && msg.createVmWizardButtonNext() }
          { isPrimaryCreate && msg.createVmWizardButtonCreate() }
          { isPrimaryClose && msg.createVmWizardButtonCloseAndNavigate() }
          <Icon type='fa' name='angle-right' />
        </Button>
      </Wizard.Footer>
    </Wizard>
  }
}
CreateVmWizard.propTypes = {
  show: PropTypes.bool,
  onHide: PropTypes.func,

  clusters: PropTypes.object.isRequired,
  templates: PropTypes.object.isRequired,
  blankTemplateId: PropTypes.string.isRequired,
  userMessages: PropTypes.object.isRequired,
  actionResults: PropTypes.object.isRequired,

  onCreate: PropTypes.func,
  navigateToVm: PropTypes.func,
}

export default connect(
  (state) => ({
    clusters: state.clusters,
    templates: state.templates,
    blankTemplateId: state.config.get('blankTemplateId'),
    userMessages: state.userMessages,
    actionResults: state.vms.get('correlationResult'),
  }),
  (dispatch) => ({
    onCreate: (basic, nics, disks, correlationId) => dispatch(
      Actions.composeAndCreateVm({ basic, nics, disks }, { correlationId })
    ),
    navigateToVm: (vmId) => dispatch(Actions.navigateToVmDetails(vmId)),
  })
)(CreateVmWizard)
<|endoftext|>


<commit_before>(function() {
  'use strict';

  /**
   * @ngdoc service
   * @name $mdDateLocaleProvider
   * @module material.components.datepicker
   *
   * @description
   * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
   * This provider that allows the user to specify messages, formatters, and parsers for date
   * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
   * components that that deal with dates.
   *
   * @property {(Array<string>)=} months Array of month names (in order).
   * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
   * @property {(Array<string>)=} days Array of the days of the week (in order).
   * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
   * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
   *     using a numeral system other than [1, 2, 3...].
   * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
   *    etc.
   * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
   * @property {(function(Date): string)=} formatDate Function to format a date object to a string.
   * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
   *     a month given a date.
   * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
   *     a week given the week number.
   * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
   * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
   *     current locale.
   *
   * @usage
   * <hljs lang="js">
   *   myAppModule.config(function($mdDateLocaleProvider) {
   *
   *     // Example of a French localization.
   *     $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
   *     $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
   *     $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
   *     $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
   *
   *     // Can change week display to start on Monday.
   *     $mdDateLocaleProvider.firstDayOfWeek = 1;
   *
   *     // Optional.
   *     $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
   *
   *     // Example uses moment.js to parse and format dates.
   *     $mdDateLocaleProvider.parseDate = function(dateString) {
   *       var m = moment(dateString, 'L', true);
   *       return m.isValid() ? m.toDate() : new Date(NaN);
   *     };
   *
   *     $mdDateLocaleProvider.formatDate = function(date) {
   *       return moment(date).format('L');
   *     };
   *
   *     $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
   *       return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
   *     };
   *
   *     // In addition to date display, date components also need localized messages
   *     // for aria-labels for screen-reader users.
   *
   *     $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
   *       return 'Semaine ' + weekNumber;
   *     };
   *
   *     $mdDateLocaleProvider.msgCalendar = 'Calendrier';
   *     $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
   *
   * });
   * </hljs>
   *
   */

  angular.module('material.components.datepicker').config(function($provide) {
    // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.

    /** @constructor */
    function DateLocaleProvider() {
      /** Array of full month names. E.g., ['January', 'Febuary', ...] */
      this.months = null;

      /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
      this.shortMonths = null;

      /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
      this.days = null;

      /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
      this.shortDays = null;

      /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
      this.dates = null;

      /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
      this.firstDayOfWeek = 0;

      /**
       * Function that converts the date portion of a Date to a string.
       * @type {(function(Date): string)}
       */
      this.formatDate = null;

      /**
       * Function that converts a date string to a Date object (the date portion)
       * @type {function(string): Date}
       */
      this.parseDate = null;

      /**
       * Function that formats a Date into a month header string.
       * @type {function(Date): string}
       */
      this.monthHeaderFormatter = null;

      /**
       * Function that formats a week number into a label for the week.
       * @type {function(number): string}
       */
      this.weekNumberFormatter = null;

      /**
       * Function that formats a date into a long aria-label that is read
       * when the focused date changes.
       * @type {function(Date): string}
       */
      this.longDateFormatter = null;

      /**
       * ARIA label for the calendar "dialog" used in the datepicker.
       * @type {string}
       */
      this.msgCalendar = '';

      /**
       * ARIA label for the datepicker's "Open calendar" buttons.
       * @type {string}
       */
      this.msgOpenCalendar = '';
    }

    /**
     * Factory function that returns an instance of the dateLocale service.
     * @ngInject
     * @param $locale
     * @returns {DateLocale}
     */
    DateLocaleProvider.prototype.$get = function($locale) {
      /**
       * Default date-to-string formatting function.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultFormatDate(date) {
        if (!date) {
          return '';
        }

        // All of the dates created through ng-material *should* be set to midnight.
        // If we encounter a date where the localeTime shows at 11pm instead of midnight,
        // we have run into an issue with DST where we need to increment the hour by one:
        // var d = new Date(1992, 9, 8, 0, 0, 0);
        // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
        var localeTime = date.toLocaleTimeString();
        var formatDate = date;
        if (date.getHours() == 0 &&
            (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
          formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
        }

        return formatDate.toLocaleDateString();
      }

      /**
       * Default string-to-date parsing function.
       * @param {string} dateString
       * @returns {!Date}
       */
      function defaultParseDate(dateString) {
        return new Date(dateString);
      }

      /**
       * Default function to determine whether a string makes sense to be
       * parsed to a Date object.
       *
       * This is very permissive and is just a basic sanity check to ensure that
       * things like single integers aren't able to be parsed into dates.
       * @param {string} dateString
       * @returns {boolean}
       */
      function defaultIsDateComplete(dateString) {
        dateString = dateString.trim();

        // Looks for three chunks of content (either numbers or text) separated
        // by delimiters.
        var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
        return re.test(dateString);
      }

      /**
       * Default date-to-string formatter to get a month header.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultMonthHeaderFormatter(date) {
        return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
      }

      /**
       * Default week number formatter.
       * @param number
       * @returns {string}
       */
      function defaultWeekNumberFormatter(number) {
        return 'Week ' + number;
      }

      /**
       * Default formatter for date cell aria-labels.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultLongDateFormatter(date) {
        // Example: 'Thursday June 18 2015'
        return [
          service.days[date.getDay()],
          service.months[date.getMonth()],
          service.dates[date.getDate()],
          date.getFullYear()
        ].join(' ');
      }

      // The default "short" day strings are the first character of each day,
      // e.g., "Monday" => "M".
      var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) {
        return day[0];
      });

      // The default dates are simply the numbers 1 through 31.
      var defaultDates = Array(32);
      for (var i = 1; i <= 31; i++) {
        defaultDates[i] = i;
      }

      // Default ARIA messages are in English (US).
      var defaultMsgCalendar = 'Calendar';
      var defaultMsgOpenCalendar = 'Open calendar';

      var service = {
        months: this.months || $locale.DATETIME_FORMATS.MONTH,
        shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
        days: this.days || $locale.DATETIME_FORMATS.DAY,
        shortDays: this.shortDays || defaultShortDays,
        dates: this.dates || defaultDates,
        firstDayOfWeek: this.firstDayOfWeek || 0,
        formatDate: this.formatDate || defaultFormatDate,
        parseDate: this.parseDate || defaultParseDate,
        isDateComplete: this.isDateComplete || defaultIsDateComplete,
        monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
        weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
        longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
        msgCalendar: this.msgCalendar || defaultMsgCalendar,
        msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar
      };

      return service;
    };

    $provide.provider('$mdDateLocale', new DateLocaleProvider());
  });
})();
<commit_msg>docs(datepicker): fix typo in dateLocaleProvider<commit_after>(function() {
  'use strict';

  /**
   * @ngdoc service
   * @name $mdDateLocaleProvider
   * @module material.components.datepicker
   *
   * @description
   * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
   * This provider that allows the user to specify messages, formatters, and parsers for date
   * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
   * components that deal with dates.
   *
   * @property {(Array<string>)=} months Array of month names (in order).
   * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
   * @property {(Array<string>)=} days Array of the days of the week (in order).
   * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
   * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
   *     using a numeral system other than [1, 2, 3...].
   * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
   *    etc.
   * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
   * @property {(function(Date): string)=} formatDate Function to format a date object to a string.
   * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
   *     a month given a date.
   * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
   *     a week given the week number.
   * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
   * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
   *     current locale.
   *
   * @usage
   * <hljs lang="js">
   *   myAppModule.config(function($mdDateLocaleProvider) {
   *
   *     // Example of a French localization.
   *     $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
   *     $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
   *     $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
   *     $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
   *
   *     // Can change week display to start on Monday.
   *     $mdDateLocaleProvider.firstDayOfWeek = 1;
   *
   *     // Optional.
   *     $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
   *
   *     // Example uses moment.js to parse and format dates.
   *     $mdDateLocaleProvider.parseDate = function(dateString) {
   *       var m = moment(dateString, 'L', true);
   *       return m.isValid() ? m.toDate() : new Date(NaN);
   *     };
   *
   *     $mdDateLocaleProvider.formatDate = function(date) {
   *       return moment(date).format('L');
   *     };
   *
   *     $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
   *       return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
   *     };
   *
   *     // In addition to date display, date components also need localized messages
   *     // for aria-labels for screen-reader users.
   *
   *     $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
   *       return 'Semaine ' + weekNumber;
   *     };
   *
   *     $mdDateLocaleProvider.msgCalendar = 'Calendrier';
   *     $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
   *
   * });
   * </hljs>
   *
   */

  angular.module('material.components.datepicker').config(function($provide) {
    // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.

    /** @constructor */
    function DateLocaleProvider() {
      /** Array of full month names. E.g., ['January', 'Febuary', ...] */
      this.months = null;

      /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
      this.shortMonths = null;

      /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
      this.days = null;

      /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
      this.shortDays = null;

      /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
      this.dates = null;

      /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
      this.firstDayOfWeek = 0;

      /**
       * Function that converts the date portion of a Date to a string.
       * @type {(function(Date): string)}
       */
      this.formatDate = null;

      /**
       * Function that converts a date string to a Date object (the date portion)
       * @type {function(string): Date}
       */
      this.parseDate = null;

      /**
       * Function that formats a Date into a month header string.
       * @type {function(Date): string}
       */
      this.monthHeaderFormatter = null;

      /**
       * Function that formats a week number into a label for the week.
       * @type {function(number): string}
       */
      this.weekNumberFormatter = null;

      /**
       * Function that formats a date into a long aria-label that is read
       * when the focused date changes.
       * @type {function(Date): string}
       */
      this.longDateFormatter = null;

      /**
       * ARIA label for the calendar "dialog" used in the datepicker.
       * @type {string}
       */
      this.msgCalendar = '';

      /**
       * ARIA label for the datepicker's "Open calendar" buttons.
       * @type {string}
       */
      this.msgOpenCalendar = '';
    }

    /**
     * Factory function that returns an instance of the dateLocale service.
     * @ngInject
     * @param $locale
     * @returns {DateLocale}
     */
    DateLocaleProvider.prototype.$get = function($locale) {
      /**
       * Default date-to-string formatting function.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultFormatDate(date) {
        if (!date) {
          return '';
        }

        // All of the dates created through ng-material *should* be set to midnight.
        // If we encounter a date where the localeTime shows at 11pm instead of midnight,
        // we have run into an issue with DST where we need to increment the hour by one:
        // var d = new Date(1992, 9, 8, 0, 0, 0);
        // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
        var localeTime = date.toLocaleTimeString();
        var formatDate = date;
        if (date.getHours() == 0 &&
            (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
          formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
        }

        return formatDate.toLocaleDateString();
      }

      /**
       * Default string-to-date parsing function.
       * @param {string} dateString
       * @returns {!Date}
       */
      function defaultParseDate(dateString) {
        return new Date(dateString);
      }

      /**
       * Default function to determine whether a string makes sense to be
       * parsed to a Date object.
       *
       * This is very permissive and is just a basic sanity check to ensure that
       * things like single integers aren't able to be parsed into dates.
       * @param {string} dateString
       * @returns {boolean}
       */
      function defaultIsDateComplete(dateString) {
        dateString = dateString.trim();

        // Looks for three chunks of content (either numbers or text) separated
        // by delimiters.
        var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
        return re.test(dateString);
      }

      /**
       * Default date-to-string formatter to get a month header.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultMonthHeaderFormatter(date) {
        return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
      }

      /**
       * Default week number formatter.
       * @param number
       * @returns {string}
       */
      function defaultWeekNumberFormatter(number) {
        return 'Week ' + number;
      }

      /**
       * Default formatter for date cell aria-labels.
       * @param {!Date} date
       * @returns {string}
       */
      function defaultLongDateFormatter(date) {
        // Example: 'Thursday June 18 2015'
        return [
          service.days[date.getDay()],
          service.months[date.getMonth()],
          service.dates[date.getDate()],
          date.getFullYear()
        ].join(' ');
      }

      // The default "short" day strings are the first character of each day,
      // e.g., "Monday" => "M".
      var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) {
        return day[0];
      });

      // The default dates are simply the numbers 1 through 31.
      var defaultDates = Array(32);
      for (var i = 1; i <= 31; i++) {
        defaultDates[i] = i;
      }

      // Default ARIA messages are in English (US).
      var defaultMsgCalendar = 'Calendar';
      var defaultMsgOpenCalendar = 'Open calendar';

      var service = {
        months: this.months || $locale.DATETIME_FORMATS.MONTH,
        shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
        days: this.days || $locale.DATETIME_FORMATS.DAY,
        shortDays: this.shortDays || defaultShortDays,
        dates: this.dates || defaultDates,
        firstDayOfWeek: this.firstDayOfWeek || 0,
        formatDate: this.formatDate || defaultFormatDate,
        parseDate: this.parseDate || defaultParseDate,
        isDateComplete: this.isDateComplete || defaultIsDateComplete,
        monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
        weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
        longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
        msgCalendar: this.msgCalendar || defaultMsgCalendar,
        msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar
      };

      return service;
    };

    $provide.provider('$mdDateLocale', new DateLocaleProvider());
  });
})();
<|endoftext|>