<commit_before>/**
 * Simple HTTP requests monitor and analyzer
 */
exports.version = '1.2';

exports.module = function(phantomas) {
	// imports
	var HTTP_STATUS_CODES = phantomas.require('http').STATUS_CODES,
		parseUrl = phantomas.require('url').parse;

	var requests = [];

	// register metric
	phantomas.setMetric('requests');
	phantomas.setMetric('gzipRequests');
	phantomas.setMetric('postRequests');
	phantomas.setMetric('httpsRequests');
	phantomas.setMetric('notFound');
	phantomas.setMetric('timeToFirstByte');
	phantomas.setMetric('timeToLastByte');
	phantomas.setMetric('bodySize'); // content only
	phantomas.setMetric('contentLength'); // content only

	// parse given URL to get protocol and domain
	function parseEntryUrl(entry) {
		var parsed;

		if (entry.url.indexOf('data:') !== 0) {
			// @see http://nodejs.org/api/url.html#url_url
			parsed = parseUrl(entry.url) || {};

			entry.protocol = parsed.protocol.replace(':', '');
			entry.domain = parsed.hostname;
			entry.query = parsed.query;

			if (entry.protocol === 'https') {
				entry.isSSL = true;
			}
		}
		else {
			// base64 encoded data
			entry.domain = false;
			entry.protocol = false;
			entry.isBase64 = true;
		}
	}

	// when the monitoring started?
	var loadStartedTime;
	phantomas.on('loadStarted', function(res) {
		loadStartedTime = Date.now();
	});

	phantomas.on('onResourceRequested', function(res, request) {
		// store current request data
		var entry = requests[res.id] = {
			id: res.id,
			url: res.url,
			method: res.method,
			requestHeaders: {},
			sendTime: res.time,
			bodySize: 0,
			isBlocked: false
		};

		// allow modules to block requests
		entry.block = function() {
			this.isBlocked = true;
		};

		res.headers.forEach(function(header) {
			entry.requestHeaders[header.name] = header.value;

			switch (header.name.toLowerCase()) {
				// AJAX requests
				case 'x-requested-with':
					if (header.value === 'XMLHttpRequest') {
						entry.isAjax = true;
					}
					break;
			}
		});

		parseEntryUrl(entry);

		if (entry.isBase64) {
			return;
		}

		// give modules a chance to block requests using entry.block()
		// @see https://github.com/ariya/phantomjs/issues/10230
		phantomas.emit('beforeSend', entry, res);

		if ( (entry.isBlocked === true) && (typeof request !== 'undefined') ) {
			phantomas.log('Blocked request: <' + entry.url + '>');
			request.abort();
			return;
		}

		// proceed
		phantomas.emit('send', entry, res);
	});

	phantomas.on('onResourceReceived', function(res) {
		// current request data
		var entry = requests[res.id] || {};

		// fix for blocked requests still "emitting" onResourceReceived with "stage" = 'end' and empty "status" (issue #122)
		if (res.status === null ) {
			if (entry.isBlocked) {
				return;
			} else if (!entry.isBase64) {
				phantomas.log('Blocked request by phantomjs: <' + entry.url + '>');
				phantomas.emit('abort', entry, res);
			}
		}

		switch(res.stage) {
			// the beginning of response
			case 'start':
				entry.recvStartTime = res.time;
				entry.timeToFirstByte = res.time - entry.sendTime;

				// FIXME: buggy
				// @see https://github.com/ariya/phantomjs/issues/10169
				entry.bodySize += res.bodySize || 0;
				break;

			// the end of response
			case 'end':
				// timing
				entry.recvEndTime = res.time;
				entry.timeToLastByte = res.time - entry.sendTime;
				entry.receiveTime = entry.recvEndTime - entry.recvStartTime;

				// request method
				switch(entry.method) {
					case 'POST':
						phantomas.incrMetric('postRequests');
						phantomas.addOffender('postRequests', entry.url);
						break;
				}

				// asset type
				entry.type = 'other';

				// analyze HTTP headers
				entry.headers = {};
				res.headers.forEach(function(header) {
					entry.headers[header.name] = header.value;

					switch (header.name.toLowerCase()) {
						// TODO: why it's not gzipped?
						// because: https://github.com/ariya/phantomjs/issues/10156
						// should equal bodySize
						case 'content-length':
							entry.contentLength = parseInt(header.value, 10);
							break;

						// detect content type
						case 'content-type':
							// parse header value
							var value = header.value.split(';').shift().toLowerCase();

							switch(value) {
								case 'text/html':
									entry.type = 'html';
									entry.isHTML = true;
									break;

								case 'text/css':
									entry.type = 'css';
									entry.isCSS = true;
									break;

								case 'application/x-javascript':
								case 'application/javascript':
								case 'text/javascript':
									entry.type = 'js';
									entry.isJS = true;
									break;

								case 'application/json':
									entry.type = 'json';
									entry.isJSON = true;
									break;

								case 'image/png':
								case 'image/jpeg':
								case 'image/gif':
								case 'image/svg+xml':
									entry.type = 'image';
									entry.isImage = true;
									break;

								// @see http://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts
								case 'application/font-wof':
								case 'application/font-woff':
								case 'application/vnd.ms-fontobject':
								case 'application/x-font-opentype':
								case 'application/x-font-truetype':
								case 'application/x-font-ttf':
								case 'application/x-font-woff':
									entry.type = 'webfont';
									entry.isWebFont = true;
									break;

								case 'application/octet-stream':
									var ext = (entry.url || '').split('.').pop();

									switch(ext) {
										// @see http://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts#comment-8077637
										case 'otf':
											entry.type = 'webfont';
											entry.isWebFont = true;
											break;
									}
									break;

								default:
									phantomas.log('Unknown content type found: ' + value + ' for <' + entry.url + '>');
							}
							break;

						// detect content encoding
						case 'content-encoding':
							if (header.value === 'gzip') {
								entry.gzip = true;
							}
							break;
					}
				});

				parseEntryUrl(entry);

				// HTTP code
				entry.status = res.status || 200 /* for base64 data */;
				entry.statusText = HTTP_STATUS_CODES[entry.status];

				switch(entry.status) {
					case 301: // Moved Permanently
					case 302: // Found
					case 303: // See Other
						entry.isRedirect = true;
						break;

					case 404: // Not Found
						phantomas.incrMetric('notFound');
						phantomas.addOffender('notFound', entry.url);
						break;
				}

				// requests stats
				if (!entry.isBase64) {
					phantomas.incrMetric('requests');

					phantomas.incrMetric('bodySize', entry.bodySize); // content only
					phantomas.incrMetric('contentLength', entry.contentLength || entry.bodySize); // content only
				}

				if (entry.gzip) {
					phantomas.incrMetric('gzipRequests');
				}

				if (entry.isSSL) {
					phantomas.incrMetric('httpsRequests');
					phantomas.addOffender('httpsRequests', entry.url);
				}

				// emit an event for other modules
				phantomas.emit(entry.isBase64 ? 'base64recv' : 'recv' , entry, res);
				//phantomas.log(entry);
				break;
		}
	});

	// TTFB / TTLB metrics
	var ttfbMeasured = false;

	phantomas.on('recv', function(entry, res) {
		// check the first response which is not a redirect (issue #74)
		if (!ttfbMeasured && !entry.isRedirect) {
			phantomas.setMetric('timeToFirstByte', entry.timeToFirstByte, true);
			phantomas.setMetric('timeToLastByte', entry.timeToLastByte, true);

			ttfbMeasured = true;

			phantomas.log('Time to first byte: set to %d ms for <%s> (HTTP %d)', entry.timeToFirstByte, entry.url, entry.status);
			phantomas.emit('responseEnd', entry, res);
		}

		// completion of the last HTTP request
		phantomas.setMetric('httpTrafficCompleted', entry.recvEndTime - loadStartedTime);
	});
};
<commit_msg>requestsMonitor: add docs with metrics metadata<commit_after>/**
 * Simple HTTP requests monitor and analyzer
 */
exports.version = '1.2';

