<commit_before>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string       // className for the outer element
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - filteredOptions
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this._optionsFilterString = '';
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
			this.setState({
				options: newProps.options,
				filteredOptions: this.filterOptions(newProps.options)
			});
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		// reset internal filter string
		this._optionsFilterString = '';

		var values = this.initValuesArray(value),
			filteredOptions = this.filterOptions(this.state.options, values);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			filteredOptions: filteredOptions,
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi && values.length ? values[0] : filteredOptions[0]
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event && event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {

		// assign an internal variable because we need to use
		// the latest value before setState() has completed.
		this._optionsFilterString = event.target.value;

		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			var filteredOptions = this.filterOptions(this.state.options);
			this.setState({
				isOpen: true,
				inputValue: event.target.value,
				filteredOptions: filteredOptions,
				focusedOption: _.contains(filteredOptions, this.state.focusedOption) ? this.state.focusedOption : filteredOptions[0]
			});
		}

	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				var options = this._optionsCache[cacheKey].options;
				this.setState(_.extend({
					options: options,
					filteredOptions: this.filterOptions(options)
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options,
				filteredOptions: this.filterOptions(data.options)
			}, state));

		}.bind(this));

	},

	filterOptions: function(options, values) {
		var filterValue = this._optionsFilterString;
		var exclude = (values || this.state.values).map(function(i) {
			return i.value;
		});
		var filterOption = function(op) {
			if (this.props.multi && _.contains(exclude, op.value)) return false;
			return (
				!filterValue
				|| op.value.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0
				|| op.label.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0
			);
		}
		return _.filter(options, filterOption, this);
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.state.filteredOptions;

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;

		var ops = _.map(this.state.filteredOptions, function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': focusedValue === op.value
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops : React.createElement('div', { className: "Select-noresults" }, "No results found");

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(React.createElement(Value, props));
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ? React.createElement('span', { className: "Select-loading" }) : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<commit_msg>Adding aria-label for clear button, fixes #16<commit_after>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string       // className for the outer element
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - filteredOptions
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this._optionsFilterString = '';
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
			this.setState({
				options: newProps.options,
				filteredOptions: this.filterOptions(newProps.options)
			});
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		// reset internal filter string
		this._optionsFilterString = '';

		var values = this.initValuesArray(value),
			filteredOptions = this.filterOptions(this.state.options, values);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			filteredOptions: filteredOptions,
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi && values.length ? values[0] : filteredOptions[0]
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event && event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		// if the event was triggered by a mousedown and not the primary
		// button, ignore it.
		if (event.type == 'mousedown' && event.button !== 0) {
			return;
		}
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {

		// assign an internal variable because we need to use
		// the latest value before setState() has completed.
		this._optionsFilterString = event.target.value;

		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			var filteredOptions = this.filterOptions(this.state.options);
			this.setState({
				isOpen: true,
				inputValue: event.target.value,
				filteredOptions: filteredOptions,
				focusedOption: _.contains(filteredOptions, this.state.focusedOption) ? this.state.focusedOption : filteredOptions[0]
			});
		}

	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				var options = this._optionsCache[cacheKey].options;
				this.setState(_.extend({
					options: options,
					filteredOptions: this.filterOptions(options)
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options,
				filteredOptions: this.filterOptions(data.options)
			}, state));

		}.bind(this));

	},

	filterOptions: function(options, values) {
		var filterValue = this._optionsFilterString;
		var exclude = (values || this.state.values).map(function(i) {
			return i.value;
		});
		var filterOption = function(op) {
			if (this.props.multi && _.contains(exclude, op.value)) return false;
			return (
				!filterValue
				|| op.value.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0
				|| op.label.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0
			);
		}
		return _.filter(options, filterOption, this);
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.state.filteredOptions;

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null;

		var ops = _.map(this.state.filteredOptions, function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': focusedValue === op.value
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops : React.createElement('div', { className: "Select-noresults" }, "No results found");

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(React.createElement(Value, props));
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ?  : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<|endoftext|>


<commit_before>'use strict';

angular.module('jlogApp.services')
    .factory('backup', [function() {
        return {
            read_result: null,
            save_url: null,
            save_name: 'battle_list.txt',
            read: function backupRead(file, success_cbk, error_cbk) {
                var success_cbk_ = typeof(success_cbk) === 'function' ?
                    success_cbk :
                    function(data) { console.log(data); };
                var error_cbk_ = typeof(error_cbk) === 'function' ?
                    error_cbk :
                    function(error) { console.log(error); };
                this.read_result = null;
                var reader = new FileReader();
                reader.onload = function(e) {
                    var data;
                    try {
                        data = JSON.parse(e.target.result);
                        success_cbk_(data);
                    }
                    catch(e) {
                        error_cbk_('invalid file');
                    }
                };
                reader.onerror = function(e) {
                    error_cbk_('error reading file');
                };
                reader.onabort = function(e) {
                    error_cbk_('abort reading file');
                };
                reader.readAsText(file);
            },
            generate: function backupGenerate(data) {
                var old_url = this.save_url;
                this.save_url = null;
                if(old_url !== null) {
                    URL.revokeObjectURL(old_url);
                }
                var string = JSON.stringify(data);
                var blob = new Blob([string]);
                var url = URL.createObjectURL(blob, { type: 'text/plain' });
                var today = new Date();
                this.save_name = 'battle_list_' + today.getTime() + '.txt';
                this.save_url = url;
            }
        };
    }]);
<commit_msg>Made backup file generation work in chrome.<commit_after>'use strict';

angular.module('jlogApp.services')
    .factory('backup', [function() {
        return {
            read_result: null,
            save_url: null,
            save_name: 'battle_list.txt',
            read: function backupRead(file, success_cbk, error_cbk) {
                var success_cbk_ = typeof(success_cbk) === 'function' ?
                    success_cbk :
                    function(data) { console.log(data); };
                var error_cbk_ = typeof(error_cbk) === 'function' ?
                    error_cbk :
                    function(error) { console.log(error); };
                this.read_result = null;
                var reader = new FileReader();
                reader.onload = function(e) {
                    var data;
                    try {
                        data = JSON.parse(e.target.result);
                        success_cbk_(data);
                    }
                    catch(e) {
                        error_cbk_('invalid file');
                    }
                };
                reader.onerror = function(e) {
                    error_cbk_('error reading file');
                };
                reader.onabort = function(e) {
                    error_cbk_('abort reading file');
                };
                reader.readAsText(file);
            },
            generate: function backupGenerate(data) {
                console.log('generate backup file');
                var old_url = this.save_url;
                this.save_url = null;
                if(old_url !== null) {
                    URL.revokeObjectURL(old_url);
                }
                var string = JSON.stringify(data);
                var blob = new Blob([string], {type: 'application/octet-stream'});
                window.URL = window.URL || window.webkitURL;
                var url = window.URL.createObjectURL(blob);
                var today = new Date();
                this.save_name = 'battle_list_' + today.getTime() + '.txt';
                this.save_url = url;
            }
        };
    }]);
<|endoftext|>


<commit_before>/**
 * Middleware
 * @flow
 */

/**
 * Ensure session.userId is set on every request
 * Create the user if they're don't have an id yet
 */
export function setUser () {
  return function * setUserMiddleware (next) {
    const { User, r } = this.models
    if (!this.session.userId) {
      const userId = yield User.create(r)
      this.session.userId = userId
    }
    yield next
  }
}

/**
 * Middleware to set XSRF-TOKEN cookie on every response
 */
export function setCsrfToken () {
  return function * setCsrfCookieMidleware (next) {
    yield next
    this.cookies.set('XSRF-TOKEN', this.csrf, {
      httpOnly: false
    })
  }
}

/**
 * Middleware to expose errors when not in production mode
 * @TODO: log errors somewhere when in production
 */
export function exposeError (env) {
  return env === 'production'
    ? noopMiddleware
    : exposeErrorMiddleware

  function * exposeErrorMiddleware (next) {
    try {
      yield next
    } catch (err) {
      err.expose = true
      throw err
    }
  }

  function * noopMiddleware (next) {
    return yield next
  }
}
<commit_msg>Add setRequestId middleware<commit_after>/**
 * Middleware
 * @flow
 */
import uuid from 'node-uuid'

/**
 * Ensure session.userId is set on every request
 * Create the user if they're don't have an id yet
 */
export function setUser () {
  return function * setUserMiddleware (next) {
    const { User, r } = this.models
    if (!this.session.userId) {
      const userId = yield User.create(r)
      this.session.userId = userId
    }
    yield next
  }
}

/**
 * Adds a uuid to each request
 */
export function setRequestId () {
  return function * setRequestIdMiddleware (next) {
    this.request.requestId = this.req.requestId = uuid.v4()
    yield next
  }
}

/**
 * Middleware to set XSRF-TOKEN cookie on every response
 */
export function setCsrfToken () {
  return function * setCsrfCookieMidleware (next) {
    yield next
    this.cookies.set('XSRF-TOKEN', this.csrf, {
      httpOnly: false
    })
  }
}

/**
 * Middleware to expose errors when not in production mode
 * @TODO: log errors somewhere when in production
 */
export function exposeError (env) {
  return env === 'production'
    ? noopMiddleware
    : exposeErrorMiddleware

  function * exposeErrorMiddleware (next) {
    try {
      yield next
    } catch (err) {
      err.expose = true
      throw err
    }
  }

  function * noopMiddleware (next) {
    return yield next
  }
}
<|endoftext|>


<commit_before>var MainGame = function() {
};

function arraysEqual(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a.length != b.length) return false;

    // If you don't care about the order of the elements inside
    // the array, you should sort both arrays here.

    for (var i = 0; i < a.length; ++i) {
        if (a[i] !== b[i]) return false;
    }
    return true;
}

function echo(a) {
    console.log(a);
}

MainGame.prototype = {

    init: function() {

    },

    preload: function() {
        this.load.tilemap('map', 'res/map/DistCSV', null, Phaser.Tilemap.CSV);
        this.load.image('tiles', 'res/img/sprites.png');
        this.load.image("player", "res/img/player.png");
        this.load.image("enemy", "res/img/enemy.png");
        this.load.image("path", "res/img/path.png");
        this.load.image("bullet", "res/img/bullet.png");
        this.load.image("add_1", "res/img/add_1.png");
        this.load.image("add_2", "res/img/add_2.png");
        this.load.image("add_3", "res/img/add_3.png");
        this.load.image("food", "res/img/food.png");
    },

    add1: function() {
        if (this.money >= 100) {
            this.money-=100;
            this.moneyText.text = "Money: " + this.money;
            var player = this.add.sprite(32*27, 32*(27), "player");
            player.selected = false;
            player.alpha = 1;
            player.tint = 0x00ff00;
            this.physics.arcade.enable(player);
            //player.body.static = true;
            this.players.add(player);
        }
    },

    create: function() {

        this.physics.startSystem(Phaser.Physics.ARCADE);
        this.stage.backgroundColor = 0xffffff;
        //Tracking Variables
        this.startX = 0;
        this.startY = 0;
        this.endX = 0;
        this.endY = 0;
        this.shotCooldown = 20;
        this.cooldown = 0;

        this.bullets = [];
        this.food = this.add.group();

        this.food.create(64, 64, "food");

        this.add.button( 100, 30*32, 'add_1', this.add1, this, 2, 1, 0);

        this.money = 100;

        this.moneyText = this.add.text(20*30, 32*30, "Money: " + this.money);
        //SCREW THIS WITH A VENGEANCE

        //  Because we're loading CSV map data we have to specify the tile size here or we can't render it
        this.map = this.add.tilemap('map', 32, 32);

        //  Now add in the tileset
        this.map.addTilesetImage('tiles');
        this.map.setCollision(1);
        //  Create our layer
        this.layer = this.map.createLayer(0);

        //  Resize the world
        this.layer.resizeWorld();
        this.grid = [];
        for (var y = 0; y<30; y++) {
            var toPush = [];
            for (var x = 0; x < 30; x++) {
                toPush.push((this.map.getTile(this.layer.getTileX(x*32), this.layer.getTileY(y*32)).index==1)?1:0);
            }
            this.grid.push(toPush);
        }

        this.pf = new PF.Grid(this.grid);
        this.path = new PF.AStarFinder();

        this.players = this.add.group();

        for (var i = 0; i<2; i++) {
            var player = this.add.sprite(32*27, 32*(25+i*2), "player");
            player.selected = false;
            player.alpha = 1;
            player.tint = 0x00ff00;
            this.physics.arcade.enable(player);
            //player.body.static = true;
            this.players.add(player);
        }
        echo(this.players);
        this.enemy = this.add.sprite(32*28, 64, "enemy");
        this.physics.arcade.enable(this.enemy);
        this.enemy.goal = [32*28, 64];

    },

    convertToWorld: function(q) {
        var pathx = [];
        var pathy = [];

      for (var z=1; z< q.length; z++) {
              pathx.push(q[z][0]*32);
              pathy.push(q[z][1]*32);
      }
        return [pathx, pathy];
    },

    distance: function (x1, y1, x2, y2) {

        var dx = x1 - x2;
        var dy = y1 - y2;

        return Math.sqrt(dx * dx + dy * dy);

    },

    killThings: function(obj1, obj2) {
        obj1.x = null;
        obj1.y = null;
        obj2.x = null;
        obj2.y = null;
        obj1.kill();
        obj2.kill();
    },

    eat: function(obj1, obj2) {
        this.money += 100;
        this.moneyText.text = "Money: " + this.money
        obj2.x = null;
        obj2.y = null;
        obj2.kill();
    },

    wall: function(obj1, obj2) {
        obj1.x = null;
        obj1.y = null;
        obj1.kill();
    },

    win: function(obj1, obj2) {
        this.enemy.kill();
    },

    update: function() {

        this.physics.arcade.overlap(this.players, this.enemy, this.win, null, this);
        this.physics.arcade.overlap(this.players, this.food, this.eat, null, this);
        for (var i in this.bullets) {
            this.physics.arcade.overlap (this.players, this.bullets[i], this.killThings, null, this);
            try {
                if ((this.map.getTile(this.layer.getTileX(this.bullets[i].x), this.layer.getTileY(this.bullets[i].y)).index == 1)) {
                    this.bullets[i].x = null;
                    this.bullets[i].y = null;
                    this.bullets[i].kill();
                }
            } catch (e) {
                this.bullets[i].x = null;
                this.bullets[i].y = null;
                this.bullets[i].kill();
            }
        }

        if (this.shotCooldown > 0) {
            this.shotCooldown -= 1;
        }
        var cansee = [];

        //Raycast from player
        for (var i=0; i<=2*Math.PI; i+= Math.PI/20) {
            for (var j = 0; j<40; j++) {
                try {
                    var tile = this.map.getTile(this.layer.getTileX(this.enemy.x+16 + 32*j*Math.cos(i)), this.layer.getTileY(this.enemy.y+16 + 32*j*Math.sin(i)));
                    if (tile.index == 1) {
                        break;
                    } else {
                        for (var q in this.players.children) {
                            var p = this.players.children[q];
                            if (this.clickedOn(p, this.enemy.x+16 + 32*j*Math.cos(i), this.enemy.y+16 + 32*j*Math.sin(i))) {
                               cansee.push(p);
                            }
                        }

                    }
                } catch (e) {
                    break;
                }
            }
        }

        for (var i in this.players.children) {
            j = this.players.children[i];
            if (j.tint == 0x00000) {
                j.tint = 0x00ff00;
            }
            if (cansee.indexOf(j) != -1) {
                j.tint=0xff0000;
            } else {
                j.tint=0x00ff00;
            }
        }

        if (cansee.length == 0) {
            if (arraysEqual(this.enemy.goal,[this.enemy.x, this.enemy.y])) {
                //Player movement
                var path = [];
                while (path.length == 0) {
                    var gridBackup = this.pf.clone();
                    path = this.path.findPath(Math.floor(this.enemy.x / 32), Math.floor(this.enemy.y / 32),
                        Math.floor(29 * Math.random()), Math.floor(29 * Math.random()), this.pf);

                    this.pf = gridBackup;
                }
                path = this.convertToWorld(path);
                this.add.tween(this.enemy).to({x: path[0], y: path[1]}, 300 * path[0].length).start();
                this.enemy.goal = [path[0][path[0].length - 1], path[1][path[1].length - 1]];
            }
        } else {
            //Find closest enemy
            var min_dist = 100000;
            var min_p = null;
            for (var i in this.players.children) {
                var p = this.players.children[i];
                if (cansee.indexOf(p) != -1) {
                    if (p != null) {
                        var dist = this.distance(this.enemy.x, this.enemy.y, p.x, p.y);
                        if (dist < min_dist) {
                            min_dist = dist;
                            min_p = p;
                        }
                    }
                }
            }
            //Shoot
            //Get direction
            if (min_p != null) {
                echo(min_p.x);
                if (this.shotCooldown == 0) {
                    this.shotCooldown = 200;
                    var direction = -Math.atan2(min_p.x - this.enemy.x, min_p.y - this.enemy.y)+(Math.PI/2);
                    var b = this.add.sprite(this.enemy.x+16, this.enemy.y+16, "bullet");
                    this.physics.arcade.enable(b);
                    b.body.velocity.x = 500*Math.cos(direction);
                    b.body.velocity.y = 500*Math.sin(direction);
                    this.bullets.push(b);
                }

            }

        }

        if (this.input.mousePointer.isDown && this.cooldown <= 0) {
            for (i in this.players.children) {
                this.cooldown = 20;
                if (this.clickedOn(this.players.children[i], this.input.mousePointer.x, this.input.mousePointer.y)) {
                    if (!this.players.children[i].selected) {
                        this.players.children[i].selected = true;
                        this.players.children[i].tween = this.add.tween(this.players.children[i]).to({alpha: 0.5}, 100, "Linear", true, 0, -1);
                        this.players.children[i].tween.yoyo(true, 0);

                        this.startX = Math.floor(this.players.children[i].x / 32);
                        this.startY = Math.floor(this.players.children[i].y / 32);
                    } else {
                        this.players.children[i].selected = false;
                        this.players.children[i].tween.stop();
                        this.players.children[i].tween = undefined;
                        this.players.children[i].alpha = 1;
                    }
                } else {
                    if (this.players.children[i].selected) {
                        this.endX = Math.floor(this.input.mousePointer.x / 32);
                        this.endY = Math.floor(this.input.mousePointer.y / 32);
                        var gridBackup = this.pf.clone();
                        var x = this.path.findPath(Math.floor(this.players.children[i].x / 32), Math.floor(this.players.children[i].y / 32), this.endX, this.endY, this.pf);
                        x = this.convertToWorld(x);
                        if (x[0][0] != undefined) {
                            var t = this.add.tween(this.players.children[i]).to({x: x[0], y: x[1]}, 300 * x[0].length).start();
                        }
                        this.pf = gridBackup;
                        this.players.children[i].selected = false;
                        this.players.children[i].tween.stop();
                        this.players.children[i].tween = undefined;
                        this.players.children[i].alpha = 1;
                        break;

                    }
                }

            }
        }
        else {
                if (this.cooldown > 0) {
                    this.cooldown -= 1;
                }
            }

    },

    clickedOn: function(spr, x, y) {
        return x > spr.x && x < spr.x + spr.width && y > spr.y && y < spr.y + spr.height;
    }

};
<commit_msg>make create_food, player functions<commit_after>var MainGame = function() {
};

function arraysEqual(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    if (a.length != b.length) return false;

    // If you don't care about the order of the elements inside
    // the array, you should sort both arrays here.

    for (var i = 0; i < a.length; ++i) {
        if (a[i] !== b[i]) return false;
    }
    return true;
}

function echo(a) {
    console.log(a);
}

MainGame.prototype = {

    init: function() {

    },

    preload: function() {
        this.load.tilemap('map', 'res/map/DistCSV', null, Phaser.Tilemap.CSV);
        this.load.image('tiles', 'res/img/sprites.png');
        this.load.image("player", "res/img/player.png");
        this.load.image("enemy", "res/img/enemy.png");
        this.load.image("path", "res/img/path.png");
        this.load.image("bullet", "res/img/bullet.png");
        this.load.image("add_1", "res/img/add_1.png");
        this.load.image("add_2", "res/img/add_2.png");
        this.load.image("add_3", "res/img/add_3.png");
        this.load.image("food", "res/img/food.png");
    },

    add1: function() {
        if (this.money >= 100) {
            this.money-=100;
            this.moneyText.text = "Money: " + this.money;
            var player = this.add.sprite(32*27, 32*(27), "player");
            player.selected = false;
            player.alpha = 1;
            player.tint = 0x00ff00;
            this.physics.arcade.enable(player);
            //player.body.static = true;
            this.players.add(player);
        }
    },

    create: function() {

        this.physics.startSystem(Phaser.Physics.ARCADE);
        this.stage.backgroundColor = 0xffffff;
        //Tracking Variables
        this.startX = 0;
        this.startY = 0;
        this.endX = 0;
        this.endY = 0;
        this.shotCooldown = 20;
        this.cooldown = 0;

        this.bullets = [];

        this.add.button( 100, 30*32, 'add_1', this.add1, this, 2, 1, 0);

        this.money = 100;

        this.moneyText = this.add.text(20*30, 32*30, "Money: " + this.money);
        //SCREW THIS WITH A VENGEANCE

        //  Because we're loading CSV map data we have to specify the tile size here or we can't render it
        this.map = this.add.tilemap('map', 32, 32);

        //  Now add in the tileset
        this.map.addTilesetImage('tiles');
        this.map.setCollision(1);
        //  Create our layer
        this.layer = this.map.createLayer(0);

        //  Resize the world
        this.layer.resizeWorld();
        this.grid = [];
        for (var y = 0; y<30; y++) {
            var toPush = [];
            for (var x = 0; x < 30; x++) {
                toPush.push((this.map.getTile(this.layer.getTileX(x*32), this.layer.getTileY(y*32)).index==1)?1:0);
            }
            this.grid.push(toPush);
        }

        this.pf = new PF.Grid(this.grid);
        this.path = new PF.AStarFinder();
        this.food = this.add.group();
        this.players = this.add.group();

        for (var i = 0; i<2; i++) {
            this.createPlayer();
        }
        echo(this.players);
        this.enemy = this.add.sprite(32*28, 64, "enemy");
        this.physics.arcade.enable(this.enemy);
        this.enemy.goal = [32*28, 64];
        this.createFud();

    },

    rand: function(lo, hi) {
        var d = hi-lo;
        return lo + Math.floor(Math.random()*d)
    },

    createPlayer: function() {
        var player = this.add.sprite(32*24 + 32*this.rand(0,4),  32*24 + 32*this.rand(0,4), "player");
        player.selected = false;
        player.alpha = 1;
        player.tint = 0x00ff00;
        this.physics.arcade.enable(player);
        this.players.add(player);
    },

    createFud: function() {
        var x = 0;
        var y = 0;
        while(this.map.getTile(this.layer.getTileX(x*32), this.layer.getTileY(y*32)).index==1) {
            x = Math.floor(Math.random()*30);
            y = Math.floor(Math.random()*30);
        }
        var fud = this.add.sprite(x*32, y*32, "food");
        this.physics.arcade.enable(fud);
        this.food.add(fud);
    },

    convertToWorld: function(q) {
        var pathx = [];
        var pathy = [];

      for (var z=1; z< q.length; z++) {
              pathx.push(q[z][0]*32);
              pathy.push(q[z][1]*32);
      }
        return [pathx, pathy];
    },

    distance: function (x1, y1, x2, y2) {

        var dx = x1 - x2;
        var dy = y1 - y2;

        return Math.sqrt(dx * dx + dy * dy);

    },

    killThings: function(obj1, obj2) {
        obj1.x = null;
        obj1.y = null;
        obj2.x = null;
        obj2.y = null;
        obj1.kill();
        obj2.kill();
    },

    eat: function(obj1, obj2) {
        this.money += 100;
        this.moneyText.text = "Money: " + this.money
        obj2.x = null;
        obj2.y = null;
        obj2.kill();
        this.createFud();
    },

    wall: function(obj1, obj2) {
        obj1.x = null;
        obj1.y = null;
        obj1.kill();
    },

    win: function(obj1, obj2) {
        this.enemy.kill();
    },

    update: function() {

        this.physics.arcade.overlap(this.players, this.enemy, this.win, null, this);
        this.physics.arcade.overlap(this.players, this.food, this.eat, null, this);
        for (var i in this.bullets) {
            this.physics.arcade.overlap (this.players, this.bullets[i], this.killThings, null, this);
            try {
                if ((this.map.getTile(this.layer.getTileX(this.bullets[i].x), this.layer.getTileY(this.bullets[i].y)).index == 1)) {
                    this.bullets[i].x = null;
                    this.bullets[i].y = null;
                    this.bullets[i].kill();
                }
            } catch (e) {
                this.bullets[i].x = null;
                this.bullets[i].y = null;
                this.bullets[i].kill();
            }
        }

        if (this.shotCooldown > 0) {
            this.shotCooldown -= 1;
        }
        var cansee = [];

        //Raycast from player
        for (var i=0; i<=2*Math.PI; i+= Math.PI/20) {
            for (var j = 0; j<40; j++) {
                try {
                    var tile = this.map.getTile(this.layer.getTileX(this.enemy.x+16 + 32*j*Math.cos(i)), this.layer.getTileY(this.enemy.y+16 + 32*j*Math.sin(i)));
                    if (tile.index == 1) {
                        break;
                    } else {
                        for (var q in this.players.children) {
                            var p = this.players.children[q];
                            if (this.clickedOn(p, this.enemy.x+16 + 32*j*Math.cos(i), this.enemy.y+16 + 32*j*Math.sin(i))) {
                               cansee.push(p);
                            }
                        }

                    }
                } catch (e) {
                    break;
                }
            }
        }

        for (var i in this.players.children) {
            j = this.players.children[i];
            if (j.tint == 0x00000) {
                j.tint = 0x00ff00;
            }
            if (cansee.indexOf(j) != -1) {
                j.tint=0xff0000;
            } else {
                j.tint=0x00ff00;
            }
        }

        if (cansee.length == 0) {
            if (arraysEqual(this.enemy.goal,[this.enemy.x, this.enemy.y])) {
                //Player movement
                var path = [];
                while (path.length == 0) {
                    var gridBackup = this.pf.clone();
                    path = this.path.findPath(Math.floor(this.enemy.x / 32), Math.floor(this.enemy.y / 32),
                        Math.floor(29 * Math.random()), Math.floor(29 * Math.random()), this.pf);

                    this.pf = gridBackup;
                }
                path = this.convertToWorld(path);
                this.add.tween(this.enemy).to({x: path[0], y: path[1]}, 300 * path[0].length).start();
                this.enemy.goal = [path[0][path[0].length - 1], path[1][path[1].length - 1]];
            }
        } else {
            //Find closest enemy
            var min_dist = 100000;
            var min_p = null;
            for (var i in this.players.children) {
                var p = this.players.children[i];
                if (cansee.indexOf(p) != -1) {
                    if (p != null) {
                        var dist = this.distance(this.enemy.x, this.enemy.y, p.x, p.y);
                        if (dist < min_dist) {
                            min_dist = dist;
                            min_p = p;
                        }
                    }
                }
            }
            //Shoot
            //Get direction
            if (min_p != null) {
                echo(min_p.x);
                if (this.shotCooldown == 0) {
                    this.shotCooldown = 200;
                    var direction = -Math.atan2(min_p.x - this.enemy.x, min_p.y - this.enemy.y)+(Math.PI/2);
                    var b = this.add.sprite(this.enemy.x+16, this.enemy.y+16, "bullet");
                    this.physics.arcade.enable(b);
                    b.body.velocity.x = 500*Math.cos(direction);
                    b.body.velocity.y = 500*Math.sin(direction);
                    this.bullets.push(b);

                    var path = [];
                    while (path.length == 0) {
                        var gridBackup = this.pf.clone();
                        path = this.path.findPath(Math.floor(this.enemy.x / 32), Math.floor(this.enemy.y / 32),
                            Math.floor(29 * Math.random()), Math.floor(29 * Math.random()), this.pf);

                        this.pf = gridBackup;
                    }
                    path = this.convertToWorld(path);
                    this.add.tween(this.enemy).to({x: path[0], y: path[1]}, 300 * path[0].length).start();
                    this.enemy.goal = [path[0][path[0].length - 1], path[1][path[1].length - 1]];
                }

            }

        }

        if (this.input.mousePointer.isDown && this.cooldown <= 0) {
            for (i in this.players.children) {
                this.cooldown = 20;
                if (this.clickedOn(this.players.children[i], this.input.mousePointer.x, this.input.mousePointer.y)) {
                    if (!this.players.children[i].selected) {
                        this.players.children[i].selected = true;
                        this.players.children[i].tween = this.add.tween(this.players.children[i]).to({alpha: 0.5}, 100, "Linear", true, 0, -1);
                        this.players.children[i].tween.yoyo(true, 0);

                        this.startX = Math.floor(this.players.children[i].x / 32);
                        this.startY = Math.floor(this.players.children[i].y / 32);
                    } else {
                        this.players.children[i].selected = false;
                        this.players.children[i].tween.stop();
                        this.players.children[i].tween = undefined;
                        this.players.children[i].alpha = 1;
                    }
                } else {
                    if (this.players.children[i].selected) {
                        this.endX = Math.floor(this.input.mousePointer.x / 32);
                        this.endY = Math.floor(this.input.mousePointer.y / 32);
                        var gridBackup = this.pf.clone();
                        var x = this.path.findPath(Math.floor(this.players.children[i].x / 32), Math.floor(this.players.children[i].y / 32), this.endX, this.endY, this.pf);
                        x = this.convertToWorld(x);
                        if (x[0][0] != undefined) {
                            var t = this.add.tween(this.players.children[i]).to({x: x[0], y: x[1]}, 300 * x[0].length).start();
                        }
                        this.pf = gridBackup;
                        this.players.children[i].selected = false;
                        this.players.children[i].tween.stop();
                        this.players.children[i].tween = undefined;
                        this.players.children[i].alpha = 1;
                        break;

                    }
                }

            }
        }
        else {
                if (this.cooldown > 0) {
                    this.cooldown -= 1;
                }
            }

    },

    clickedOn: function(spr, x, y) {
        return x > spr.x && x < spr.x + spr.width && y > spr.y && y < spr.y + spr.height;
    }

};
<|endoftext|>


<commit_before>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string       // className for the outer element
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (newProps.options !== this.props.options) {
			this.setState({ options: newProps.options });
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		var values = this.initValuesArray(value);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi ? values[0] : null
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function() {
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {
		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			this.setState({
				isOpen: true,
				inputValue: event.target.value
			});
		}
	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				this.setState(_.extend({
					options: this._optionsCache[cacheKey].options
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options
			}, state));

		}.bind(this));

	},

	filterOptions: function() {
		var values = this.state.values.map(function(i) {
			return i.value;
		});
		var filterOption = function(op) {
			if (this.props.multi && _.contains(values, op.value)) return false;
			return (
				!this.state.inputValue
				|| op.value.toLowerCase().indexOf(this.state.inputValue.toLowerCase()) >= 0
				|| op.label.toLowerCase().indexOf(this.state.inputValue.toLowerCase()) >= 0
			);
		}
		return _.filter(this.state.options, filterOption, this);
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.filterOptions();

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var ops = _.map(this.filterOptions(), function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': this.state.focusedOption === op
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops : React.createElement('div', { className: "Select-noresults" }, "No results found");

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(React.createElement(Value, props));
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ? React.createElement('span', { className: "Select-loading" }) : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<commit_msg>Fixing options comparison in willReceiveProps<commit_after>var _ = require('underscore'),
	React = require('react'),
	Input = require('react-input-autosize'),
	classes = require('classnames'),
	Value = require('./Value');

var requestId = 0;

var Select = React.createClass({

	displayName: 'Select',

	propTypes: {
		value: React.PropTypes.any,             // initial field value
		multi: React.PropTypes.bool,            // multi-value input
		options: React.PropTypes.array,         // array of options
		delimiter: React.PropTypes.string,      // delimiter to use to join multiple values
		asyncOptions: React.PropTypes.func,     // function to call to get options
		autoload: React.PropTypes.bool,         // whether to auto-load the default async options set
		placeholder: React.PropTypes.string,    // field placeholder, displayed when there's no value
		name: React.PropTypes.string,           // field name, for hidden <input /> tag
		onChange: React.PropTypes.func,         // onChange handler: function(newValue) {}
		className: React.PropTypes.string       // className for the outer element
	},

	getDefaultProps: function() {
		return {
			value: undefined,
			options: [],
			delimiter: ',',
			asyncOptions: undefined,
			autoload: true,
			placeholder: '',
			name: undefined,
			onChange: undefined,
			className: undefined
		};
	},

	getInitialState: function() {
		return {
			/*
			 * set by getStateFromValue on componentWillMount:
			 * - value
			 * - values
			 * - inputValue
			 * - placeholder
			 * - focusedOption
			*/
			options: this.props.options,
			isFocused: false,
			isOpen: false,
			isLoading: false
		};
	},

	componentWillMount: function() {
		this._optionsCache = {};
		this.setState(this.getStateFromValue(this.props.value));

		if (this.props.asyncOptions && this.props.autoload) {
			this.autoloadAsyncOptions();
		}
	},

	componentWillUnmount: function() {
		clearTimeout(this._blurTimeout);
		clearTimeout(this._focusTimeout);
	},

	componentWillReceiveProps: function(newProps) {
		if (newProps.value !== this.state.value) {
			this.setState(this.getStateFromValue(newProps.value));
		}
		if (JSON.stringify(newProps.options) !== JSON.stringify(this.props.options)) {
			this.setState({ options: newProps.options });
		}
	},

	componentDidUpdate: function() {
		if (this._focusAfterUpdate) {
			clearTimeout(this._blurTimeout);
			this._focusTimeout = setTimeout(function() {
				this.refs.input.focus();
				this._focusAfterUpdate = false;
			}.bind(this), 50);
		}
	},

	initValuesArray: function(values) {

		if (!Array.isArray(values)) {
			if ('string' === typeof values) {
				values = values.split(this.props.delimiter);
			} else {
				values = values ? [values] : []
			}
		};

		return values.map(function(val) {
			return ('string' === typeof val) ? val = _.findWhere(this.state.options, { value: val }) || { value: val, label: val } : val;
		}.bind(this));

	},

	getStateFromValue: function(value) {

		var values = this.initValuesArray(value);

		return {
			value: values.map(function(v) { return v.value }).join(this.props.delimiter),
			values: values,
			inputValue: '',
			placeholder: !this.props.multi && values.length ? values[0].label : this.props.placeholder || 'Select...',
			focusedOption: !this.props.multi ? values[0] : null
		};

	},

	setValue: function(value) {
		this._focusAfterUpdate = true;
		var newState = this.getStateFromValue(value);
		newState.isOpen = false;
		this.fireChangeEvent(newState);
		this.setState(newState);
	},

	selectValue: function(value) {
		this[this.props.multi ? 'addValue' : 'setValue'](value);
	},

	addValue: function(value) {
		this.setValue(this.state.values.concat(value));
	},

	popValue: function() {
		this.setValue(_.initial(this.state.values));
	},

	removeValue: function(value) {
		this.setValue(_.without(this.state.values, value));
	},

	clearValue: function() {
		this.setValue(null);
	},

	resetValue: function() {
		this.setValue(this.state.value);
	},

	fireChangeEvent: function(newState) {
		if (newState.value !== this.state.value && this.props.onChange) {
			this.props.onChange(newState.value, newState.values);
		}
	},

	handleMouseDown: function(event) {
		event.stopPropagation();
		event.preventDefault();
		if (this.state.isFocused) {
			this.setState({
				isOpen: true
			});
		} else {
			this._openAfterFocus = true;
			this.refs.input.focus();
		}
	},

	handleInputFocus: function() {
		this.setState({
			isFocused: true,
			isOpen: this.state.isOpen || this._openAfterFocus
		});
		this._openAfterFocus = false;
	},

	handleInputBlur: function(event) {
		this._blurTimeout = setTimeout(function() {
			if (this._focusAfterUpdate) return;
			this.setState({
				isOpen: false,
				isFocused: false
			});
		}.bind(this), 50);
	},

	handleKeyDown: function(event) {

		switch (event.keyCode) {

			case 8: // backspace
				if (!this.state.inputValue) {
					this.popValue();
				}
				return;
			break;

			case 9: // tab
				if (event.shiftKey || !this.state.isOpen || !this.state.focusedOption) {
					return;
				}
				this.selectFocusedOption();
			break;

			case 13: // enter
				this.selectFocusedOption();
			break;

			case 27: // escape
				if (this.state.isOpen) {
					this.resetValue();
				} else {
					this.clearValue();
				}
			break;

			case 38: // up
				this.focusPreviousOption();
			break;

			case 40: // down
				this.focusNextOption();
			break;

			default: return;
		}

		event.preventDefault();

	},

	handleInputChange: function(event) {
		if (this.props.asyncOptions) {
			this.setState({
				isLoading: true,
				inputValue: event.target.value
			});
			this.loadAsyncOptions(event.target.value, {
				isLoading: false,
				isOpen: true
			});
		} else {
			this.setState({
				isOpen: true,
				inputValue: event.target.value
			});
		}
	},

	autoloadAsyncOptions: function() {
		this.loadAsyncOptions('', {}, function() {});
	},

	loadAsyncOptions: function(input, state) {

		for (var i = 0; i <= input.length; i++) {
			var cacheKey = input.slice(0, i);
			if (this._optionsCache[cacheKey] && (input === cacheKey || this._optionsCache[cacheKey].complete)) {
				this.setState(_.extend({
					options: this._optionsCache[cacheKey].options
				}, state));
				return;
			}
		}

		var thisRequestId = this._currentRequestId = requestId++;

		this.props.asyncOptions(input, function(err, data) {

			this._optionsCache[input] = data;

			if (thisRequestId !== this._currentRequestId) {
				return;
			}

			this.setState(_.extend({
				options: data.options
			}, state));

		}.bind(this));

	},

	filterOptions: function() {
		var values = this.state.values.map(function(i) {
			return i.value;
		});
		var filterOption = function(op) {
			if (this.props.multi && _.contains(values, op.value)) return false;
			return (
				!this.state.inputValue
				|| op.value.toLowerCase().indexOf(this.state.inputValue.toLowerCase()) >= 0
				|| op.label.toLowerCase().indexOf(this.state.inputValue.toLowerCase()) >= 0
			);
		}
		return _.filter(this.state.options, filterOption, this);
	},

	selectFocusedOption: function() {
		return this.selectValue(this.state.focusedOption);
	},

	focusOption: function(op) {
		this.setState({
			focusedOption: op
		});
	},

	focusNextOption: function() {
		this.focusAdjacentOption('next');
	},

	focusPreviousOption: function() {
		this.focusAdjacentOption('previous');
	},

	focusAdjacentOption: function(dir) {

		var ops = this.filterOptions();

		if (!this.state.isOpen) {
			this.setState({
				isOpen: true,
				inputValue: '',
				focusedOption: this.state.focusedOption || ops[dir === 'next' ? 0 : ops.length - 1]
			});
			return;
		}

		if (!ops.length) {
			return;
		}

		var focusedIndex = -1;

		for (var i = 0; i < ops.length; i++) {
			if (this.state.focusedOption === ops[i]) {
				focusedIndex = i;
				break;
			}
		}

		var focusedOption = ops[0];

		if (dir === 'next' && focusedIndex > -1 && focusedIndex < ops.length - 1) {
			focusedOption = ops[focusedIndex + 1];
		} else if (dir === 'previous') {
			if (focusedIndex > 0) {
				focusedOption = ops[focusedIndex - 1];
			} else {
				focusedOption = ops[ops.length - 1];
			}
		}

		this.setState({
			focusedOption: focusedOption
		});

	},

	unfocusOption: function(op) {
		if (this.state.focusedOption === op) {
			this.setState({
				focusedOption: null
			});
		}
	},

	buildMenu: function() {

		var ops = _.map(this.filterOptions(), function(op) {

			var optionClass = classes({
				'Select-option': true,
				'is-focused': this.state.focusedOption === op
			});

			var mouseEnter = this.focusOption.bind(this, op),
				mouseLeave = this.unfocusOption.bind(this, op),
				mouseDown = this.selectValue.bind(this, op);

			return
{op.label}
;

		}, this);

		return ops.length ? ops : React.createElement('div', { className: "Select-noresults" }, "No results found");

	},

	render: function() {

		var selectClass = classes('Select', this.props.className, {
			'is-multi': this.props.multi,
			'is-open': this.state.isOpen,
			'is-focused': this.state.isFocused,
			'is-loading': this.state.isLoading,
			'has-value': this.state.value
		});

		var value = [];

		if (this.props.multi) {
			this.state.values.forEach(function(val) {
				props = _.extend({
					key: val.value,
					onRemove: this.removeValue.bind(this, val)
				}, val);
				value.push(React.createElement(Value, props));
			}, this);
		}

		if (!this.state.inputValue && (!this.props.multi || !value.length)) {
			value.push(
{this.state.placeholder}
);
		}

		var loading = this.state.isLoading ? React.createElement('span', { className: "Select-loading" }) : null;
		var clear = this.state.value ?  : null;
		var menu = this.state.isOpen ?
{this.buildMenu()}
 : null;

		return (

				<input type="hidden" ref="value" name={this.props.name} value={this.state.value} />

					{value}
					<Input className="Select-input" tabIndex={this.props.tabIndex} ref="input" value={this.state.inputValue} onFocus={this.handleInputFocus} onBlur={this.handleInputBlur} onChange={this.handleInputChange} minWidth="5" />

					{loading}
					{clear}

				{menu}

		);

	}

});

module.exports = Select;
<|endoftext|>


<commit_before>// Copyright 2021-2022, University of Colorado Boulder

/**
 * The Toolbar along the left edge of the screen with controls related to the sim or active screen. Currently, it
 * only contains controls related to the Voicing feature. It isn't always displayed and the user must request it
 * from the PreferencesDialog. In order to be used, both voicing AND the Toolbar must be enabled by the user.
 * Voicing can be used without this shown.
 *
 * When open, the sim will resize and shift to the right to create space. Screen bounds are adjusted so that
 * simulation components will never overlap with the Toolbar.
 *
 * @author Jesse Greenberg
 */

import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../axon/js/NumberProperty.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import Matrix3 from '../../../dot/js/Matrix3.js';
import { Shape } from '../../../kite/js/imports.js';
import merge from '../../../phet-core/js/merge.js';
import { Node, Path, Rectangle, voicingManager, voicingUtteranceQueue } from '../../../scenery/js/imports.js';
import ButtonNode from '../../../sun/js/buttons/ButtonNode.js';
import RoundPushButton from '../../../sun/js/buttons/RoundPushButton.js';
import Tandem from '../../../tandem/js/Tandem.js';
import audioManager from '../audioManager.js';
import joist from '../joist.js';
import joistStrings from '../joistStrings.js';
import VoicingToolbarAlertManager from './VoicingToolbarAlertManager.js';
import VoicingToolbarItem from './VoicingToolbarItem.js';

// constants
const MAX_ANIMATION_SPEED = 250; // in view coordinates per second, assuming 60 fps
const CONTENT_TOP_MARGIN = 15; // margin between top of Toolbar and contents

// constants
const openToolbarString = joistStrings.a11y.toolbar.openToolbar;
const closeToolbarString = joistStrings.a11y.toolbar.closeToolbar;
const hideToolbarString = joistStrings.a11y.toolbar.hideToolbar;
const showToolbarString = joistStrings.a11y.toolbar.showToolbar;
const toolbarShownString = joistStrings.a11y.toolbar.toolbarShown;
const toolbarHiddenString = joistStrings.a11y.toolbar.toolbarHidden;

class Toolbar extends Node {

  /**
   * @param {Sim} sim
   * @param {Object} [options]
   */
  constructor( sim, options ) {

    options = merge( {

      // pdom
      tagName: 'div',

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

    super( options );

    // @private {BooleanProperty} - Whether or not the Toolbar is enabled (visible to the user)
    this.isEnabledProperty = sim.preferencesManager.toolbarEnabledProperty;

    // @private {Rectangle} - the Rectangle for the Toolbar that surrounds all content, bounds set once
    // content is created and in layout to fill height of screen
    this.backgroundRectangle = new Rectangle( 0, 0, 0, 0, {
      fill: sim.lookAndFeel.navigationBarFillProperty
    } );

    // @public {NumberProperty} - The position of the right edge of the backgroundRectangle in local coordinates.
    // This is what controls the position of the Toolbar as it is open/closed/removed/animating.
    this.rightPositionProperty = new NumberProperty( 0 );

    // @private {number} - The target position for the rightPositionProperty, to support animation. In step,
    // the rightPositionProperty will be changed until the rightPositionProperty equals the rightDestinationPosition.
    this.rightDestinationPosition = 0;

    // @private {BooleanProperSty} - Whether or not the Toolbar is open or closed. This is different from
    // whether or not it is showing.
    this.openProperty = new BooleanProperty( true );

    // @public (read-only) {DerivedProperty.<boolean>} - Whether the Toolbar is shown to the user. At this time,
    // that is true if the toolbar is enabled, voicing is enabled, and if all audio is enabled. The Toolbar only
    // includes controls related to audio (voicing) so when audio is disabled there is no need to show it.
    this.isShowingProperty = DerivedProperty.and( [ this.isEnabledProperty, voicingManager.enabledProperty, audioManager.audioEnabledProperty ] );

    // @private {number} - Scale applied to the Toolbar and its contents in response to layout and window resizing.
    this.layoutScale = 1;

    // @private {VoicingToolbarItem} - Contents for the Toolbar, currently only controls related to the voicing
    // feature.
    const voicingAlertManager = new VoicingToolbarAlertManager( sim.screenProperty );
    this.menuContent = new VoicingToolbarItem( voicingAlertManager, sim.lookAndFeel, {
      tandem: options.tandem.createTandem( 'menuContent' )
    } );

    // icon for the openButton
    const chevronIcon = new DoubleChevron();

    // @private {RoundPushButton}
    this.openButton = new RoundPushButton( {
      content: chevronIcon,
      listener: () => this.openProperty.toggle(),
      buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy,
      baseColor: 'lightgrey',

      // phet-io - opting out of Tandems for now
      tandem: Tandem.OPT_OUT
    } );

    // @private {number} - width of content for the toolbar in the local coordinate frame
    this.contentWidth = this.menuContent.localBounds.width;

    // @private {number} - Margin between toolbar content and edge of the backgroundRectangle, in the local coordinate
    // frame. Also used to determine the right position when closed. This is the width of the button so that when the
    // Toolbar is closed only the button is shown and all other content is hidden.
    this.contentMargin = this.openButton.localBounds.width;

    // a parent for all Nodes of the toolbar, so we can set visibility of this group internally when
    // the isShowingProperty changes
    const contentParent = new Node( {
      children: [ this.backgroundRectangle, this.menuContent, this.openButton ]
    } );
    this.addChild( contentParent );

    // move to destination position in the animation frame
    const timerListener = dt => {
      this.step( dt );
    };
    stepTimer.addListener( timerListener );

    const isOpenListener = ( open, oldValue ) => {

      // rotate chevron to indicate direction of toolbar movement
      chevronIcon.matrix = open ? Matrix3.scaling( -1, 1 ) : Matrix3.IDENTITY;

      // when closed, menu content should be hidden from screen readers and the navigation order
      this.menuContent.pdomVisible = open;

      this.openButton.innerContent = open ? closeToolbarString : openToolbarString;
      this.openButton.voicingNameResponse = open ? hideToolbarString : showToolbarString;

      this.updateDestinationPosition();

      if ( oldValue !== null ) {
        const alert = open ? toolbarShownString : toolbarHiddenString;
        this.openButton.voicingSpeakContextResponse( {
          contextResponse: alert
        } );

        voicingUtteranceQueue.addToBack( alert );
        this.alertDescriptionUtterance( alert );
      }
    };
    this.openProperty.link( isOpenListener );

    // when shown or hidden update destination positions so it animates open or close
    const isShowingListener = showing => {
      this.updateDestinationPosition();

      // when now showing, this entire toolbar should be hidden for Interactive Description, but we don't use
      // visibility directly because we want to see the Toolbar animate away
      contentParent.pdomVisible = showing;
    };
    this.isShowingProperty.link( isShowingListener );

    // @private
    this.disposeToolbar = () => {
      this.isShowingProperty.unlink( isShowingListener );
      this.openProperty.unlink( isOpenListener );
    };
  }

  /**
   * Returns the width of the Toolbar that can be seen on screen. This can be any value from the full width of the
   * Toolbar to zero width, depending on whether it is open, closed, removed entirely, or animating.
   * @public
   *
   * @returns {number}
   */
  getDisplayedWidth() {
    return this.rightPositionProperty.value * this.layoutScale + this.openButton.width / 2;
  }

  /**
   * Update rightDestinationPosition so that the Toolbar will animate towards opening, closing, or being removed
   * entirely from view.
   * @private
   */
  updateDestinationPosition() {
    if ( this.isShowingProperty.value ) {
      // the Toolbar is enabled and should either show all content or just the openButton
      this.rightDestinationPosition = this.openProperty.value ? this.contentWidth + this.contentMargin * 2 : this.contentMargin;
    }
    else {
      // no aspect of the menu should be visible
      this.rightDestinationPosition = -this.contentMargin / 2;
    }
  }

  /**
   * Animated the Toolbar as it opens and closes.
   * @private
   *
   * @param {number} dt
   */
  step( dt ) {
    const distance = Math.abs( this.rightPositionProperty.value - this.rightDestinationPosition );
    if ( distance !== 0 ) {
      const animationDistance = Math.min( distance, MAX_ANIMATION_SPEED * dt );
      const currentPosition = this.rightPositionProperty.value;
      this.rightPositionProperty.value = this.rightDestinationPosition > currentPosition ?
                                         currentPosition + animationDistance :
                                         currentPosition - animationDistance;
    }
  }

  /**
   * Layout for the Toolbar, called whenever position changes or window is resized.
   * @public
   *
   * @param {number} scale
   * @param {number} height
   */
  layout( scale, height ) {
    this.layoutScale = scale;
    this.menuContent.setScaleMagnitude( scale );
    this.openButton.setScaleMagnitude( scale );
    this.backgroundRectangle.rectWidth = scale * ( this.contentWidth + this.contentMargin * 2 );
    this.backgroundRectangle.rectHeight = height;
    this.backgroundRectangle.right = this.rightPositionProperty.value * scale;
    this.openButton.center = this.backgroundRectangle.rightCenter;
    this.menuContent.centerTop = this.backgroundRectangle.centerTop.plusXY( 0, CONTENT_TOP_MARGIN );
  }

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

/**
 * The icon for the button that opens and closes the Toolbar.
 */
class DoubleChevron extends Path {
  constructor() {

    // spacing and dimensions for the arrows
    const chevronSpacing = 8;
    const chevronWidth = 5;
    const chevronHeight = 12;

    const chevronShape = new Shape();
    for ( let i = 0; i < 2; i++ ) {
      const left = i * chevronSpacing;
      chevronShape.moveTo( left, 0 );
      chevronShape.lineTo( left + chevronWidth, chevronHeight / 2 );
      chevronShape.lineTo( left, chevronHeight );
    }

    super( chevronShape, {
      stroke: 'black',
      lineWidth: 3.5,
      lineCap: 'round'
    } );
  }
}

joist.register( 'Toolbar', Toolbar );
export default Toolbar;<commit_msg>remove voicingUtteranceQueue usage, see https://github.com/phetsims/scenery/issues/1403<commit_after>// Copyright 2021-2022, University of Colorado Boulder

/**
 * The Toolbar along the left edge of the screen with controls related to the sim or active screen. Currently, it
 * only contains controls related to the Voicing feature. It isn't always displayed and the user must request it
 * from the PreferencesDialog. In order to be used, both voicing AND the Toolbar must be enabled by the user.
 * Voicing can be used without this shown.
 *
 * When open, the sim will resize and shift to the right to create space. Screen bounds are adjusted so that
 * simulation components will never overlap with the Toolbar.
 *
 * @author Jesse Greenberg
 */

import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../axon/js/NumberProperty.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import Matrix3 from '../../../dot/js/Matrix3.js';
import { Shape } from '../../../kite/js/imports.js';
import merge from '../../../phet-core/js/merge.js';
import { Node, Path, Rectangle, voicingManager } from '../../../scenery/js/imports.js';
import ButtonNode from '../../../sun/js/buttons/ButtonNode.js';
import RoundPushButton from '../../../sun/js/buttons/RoundPushButton.js';
import Tandem from '../../../tandem/js/Tandem.js';
import audioManager from '../audioManager.js';
import joist from '../joist.js';
import joistStrings from '../joistStrings.js';
import VoicingToolbarAlertManager from './VoicingToolbarAlertManager.js';
import VoicingToolbarItem from './VoicingToolbarItem.js';

// constants
const MAX_ANIMATION_SPEED = 250; // in view coordinates per second, assuming 60 fps
const CONTENT_TOP_MARGIN = 15; // margin between top of Toolbar and contents

// constants
const openToolbarString = joistStrings.a11y.toolbar.openToolbar;
const closeToolbarString = joistStrings.a11y.toolbar.closeToolbar;
const hideToolbarString = joistStrings.a11y.toolbar.hideToolbar;
const showToolbarString = joistStrings.a11y.toolbar.showToolbar;
const toolbarShownString = joistStrings.a11y.toolbar.toolbarShown;
const toolbarHiddenString = joistStrings.a11y.toolbar.toolbarHidden;

class Toolbar extends Node {

  /**
   * @param {Sim} sim
   * @param {Object} [options]
   */
  constructor( sim, options ) {

    options = merge( {

      // pdom
      tagName: 'div',

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

    super( options );

    // @private {BooleanProperty} - Whether or not the Toolbar is enabled (visible to the user)
    this.isEnabledProperty = sim.preferencesManager.toolbarEnabledProperty;

    // @private {Rectangle} - the Rectangle for the Toolbar that surrounds all content, bounds set once
    // content is created and in layout to fill height of screen
    this.backgroundRectangle = new Rectangle( 0, 0, 0, 0, {
      fill: sim.lookAndFeel.navigationBarFillProperty
    } );

    // @public {NumberProperty} - The position of the right edge of the backgroundRectangle in local coordinates.
    // This is what controls the position of the Toolbar as it is open/closed/removed/animating.
    this.rightPositionProperty = new NumberProperty( 0 );

    // @private {number} - The target position for the rightPositionProperty, to support animation. In step,
    // the rightPositionProperty will be changed until the rightPositionProperty equals the rightDestinationPosition.
    this.rightDestinationPosition = 0;

    // @private {BooleanProperSty} - Whether or not the Toolbar is open or closed. This is different from
    // whether or not it is showing.
    this.openProperty = new BooleanProperty( true );

    // @public (read-only) {DerivedProperty.<boolean>} - Whether the Toolbar is shown to the user. At this time,
    // that is true if the toolbar is enabled, voicing is enabled, and if all audio is enabled. The Toolbar only
    // includes controls related to audio (voicing) so when audio is disabled there is no need to show it.
    this.isShowingProperty = DerivedProperty.and( [ this.isEnabledProperty, voicingManager.enabledProperty, audioManager.audioEnabledProperty ] );

    // @private {number} - Scale applied to the Toolbar and its contents in response to layout and window resizing.
    this.layoutScale = 1;

    // @private {VoicingToolbarItem} - Contents for the Toolbar, currently only controls related to the voicing
    // feature.
    const voicingAlertManager = new VoicingToolbarAlertManager( sim.screenProperty );
    this.menuContent = new VoicingToolbarItem( voicingAlertManager, sim.lookAndFeel, {
      tandem: options.tandem.createTandem( 'menuContent' )
    } );

    // icon for the openButton
    const chevronIcon = new DoubleChevron();

    // @private {RoundPushButton}
    this.openButton = new RoundPushButton( {
      content: chevronIcon,
      listener: () => this.openProperty.toggle(),
      buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy,
      baseColor: 'lightgrey',

      // phet-io - opting out of Tandems for now
      tandem: Tandem.OPT_OUT
    } );

    // @private {number} - width of content for the toolbar in the local coordinate frame
    this.contentWidth = this.menuContent.localBounds.width;

    // @private {number} - Margin between toolbar content and edge of the backgroundRectangle, in the local coordinate
    // frame. Also used to determine the right position when closed. This is the width of the button so that when the
    // Toolbar is closed only the button is shown and all other content is hidden.
    this.contentMargin = this.openButton.localBounds.width;

    // a parent for all Nodes of the toolbar, so we can set visibility of this group internally when
    // the isShowingProperty changes
    const contentParent = new Node( {
      children: [ this.backgroundRectangle, this.menuContent, this.openButton ]
    } );
    this.addChild( contentParent );

    // move to destination position in the animation frame
    const timerListener = dt => {
      this.step( dt );
    };
    stepTimer.addListener( timerListener );

    const isOpenListener = ( open, oldValue ) => {

      // rotate chevron to indicate direction of toolbar movement
      chevronIcon.matrix = open ? Matrix3.scaling( -1, 1 ) : Matrix3.IDENTITY;

      // when closed, menu content should be hidden from screen readers and the navigation order
      this.menuContent.pdomVisible = open;

      this.openButton.innerContent = open ? closeToolbarString : openToolbarString;
      this.openButton.voicingNameResponse = open ? hideToolbarString : showToolbarString;

      this.updateDestinationPosition();

      if ( oldValue !== null ) {
        const alert = open ? toolbarShownString : toolbarHiddenString;
        this.openButton.voicingSpeakContextResponse( {
          contextResponse: alert
        } );

        this.alertDescriptionUtterance( alert );
      }
    };
    this.openProperty.link( isOpenListener );

    // when shown or hidden update destination positions so it animates open or close
    const isShowingListener = showing => {
      this.updateDestinationPosition();

      // when now showing, this entire toolbar should be hidden for Interactive Description, but we don't use
      // visibility directly because we want to see the Toolbar animate away
      contentParent.pdomVisible = showing;
    };
    this.isShowingProperty.link( isShowingListener );

    // @private
    this.disposeToolbar = () => {
      this.isShowingProperty.unlink( isShowingListener );
      this.openProperty.unlink( isOpenListener );
    };
  }

  /**
   * Returns the width of the Toolbar that can be seen on screen. This can be any value from the full width of the
   * Toolbar to zero width, depending on whether it is open, closed, removed entirely, or animating.
   * @public
   *
   * @returns {number}
   */
  getDisplayedWidth() {
    return this.rightPositionProperty.value * this.layoutScale + this.openButton.width / 2;
  }

  /**
   * Update rightDestinationPosition so that the Toolbar will animate towards opening, closing, or being removed
   * entirely from view.
   * @private
   */
  updateDestinationPosition() {
    if ( this.isShowingProperty.value ) {
      // the Toolbar is enabled and should either show all content or just the openButton
      this.rightDestinationPosition = this.openProperty.value ? this.contentWidth + this.contentMargin * 2 : this.contentMargin;
    }
    else {
      // no aspect of the menu should be visible
      this.rightDestinationPosition = -this.contentMargin / 2;
    }
  }

  /**
   * Animated the Toolbar as it opens and closes.
   * @private
   *
   * @param {number} dt
   */
  step( dt ) {
    const distance = Math.abs( this.rightPositionProperty.value - this.rightDestinationPosition );
    if ( distance !== 0 ) {
      const animationDistance = Math.min( distance, MAX_ANIMATION_SPEED * dt );
      const currentPosition = this.rightPositionProperty.value;
      this.rightPositionProperty.value = this.rightDestinationPosition > currentPosition ?
                                         currentPosition + animationDistance :
                                         currentPosition - animationDistance;
    }
  }

  /**
   * Layout for the Toolbar, called whenever position changes or window is resized.
   * @public
   *
   * @param {number} scale
   * @param {number} height
   */
  layout( scale, height ) {
    this.layoutScale = scale;
    this.menuContent.setScaleMagnitude( scale );
    this.openButton.setScaleMagnitude( scale );
    this.backgroundRectangle.rectWidth = scale * ( this.contentWidth + this.contentMargin * 2 );
    this.backgroundRectangle.rectHeight = height;
    this.backgroundRectangle.right = this.rightPositionProperty.value * scale;
    this.openButton.center = this.backgroundRectangle.rightCenter;
    this.menuContent.centerTop = this.backgroundRectangle.centerTop.plusXY( 0, CONTENT_TOP_MARGIN );
  }

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

/**
 * The icon for the button that opens and closes the Toolbar.
 */
class DoubleChevron extends Path {
  constructor() {

    // spacing and dimensions for the arrows
    const chevronSpacing = 8;
    const chevronWidth = 5;
    const chevronHeight = 12;

    const chevronShape = new Shape();
    for ( let i = 0; i < 2; i++ ) {
      const left = i * chevronSpacing;
      chevronShape.moveTo( left, 0 );
      chevronShape.lineTo( left + chevronWidth, chevronHeight / 2 );
      chevronShape.lineTo( left, chevronHeight );
    }

    super( chevronShape, {
      stroke: 'black',
      lineWidth: 3.5,
      lineCap: 'round'
    } );
  }
}

joist.register( 'Toolbar', Toolbar );
export default Toolbar;<|endoftext|>