exports.module = function(phantomas) {
	// imports
	var HTTP_STATUS_CODES = phantomas.require('http').STATUS_CODES,
		parseUrl = phantomas.require('url').parse;

	var requests = [];

	// register metric
	phantomas.setMetric('requests');              // @desc total number of HTTP requests made
	phantomas.setMetric('gzipRequests');          // @desc number of gzipped HTTP responses @unreliable
	phantomas.setMetric('postRequests');          // @desc number of POST requests
	phantomas.setMetric('httpsRequests');         // @desc number of HTTPS requests
	phantomas.setMetric('notFound');              // @desc number of HTTP 404 responses
	phantomas.setMetric('timeToFirstByte');       // @desc time it took to receive the first byte of the first response (that was not a redirect)
	phantomas.setMetric('timeToLastByte');        // @desc time it took to receive the last byte of the first response (that was not a redirect)
	phantomas.setMetric('bodySize');              // @desc size of the uncompressed content of all responses @unreliable
	phantomas.setMetric('contentLength');         // @desc size of the content of all responses (based on Content-Length header) @unreliable
	phantomas.setMetric('httpTrafficCompleted');  // @desc time it took to receive the last byte of the last HTTP response

	// parse given URL to get protocol and domain
	function parseEntryUrl(entry) {
		var parsed;

		if (entry.url.indexOf('data:') !== 0) {
			// @see http://nodejs.org/api/url.html#url_url
			parsed = parseUrl(entry.url) || {};

			entry.protocol = parsed.protocol.replace(':', '');
			entry.domain = parsed.hostname;
			entry.query = parsed.query;

			if (entry.protocol === 'https') {
				entry.isSSL = true;
			}
		}
		else {
			// base64 encoded data
			entry.domain = false;
			entry.protocol = false;
			entry.isBase64 = true;
		}
	}

	// when the monitoring started?
	var loadStartedTime;
	phantomas.on('loadStarted', function(res) {
		loadStartedTime = Date.now();
	});

	phantomas.on('onResourceRequested', function(res, request) {
		// store current request data
		var entry = requests[res.id] = {
			id: res.id,
			url: res.url,
			method: res.method,
			requestHeaders: {},
			sendTime: res.time,
			bodySize: 0,
			isBlocked: false
		};

		// allow modules to block requests
		entry.block = function() {
			this.isBlocked = true;
		};

		res.headers.forEach(function(header) {
			entry.requestHeaders[header.name] = header.value;

			switch (header.name.toLowerCase()) {
				// AJAX requests
				case 'x-requested-with':
					if (header.value === 'XMLHttpRequest') {
						entry.isAjax = true;
					}
					break;
			}
		});

		parseEntryUrl(entry);

		if (entry.isBase64) {
			return;
		}

		// give modules a chance to block requests using entry.block()
		// @see https://github.com/ariya/phantomjs/issues/10230
		phantomas.emit('beforeSend', entry, res); // @desc allows the request to be blocked

		if ( (entry.isBlocked === true) && (typeof request !== 'undefined') ) {
			phantomas.log('Blocked request: <' + entry.url + '>');
			request.abort();
			return;
		}

		// proceed
		phantomas.emit('send', entry, res); // @desc request has been sent
	});

	phantomas.on('onResourceReceived', function(res) {
		// current request data
		var entry = requests[res.id] || {};

		// fix for blocked requests still "emitting" onResourceReceived with "stage" = 'end' and empty "status" (issue #122)
		if (res.status === null ) {
			if (entry.isBlocked) {
				return;
			} else if (!entry.isBase64) {
				phantomas.log('Blocked request by phantomjs: <' + entry.url + '>');
				phantomas.emit('abort', entry, res); // @desc request has been blocked
			}
		}

		switch(res.stage) {
			// the beginning of response
			case 'start':
				entry.recvStartTime = res.time;
				entry.timeToFirstByte = res.time - entry.sendTime;

				// FIXME: buggy
				// @see https://github.com/ariya/phantomjs/issues/10169
				entry.bodySize += res.bodySize || 0;
				break;

			// the end of response
			case 'end':
				// timing
				entry.recvEndTime = res.time;
				entry.timeToLastByte = res.time - entry.sendTime;
				entry.receiveTime = entry.recvEndTime - entry.recvStartTime;

				// request method
				switch(entry.method) {
					case 'POST':
						phantomas.incrMetric('postRequests');
						phantomas.addOffender('postRequests', entry.url);
						break;
				}

				// asset type
				entry.type = 'other';

				// analyze HTTP headers
				entry.headers = {};
				res.headers.forEach(function(header) {
					entry.headers[header.name] = header.value;

					switch (header.name.toLowerCase()) {
						// TODO: why it's not gzipped?
						// because: https://github.com/ariya/phantomjs/issues/10156
						case 'content-length':
							entry.contentLength = parseInt(header.value, 10);
							break;

						// detect content type
						case 'content-type':
							// parse header value
							var value = header.value.split(';').shift().toLowerCase();

							switch(value) {
								case 'text/html':
									entry.type = 'html';
									entry.isHTML = true;
									break;

								case 'text/css':
									entry.type = 'css';
									entry.isCSS = true;
									break;

								case 'application/x-javascript':
								case 'application/javascript':
								case 'text/javascript':
									entry.type = 'js';
									entry.isJS = true;
									break;

								case 'application/json':
									entry.type = 'json';
									entry.isJSON = true;
									break;

								case 'image/png':
								case 'image/jpeg':
								case 'image/gif':
								case 'image/svg+xml':
									entry.type = 'image';
									entry.isImage = true;
									break;

								// @see http://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts
								case 'application/font-wof':
								case 'application/font-woff':
								case 'application/vnd.ms-fontobject':
								case 'application/x-font-opentype':
								case 'application/x-font-truetype':
								case 'application/x-font-ttf':
								case 'application/x-font-woff':
									entry.type = 'webfont';
									entry.isWebFont = true;
									break;

								case 'application/octet-stream':
									var ext = (entry.url || '').split('.').pop();

									switch(ext) {
										// @see http://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts#comment-8077637
										case 'otf':
											entry.type = 'webfont';
											entry.isWebFont = true;
											break;
									}
									break;

								default:
									phantomas.log('Unknown content type found: ' + value + ' for <' + entry.url + '>');
							}
							break;

						// detect content encoding
						case 'content-encoding':
							if (header.value === 'gzip') {
								entry.gzip = true;
							}
							break;
					}
				});

				parseEntryUrl(entry);

				// HTTP code
				entry.status = res.status || 200 /* for base64 data */;
				entry.statusText = HTTP_STATUS_CODES[entry.status];

				switch(entry.status) {
					case 301: // Moved Permanently
					case 302: // Found
					case 303: // See Other
						entry.isRedirect = true;
						break;

					case 404: // Not Found
						phantomas.incrMetric('notFound');
						phantomas.addOffender('notFound', entry.url);
						break;
				}

				// requests stats
				if (!entry.isBase64) {
					phantomas.incrMetric('requests');

					phantomas.incrMetric('bodySize', entry.bodySize); // content only
					phantomas.incrMetric('contentLength', entry.contentLength || entry.bodySize); // content only
				}

				if (entry.gzip) {
					phantomas.incrMetric('gzipRequests');
				}

				if (entry.isSSL) {
					phantomas.incrMetric('httpsRequests');
					phantomas.addOffender('httpsRequests', entry.url);
				}

				if (entry.isBase64) {
					phantomas.emit('base64recv', entry, res); // @desc base64-encoded "response" has been received
				}
				else {
					phantomas.emit('recv' , entry, res); // @desc response has been received
				}
				break;
		}
	});

	// TTFB / TTLB metrics
	var ttfbMeasured = false;

	phantomas.on('recv', function(entry, res) {
		// check the first response which is not a redirect (issue #74)
		if (!ttfbMeasured && !entry.isRedirect) {
			phantomas.setMetric('timeToFirstByte', entry.timeToFirstByte, true);
			phantomas.setMetric('timeToLastByte', entry.timeToLastByte, true);

			ttfbMeasured = true;

			phantomas.log('Time to first byte: set to %d ms for <%s> (HTTP %d)', entry.timeToFirstByte, entry.url, entry.status);
			phantomas.emit('responseEnd', entry, res); // @desc the first response (that was not a redirect) fully received
		}

		// completion of the last HTTP request
		phantomas.setMetric('httpTrafficCompleted', entry.recvEndTime - loadStartedTime);
	});
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import { events } from '../utils';
import { getViewport } from '../utils/utils';
import InjectMenuItem from './MenuItem.js';
import theme from './theme.css';

const POSITION = {
  AUTO: 'auto',
  STATIC: 'static',
  TOP_LEFT: 'topLeft',
  TOP_RIGHT: 'topRight',
  BOTTOM_LEFT: 'bottomLeft',
  BOTTOM_RIGHT: 'bottomRight',
};

const factory = (MenuItem) => {
  class Menu extends Component {
    static propTypes = {
      active: PropTypes.bool,
      children: PropTypes.node,
      className: PropTypes.string,
      onHide: PropTypes.func,
      onSelect: PropTypes.func,
      onShow: PropTypes.func,
      outline: PropTypes.bool,
      position: PropTypes.oneOf(Object.keys(POSITION).map(key => POSITION[key])),
      selectable: PropTypes.bool,
      selected: PropTypes.any,
    };

    static defaultProps = {
      active: false,
      outline: true,
      position: POSITION.STATIC,
      selectable: true,
    };

    state = {
      active: this.props.active,
    };

    componentDidMount () {
      this.positionTimeoutHandle = setTimeout(() => {
        const { width, height } = this.menuNode.getBoundingClientRect();
        const position = this.props.position === POSITION.AUTO
          ? this.calculatePosition()
          : this.props.position;
        this.setState({ position, width, height });
      });
    }

    componentWillReceiveProps (nextProps) {
      if (this.props.position !== nextProps.position) {
        const position = nextProps.position === POSITION.AUTO
          ? this.calculatePosition()
          : nextProps.position;
        this.setState({ position });
      }

      /**
       * If the menu is going to be activated via props and its not active, verify
       * the position is appropriated and then show it recalculating position if its
       * wrong. It should be shown in two consecutive setState.
       */
      if (!this.props.active && nextProps.active && !this.state.active) {
        if (nextProps.position === POSITION.AUTO) {
          const position = this.calculatePosition();
          if (this.state.position !== position) {
            this.setState({ position, active: false }, () => {
              this.activateTimeoutHandle = setTimeout(() => {
                this.show();
              }, 20);
            });
          } else {
            this.show();
          }
        } else {
          this.show();
        }
      }

      /**
       * If the menu is being deactivated via props and the current state is
       * active, it should be hid.
       */
      if (this.props.active && !nextProps.active && this.state.active) {
        this.hide();
      }
    }

    componentWillUpdate (nextProps, nextState) {
      if (!this.state.active && nextState.active) {
        events.addEventsToDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      }
    }

    componentDidUpdate (prevProps, prevState) {
      if (prevState.active && !this.state.active) {
        if (this.props.onHide) {
          this.props.onHide();
        }
        events.removeEventsFromDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      } else if (!prevState.active && this.state.active && this.props.onShow) {
        this.props.onShow();
      }
    }

    componentWillUnmount () {
      if (this.state.active) {
        events.removeEventsFromDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      }
      clearTimeout(this.positionTimeoutHandle);
      clearTimeout(this.activateTimeoutHandle);
    }

    handleDocumentClick = (event) => {
      if (this.state.active && !events.targetIsDescendant(event, ReactDOM.findDOMNode(this))) {
        this.setState({ active: false });
      }
    };

    handleSelect = (item, event) => {
      const { value, onClick } = item.props;
      if (onClick) {
        event.persist();
      }
      this.setState({ active: false }, () => {
        if (onClick) {
          onClick(event);
        }
        if (this.props.onSelect) {
          this.props.onSelect(value);
        }
      });
    };

    calculatePosition () {
      const parentNode = ReactDOM.findDOMNode(this).parentNode;
      if (!parentNode) {
        return;
      }
      const { top, left, height, width } = parentNode.getBoundingClientRect();
      const { height: wh, width: ww } = getViewport();
      const toTop = top < ((wh / 2) - height / 2);
      const toLeft = left < ((ww / 2) - width / 2);
      return `${toTop ? 'top' : 'bottom'}${toLeft ? 'Left' : 'Right'}`;
    }

    getMenuStyle () {
      const { width, height, position } = this.state;
      if (position !== POSITION.STATIC) {
        if (this.state.active) {
          return { clip: `rect(0 ${width}px ${height}px 0)` };
        } else if (position === POSITION.TOP_RIGHT) {
          return { clip: `rect(0 ${width}px 0 ${width}px)` };
        } else if (position === POSITION.BOTTOM_RIGHT) {
          return { clip: `rect(${height}px ${width}px ${height}px ${width}px)` };
        } else if (position === POSITION.BOTTOM_LEFT) {
          return { clip: `rect(${height}px 0 ${height}px 0)` };
        } else if (position === POSITION.TOP_LEFT) {
          return { clip: 'rect(0 0 0 0)' };
        }
      }
    }

    getRootStyle () {
      if (this.state.position !== POSITION.STATIC) {
        return { width: this.state.width, height: this.state.height };
      }
    }

    renderItems () {
      // Because React Hot Loader creates proxied versions of your components,
      // comparing reference types of elements won't work
      // https://github.com/gaearon/react-hot-loader/blob/master/docs/Known%20Limitations.md#checking-element-types
      const MenuItemType = (<MenuItem />).type;

      return React.Children.map(this.props.children, (item) => {
        if (!item) {
          return item;
        }

        if (item.type === MenuItemType) {
          return React.cloneElement(item, {
            selected: typeof item.props.value !== 'undefined' &&
              this.props.selectable &&
              item.props.value === this.props.selected,
            onClick: this.handleSelect.bind(this, item),
          });
        } else {
          return React.cloneElement(item);
        }
      });
    }

    show () {
      const { width, height } = this.menuNode.getBoundingClientRect();
      this.setState({ active: true, width, height });
    }

    hide () {
      this.setState({ active: false });
    }

    render () {
      const outlineStyle = { width: this.state.width, height: this.state.height };
      const className = cx([ theme.menu, theme[this.state.position] ], {
        [theme.active]: this.state.active,
      }, this.props.className);

      return (

          {this.props.outline ?
 : null}

 {
            this.menuNode = node;
          }} className={theme.menuInner} style={this.getMenuStyle()}>
            {this.renderItems()}

      );
    }
  }

  return Menu;
};

const Menu = factory(InjectMenuItem);
export default Menu;
export {
  factory as menuFactory,
  Menu,
};
<commit_msg>Converted Menu to PureComponent<commit_after>import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import { events } from '../utils';
import { getViewport } from '../utils/utils';
import InjectMenuItem from './MenuItem.js';
import theme from './theme.css';

const POSITION = {
  AUTO: 'auto',
  STATIC: 'static',
  TOP_LEFT: 'topLeft',
  TOP_RIGHT: 'topRight',
  BOTTOM_LEFT: 'bottomLeft',
  BOTTOM_RIGHT: 'bottomRight',
};

const factory = (MenuItem) => {
  class Menu extends PureComponent {
    static propTypes = {
      active: PropTypes.bool,
      children: PropTypes.node,
      className: PropTypes.string,
      onHide: PropTypes.func,
      onSelect: PropTypes.func,
      onShow: PropTypes.func,
      outline: PropTypes.bool,
      position: PropTypes.oneOf(Object.keys(POSITION).map(key => POSITION[key])),
      selectable: PropTypes.bool,
      selected: PropTypes.any,
    };

    static defaultProps = {
      active: false,
      outline: true,
      position: POSITION.STATIC,
      selectable: true,
    };

    state = {
      active: this.props.active,
    };

    componentDidMount () {
      this.positionTimeoutHandle = setTimeout(() => {
        const { width, height } = this.menuNode.getBoundingClientRect();
        const position = this.props.position === POSITION.AUTO
          ? this.calculatePosition()
          : this.props.position;
        this.setState({ position, width, height });
      });
    }

    componentWillReceiveProps (nextProps) {
      if (this.props.position !== nextProps.position) {
        const position = nextProps.position === POSITION.AUTO
          ? this.calculatePosition()
          : nextProps.position;
        this.setState({ position });
      }

      /**
       * If the menu is going to be activated via props and its not active, verify
       * the position is appropriated and then show it recalculating position if its
       * wrong. It should be shown in two consecutive setState.
       */
      if (!this.props.active && nextProps.active && !this.state.active) {
        if (nextProps.position === POSITION.AUTO) {
          const position = this.calculatePosition();
          if (this.state.position !== position) {
            this.setState({ position, active: false }, () => {
              this.activateTimeoutHandle = setTimeout(() => {
                this.show();
              }, 20);
            });
          } else {
            this.show();
          }
        } else {
          this.show();
        }
      }

      /**
       * If the menu is being deactivated via props and the current state is
       * active, it should be hid.
       */
      if (this.props.active && !nextProps.active && this.state.active) {
        this.hide();
      }
    }

    componentWillUpdate (nextProps, nextState) {
      if (!this.state.active && nextState.active) {
        events.addEventsToDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      }
    }

    componentDidUpdate (prevProps, prevState) {
      if (prevState.active && !this.state.active) {
        if (this.props.onHide) {
          this.props.onHide();
        }
        events.removeEventsFromDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      } else if (!prevState.active && this.state.active && this.props.onShow) {
        this.props.onShow();
      }
    }

    componentWillUnmount () {
      if (this.state.active) {
        events.removeEventsFromDocument({
          click: this.handleDocumentClick,
          touchstart: this.handleDocumentClick,
        });
      }
      clearTimeout(this.positionTimeoutHandle);
      clearTimeout(this.activateTimeoutHandle);
    }

    handleDocumentClick = (event) => {
      if (this.state.active && !events.targetIsDescendant(event, ReactDOM.findDOMNode(this))) {
        this.setState({ active: false });
      }
    };

    handleSelect = (item, event) => {
      const { value, onClick } = item.props;
      if (onClick) {
        event.persist();
      }
      this.setState({ active: false }, () => {
        if (onClick) {
          onClick(event);
        }
        if (this.props.onSelect) {
          this.props.onSelect(value);
        }
      });
    };

    calculatePosition () {
      const parentNode = ReactDOM.findDOMNode(this).parentNode;
      if (!parentNode) {
        return;
      }
      const { top, left, height, width } = parentNode.getBoundingClientRect();
      const { height: wh, width: ww } = getViewport();
      const toTop = top < ((wh / 2) - height / 2);
      const toLeft = left < ((ww / 2) - width / 2);
      return `${toTop ? 'top' : 'bottom'}${toLeft ? 'Left' : 'Right'}`;
    }

    getMenuStyle () {
      const { width, height, position } = this.state;
      if (position !== POSITION.STATIC) {
        if (this.state.active) {
          return { clip: `rect(0 ${width}px ${height}px 0)` };
        } else if (position === POSITION.TOP_RIGHT) {
          return { clip: `rect(0 ${width}px 0 ${width}px)` };
        } else if (position === POSITION.BOTTOM_RIGHT) {
          return { clip: `rect(${height}px ${width}px ${height}px ${width}px)` };
        } else if (position === POSITION.BOTTOM_LEFT) {
          return { clip: `rect(${height}px 0 ${height}px 0)` };
        } else if (position === POSITION.TOP_LEFT) {
          return { clip: 'rect(0 0 0 0)' };
        }
      }
    }

    getRootStyle () {
      if (this.state.position !== POSITION.STATIC) {
        return { width: this.state.width, height: this.state.height };
      }
    }

    renderItems () {
      // Because React Hot Loader creates proxied versions of your components,
      // comparing reference types of elements won't work
      // https://github.com/gaearon/react-hot-loader/blob/master/docs/Known%20Limitations.md#checking-element-types
      const MenuItemType = (<MenuItem />).type;

      return React.Children.map(this.props.children, (item) => {
        if (!item) {
          return item;
        }

        if (item.type === MenuItemType) {
          return React.cloneElement(item, {
            selected: typeof item.props.value !== 'undefined' &&
              this.props.selectable &&
              item.props.value === this.props.selected,
            onClick: this.handleSelect.bind(this, item),
          });
        } else {
          return React.cloneElement(item);
        }
      });
    }

    show () {
      const { width, height } = this.menuNode.getBoundingClientRect();
      this.setState({ active: true, width, height });
    }

    hide () {
      this.setState({ active: false });
    }

    render () {
      const outlineStyle = { width: this.state.width, height: this.state.height };
      const className = cx([ theme.menu, theme[this.state.position] ], {
        [theme.active]: this.state.active,
      }, this.props.className);

      return (

          {this.props.outline ?
 : null}

 {
            this.menuNode = node;
          }} className={theme.menuInner} style={this.getMenuStyle()}>
            {this.renderItems()}

      );
    }
  }

  return Menu;
};

const Menu = factory(InjectMenuItem);
export default Menu;
export {
  factory as menuFactory,
  Menu,
};
<|endoftext|>


<commit_before>'use babel';
/**
  BreakpointStore
  @description stores information about current breakpoints and provides
    actions to create & clear breakpoints
**/

const Store = require('flux/utils').Store;
import DelveDispatcher from '../dispatcher/delveDispatcher';
import Constants from '../constants/breakpointConstants';

let _breakpoints = [];

/**
  @name BreakpointStore#makePending
  @desc signal that the breakpoint w/ the given id has a pending change
  @param {number} id
**/
function makePending(id) {
  let bp = _breakpoints.find(b => b.id === id);
  bp.pendingChange = true;
}

/**
  @name BreakpointStore#addBreakpoints
  @desc adds the given breakpoints to the list
  @param {error} err => if present removes the pending breakpoint
  @param {object} bp => the breakpoint in the format defined by Delvejs
**/
function addBreakpoints(err, bp) {
  // if we have more than one breakpoint just push them all since we won't
  // have added any pending ones
  if (Array.isArray(bp) && bp.length > 1) {
    if (err) return atom.notifications.addError('Error adding breakpoints', {
      detail: err.toString(), dismissable: true
    });

    return _breakpoints
    .push(bp.map(b => { b.pendingChange = false; return b; }));
  }

  let idx = _breakpoints.findIndex(b => b.id < 0 && b.name === bp.name);

  if (err) {
    if (idx >= 0) _breakpoints.splice(idx, 1);
    return atom.notifications.addError('Error adding breakpoint', {
      detail: err.toString(), dismissable: true
    });
  }

  bp.pendingChange = false;
  if (idx >= 0) _breakpoints[idx] = bp;
  else _breakpoints.push(bp);
}

/**
  @name BreakpointStore#addPending
  @desc adds a pending breakpoint to the list
  @param {string} name
  @param {object} location => if null this is a noop
  @param {string} location.file
  @param {number} location.line
**/
function addPending(name, location) {
  // don't add anything if we don't know where the added breakpoint(s) will be,
  // i.e. in the case of adding by pattern where we don't know how many will
  // be added or where, if any at all
  if (location === null) return;
  _breakpoints.push({id: -1, name, location, pendingChange: true});
}

/**
  @name BreakpointStore#updateBreakpoint
  @desc updates the breakpoint w/ the given id w/ the given prop changes
  @param {error} err
  @param {number} id
  @param {object} updatedProps
**/
function updateBreakpoint(err, id, updatedProps) {
  let bp = _breakpoints.find(b => b.id === id);
  bp.pendingChange = false;

  if (err)
    return atom.notifications.addError('Error setting breakpoint condition', {
      detail: err.toString(), dismissable: true
    });

  bp = Object.assign(bp, updatedProps);
}

/**
  @name BreakpointStore#clearBreakpoint
  @desc removes the breakpoint from the list
  @param {error} err => removes even if there's an error
  @param {number} id
**/
function clearBreakpoint(err, id) {
  let idx = _breakpoints.findIndex(b => b.id === id);
  if (idx < 0) return;

  if (err) console.debug(
      `Received error deleting breakpoint ${id}, assuming destroyed`, err
    );

  _breakpoints.slice(idx, 1);
}

/**
  @name BreakpointStore#clearAllBreakpoints
  @desc clears all non-pending breakpoints
  @param {error} err => does *not* remove on error
**/
function clearAllBreakpoints(err) {
  if (err) return atom.notifications.addError('Error clearing all breakpoints',
    { detail: err.toString(), dismissable: true }
  );

  let pending = _breakpoints.filter(bp => bp.pendingChange);
  _breakpoints = [].concat(pending);
}

class BreakpointStore extends Store {
  constructor(dispatcher) {
    super(dispatcher);
  }

  getBreakpoints() {
    return _breakpoints;
  }

  getBreakpointsForFile(file) {
    return _breakpoints.filter(bp => bp.location.file === file);
  }

  getBreakpointById(id) {
    return _breakpoints.find(bp => bp.id === id);
  }

  __onDispatch(payload) {
    let didChange = true;
    switch (payload.type) {
    case Constants.BREAKPOINT_CREATE_REQUEST:
      addPending(payload.name, payload.location);
      break;
    case Constants.BREAKPOINT_CREATE_SUCCESS:
      addBreakpoints(null, payload.created);
      break;
    case Constants.BREAKPOINT_CREATE_FAILURE:
      let failedBpData;
      if (payload.expectingMult) failedBpData = [{}, {}];
      else failedBpData = { name: payload.name };

      addBreakpoints(payload.error, failedBpData);
      break;

    case Constants.BREAKPOINT_SET_COND_REQUEST:
      makePending(payload.bpId);
      break;
    case Constants.BREAKPOINT_SET_COND_SUCCESS:
      updateBreakpoint(null, payload.bpId, {condition: payload.condition});
      break;
    case Constants.BREAKPOINT_SET_COND_FAILURE:
      updateBreakpoint(payload.error, payload.bpId);
      break;

    case Constants.BREAKPOINT_CLEAR_REQUEST:
      makePending(payload.bpId);
      break;
    case Constants.BREAKPOINT_CLEAR_SUCCESS:
      clearBreakpoint(null, payload.bpId);
      break;
    case Constants.BREAKPOINT_CLEAR_FAILURE:
      clearBreakpoint(payload.error, payload.bpId);
      break;

    case Constants.BREAKPOINT_CLEAR_ALL_REQUEST:
      break;
    case Constants.BREAKPOINT_CLEAR_ALL_SUCCESS:
      clearAllBreakpoints(null);
      break;
    case Constants.BREAKPOINT_CLEAR_ALL_FAILURE:
      clearAllBreakpoints(payload.error);
      break;

    default: didChange = false;
    }

    if (didChange) this.__emitChange();
  }
}

const bpStore = new BreakpointStore(DelveDispatcher);
export default bpStore;
<commit_msg>Find existing pending bps by location not by name<commit_after>'use babel';
/**
  BreakpointStore
  @description stores information about current breakpoints and provides
    actions to create & clear breakpoints
**/

const Store = require('flux/utils').Store;
import DelveDispatcher from '../dispatcher/delveDispatcher';
import Constants from '../constants/breakpointConstants';

let _breakpoints = [];

/**
  @name BreakpointStore#makePending
  @desc signal that the breakpoint w/ the given id has a pending change
  @param {number} id
**/
function makePending(id) {
  let bp = _breakpoints.find(b => b.id === id);
  bp.pendingChange = true;
}

/**
  @name BreakpointStore#addBreakpoints
  @desc adds the given breakpoints to the list
  @param {error} err => if present removes the pending breakpoint
  @param {object} bp => the breakpoint in the format defined by Delvejs
**/
function addBreakpoints(err, bp) {
  // if we have more than one breakpoint just push them all since we won't
  // have added any pending ones
  if (Array.isArray(bp) && bp.length > 1) {
    if (err) return atom.notifications.addError('Error adding breakpoints', {
      detail: err.toString(), dismissable: true
    });

    return _breakpoints
    .push(bp.map(b => { b.pendingChange = false; return b; }));
  }

  let idx = _breakpoints.findIndex(b => b.id < 0
    && b.location.file == bp.location.file
    && b.location.line == bp.location.line);

  if (err) {
    if (idx >= 0) _breakpoints.splice(idx, 1);
    return atom.notifications.addError('Error adding breakpoint', {
      detail: err.toString(), dismissable: true
    });
  }

  bp.pendingChange = false;
  if (idx >= 0) _breakpoints[idx] = bp;
  else _breakpoints.push(bp);
}

/**
  @name BreakpointStore#addPending
  @desc adds a pending breakpoint to the list
  @param {string} name
  @param {object} location => if null this is a noop
  @param {string} location.file
  @param {number} location.line
**/
function addPending(name, location) {
  // don't add anything if we don't know where the added breakpoint(s) will be,
  // i.e. in the case of adding by pattern where we don't know how many will
  // be added or where, if any at all
  if (location === null) return;
  _breakpoints.push({id: -1, name, location, pendingChange: true});
}

/**
  @name BreakpointStore#updateBreakpoint
  @desc updates the breakpoint w/ the given id w/ the given prop changes
  @param {error} err
  @param {number} id
  @param {object} updatedProps
**/
function updateBreakpoint(err, id, updatedProps) {
  let bp = _breakpoints.find(b => b.id === id);
  bp.pendingChange = false;

  if (err)
    return atom.notifications.addError('Error setting breakpoint condition', {
      detail: err.toString(), dismissable: true
    });

  bp = Object.assign(bp, updatedProps);
}

/**
  @name BreakpointStore#clearBreakpoint
  @desc removes the breakpoint from the list
  @param {error} err => removes even if there's an error
  @param {number} id
**/
function clearBreakpoint(err, id) {
  let idx = _breakpoints.findIndex(b => b.id === id);
  if (idx < 0) return;

  if (err) console.debug(
      `Received error deleting breakpoint ${id}, assuming destroyed`, err
    );

  _breakpoints.slice(idx, 1);
}

/**
  @name BreakpointStore#clearAllBreakpoints
  @desc clears all non-pending breakpoints
  @param {error} err => does *not* remove on error
**/
function clearAllBreakpoints(err) {
  if (err) return atom.notifications.addError('Error clearing all breakpoints',
    { detail: err.toString(), dismissable: true }
  );

  let pending = _breakpoints.filter(bp => bp.pendingChange);
  _breakpoints = [].concat(pending);
}

class BreakpointStore extends Store {
  constructor(dispatcher) {
    super(dispatcher);
  }

  getBreakpoints() {
    return _breakpoints;
  }

  getBreakpointsForFile(file) {
    return _breakpoints.filter(bp => bp.location.file === file);
  }

  getBreakpointById(id) {
    return _breakpoints.find(bp => bp.id === id);
  }

  __onDispatch(payload) {
    let didChange = true;
    switch (payload.type) {
    case Constants.BREAKPOINT_CREATE_REQUEST:
      addPending(payload.name, payload.location);
      break;
    case Constants.BREAKPOINT_CREATE_SUCCESS:
      addBreakpoints(null, payload.created);
      break;
    case Constants.BREAKPOINT_CREATE_FAILURE:
      let failedBpData;
      if (payload.expectingMult) failedBpData = [{}, {}];
      else failedBpData = { name: payload.name };

      addBreakpoints(payload.error, failedBpData);
      break;

    case Constants.BREAKPOINT_SET_COND_REQUEST:
      makePending(payload.bpId);
      break;
    case Constants.BREAKPOINT_SET_COND_SUCCESS:
      updateBreakpoint(null, payload.bpId, {condition: payload.condition});
      break;
    case Constants.BREAKPOINT_SET_COND_FAILURE:
      updateBreakpoint(payload.error, payload.bpId);
      break;

    case Constants.BREAKPOINT_CLEAR_REQUEST:
      makePending(payload.bpId);
      break;
    case Constants.BREAKPOINT_CLEAR_SUCCESS:
      clearBreakpoint(null, payload.bpId);
      break;
    case Constants.BREAKPOINT_CLEAR_FAILURE:
      clearBreakpoint(payload.error, payload.bpId);
      break;

    case Constants.BREAKPOINT_CLEAR_ALL_REQUEST:
      break;
    case Constants.BREAKPOINT_CLEAR_ALL_SUCCESS:
      clearAllBreakpoints(null);
      break;
    case Constants.BREAKPOINT_CLEAR_ALL_FAILURE:
      clearAllBreakpoints(payload.error);
      break;

    default: didChange = false;
    }

    if (didChange) this.__emitChange();
  }
}

const bpStore = new BreakpointStore(DelveDispatcher);
export default bpStore;
<|endoftext|>


<commit_before>var debug = require('debug')('kwiz:core')
var _ = require('lodash')
var Mingo = require('mingo')
var defaultHandlers = require('./handlers')

var kwiz = Kwiz.prototype
module.exports = Kwiz

function flatten (messages, parentCriteria) {
  var result = []
  for (var i = 0; i < messages.length; i++) {
    var message = messages[i]
    if (!message.messages) {
      if (parentCriteria.length > 0) {
        message.criteria = {$and: _.filter(parentCriteria.concat(message.criteria))}
      }
      result.push(message)
    } else {
      result = result.concat(flatten(message.messages,  _.filter(parentCriteria.concat(message.criteria))))
    }
  }
  return result
}

function Kwiz (quizDefinition, state) {
  if (!(this instanceof Kwiz)) {
    return new Kwiz(token)
  }
  debug('Initialize', quizDefinition, state)
  this.quiz = Object.assign({messages: []}, quizDefinition)
  this.quiz.messages = flatten(this.quiz.messages, [])
  this.state = Object.assign({answers: {}}, state)
  this.handlers = defaultHandlers
  this.Handlebars = require('handlebars')
}

kwiz.finish = function (result) {
  this.state.completed = true
  return Promise.resolve(result)
}

kwiz.addHandler = function (type, handler) {
  this.handlers[type] = handler
}

kwiz.getState = function () {
  return this.state
}

kwiz.transform = function (answer) {
  var previousMessage = this.state.cursor >= 0 ? this.quiz.messages[this.state.cursor] : {}
  if (previousMessage.question) {
    if (!answer) {
      return Promise.reject(previousMessage.question.hint || 'Answer is required')
    }
    var handler = this.handlers[previousMessage.question.type]
    if (handler) {
      return handler(previousMessage.question, answer)
    }
  }
  return Promise.resolve(answer)
}

kwiz.processMessage = function (answer) {
  if (this.quiz.messages.length == 0) {
    return this.finish({
      completed: true
    })
  }
  return this.transform(answer)
    .then(function (answer) {
      var state = this.state
      var previousMessage = state.cursor >= 0 ? this.quiz.messages[state.cursor] : {}
      if (previousMessage.question) {
        this.state.answers[previousMessage.question.id] = answer
      }

      while(true) {
        state.cursor++
        if (state.cursor >= this.quiz.messages.length) {
          return this.finish({
            completed: true
          })
        }

        var message = this.quiz.messages[state.cursor]
        if (message.criteria) {
          var query = new Mingo.Query(message.criteria)
          if (!query.test(this.state)) {
            continue
          }
        }

        if (state.cursor === (this.quiz.messages.length - 1) && !message.answer) {
          return this.finish({
            message: this.Handlebars.compile(message.message)(state),
            completed: true
          })
        }
        return Promise.resolve({
          message: this.Handlebars.compile(message.message)(state)
        })
      }
    }.bind(this))
    .catch(function (error) {
      if (!_.isString(error)) {
        throw error
      }
      return {
        message: this.Handlebars.compile(error)(this.state),
        error: true
      }
    }.bind(this))
}

kwiz.start = function () {
  this.state.cursor = -1
  this.state.completed = false
  return this.processMessage()
}
<commit_msg>cleanup<commit_after>var debug = require('debug')('kwiz:core')
var _ = require('lodash')
var Mingo = require('mingo')
var defaultHandlers = require('./handlers')

var kwiz = Kwiz.prototype
module.exports = Kwiz

function Kwiz (quizDefinition, state) {
  if (!(this instanceof Kwiz)) {
    return new Kwiz(quizDefinition, state)
  }
  debug('Initialize', quizDefinition, state)
  this.quiz = Object.assign({messages: []}, quizDefinition)
  this.quiz.messages = flatten(this.quiz.messages, [])
  this.state = Object.assign({answers: {}}, state)
  this.handlers = defaultHandlers
  this.Handlebars = require('handlebars')
}

kwiz.finish = function (result) {
  this.state.completed = true
  return Promise.resolve(result)
}

kwiz.addHandler = function (type, handler) {
  this.handlers[type] = handler
}

kwiz.getState = function () {
  return this.state
}

kwiz.processMessage = function (answer) {
  if (this.quiz.messages.length == 0) {
    return this.finish({
      completed: true
    })
  }
  return this.transform(answer)
    .then(function (answer) {
      var state = this.state
      var previousMessage = state.cursor >= 0 ? this.quiz.messages[state.cursor] : {}
      if (previousMessage.question) {
        this.state.answers[previousMessage.question.id] = answer
      }

      while(true) {
        state.cursor++
        if (state.cursor >= this.quiz.messages.length) {
          return this.finish({
            completed: true
          })
        }

        var message = this.quiz.messages[state.cursor]
        if (message.criteria) {
          var query = new Mingo.Query(message.criteria)
          if (!query.test(this.state)) {
            continue
          }
        }

        if (state.cursor === (this.quiz.messages.length - 1) && !message.answer) {
          return this.finish({
            message: this.Handlebars.compile(message.message)(state),
            completed: true
          })
        }
        return Promise.resolve({
          message: this.Handlebars.compile(message.message)(state)
        })
      }
    }.bind(this))
    .catch(function (error) {
      if (!_.isString(error)) {
        throw error
      }
      return {
        message: this.Handlebars.compile(error)(this.state),
        error: true
      }
    }.bind(this))
}

kwiz.transform = function (answer) {
  var previousMessage = this.state.cursor >= 0 ? this.quiz.messages[this.state.cursor] : {}
  if (previousMessage.question) {
    if (!answer) {
      return Promise.reject(previousMessage.question.hint || 'Answer is required')
    }
    var handler = this.handlers[previousMessage.question.type]
    if (handler) {
      return handler(previousMessage.question, answer)
    }
  }
  return Promise.resolve(answer)
}

kwiz.start = function () {
  this.state.cursor = -1
  this.state.completed = false
  return this.processMessage()
}

function flatten (messages, parentCriteria) {
  var result = []
  for (var i = 0; i < messages.length; i++) {
    var message = messages[i]
    if (!message.messages) {
      if (parentCriteria.length > 0) {
        message.criteria = {$and: _.filter(parentCriteria.concat(message.criteria))}
      }
      result.push(message)
    } else {
      result = result.concat(flatten(message.messages, _.filter(parentCriteria.concat(message.criteria))))
    }
  }
  return result
}
<|endoftext|>


<commit_before>var GENEMAP = GENEMAP || {};

GENEMAP.MenuBar = function (userConfig) {
  var defaultConfig = {
    onNetworkBtnClick: $.noop,
    onFitBtnClick: $.noop,
    onTagBtnClick: $.noop,
    onResetBtnClick: $.noop,
    onSetMaxGenesClick : $.noop,
    initialMaxGenes : 1000,
  };

  var config = _.merge({}, defaultConfig, userConfig);

  // the target element that is going to contain the menu buttons
  var target;

  // click handler for the network view button
  var myOnNetworkBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onNetworkBtnClick();
  };

  var myOnTagBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onTagBtnClick();
  };

  var myOnFitBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onFitBtnClick();
  };

  var myOnResetBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onResetBtnClick();
  };

  var buildDropdown = function(selection, id, data, callback, initialValue){
    dropDown = selection.attr( {'class': 'menu-dropdown bootstrap'}).selectAll('.btn-group').data([null]);

    var dropdownSpanEnter = dropDown.enter();

    var dropdown = dropdownSpanEnter
      .append('span') .attr({ 'class': 'btn-group'});

    var dropdownButtonAttr = {
      'class' : 'menu-dropdown btn btn-default dropdown-toggle' ,
      'id' : id,
      'type' : "button",
      'data-toggle' : "dropdown",
      'aria-haspopup' : 'true'};

    maxLabelWidth = data.reduce( function(max, d){return Math.max(max, d.toString().length)}, 0);

    button = dropdown
      .append('button').attr( dropdownButtonAttr).style( {width: maxLabelWidth + 1 + 'em'});

      button.append('span').attr( {class: 'title'}).text(initialValue);
      button.append( 'span').attr({'class':'caret'}).style({"position": "absolute", "left": "80%", "top" : "45%"});

    dropdown
      .insert( 'ul').attr( {'class': 'dropdown-menu', 'aria-labelledby': id });

    dropdownItems = dropdown.select('.dropdown-menu').selectAll('.dropdown-item').data(data);
    dropdownItems.enter()
      .append('li').on('click', function(d){
      dropdown.select('span.title').text(d);
      callback(d);
    })
      .append('a').attr({'class': 'dropdown-item', 'href' : '#'}).text(function(d){return d} );
  }

  var drawMenu = function () {

    var menu = d3.select(target).selectAll('.genemap-menu').data([null]);
    menu.enter().append('div').classed('genemap-menu', true);

    var menuItems = menu.selectAll('span').data(
      ['network-btn', 'tag-btn', 'fit-btn', 'reset-btn', 'ngenes-dropdown']);
    menuItems.enter().append('span');
    menuItems.attr({
      class: function (d) {
        return d;
      },
    });

    menu.select('.network-btn').on('click', myOnNetworkBtnClick);

    menu.select('.tag-btn')
      .on('click', myOnTagBtnClick);

    menu.select('.fit-btn')
      .on('click', myOnFitBtnClick);

    menu.select('.reset-btn')
      .on('click', myOnResetBtnClick);

    var dropdownSpan = menu.select('.ngenes-dropdown');
    dropdownSpan.text("Max genes to display: ");
    buildDropdown( dropdownSpan, 'ngenes-dropdown', [50, 100, 200, 500, 1000],
      config.onSetMaxGenesClick, config.initialMaxGenes);
  }

  // attach the menu bar to the target element
  function my(selection) {
    selection.each(function (d) {
      var _this = this;

      target = _this;

      // draw the map SVG
      drawMenu();
    });
  }

  my.onNetworkBtnClick = function (value) {
    if (!arguments.length) {
      return config.onNetworkBtnClick;
    }

    config.onNetworkBtnClick = value;
    return my;
  };

  my.onTagBtnClick = function (value) {
    if (!arguments.length) {
      return config.onTagBtnClick;
    }

    config.onTagBtnClick = value;
    return my;
  };

  my.onFitBtnClick = function (value) {
    if (!arguments.length) {
      return config.onFitBtnClick;
    }

    config.onFitBtnClick = value;
    return my;
  };

  my.onResetBtnClick = function (value) {
    if (!arguments.length) {
      return config.onResetBtnClick;
    }

    config.onResetBtnClick = value;
    return my;
  };

  my.onSetMaxGenesClick = function (value) {
    if (!arguments.length) {
      return config.onSetMaxGenesClick;
    }

    config.onSetMaxGenesClick = value;
    return my;
  };

  my.initialMaxGenes = function (value) {
    if (!arguments.length) {
      return config.initialMaxGenes;
    }

    config.initialMaxGenes = value;
    return my;
  }

  // sets the tag button state to the specified value
  // value should be 'show', 'hide', 'auto' or 'manual'
  my.setTabButtonState = function (value) {
    var btn = d3.select(target).select('.tag-btn');
    if (value === 'show') {
      btn.classed('show-label', true);
      btn.classed('hide-label', false);
      btn.classed('auto-label', false);
      btn.classed('manual-label', false);
      btn.attr('title', 'Show Labels');

    } else if (value === 'hide') {
      btn.classed('show-label', false);
      btn.classed('hide-label', true);
      btn.classed('auto-label', false);
      btn.classed('manual-label', false);
      btn.attr('title', 'Hide Labels');
    } else if (value === 'manual') {
      btn.classed('show-label', false);
      btn.classed('hide-label', false);
      btn.classed('auto-label', false);
      btn.classed('manual-label', true);
      btn.attr('title', 'Manual Labels');
    } else {
      btn.classed('show-label', false);
      btn.classed('hide-label', false);
      btn.classed('auto-label', true);
      btn.classed('manual-label', false);
      btn.attr('title', 'Automatic Labels');
    }
  };

  my.getTagButtonState = function () {
    var btn = d3.select(target).select('.tag-btn');

    if (btn.classed('show-label')) {
      return 'show';
    } else if (btn.classed('hide-label')) {
      return 'hide';
    } else if (btn.classed('auto-label')) {
      return 'auto';
    } else {
      return 'manual';
    }
  };

  // sets the enabled state of the fit button
  my.setFitButtonEnabled = function (value) {
    d3.select(target).select('.fit-btn').classed('disabled', !value);
  };

  my.setNetworkButtonEnabled = function (value) {
    d3.select(target).select('.network-btn').classed('disabled', !value);
  };

  return my;
};
<commit_msg>Add tool tips to buttons<commit_after>var GENEMAP = GENEMAP || {};

GENEMAP.MenuBar = function (userConfig) {
  var defaultConfig = {
    onNetworkBtnClick: $.noop,
    onFitBtnClick: $.noop,
    onTagBtnClick: $.noop,
    onResetBtnClick: $.noop,
    onSetMaxGenesClick : $.noop,
    initialMaxGenes : 1000,
  };

  var config = _.merge({}, defaultConfig, userConfig);

  // the target element that is going to contain the menu buttons
  var target;

  // click handler for the network view button
  var myOnNetworkBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onNetworkBtnClick();
  };

  var myOnTagBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onTagBtnClick();
  };

  var myOnFitBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onFitBtnClick();
  };

  var myOnResetBtnClick = function () {
    if ($(this).hasClass('disabled')) {
      return;
    }

    config.onResetBtnClick();
  };

  var buildDropdown = function(selection, id, data, callback, initialValue){
    dropDown = selection.attr( {'class': 'menu-dropdown bootstrap'}).selectAll('.btn-group').data([null]);

    var dropdownSpanEnter = dropDown.enter();

    var dropdown = dropdownSpanEnter
      .append('span') .attr({ 'class': 'btn-group'});

    var dropdownButtonAttr = {
      'class' : 'menu-dropdown btn btn-default dropdown-toggle' ,
      'id' : id,
      'type' : "button",
      'data-toggle' : "dropdown",
      'aria-haspopup' : 'true'};

    maxLabelWidth = data.reduce( function(max, d){return Math.max(max, d.toString().length)}, 0);

    button = dropdown
      .append('button').attr( dropdownButtonAttr).style( {width: maxLabelWidth + 1 + 'em'});

      button.append('span').attr( {class: 'title'}).text(initialValue);
      button.append( 'span').attr({'class':'caret'}).style({"position": "absolute", "left": "80%", "top" : "45%"});

    dropdown
      .insert( 'ul').attr( {'class': 'dropdown-menu', 'aria-labelledby': id });

    dropdownItems = dropdown.select('.dropdown-menu').selectAll('.dropdown-item').data(data);
    dropdownItems.enter()
      .append('li').on('click', function(d){
      dropdown.select('span.title').text(d);
      callback(d);
    })
      .append('a').attr({'class': 'dropdown-item', 'href' : '#'}).text(function(d){return d} );
  }

  var drawMenu = function () {

    var menu = d3.select(target).selectAll('.genemap-menu').data([null]);
    menu.enter().append('div').classed('genemap-menu', true);

    var menuItems = menu.selectAll('span').data(
      ['network-btn', 'tag-btn', 'fit-btn', 'reset-btn', 'ngenes-dropdown']);
    menuItems.enter().append('span');
    menuItems.attr({
      class: function (d) {
        return d;
      },
    });

    menu.select('.network-btn')
      .attr( 'title', 'Launch network view')
      .on('click', myOnNetworkBtnClick);

    menu.select('.tag-btn')
      .on('click', myOnTagBtnClick);

    menu.select('.fit-btn')
      .attr( 'title', 'Reset pan and zoom')
      .on('click', myOnFitBtnClick);

    menu.select('.reset-btn')
      .attr( 'title', 'Reset selections')
      .on('click', myOnResetBtnClick);

    var dropdownSpan = menu.select('.ngenes-dropdown');
    dropdownSpan.text("Max genes to display: ");
    buildDropdown( dropdownSpan, 'ngenes-dropdown', [50, 100, 200, 500, 1000],
      config.onSetMaxGenesClick, config.initialMaxGenes);
  }

  // attach the menu bar to the target element
  function my(selection) {
    selection.each(function (d) {
      var _this = this;

      target = _this;

      // draw the map SVG
      drawMenu();
    });
  }

  my.onNetworkBtnClick = function (value) {
    if (!arguments.length) {
      return config.onNetworkBtnClick;
    }

    config.onNetworkBtnClick = value;
    return my;
  };

  my.onTagBtnClick = function (value) {
    if (!arguments.length) {
      return config.onTagBtnClick;
    }

    config.onTagBtnClick = value;
    return my;
  };

  my.onFitBtnClick = function (value) {
    if (!arguments.length) {
      return config.onFitBtnClick;
    }

    config.onFitBtnClick = value;
    return my;
  };

  my.onResetBtnClick = function (value) {
    if (!arguments.length) {
      return config.onResetBtnClick;
    }

    config.onResetBtnClick = value;
    return my;
  };

  my.onSetMaxGenesClick = function (value) {
    if (!arguments.length) {
      return config.onSetMaxGenesClick;
    }

    config.onSetMaxGenesClick = value;
    return my;
  };

  my.initialMaxGenes = function (value) {
    if (!arguments.length) {
      return config.initialMaxGenes;
    }

    config.initialMaxGenes = value;
    return my;
  }

  // sets the tag button state to the specified value
  // value should be 'show', 'hide', 'auto' or 'manual'
  my.setTabButtonState = function (value) {
    var btn = d3.select(target).select('.tag-btn');
    if (value === 'show') {
      btn.classed('show-label', true);
      btn.classed('hide-label', false);
      btn.classed('auto-label', false);
      btn.classed('manual-label', false);
      btn.attr('title', 'Show Labels');

    } else if (value === 'hide') {
      btn.classed('show-label', false);
      btn.classed('hide-label', true);
      btn.classed('auto-label', false);
      btn.classed('manual-label', false);
      btn.attr('title', 'Hide Labels');
    } else if (value === 'manual') {
      btn.classed('show-label', false);
      btn.classed('hide-label', false);
      btn.classed('auto-label', false);
      btn.classed('manual-label', true);
      btn.attr('title', 'Manual Labels');
    } else {
      btn.classed('show-label', false);
      btn.classed('hide-label', false);
      btn.classed('auto-label', true);
      btn.classed('manual-label', false);
      btn.attr('title', 'Automatic Labels');
    }
  };

  my.getTagButtonState = function () {
    var btn = d3.select(target).select('.tag-btn');

    if (btn.classed('show-label')) {
      return 'show';
    } else if (btn.classed('hide-label')) {
      return 'hide';
    } else if (btn.classed('auto-label')) {
      return 'auto';
    } else {
      return 'manual';
    }
  };

  // sets the enabled state of the fit button
  my.setFitButtonEnabled = function (value) {
    d3.select(target).select('.fit-btn').classed('disabled', !value);
  };

  my.setNetworkButtonEnabled = function (value) {
    d3.select(target).select('.network-btn').classed('disabled', !value);
  };

  return my;
};
<|endoftext|>


<commit_before>var Class = require('../utils/Class');
var Extend = require('../utils/object/Extend');
var EventDispatcher = require('../events/EventDispatcher');
var NOOP = require('../utils/NOOP');
//  Phaser.Sound.BaseSound
var BaseSound = new Class({
    initialize: function BaseSound(manager, key, config) {
        /**
         * Local reference to the sound manager.
         *
         * @private
         * @property {Phaser.Sound.BaseSoundManager} manager
         */
        this.manager = manager;
        /**
         * [description]
         *
         * @property {string} key
         */
        this.key = key;
        /**
         * [description]
         *
         * Default values will be set by properties' setters.
         *
         * @private
         * @property {ISoundConfig} config
         */
        this.config = {};
        /**
         * Reference to the currently used config.
         * It could be default config or marker config.
         *
         * @private
         * @property {ISoundConfig} currentConfig
         */
        this.currentConfig = this.config;
        /**
         * [description]
         *
         * @property {boolean} mute
         */
        this.mute = false;
        /**
         * [description]
         *
         * @property {number} volume
         */
        this.volume = 1;
        /**
         * Defines the speed at which the audio asset will be played.
         * Value of 1.0 plays the audio at full speed, 0.5 plays the audio
         * at half speed and 2.0 doubles the audio's playback speed.
         * This value gets multiplied by global rate to have the final playback speed.
         *
         * @property {number} rate
         */
        this.rate = 1;
        /**
         * Represents detuning of sound in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29).
         * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent).
         *
         * @property {number} detune
         */
        this.detune = 0;
        /**
         * A property that holds the value of sound's actual playback rate,
         * after its rate and detune values has been combined with global
         * rate and detune values.
         *
         * @property {number} totalRate
         */
        this.totalRate = 1;
        /**
         * [description]
         *
         * @property {number} seek
         */
        this.seek = 0;
        /**
         * [description]
         *
         * @property {boolean} loop
         */
        this.loop = false;
        /**
         * [description]
         *
         * @property {number} pan
         */
        this.pan = 0;
        this.config = Extend(this.config, config);
        /**
         * A value representing the duration, in seconds.
         * It could be total sound duration or a marker duration.
         *
         * @readonly
         * @property {number} duration
         */
        this.duration = 0;
        /**
         * Duration of the entire sound.
         *
         * @readonly
         * @property {number}
         */
        this.totalDuration = 0;
        /**
         * Flag indicating if sound is currently playing.
         *
         * @readonly
         * @property {boolean} isPlaying
         */
        this.isPlaying = false;
        /**
         * Flag indicating if sound is currently paused.
         *
         * @readonly
         * @property {boolean} isPaused
         */
        this.isPaused = false;
        /**
         * Object containing markers definitions.
         *
         * @readonly
         * @property {{}} markers
         */
        this.markers = {};
        /**
         * Currently playing marker.
         * 'null' if whole sound is playing.
         *
         * @readonly
         * @property {ISoundMarker} currentMarker
         */
        this.currentMarker = null;
        /**
         * [description]
         *
         * @private
         * @property {Phaser.Tween}
         */
        this.fadeTween = null; // TODO see how to use global tween
        /**
         * Event dispatches used to handle all sound instance
         * relevant events.
         *
         * @property {Phaser.Events.EventDispatcher}
         */
        this.events = new EventDispatcher();
    },
    addMarker: function (marker) {
        if (!marker) {
            console.error('addMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('addMarker - Marker has to have a valid name!');
            return false;
        }
        if (this.markers[marker.name]) {
            console.error('addMarker - Marker with name \'' + marker.name + '\' already exists for sound \'' + this.key + '\'!');
            return false;
        }
        marker = Extend(true, {
            name: '',
            start: 0,
            duration: this.totalDuration,
            config: {
                mute: false,
                volume: 1,
                rate: 1,
                detune: 0,
                seek: 0,
                loop: false,
                pan: 0
            }
        }, marker);
        this.markers[marker.name] = marker;
        return true;
    },
    updateMarker: function (marker) {
        if (!marker) {
            console.error('updateMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('updateMarker - Marker has to have a valid name!');
            return false;
        }
        if (!this.markers[marker.name]) {
            console.error('updateMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return false;
        }
        this.markers[marker.name] = Extend(true, this.markers[marker.name], marker);
        return true;
    },
    removeMarker: function (markerName) {
        var marker = this.markers[markerName];
        if (!marker) {
            console.error('removeMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return null;
        }
        this.markers[markerName] = null;
        return marker;
    },
    play: function (markerName, config) {
        if (markerName === void 0) { markerName = ''; }
        if (typeof markerName === 'object') {
            config = markerName;
            markerName = '';
        }
        if (typeof markerName !== 'string') {
            console.error('Sound marker name has to be a string!');
            return null;
        }
        if (!markerName) {
            this.currentMarker = null;
            this.currentConfig = this.config;
            this.duration = this.totalDuration;
        }
        else {
            if (!this.markers[markerName]) {
                console.error('No marker with name \'' + markerName + '\' found for sound \'' + this.key + '\'!');
                return null;
            }
            this.currentMarker = this.markers[markerName];
            this.currentConfig = this.currentMarker.config;
            this.duration = this.currentMarker.duration;
        }
        this.currentConfig = Extend(this.currentConfig, config);
        this.isPlaying = true;
        this.isPaused = false;
        return this;
    },
    pause: function () {
        if (this.isPaused || !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = true;
        return true;
    },
    resume: function () {
        if (!this.isPaused || this.isPlaying) {
            return false;
        }
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    stop: function () {
        if (!this.isPaused && !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = false;
        return true;
    },
    /**
     * @private
     */
    applyConfig: function () {
        this.mute = this.currentConfig.mute;
        this.volume = this.currentConfig.volume;
        this.rate = this.currentConfig.rate;
        this.detune = this.currentConfig.detune;
        // TODO assign other config values to buffer source
    },
    fadeTo: function (volume, duration) {
        return null;
    },
    /**
     * @private
     */
    update: NOOP,
    destroy: function () {
    },
    /**
     * @private
     */
    setRate: function () {
        var cent = 1.0005777895065548; // Math.pow(2, 1/1200);
        var totalDetune = this.currentConfig.detune + this.manager.detune;
        var detuneRate = Math.pow(cent, totalDetune);
        this.totalRate = this.currentConfig.rate * this.manager.rate * detuneRate;
    }
});
module.exports = BaseSound;
<commit_msg>Moved isPlaying and isPaused flags initialisation above setting config options since they are used in some of the config options' setters (detune, rate)<commit_after>var Class = require('../utils/Class');
var Extend = require('../utils/object/Extend');
var EventDispatcher = require('../events/EventDispatcher');
var NOOP = require('../utils/NOOP');
//  Phaser.Sound.BaseSound
var BaseSound = new Class({
    initialize: function BaseSound(manager, key, config) {
        /**
         * Local reference to the sound manager.
         *
         * @private
         * @property {Phaser.Sound.BaseSoundManager} manager
         */
        this.manager = manager;
        /**
         * [description]
         *
         * @property {string} key
         */
        this.key = key;
        /**
         * [description]
         *
         * Default values will be set by properties' setters.
         *
         * @private
         * @property {ISoundConfig} config
         */
        this.config = {};
        /**
         * Reference to the currently used config.
         * It could be default config or marker config.
         *
         * @private
         * @property {ISoundConfig} currentConfig
         */
        this.currentConfig = this.config;
        /**
         * Flag indicating if sound is currently playing.
         *
         * @readonly
         * @property {boolean} isPlaying
         */
        this.isPlaying = false;
        /**
         * Flag indicating if sound is currently paused.
         *
         * @readonly
         * @property {boolean} isPaused
         */
        this.isPaused = false;
        /**
         * [description]
         *
         * @property {boolean} mute
         */
        this.mute = false;
        /**
         * [description]
         *
         * @property {number} volume
         */
        this.volume = 1;
        /**
         * Defines the speed at which the audio asset will be played.
         * Value of 1.0 plays the audio at full speed, 0.5 plays the audio
         * at half speed and 2.0 doubles the audio's playback speed.
         * This value gets multiplied by global rate to have the final playback speed.
         *
         * @property {number} rate
         */
        this.rate = 1;
        /**
         * Represents detuning of sound in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29).
         * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent).
         *
         * @property {number} detune
         */
        this.detune = 0;
        /**
         * A property that holds the value of sound's actual playback rate,
         * after its rate and detune values has been combined with global
         * rate and detune values.
         *
         * @property {number} totalRate
         */
        this.totalRate = 1;
        /**
         * [description]
         *
         * @property {number} seek
         */
        this.seek = 0;
        /**
         * [description]
         *
         * @property {boolean} loop
         */
        this.loop = false;
        this.config = Extend(this.config, config);
        /**
         * A value representing the duration, in seconds.
         * It could be total sound duration or a marker duration.
         *
         * @readonly
         * @property {number} duration
         */
        this.duration = 0;
        /**
         * Duration of the entire sound.
         *
         * @readonly
         * @property {number}
         */
        this.totalDuration = 0;
        /**
         * Object containing markers definitions.
         *
         * @readonly
         * @property {{}} markers
         */
        this.markers = {};
        /**
         * Currently playing marker.
         * 'null' if whole sound is playing.
         *
         * @readonly
         * @property {ISoundMarker} currentMarker
         */
        this.currentMarker = null;
        /**
         * [description]
         *
         * @private
         * @property {Phaser.Tween}
         */
        this.fadeTween = null; // TODO see how to use global tween
        /**
         * Event dispatches used to handle all sound instance
         * relevant events.
         *
         * @property {Phaser.Events.EventDispatcher}
         */
        this.events = new EventDispatcher();
    },
    addMarker: function (marker) {
        if (!marker) {
            console.error('addMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('addMarker - Marker has to have a valid name!');
            return false;
        }
        if (this.markers[marker.name]) {
            console.error('addMarker - Marker with name \'' + marker.name + '\' already exists for sound \'' + this.key + '\'!');
            return false;
        }
        marker = Extend(true, {
            name: '',
            start: 0,
            duration: this.totalDuration,
            config: {
                mute: false,
                volume: 1,
                rate: 1,
                detune: 0,
                seek: 0,
                loop: false
            }
        }, marker);
        this.markers[marker.name] = marker;
        return true;
    },
    updateMarker: function (marker) {
        if (!marker) {
            console.error('updateMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('updateMarker - Marker has to have a valid name!');
            return false;
        }
        if (!this.markers[marker.name]) {
            console.error('updateMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return false;
        }
        this.markers[marker.name] = Extend(true, this.markers[marker.name], marker);
        return true;
    },
    removeMarker: function (markerName) {
        var marker = this.markers[markerName];
        if (!marker) {
            console.error('removeMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return null;
        }
        this.markers[markerName] = null;
        return marker;
    },
    play: function (markerName, config) {
        if (markerName === void 0) { markerName = ''; }
        if (typeof markerName === 'object') {
            config = markerName;
            markerName = '';
        }
        if (typeof markerName !== 'string') {
            console.error('Sound marker name has to be a string!');
            return null;
        }
        if (!markerName) {
            this.currentMarker = null;
            this.currentConfig = this.config;
            this.duration = this.totalDuration;
        }
        else {
            if (!this.markers[markerName]) {
                console.error('No marker with name \'' + markerName + '\' found for sound \'' + this.key + '\'!');
                return null;
            }
            this.currentMarker = this.markers[markerName];
            this.currentConfig = this.currentMarker.config;
            this.duration = this.currentMarker.duration;
        }
        this.currentConfig = Extend(this.currentConfig, config);
        this.isPlaying = true;
        this.isPaused = false;
        return this;
    },
    pause: function () {
        if (this.isPaused || !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = true;
        return true;
    },
    resume: function () {
        if (!this.isPaused || this.isPlaying) {
            return false;
        }
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    stop: function () {
        if (!this.isPaused && !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = false;
        return true;
    },
    /**
     * @private
     */
    applyConfig: function () {
        this.mute = this.currentConfig.mute;
        this.volume = this.currentConfig.volume;
        this.rate = this.currentConfig.rate;
        this.detune = this.currentConfig.detune;
        // TODO assign other config values to buffer source
    },
    fadeTo: function (volume, duration) {
        return null;
    },
    /**
     * @private
     */
    update: NOOP,
    destroy: function () {
    },
    /**
     * @private
     */
    setRate: function () {
        var cent = 1.0005777895065548; // Math.pow(2, 1/1200);
        var totalDetune = this.currentConfig.detune + this.manager.detune;
        var detuneRate = Math.pow(cent, totalDetune);
        this.totalRate = this.currentConfig.rate * this.manager.rate * detuneRate;
    }
});
module.exports = BaseSound;
<|endoftext|>


<commit_before>// @flow
import React, {PropTypes} from 'react'
import {
  StyleSheet,
  View,
  ListView,
  Platform,
  Text,
  Navigator,
  TouchableHighlight,
  RefreshControl,
} from 'react-native'

import delay from 'delay'

import Icon from 'react-native-vector-icons/Ionicons'
import type {StoryType} from './types'
import LoadingView from '../components/loading'
import * as c from '../components/colors'

let Entities = require('html-entities').AllHtmlEntities
const entities = new Entities()

export default class NewsContainer extends React.Component {
  static propTypes = {
    name: PropTypes.string.isRequired,
    navigator: PropTypes.instanceOf(Navigator).isRequired,
    route: PropTypes.object.isRequired,
    url: PropTypes.string.isRequired,
  }

  state = {
    dataSource: new ListView.DataSource({
      rowHasChanged: (r1: StoryType, r2: StoryType) => r1.title != r2.title,
    }),
    refreshing: false,
    loaded: false,
    error: false,
    noNews: false,
  }

  componentWillMount() {
    this.refresh()
  }

  fetchData = async () => {
    try {
      let response = await fetch(this.props.url).then(r => r.json())
      let entries = response.responseData.feed.entries
      if (!entries.length) {
        this.setState({noNews: true})
      }
      this.setState({dataSource: this.state.dataSource.cloneWithRows(entries)})
    } catch (error) {
      this.setState({error: true})
      console.error(error)
    }

    this.setState({loaded: true})
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.fetchData()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    let elapsed = start - Date.now()
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }
    this.setState(() => ({refreshing: false}))
  }

  renderRow = (story: StoryType) => {
    let title = entities.decode(story.title)
    let snippet = entities.decode(story.contentSnippet)
    return (
      <TouchableHighlight underlayColor={'#ebebeb'} onPress={() => this.onPressNews(title, story)}>
        <View style={[styles.row]}>
          <View style={[styles.rowContainer]}>
            <Text style={styles.itemTitle} numberOfLines={1}>{title}</Text>
            <Text style={styles.itemPreview} numberOfLines={2}>{snippet}</Text>
          </View>
          <Icon style={[styles.arrowIcon]} name='ios-arrow-forward' />
        </View>
      </TouchableHighlight>
    )
  }

  onPressNews = (title, story: StoryType) => {
    this.props.navigator.push({
      id: 'NewsItemView',
      index: this.props.route.index + 1,
      title: title,
      backButtonTitle: this.props.name,
      props: {story},
    })
  }

  render() {
    if (!this.state.loaded) {
      return <LoadingView />
    }

    if (this.state.noNews) {
      return (
        <View style={{
          flex: 1,
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor: '#ffffff',
        }}>
          <Text>
            No events.
          </Text>
        </View>
      )
    }

    return (
      <ListView
        style={styles.listContainer}
        contentInset={{bottom: Platform.OS === 'ios' ? 49 : 0}}
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        pageSize={5}
        refreshControl={
          <RefreshControl
            refreshing={this.state.refreshing}
            onRefresh={this.refresh}
          />
        }
      />
    )
  }
}

const styles = StyleSheet.create({
  listContainer: {
    paddingBottom: 50,
    backgroundColor: '#ffffff',
  },
  row: {
    flexDirection: 'row',
    alignItems: 'flex-start',
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#ebebeb',
    marginLeft: 20,
    paddingRight: 10,
    paddingTop: 8,
    paddingBottom: 8,
  },
  arrowIcon: {
    color: c.iosDisabledText,
    fontSize: 20,
    marginRight: 6,
    marginLeft: 6,
    marginTop: 0,
  },
  rowContainer: {
    flexDirection: 'column',
    flex: 1,
  },
  itemTitle: {
    color: c.black,
    paddingBottom: 3,
    fontSize: 16,
    textAlign: 'left',
  },
  itemPreview: {
    color: c.iosDisabledText,
    fontSize: 13,
    textAlign: 'left',
  },
})
<commit_msg>add flow annotation (#413)<commit_after>// @flow
import React, {PropTypes} from 'react'
import {
  StyleSheet,
  View,
  ListView,
  Platform,
  Text,
  Navigator,
  TouchableHighlight,
  RefreshControl,
} from 'react-native'

import delay from 'delay'

import Icon from 'react-native-vector-icons/Ionicons'
import type {StoryType} from './types'
import LoadingView from '../components/loading'
import * as c from '../components/colors'

let Entities = require('html-entities').AllHtmlEntities
const entities = new Entities()

export default class NewsContainer extends React.Component {
  static propTypes = {
    name: PropTypes.string.isRequired,
    navigator: PropTypes.instanceOf(Navigator).isRequired,
    route: PropTypes.object.isRequired,
    url: PropTypes.string.isRequired,
  }

  state = {
    dataSource: new ListView.DataSource({
      rowHasChanged: (r1: StoryType, r2: StoryType) => r1.title != r2.title,
    }),
    refreshing: false,
    loaded: false,
    error: false,
    noNews: false,
  }

  componentWillMount() {
    this.refresh()
  }

  fetchData = async () => {
    try {
      let response = await fetch(this.props.url).then(r => r.json())
      let entries = response.responseData.feed.entries
      if (!entries.length) {
        this.setState({noNews: true})
      }
      this.setState({dataSource: this.state.dataSource.cloneWithRows(entries)})
    } catch (error) {
      this.setState({error: true})
      console.error(error)
    }

    this.setState({loaded: true})
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.fetchData()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    let elapsed = start - Date.now()
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }
    this.setState(() => ({refreshing: false}))
  }

  renderRow = (story: StoryType) => {
    let title = entities.decode(story.title)
    let snippet = entities.decode(story.contentSnippet)
    return (
      <TouchableHighlight underlayColor={'#ebebeb'} onPress={() => this.onPressNews(title, story)}>
        <View style={[styles.row]}>
          <View style={[styles.rowContainer]}>
            <Text style={styles.itemTitle} numberOfLines={1}>{title}</Text>
            <Text style={styles.itemPreview} numberOfLines={2}>{snippet}</Text>
          </View>
          <Icon style={[styles.arrowIcon]} name='ios-arrow-forward' />
        </View>
      </TouchableHighlight>
    )
  }

  onPressNews = (title: string, story: StoryType) => {
    this.props.navigator.push({
      id: 'NewsItemView',
      index: this.props.route.index + 1,
      title: title,
      backButtonTitle: this.props.name,
      props: {story},
    })
  }

  render() {
    if (!this.state.loaded) {
      return <LoadingView />
    }

    if (this.state.noNews) {
      return (
        <View style={{
          flex: 1,
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor: '#ffffff',
        }}>
          <Text>
            No events.
          </Text>
        </View>
      )
    }

    return (
      <ListView
        style={styles.listContainer}
        contentInset={{bottom: Platform.OS === 'ios' ? 49 : 0}}
        dataSource={this.state.dataSource}
        renderRow={this.renderRow}
        pageSize={5}
        refreshControl={
          <RefreshControl
            refreshing={this.state.refreshing}
            onRefresh={this.refresh}
          />
        }
      />
    )
  }
}

const styles = StyleSheet.create({
  listContainer: {
    paddingBottom: 50,
    backgroundColor: '#ffffff',
  },
  row: {
    flexDirection: 'row',
    alignItems: 'flex-start',
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#ebebeb',
    marginLeft: 20,
    paddingRight: 10,
    paddingTop: 8,
    paddingBottom: 8,
  },
  arrowIcon: {
    color: c.iosDisabledText,
    fontSize: 20,
    marginRight: 6,
    marginLeft: 6,
    marginTop: 0,
  },
  rowContainer: {
    flexDirection: 'column',
    flex: 1,
  },
  itemTitle: {
    color: c.black,
    paddingBottom: 3,
    fontSize: 16,
    textAlign: 'left',
  },
  itemPreview: {
    color: c.iosDisabledText,
    fontSize: 13,
    textAlign: 'left',
  },
})
<|endoftext|>