<commit_before>import React, { Component, PropTypes } from 'react';
import { has } from '../utils/helpers';
import buttonStyle from '../style/button';
import unionClassNames from '../utils/union-class-names';
import { injectStyles, removeStyle } from '../utils/inject-style';
import config from '../config/button';

const buttonTypes = ['button', 'submit', 'reset'];

/**
 * Returns an object with properties that are relevant for the button element.
 *
 * In case a wrong or no type is defined the type of the child button will be
 * set to `button`.
 */
function sanitizeChildProps(properties) {
  const {
    className,
    style,
    hoverStyle,
    focusStyle,
    activeStyle,
    disabledStyle,
    disabledHoverStyle,
    primary,
    onTouchStart,
    onTouchEnd,
    onTouchCancel,
    onMouseDown,
    onMouseEnter,
    onMouseLeave,
    onFocus,
    onBlur,
    ...childProps,
  } = properties;
  return childProps;
}

/**
 * Update hover, focus & active style for the speficied styleId.
 *
 * @param styleId {string} - a unique id that exists as class attribute in the DOM
 * @param properties {object} - the components properties optionally containing custom styles
 */
function updatePseudoClassStyle(styleId, properties, preventFocusStyleForTouchAndClick) {
  const baseStyle = properties.primary ? buttonStyle.primaryStyle : buttonStyle.style;
  const baseDisabledStyle = properties.primary ? buttonStyle.primaryDisabledStyle : buttonStyle.disabledStyle;
  const disabledStyle = {
    ...baseStyle,
    ...properties.style,
    ...baseDisabledStyle,
    ...properties.disabledStyle,
  };
  const baseActiveStyle = properties.primary ? buttonStyle.primaryActiveStyle : buttonStyle.activeStyle;
  const activeStyle = {
    ...baseActiveStyle,
    ...properties.activeStyle,
  };

  let focusStyle;
  if (preventFocusStyleForTouchAndClick) {
    focusStyle = { outline: 0 };
  } else {
    const baseFocusStyle = properties.primary ? buttonStyle.primaryFocusStyle : buttonStyle.focusStyle;
    focusStyle = {
      ...baseFocusStyle,
      ...properties.focusStyle,
    };
  }

  const styles = [
    {
      id: styleId,
      style: activeStyle,
      pseudoClass: 'active',
    },
    {
      id: styleId,
      style: disabledStyle,
      pseudoClass: 'active',
      disabled: true,
    },
    {
      id: styleId,
      style: focusStyle,
      pseudoClass: 'focus',
    },
  ];

  injectStyles(styles);
}

/**
 * Button component
 *
 * The button behaves exactly like a normal html button except:
 * - Once a user clicks on the button it will loose focus
 * - By default every button is of type="button" instead of "submit"
 */
export default class Button extends Component {

  constructor(properties) {
    super(properties);

    this.preventFocusStyleForTouchAndClick = has(properties, 'preventFocusStyleForTouchAndClick') ? properties.preventFocusStyleForTouchAndClick : config.preventFocusStyleForTouchAndClick;

    this.state = {
      childProps: sanitizeChildProps(properties),

      // used for touch devices like iOS Chrome/Safari where the active
      // pseudoClass is not supported on touch
      isActive: false,
      isHovered: false,

      // Note: On touch devices mouseEnter is fired while mouseLeave is not.
      // This would result in a hover effect that keeps active until another
      // element is focused on. This would result in the same behaviour as using
      // the :hover pseudo class. To prevent it from happening activating the
      // hover state is prevented when a touch event has been triggered before.
      // source: http://stackoverflow.com/a/22444532/837709
      isIgnoringHover: false,
    };

    // The focused attribute is used to apply the one-time focus animation.
    // As it is reset after every render it can't be set inside state as this
    // would trigger an endless loop.
    this.focused = false;

    // This used to determine if the one-time focus animation should be prevented.
    this.mouseDownOnButton = false;
  }

  static displayName = 'Button';

  static propTypes = {
    activeStyle: PropTypes.object,
    children: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.node),
      PropTypes.node,
    ]),
    className: PropTypes.string,
    disabled: PropTypes.bool,
    type: PropTypes.oneOf(buttonTypes),
    style: PropTypes.object,
    focusStyle: PropTypes.object,
    hoverStyle: PropTypes.object,
    disabledStyle: PropTypes.object,
    disabledHoverStyle: PropTypes.object,
    onTouchStart: PropTypes.func,
    onTouchEnd: PropTypes.func,
    onTouchCancel: PropTypes.func,
    onMouseDown: PropTypes.func,
    onMouseEnter: PropTypes.func,
    onMouseLeave: PropTypes.func,
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    preventFocusStyleForTouchAndClick: PropTypes.bool,
    primary: PropTypes.bool,
  };

  static defaultProps = {
    primary: false,
    disabled: false,
    type: 'button',
  };

  /**
   * Generates the style-id & inject the focus & active style.
   *
   * The style-id is based on React's unique DOM node id.
   */
  componentWillMount() {
    const id = this._reactInternalInstance._rootNodeID.replace(/[\.\:\$\/\=]/g, '-');
    this.styleId = `style-id${id}`;
    updatePseudoClassStyle(this.styleId, this.props, this.preventFocusStyleForTouchAndClick);
  }

  /**
   * Update the childProps based on the updated properties of the button.
   */
  componentWillReceiveProps(properties) {
    this.preventFocusStyleForTouchAndClick = has(properties, 'preventFocusStyleForTouchAndClick') ? properties.preventFocusStyleForTouchAndClick : config.preventFocusStyleForTouchAndClick;

    this.setState({
      childProps: sanitizeChildProps(properties),
    });
    removeStyle(this.styleId);
    updatePseudoClassStyle(this.styleId, properties, this.preventFocusStyleForTouchAndClick);
  }

  /**
   * Deactivate the focused attribute in order to make sure the focus animation
   * only runs once when the component is focused on & not after re-rendering
   * e.g when the user clicks the button.
   */
  componentDidUpdate() {
    this.focused = false;
    this.mouseDownOnButton = false;
  }

  /**
   * Remove a component's associated styles whenever it gets removed from the DOM.
   */
  componentWillUnmount() {
    removeStyle(this.styleId);
  }

  /**
   * Activate the focused attribute used to determine when to show the
   * one-time focus animation and trigger a render.
   */
  _onFocus(event) {
    this.focused = true;
    this.forceUpdate();

    if (this.props.onFocus) {
      this.props.onFocus(event);
    }
  }

  /**
   * Deactivate the focused attribute used to determine when to show the
   * one-time focus animation and trigger a render.
   */
  _onBlur(event) {
    this.focused = false;
    this.setState({ isActive: false });

    if (this.props.onBlur) {
      this.props.onBlur(event);
    }
  }

  _onMouseDown(event) {
    if (event.button === 0 && !this.props.disabled) {
      this.mouseDownOnButton = true;
    }

    if (this.props.onMouseDown) {
      this.props.onMouseDown(event);
    }
  }

  /**
   * Updates the button to be pressed.
   */
  _onTouchStart(event) {
    if (!this.props.disabled && event.touches.length === 1) {
      this.setState({
        isActive: true,
        isIgnoringHover: true,
      });
    }

    if (this.props.onTouchStart) {
      this.props.onTouchStart(event);
    }
  }

  /**
   * Updates the button to be release.
   */
  _onTouchEnd(event) {
    this.setState({
      isActive: false,
      isIgnoringHover: true,
    });

    if (this.props.onTouchEnd) {
      this.props.onTouchEnd(event);
    }
  }

  /**
   * Updates the button to be release.
   */
  _onTouchCancel(event) {
    this.setState({
      isActive: false,
      isIgnoringHover: true,
    });

    if (this.props.onTouchEnd) {
      this.props.onTouchEnd(event);
    }
  }

  /**
   * As soon as the mouse enters the component the isHovered state is activated.
   */
  _onMouseEnter(event) {
    if (!this.state.isIgnoringHover) {
      this.setState({
        isHovered: true,
        isIgnoringHover: false,
      });
    }

    if (this.props.onMouseEnter) {
      this.props.onMouseEnter(event);
    }
  }

  /**
   * Deactivate the isHovered state.
   */
  _onMouseLeave(event) {
    this.setState({
      isHovered: false,
    });

    if (this.props.onMouseLeave) {
      this.props.onMouseLeave(event);
    }
  }

  render() {
    const baseStyle = this.props.primary ? buttonStyle.primaryStyle : buttonStyle.style;
    let combinedStyle = {
      ...baseStyle,
      ...this.props.style,
    };

    if (this.state.isHovered) {
      const baseHoverStyle = this.props.primary ? buttonStyle.primaryHoverStyle : buttonStyle.hoverStyle;
      combinedStyle = {
        ...combinedStyle,
        ...baseHoverStyle,
        ...this.props.hoverStyle,
      };
    }

    if (this.props.disabled) {
      const baseDisabledStyle = this.props.primary ? buttonStyle.primaryDisabledStyle : buttonStyle.disabledStyle;
      combinedStyle = {
        ...combinedStyle,
        ...baseDisabledStyle,
        ...this.props.disabledStyle,
      };
      if (this.state.isHovered) {
        const baseDisabledHoverStyle = this.props.primary ? buttonStyle.primaryDisabledHoverStyle : buttonStyle.disabledHoverStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseDisabledHoverStyle,
          ...this.props.disabledHoverStyle,
        };
      }
    } else {
      if (this.state.isActive) {
        const baseActiveStyle = this.props.primary ? buttonStyle.primaryActiveStyle : buttonStyle.activeStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseActiveStyle,
          ...this.props.activeStyle,
        };
      } else if (this.focused &&
                 !this.state.isActive &&
                 !this.mouseDownOnButton &&
                 this.preventFocusStyleForTouchAndClick) {
        const baseFocusStyle = this.props.primary ? buttonStyle.primaryFocusStyle : buttonStyle.focusStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseFocusStyle,
          ...this.props.focusStyle,
        };
      }
    }

    return (
      <button
        style={ combinedStyle }
        className={ unionClassNames(this.props.className, this.styleId) }
        onTouchStart={ ::this._onTouchStart }
        onTouchEnd={ ::this._onTouchEnd }
        onTouchCancel={ ::this._onTouchCancel }
        onFocus={ ::this._onFocus }
        onBlur={ ::this._onBlur }
        onMouseDown={ ::this._onMouseDown }
        onMouseEnter={ ::this._onMouseEnter }
        onMouseLeave={ ::this._onMouseLeave }
        {...this.state.childProps}
      >
        { this.props.children }
      </button>
    );
  }
}
<commit_msg>feat(Button): autobind eventhandlers on instance creation instead of every render<commit_after>import React, { Component, PropTypes } from 'react';
import { has } from '../utils/helpers';
import buttonStyle from '../style/button';
import unionClassNames from '../utils/union-class-names';
import { injectStyles, removeStyle } from '../utils/inject-style';
import config from '../config/button';

const buttonTypes = ['button', 'submit', 'reset'];

/**
 * Returns an object with properties that are relevant for the button element.
 *
 * In case a wrong or no type is defined the type of the child button will be
 * set to `button`.
 */
function sanitizeChildProps(properties) {
  const {
    className,
    style,
    hoverStyle,
    focusStyle,
    activeStyle,
    disabledStyle,
    disabledHoverStyle,
    primary,
    onTouchStart,
    onTouchEnd,
    onTouchCancel,
    onMouseDown,
    onMouseEnter,
    onMouseLeave,
    onFocus,
    onBlur,
    ...childProps,
  } = properties;
  return childProps;
}

/**
 * Update hover, focus & active style for the speficied styleId.
 *
 * @param styleId {string} - a unique id that exists as class attribute in the DOM
 * @param properties {object} - the components properties optionally containing custom styles
 */
function updatePseudoClassStyle(styleId, properties, preventFocusStyleForTouchAndClick) {
  const baseStyle = properties.primary ? buttonStyle.primaryStyle : buttonStyle.style;
  const baseDisabledStyle = properties.primary ? buttonStyle.primaryDisabledStyle : buttonStyle.disabledStyle;
  const disabledStyle = {
    ...baseStyle,
    ...properties.style,
    ...baseDisabledStyle,
    ...properties.disabledStyle,
  };
  const baseActiveStyle = properties.primary ? buttonStyle.primaryActiveStyle : buttonStyle.activeStyle;
  const activeStyle = {
    ...baseActiveStyle,
    ...properties.activeStyle,
  };

  let focusStyle;
  if (preventFocusStyleForTouchAndClick) {
    focusStyle = { outline: 0 };
  } else {
    const baseFocusStyle = properties.primary ? buttonStyle.primaryFocusStyle : buttonStyle.focusStyle;
    focusStyle = {
      ...baseFocusStyle,
      ...properties.focusStyle,
    };
  }

  const styles = [
    {
      id: styleId,
      style: activeStyle,
      pseudoClass: 'active',
    },
    {
      id: styleId,
      style: disabledStyle,
      pseudoClass: 'active',
      disabled: true,
    },
    {
      id: styleId,
      style: focusStyle,
      pseudoClass: 'focus',
    },
  ];

  injectStyles(styles);
}

/**
 * Button component
 *
 * The button behaves exactly like a normal html button except:
 * - Once a user clicks on the button it will loose focus
 * - By default every button is of type="button" instead of "submit"
 */
export default class Button extends Component {

  constructor(properties) {
    super(properties);

    this.preventFocusStyleForTouchAndClick = has(properties, 'preventFocusStyleForTouchAndClick') ? properties.preventFocusStyleForTouchAndClick : config.preventFocusStyleForTouchAndClick;

    this.state = {
      childProps: sanitizeChildProps(properties),

      // used for touch devices like iOS Chrome/Safari where the active
      // pseudoClass is not supported on touch
      isActive: false,
      isHovered: false,

      // Note: On touch devices mouseEnter is fired while mouseLeave is not.
      // This would result in a hover effect that keeps active until another
      // element is focused on. This would result in the same behaviour as using
      // the :hover pseudo class. To prevent it from happening activating the
      // hover state is prevented when a touch event has been triggered before.
      // source: http://stackoverflow.com/a/22444532/837709
      isIgnoringHover: false,
    };

    // The focused attribute is used to apply the one-time focus animation.
    // As it is reset after every render it can't be set inside state as this
    // would trigger an endless loop.
    this.focused = false;

    // This used to determine if the one-time focus animation should be prevented.
    this.mouseDownOnButton = false;
  }

  static displayName = 'Button';

  static propTypes = {
    activeStyle: PropTypes.object,
    children: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.node),
      PropTypes.node,
    ]),
    className: PropTypes.string,
    disabled: PropTypes.bool,
    type: PropTypes.oneOf(buttonTypes),
    style: PropTypes.object,
    focusStyle: PropTypes.object,
    hoverStyle: PropTypes.object,
    disabledStyle: PropTypes.object,
    disabledHoverStyle: PropTypes.object,
    onTouchStart: PropTypes.func,
    onTouchEnd: PropTypes.func,
    onTouchCancel: PropTypes.func,
    onMouseDown: PropTypes.func,
    onMouseEnter: PropTypes.func,
    onMouseLeave: PropTypes.func,
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    preventFocusStyleForTouchAndClick: PropTypes.bool,
    primary: PropTypes.bool,
  };

  static defaultProps = {
    primary: false,
    disabled: false,
    type: 'button',
  };

  /**
   * Generates the style-id & inject the focus & active style.
   *
   * The style-id is based on React's unique DOM node id.
   */
  componentWillMount() {
    const id = this._reactInternalInstance._rootNodeID.replace(/[\.\:\$\/\=]/g, '-');
    this.styleId = `style-id${id}`;
    updatePseudoClassStyle(this.styleId, this.props, this.preventFocusStyleForTouchAndClick);
  }

  /**
   * Update the childProps based on the updated properties of the button.
   */
  componentWillReceiveProps(properties) {
    this.preventFocusStyleForTouchAndClick = has(properties, 'preventFocusStyleForTouchAndClick') ? properties.preventFocusStyleForTouchAndClick : config.preventFocusStyleForTouchAndClick;

    this.setState({
      childProps: sanitizeChildProps(properties),
    });
    removeStyle(this.styleId);
    updatePseudoClassStyle(this.styleId, properties, this.preventFocusStyleForTouchAndClick);
  }

  /**
   * Deactivate the focused attribute in order to make sure the focus animation
   * only runs once when the component is focused on & not after re-rendering
   * e.g when the user clicks the button.
   */
  componentDidUpdate() {
    this.focused = false;
    this.mouseDownOnButton = false;
  }

  /**
   * Remove a component's associated styles whenever it gets removed from the DOM.
   */
  componentWillUnmount() {
    removeStyle(this.styleId);
  }

  /**
   * Activate the focused attribute used to determine when to show the
   * one-time focus animation and trigger a render.
   */
  _onFocus = (event) => {
    this.focused = true;
    this.forceUpdate();

    if (this.props.onFocus) {
      this.props.onFocus(event);
    }
  };

  /**
   * Deactivate the focused attribute used to determine when to show the
   * one-time focus animation and trigger a render.
   */
  _onBlur = (event) => {
    this.focused = false;
    this.setState({ isActive: false });

    if (this.props.onBlur) {
      this.props.onBlur(event);
    }
  };

  _onMouseDown = (event) => {
    if (event.button === 0 && !this.props.disabled) {
      this.mouseDownOnButton = true;
    }

    if (this.props.onMouseDown) {
      this.props.onMouseDown(event);
    }
  };

  /**
   * Updates the button to be pressed.
   */
  _onTouchStart = (event) => {
    if (!this.props.disabled && event.touches.length === 1) {
      this.setState({
        isActive: true,
        isIgnoringHover: true,
      });
    }

    if (this.props.onTouchStart) {
      this.props.onTouchStart(event);
    }
  };

  /**
   * Updates the button to be release.
   */
  _onTouchEnd = (event) => {
    this.setState({
      isActive: false,
      isIgnoringHover: true,
    });

    if (this.props.onTouchEnd) {
      this.props.onTouchEnd(event);
    }
  };

  /**
   * Updates the button to be release.
   */
  _onTouchCancel = (event) => {
    this.setState({
      isActive: false,
      isIgnoringHover: true,
    });

    if (this.props.onTouchEnd) {
      this.props.onTouchEnd(event);
    }
  };

  /**
   * As soon as the mouse enters the component the isHovered state is activated.
   */
  _onMouseEnter = (event) => {
    if (!this.state.isIgnoringHover) {
      this.setState({
        isHovered: true,
        isIgnoringHover: false,
      });
    }

    if (this.props.onMouseEnter) {
      this.props.onMouseEnter(event);
    }
  };

  /**
   * Deactivate the isHovered state.
   */
  _onMouseLeave = (event) => {
    this.setState({
      isHovered: false,
    });

    if (this.props.onMouseLeave) {
      this.props.onMouseLeave(event);
    }
  };

  render() {
    const baseStyle = this.props.primary ? buttonStyle.primaryStyle : buttonStyle.style;
    let combinedStyle = {
      ...baseStyle,
      ...this.props.style,
    };

    if (this.state.isHovered) {
      const baseHoverStyle = this.props.primary ? buttonStyle.primaryHoverStyle : buttonStyle.hoverStyle;
      combinedStyle = {
        ...combinedStyle,
        ...baseHoverStyle,
        ...this.props.hoverStyle,
      };
    }

    if (this.props.disabled) {
      const baseDisabledStyle = this.props.primary ? buttonStyle.primaryDisabledStyle : buttonStyle.disabledStyle;
      combinedStyle = {
        ...combinedStyle,
        ...baseDisabledStyle,
        ...this.props.disabledStyle,
      };
      if (this.state.isHovered) {
        const baseDisabledHoverStyle = this.props.primary ? buttonStyle.primaryDisabledHoverStyle : buttonStyle.disabledHoverStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseDisabledHoverStyle,
          ...this.props.disabledHoverStyle,
        };
      }
    } else {
      if (this.state.isActive) {
        const baseActiveStyle = this.props.primary ? buttonStyle.primaryActiveStyle : buttonStyle.activeStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseActiveStyle,
          ...this.props.activeStyle,
        };
      } else if (this.focused &&
                 !this.state.isActive &&
                 !this.mouseDownOnButton &&
                 this.preventFocusStyleForTouchAndClick) {
        const baseFocusStyle = this.props.primary ? buttonStyle.primaryFocusStyle : buttonStyle.focusStyle;
        combinedStyle = {
          ...combinedStyle,
          ...baseFocusStyle,
          ...this.props.focusStyle,
        };
      }
    }

    return (
      <button
        style={ combinedStyle }
        className={ unionClassNames(this.props.className, this.styleId) }
        onTouchStart={ this._onTouchStart }
        onTouchEnd={ this._onTouchEnd }
        onTouchCancel={ this._onTouchCancel }
        onFocus={ this._onFocus }
        onBlur={ this._onBlur }
        onMouseDown={ this._onMouseDown }
        onMouseEnter={ this._onMouseEnter }
        onMouseLeave={ this._onMouseLeave }
        {...this.state.childProps}
      >
        { this.props.children }
      </button>
    );
  }
}
<|endoftext|>


<commit_before>hqDefine("hqwebapp/js/hq.helpers", [
    'jquery',
    'knockout',
    'underscore',
    'analytix/js/google',
], function (
    $,
    ko,
    _,
    googleAnalytics
) {
    // disable-on-submit is a class for form submit buttons so they're automatically disabled when the form is submitted
    $(document).on('submit', 'form', function (ev) {
        var form = $(ev.target);
        form.find('.disable-on-submit').disableButton();
        form.find('.disable-on-submit-no-spinner').disableButtonNoSpinner();
    });
    $(document).on('submit', 'form.disable-on-submit', function (ev) {
        $(ev.target).find('[type="submit"]').disableButton();
    });
    $(document).on('reset', 'form', function (ev) {
        $(ev.target).find('.disable-on-submit').enableButton();
    });
    $(document).on('reset', 'form.disable-on-submit', function (ev) {
        $(ev.target).enableButton();
    });
    $(document).on('click', '.add-spinner-on-click', function (ev) {
        $(ev.target).addSpinnerToButton();
    });

    $(document).on('click', '.notification-close-btn', function () {
        var note_id = $(this).data('note-id');
        var post_url = $(this).data('url');
        $.post(post_url, {note_id: note_id});
        $(this).parents('.alert').hide(150);
    });

    if ($.timeago) {
        $.timeago.settings.allowFuture = true;
        $(".timeago").timeago();
    }

    window.onerror = function (message, file, line, col, error) {
        var stack = error ? error.stack : null;
        if (!stack && (
            message === 'Script error'
            || message === 'Script error.'
            || message === 'ResizeObserver loop limit exceeded'
        )) {
            return false;
        }
        $.post('/jserror/', {
            message: message,
            page: window.location.href,
            file: file,
            line: line,
            stack: stack,
        });
        return false; // let default handler run
    };

    var oldHide = $.fn.popover.Constructor.prototype.hide;

    $.fn.popover.Constructor.prototype.hide = function () {
        if (this.options.trigger === "hover" && this.tip().is(":hover")) {
            var that = this;
            setTimeout(function () {
                return that.hide.apply(that, arguments);
            }, that.options.delay.hide);
            return;
        }
        oldHide.apply(this, arguments);
    };

    $.fn.hqHelp = function () {
        var self = this;
        self.each(function (i) {
            var $self = $(self),
                $helpElem = $($self.get(i)),
                $link = $helpElem.find('a');

            var options = {
                html: true,
                trigger: 'focus',
                container: 'body',
                sanitize: false,
            };
            if (!$link.data('content')) {
                options.content = function () {
                    return $('#popover_content_wrapper').html();
                };
            }
            if (!$link.data("title")) {
                options.template = '

';
            }
            $link.popover(options);

            // Prevent jumping to the top of the page when link is clicked
            $helpElem.find('a').click(function (event) {
                googleAnalytics.track.event("Clicked Help Bubble", $(this).data('title'), '-');
                event.preventDefault();
            });
        });
    };

    $.fn.addSpinnerToButton = function () {
        $(this).find("i").addClass("hide");
        $(this).prepend(' ');
    };

    $.fn.removeSpinnerFromButton = function () {
        $(this).find("i.hide").removeClass("hide");
        $(this).find('i.fa-spin').remove();
    };

    $.fn.disableButtonNoSpinner = function () {
        $(this).prop('disabled', 'disabled')
            .addClass('disabled');
    };

    $.fn.disableButton = function () {
        $(this).disableButtonNoSpinner();
        $(this).addSpinnerToButton();
    };

    $.fn.enableButton = function () {
        $(this).removeSpinnerFromButton();
        $(this).removeClass('disabled')
            .prop('disabled', false);
    };

    $.fn.koApplyBindings = function (context) {
        if (!this.length) {
            throw new Error("No element passed to koApplyBindings");
        }
        if (this.length > 1) {
            throw new Error("Multiple elements passed to koApplyBindings");
        }
        ko.applyBindings(context, this.get(0));
        this.removeClass('ko-template');
    };

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            // Don't pass csrftoken cross domain
            // Ignore HTTP methods that do not require CSRF protection
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type) && !this.crossDomain) {
                var $csrf_token = $("#csrfTokenContainer").val();
                xhr.setRequestHeader("X-CSRFToken", $csrf_token);

                var xsrf_token = $.cookie('XSRF-TOKEN');
                xhr.setRequestHeader('X-XSRF-TOKEN', xsrf_token);
            }
            xhr.withCredentials = true;
        },
    });

    // Return something so that hqModules understands that the module has been defined
    return 1;
});
<commit_msg>Revert "Send xsrf token only for same domain requests"<commit_after>hqDefine("hqwebapp/js/hq.helpers", [
    'jquery',
    'knockout',
    'underscore',
    'analytix/js/google',
], function (
    $,
    ko,
    _,
    googleAnalytics
) {
    // disable-on-submit is a class for form submit buttons so they're automatically disabled when the form is submitted
    $(document).on('submit', 'form', function (ev) {
        var form = $(ev.target);
        form.find('.disable-on-submit').disableButton();
        form.find('.disable-on-submit-no-spinner').disableButtonNoSpinner();
    });
    $(document).on('submit', 'form.disable-on-submit', function (ev) {
        $(ev.target).find('[type="submit"]').disableButton();
    });
    $(document).on('reset', 'form', function (ev) {
        $(ev.target).find('.disable-on-submit').enableButton();
    });
    $(document).on('reset', 'form.disable-on-submit', function (ev) {
        $(ev.target).enableButton();
    });
    $(document).on('click', '.add-spinner-on-click', function (ev) {
        $(ev.target).addSpinnerToButton();
    });

    $(document).on('click', '.notification-close-btn', function () {
        var note_id = $(this).data('note-id');
        var post_url = $(this).data('url');
        $.post(post_url, {note_id: note_id});
        $(this).parents('.alert').hide(150);
    });

    if ($.timeago) {
        $.timeago.settings.allowFuture = true;
        $(".timeago").timeago();
    }

    window.onerror = function (message, file, line, col, error) {
        var stack = error ? error.stack : null;
        if (!stack && (
            message === 'Script error'
            || message === 'Script error.'
            || message === 'ResizeObserver loop limit exceeded'
        )) {
            return false;
        }
        $.post('/jserror/', {
            message: message,
            page: window.location.href,
            file: file,
            line: line,
            stack: stack,
        });
        return false; // let default handler run
    };

    var oldHide = $.fn.popover.Constructor.prototype.hide;

    $.fn.popover.Constructor.prototype.hide = function () {
        if (this.options.trigger === "hover" && this.tip().is(":hover")) {
            var that = this;
            setTimeout(function () {
                return that.hide.apply(that, arguments);
            }, that.options.delay.hide);
            return;
        }
        oldHide.apply(this, arguments);
    };

    $.fn.hqHelp = function () {
        var self = this;
        self.each(function (i) {
            var $self = $(self),
                $helpElem = $($self.get(i)),
                $link = $helpElem.find('a');

            var options = {
                html: true,
                trigger: 'focus',
                container: 'body',
                sanitize: false,
            };
            if (!$link.data('content')) {
                options.content = function () {
                    return $('#popover_content_wrapper').html();
                };
            }
            if (!$link.data("title")) {
                options.template = '

';
            }
            $link.popover(options);

            // Prevent jumping to the top of the page when link is clicked
            $helpElem.find('a').click(function (event) {
                googleAnalytics.track.event("Clicked Help Bubble", $(this).data('title'), '-');
                event.preventDefault();
            });
        });
    };

    $.fn.addSpinnerToButton = function () {
        $(this).find("i").addClass("hide");
        $(this).prepend(' ');
    };

    $.fn.removeSpinnerFromButton = function () {
        $(this).find("i.hide").removeClass("hide");
        $(this).find('i.fa-spin').remove();
    };

    $.fn.disableButtonNoSpinner = function () {
        $(this).prop('disabled', 'disabled')
            .addClass('disabled');
    };

    $.fn.disableButton = function () {
        $(this).disableButtonNoSpinner();
        $(this).addSpinnerToButton();
    };

    $.fn.enableButton = function () {
        $(this).removeSpinnerFromButton();
        $(this).removeClass('disabled')
            .prop('disabled', false);
    };

    $.fn.koApplyBindings = function (context) {
        if (!this.length) {
            throw new Error("No element passed to koApplyBindings");
        }
        if (this.length > 1) {
            throw new Error("Multiple elements passed to koApplyBindings");
        }
        ko.applyBindings(context, this.get(0));
        this.removeClass('ko-template');
    };

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            // Don't pass csrftoken cross domain
            // Ignore HTTP methods that do not require CSRF protection
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) {
                if (!this.crossDomain) {
                    var $csrf_token = $("#csrfTokenContainer").val();
                    xhr.setRequestHeader("X-CSRFToken", $csrf_token);
                }
                var xsrf_token = $.cookie('XSRF-TOKEN');
                xhr.setRequestHeader('X-XSRF-TOKEN', xsrf_token);
            }
            xhr.withCredentials = true;
        },
    });

    // Return something so that hqModules understands that the module has been defined
    return 1;
});
<|endoftext|>


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

	angular
		.module('nCore.nHttpInterceptor.factory', ['nCore.nHttpInterceptor.provider'])
		.factory('nHttpInterceptorFactory', interceptorFactory);

	/* @ngInject */
	function interceptorFactory(nHttpInterceptor, $rootScope, nLogger) {

		var service = {
			errorHandle: errorHandle
		};

		return service;

		function errorHandle(statuscode) {
			/* Convert it so match the object with keys and errormessages in */
			var getErrorMsg = 'error' + statuscode;

			/* Message it out */
			/* If it exists in translate, then message it, or else message the string */
			if($rootScope.translate[getErrorMsg]) {
				nLogger.error($rootScope.translate[getErrorMsg]);
			} else if(nHttpInterceptor.errorMessages[getErrorMsg]) {
				nLogger.error(nHttpInterceptor.errorMessages[getErrorMsg]);
			}
		}
	}

})();
<commit_msg>feat: Testing semantic-release<commit_after>(function () {
	'use strict';

	// TRY OUT SEMANTIC-RELEASE

	angular
		.module('nCore.nHttpInterceptor.factory', ['nCore.nHttpInterceptor.provider'])
		.factory('nHttpInterceptorFactory', interceptorFactory);

	/* @ngInject */
	function interceptorFactory(nHttpInterceptor, $rootScope, nLogger) {

		var service = {
			errorHandle: errorHandle
		};

		return service;

		function errorHandle(statuscode) {
			/* Convert it so match the object with keys and errormessages in */
			var getErrorMsg = 'error' + statuscode;

			/* Message it out */
			/* If it exists in translate, then message it, or else message the string */
			if($rootScope.translate[getErrorMsg]) {
				nLogger.error($rootScope.translate[getErrorMsg]);
			} else if(nHttpInterceptor.errorMessages[getErrorMsg]) {
				nLogger.error(nHttpInterceptor.errorMessages[getErrorMsg]);
			}
		}
	}

})();
<|endoftext|>


<commit_before>/**
# Object Registry

Bindinator is built around the idea of registering objects under unique names
and binding events and element properties to paths based on those names.

b8r's registry is an observable object that b8r uses to keep track of objects.
Once an object is registered, its properties will automatically be bound
to events and DOM properties by path.

    set('root', object_value); // registers the object with the name
    set('root.path.to.value', new_value); // sets the value

You can set the registry's properties by path. Root level properties must
themselves be objects.

    get('root.path.to.value'); // gets the value

You can get a property by path.

Two convenience methods are provided for working with JSON data:

    getJSON('path.to.value' [, element]);
    setJSON('path.to.value', json_string);

Note that unlike set, setJSON does not accept an optional element argument.

    call('root.path.to.method', ...args); // returns value as appropriate

You can call a method by path.

    touch(path [, source_element]);

Triggers all observers as though the value at path has changed. Useful
if you change a property independently of the registry.

    async_touch(path);

Triggers all observers asynchronousely (on requestAnimationFrame).

    isValidPath(path);

`isValidPath` returns true if the path looks OK, false otherwise.

~~~~
Test(() => b8r.isValidPath('foo')).shouldBe(true);
Test(() => b8r.isValidPath('foo.bar')).shouldBe(true);
Test(() => b8r.isValidPath('.')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[1234]')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234]')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234].')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms[id]')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms[id=1234]]')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms]')).shouldBe(false);
~~~~

*/
/* jshint latedef:false */
/* global module, require, console */
'use strict';

const {getByPath, setByPath, deleteByPath} = require('./b8r.byPath.js');
const {getDataPath, getComponentInstancePath} = require('./b8r.bindings.js');
const {logStart, logEnd} = require('./b8r.perf.js');
const registry = {};
const listeners = [];  // { path_string_or_test, callback }

const valid_path = /^(\.|[^.\[\]])+(\.[^.\[\]]+|\[\d+\]|\[[^=\[\]]+\=[^\[\]]+\])*(\.)?$/;

const isValidPath = path => valid_path.test(path);

class Listener {
  constructor(test, callback) {
    if (typeof test === 'string') {
      this.test = t =>
          t.length >= test.length && test === t.substr(0, test.length);
    } else if (test instanceof RegExp) {
      this.test = test.test.bind(test);
    } else if (test instanceof Function) {
      this.test = test;
    } else {
      throw 'expect listener test to be a string, RegExp, or test function';
  }
    if (typeof callback === 'string') {
      this.callback = (...args) => {
        if (get(callback)) {
          call(callback, ...args);
        } else {
          unobserve(this);
        }
      };
    } else if (typeof callback === 'function') {
      this.callback = callback;
    } else {
      throw 'expect callback to be a path or function';
    }
    listeners.push(this);
  }
}

const resolvePath = (path, element) => {
  if (path[0] === '.') {
    if (!element) {
      throw 'cannot evaluate relative path without element';
    }
    path = getDataPath(element) + path;
  } else if (path.substr(0, 6) === '_data_') {
    if (!element) {
      throw 'cannot evaluate _data_ path without element';
    }
    path = getDataPath(element) + path.substr(6);
  } else if (path.substr() === '_component_') {
    if (!element) {
      throw 'cannot evaluate _component_ path without element';
    }
    path = getComponentInstancePath(element) + path;
  }
  return path;
};

const get = (path, element) => {
  path = resolvePath(path, element);
  return getByPath(registry, path);
};

const getJSON = (path, element) => JSON.stringify(get(path, element));

const touch = (path, source_element) => {
  logStart('touch', path);
  listeners.filter(listener => listener.test(path))
           .forEach(listener => listener.callback(path, source_element));
  logEnd('touch', path);
};

const _async_touch_dirty_list = [];
let _async_touch_id = null;

const _async_touch = () => {
  _async_touch_dirty_list.forEach(touch);
  _async_touch_dirty_list.splice(0);
};

const async_touch = path => {
  if (_async_touch_dirty_list.indexOf(path) === -1) {
    _async_touch_dirty_list.push(path);
  }
  if (!_async_touch_id) {
    _async_touch_id = requestAnimationFrame(_async_touch);
  }
};

const set = (path, value, source_element) => {
  const path_parts = path.split(/\.|\[/);
  const model = path_parts[0];
  const existing = getByPath(registry, path);
  if (path_parts.length > 1 && !registry[model]) {
    console.error(`cannot set ${path} to ${value}, ${model} does not exist`);
  } else if (path_parts.length === 1 && typeof value !== 'object') {
    throw 'cannot set ${path}; you can only register objects at root-level';
  } else if (value === existing) {
    // nothing to see here, move along
  } else if (value && value.constructor) {
    if (path_parts.length === 1 && ! registry[path]) {
      register(path, value);
    } else {
      // we only drill down into vanilla objects
      // not arrays or instances of custom classes
      if (value.constructor === Object && existing && existing.constructor === Object) {
        // using for vs. forEachKey because set will abort loop if setting something to false
        for (let i = 0, keys = Object.keys(value); i < keys.length; i++) {
          set(`${path}.${keys[i]}`, value[keys[i]]);
        }
      } else {
        setByPath(registry, path, value);
      }
      touch(path, source_element);
    }
  } else {
    setByPath(registry, path, value);
    touch(path, source_element);
  }
  return value; // convenient for things push (see below) but maybe an anti-feature?!
};

const _register = (name, obj) => {
  registry[name] = obj;
};

const register = (name, obj, block_updates) => {
  if (name.match(/^_[^_]*_$/)) {
    throw 'cannot register object as ' + name +
      ', all names starting and ending with a single \'_\' are reserved.';
  }

  _register(name, obj, block_updates);

  if (!block_updates) {
    touch(name);
    require.lazy('./b8r.events.js').then(({play_saved_messages}) => play_saved_messages(name));
  }
};

const setJSON = (path, value) => set(path, JSON.parse(value));

const push = (path, value) => {
  const list = get(path) || set(path, []);
  if(Array.isArray(list)) {
    list.push(value);
    async_touch(path);
  }
};

const call = (path, ...args) => {
  const method = get(path);
  if (method instanceof Function) {
    return method(...args);
  } else {
    console.error(`cannot call ${path}; not a method`);
  }
};

/**
    observe('root.path.to', callback); // returns a Listener instance

You can observe a path (string). The callback will fire whenever any path
matching or starting
with the string changes. The callback will be passed the exact path that
changed.

    observe(/root.path.[^\.]+.value/, callback);

Instead of a constant path, you can pass a RegExp which will fire the callback
when a path
matching the test changes. The callback will be passed the exact path that
changed.

    observe(path => path.split('.').length === 1, callback);

Finally you can observe a path test function.

    const listener = observe(test, callback);

The `callback` can be a function or a path. If a path, the listener will
automatically be removed if the path is no longer registered (so, for example,
you can hook up a component method as a listener and it will be
'garbage collected' with the compoent.

You can remove a listener (if you kept the reference handy).

    unobserve(listener);

You can remove a listener by test, but it will remove _all_ listeners which use
that test.

~~~~
b8r.register('listener_test1', {
  flag_changed: () => {
    console.log('flag changed');
    b8r.set('listener_test2.counter', b8r.get('listener_test2.counter') + 1);
  }
});
b8r.register('listener_test2', {counter: 0, flag: false});
b8r.observe('listener_test2.flag', 'listener_test1.flag_changed');
b8r.set('listener_test2.flag', true);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to true').shouldBe(1);
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to false').shouldBe(2);
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer ignored flag set to false').shouldBe(2);
b8r.set('listener_test2.flag', true);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to true').shouldBe(3);
b8r.remove('listener_test1');
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer automatically removed').shouldBe(3);
~~~~
*/

const observe = (test, callback) => {
  return new Listener(test, callback);
};

const unobserve = test => {
  let index;
  let found = false;
  if (test instanceof Listener) {
    index = listeners.indexOf(test);
    if (index > -1) {
      listeners.splice(index, 1);
    } else {
      console.error('unobserve failed, listener not found');
    }
  } else {
    for (let i = listeners.length - 1; i >= 0; i--) {
      if (listeners[i].test === test) {
        listeners.splice(i, 1);
        found = true;
      }
    }
  }
  return found;
};

/**
    registered('root-name'); // => true | false

You can get a list of root level objects:

    models();

You can obtain a value using a path.
*/

const models = () => Object.keys(registry);

const registered = path => !!registry[path.split('.')[0]];

/**
    remove('root');
    remove('path.to.property')

Will remove the specified property from the registry (including root-level objects). If the object
does not exist, has no effect. So:

~~~~
const {register, remove, get} = b8r;
register('foo', {bar: 17, baz: {lurman: true}});
Test(() => {remove('foo.bar'); return get('foo.bar');}).shouldBe(null);
Test(() => {remove('foo.boris.yeltsin'); return get('foo.boris')}).shouldBe(null);
Test(() => {remove('foo.baz.lurman'); return Object.keys(get('foo.baz')).length}).shouldBe(0);
~~~~
*/
const remove = path => deleteByPath(registry, path);

module.exports = {
  get,
  getJSON,
  set,
  setJSON,
  push,
  call,
  touch,
  async_touch,
  observe,
  unobserve,
  models,
  _register,
  register,
  registered,
  remove,
  resolvePath,
  isValidPath,
};
<commit_msg>fix — remove() was not firing change events<commit_after>/**
# Object Registry

Bindinator is built around the idea of registering objects under unique names
and binding events and element properties to paths based on those names.

b8r's registry is an observable object that b8r uses to keep track of objects.
Once an object is registered, its properties will automatically be bound
to events and DOM properties by path.

    set('root', object_value); // registers the object with the name
    set('root.path.to.value', new_value); // sets the value

You can set the registry's properties by path. Root level properties must
themselves be objects.

    get('root.path.to.value'); // gets the value

You can get a property by path.

Two convenience methods are provided for working with JSON data:

    getJSON('path.to.value' [, element]);
    setJSON('path.to.value', json_string);

Note that unlike set, setJSON does not accept an optional element argument.

    call('root.path.to.method', ...args); // returns value as appropriate

You can call a method by path.

    touch(path [, source_element]);

Triggers all observers as though the value at path has changed. Useful
if you change a property independently of the registry.

    async_touch(path);

Triggers all observers asynchronousely (on requestAnimationFrame).

    isValidPath(path);

`isValidPath` returns true if the path looks OK, false otherwise.

~~~~
Test(() => b8r.isValidPath('foo')).shouldBe(true);
Test(() => b8r.isValidPath('foo.bar')).shouldBe(true);
Test(() => b8r.isValidPath('.')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[1234]')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234]')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234].')).shouldBe(true);
Test(() => b8r.isValidPath('airtime-rooms[id=1234')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms[id]')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms[id=1234]]')).shouldBe(false);
Test(() => b8r.isValidPath('airtime-rooms]')).shouldBe(false);
~~~~

*/
/* jshint latedef:false */
/* global module, require, console */
'use strict';

const {getByPath, setByPath, deleteByPath} = require('./b8r.byPath.js');
const {getDataPath, getComponentInstancePath} = require('./b8r.bindings.js');
const {logStart, logEnd} = require('./b8r.perf.js');
const registry = {};
const listeners = [];  // { path_string_or_test, callback }

const valid_path = /^(\.|[^.\[\]])+(\.[^.\[\]]+|\[\d+\]|\[[^=\[\]]+\=[^\[\]]+\])*(\.)?$/;

const isValidPath = path => valid_path.test(path);

class Listener {
  constructor(test, callback) {
    if (typeof test === 'string') {
      this.test = t =>
          t.length >= test.length && test === t.substr(0, test.length);
    } else if (test instanceof RegExp) {
      this.test = test.test.bind(test);
    } else if (test instanceof Function) {
      this.test = test;
    } else {
      throw 'expect listener test to be a string, RegExp, or test function';
  }
    if (typeof callback === 'string') {
      this.callback = (...args) => {
        if (get(callback)) {
          call(callback, ...args);
        } else {
          unobserve(this);
        }
      };
    } else if (typeof callback === 'function') {
      this.callback = callback;
    } else {
      throw 'expect callback to be a path or function';
    }
    listeners.push(this);
  }
}

const resolvePath = (path, element) => {
  if (path[0] === '.') {
    if (!element) {
      throw 'cannot evaluate relative path without element';
    }
    path = getDataPath(element) + path;
  } else if (path.substr(0, 6) === '_data_') {
    if (!element) {
      throw 'cannot evaluate _data_ path without element';
    }
    path = getDataPath(element) + path.substr(6);
  } else if (path.substr() === '_component_') {
    if (!element) {
      throw 'cannot evaluate _component_ path without element';
    }
    path = getComponentInstancePath(element) + path;
  }
  return path;
};

const get = (path, element) => {
  path = resolvePath(path, element);
  return getByPath(registry, path);
};

const getJSON = (path, element) => JSON.stringify(get(path, element));

const touch = (path, source_element) => {
  logStart('touch', path);
  listeners.filter(listener => listener.test(path))
           .forEach(listener => listener.callback(path, source_element));
  logEnd('touch', path);
};

const _async_touch_dirty_list = [];
let _async_touch_id = null;

const _async_touch = () => {
  _async_touch_dirty_list.forEach(touch);
  _async_touch_dirty_list.splice(0);
};

const async_touch = path => {
  if (_async_touch_dirty_list.indexOf(path) === -1) {
    _async_touch_dirty_list.push(path);
  }
  if (!_async_touch_id) {
    _async_touch_id = requestAnimationFrame(_async_touch);
  }
};

const set = (path, value, source_element) => {
  const path_parts = path.split(/\.|\[/);
  const model = path_parts[0];
  const existing = getByPath(registry, path);
  if (path_parts.length > 1 && !registry[model]) {
    console.error(`cannot set ${path} to ${value}, ${model} does not exist`);
  } else if (path_parts.length === 1 && typeof value !== 'object') {
    throw 'cannot set ${path}; you can only register objects at root-level';
  } else if (value === existing) {
    // nothing to see here, move along
  } else if (value && value.constructor) {
    if (path_parts.length === 1 && ! registry[path]) {
      register(path, value);
    } else {
      // we only drill down into vanilla objects
      // not arrays or instances of custom classes
      if (value.constructor === Object && existing && existing.constructor === Object) {
        // using for vs. forEachKey because set will abort loop if setting something to false
        for (let i = 0, keys = Object.keys(value); i < keys.length; i++) {
          set(`${path}.${keys[i]}`, value[keys[i]]);
        }
      } else {
        setByPath(registry, path, value);
      }
      touch(path, source_element);
    }
  } else {
    setByPath(registry, path, value);
    touch(path, source_element);
  }
  return value; // convenient for things push (see below) but maybe an anti-feature?!
};

const _register = (name, obj) => {
  registry[name] = obj;
};

const register = (name, obj, block_updates) => {
  if (name.match(/^_[^_]*_$/)) {
    throw 'cannot register object as ' + name +
      ', all names starting and ending with a single \'_\' are reserved.';
  }

  _register(name, obj, block_updates);

  if (!block_updates) {
    touch(name);
    require.lazy('./b8r.events.js').then(({play_saved_messages}) => play_saved_messages(name));
  }
};

const setJSON = (path, value) => set(path, JSON.parse(value));

const push = (path, value) => {
  const list = get(path) || set(path, []);
  if(Array.isArray(list)) {
    list.push(value);
    async_touch(path);
  }
};

const call = (path, ...args) => {
  const method = get(path);
  if (method instanceof Function) {
    return method(...args);
  } else {
    console.error(`cannot call ${path}; not a method`);
  }
};

/**
    observe('root.path.to', callback); // returns a Listener instance

You can observe a path (string). The callback will fire whenever any path
matching or starting
with the string changes. The callback will be passed the exact path that
changed.

    observe(/root.path.[^\.]+.value/, callback);

Instead of a constant path, you can pass a RegExp which will fire the callback
when a path
matching the test changes. The callback will be passed the exact path that
changed.

    observe(path => path.split('.').length === 1, callback);

Finally you can observe a path test function.

    const listener = observe(test, callback);

The `callback` can be a function or a path. If a path, the listener will
automatically be removed if the path is no longer registered (so, for example,
you can hook up a component method as a listener and it will be
'garbage collected' with the compoent.

You can remove a listener (if you kept the reference handy).

    unobserve(listener);

You can remove a listener by test, but it will remove _all_ listeners which use
that test.

~~~~
b8r.register('listener_test1', {
  flag_changed: () => {
    console.log('flag changed');
    b8r.set('listener_test2.counter', b8r.get('listener_test2.counter') + 1);
  }
});
b8r.register('listener_test2', {counter: 0, flag: false});
b8r.observe('listener_test2.flag', 'listener_test1.flag_changed');
b8r.set('listener_test2.flag', true);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to true').shouldBe(1);
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to false').shouldBe(2);
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer ignored flag set to false').shouldBe(2);
b8r.set('listener_test2.flag', true);
Test(() => b8r.get('listener_test2.counter'), 'observer counted flag set to true').shouldBe(3);
b8r.remove('listener_test1');
b8r.set('listener_test2.flag', false);
Test(() => b8r.get('listener_test2.counter'), 'observer automatically removed').shouldBe(3);
~~~~
*/

const observe = (test, callback) => {
  return new Listener(test, callback);
};

const unobserve = test => {
  let index;
  let found = false;
  if (test instanceof Listener) {
    index = listeners.indexOf(test);
    if (index > -1) {
      listeners.splice(index, 1);
    } else {
      console.error('unobserve failed, listener not found');
    }
  } else {
    for (let i = listeners.length - 1; i >= 0; i--) {
      if (listeners[i].test === test) {
        listeners.splice(i, 1);
        found = true;
      }
    }
  }
  return found;
};

/**
    registered('root-name'); // => true | false

You can get a list of root level objects:

    models();

You can obtain a value using a path.
*/

const models = () => Object.keys(registry);

const registered = path => !!registry[path.split('.')[0]];

/**
    remove('root');
    remove('path.to.property')

Will remove the specified property from the registry (including root-level objects). If the object
does not exist, has no effect. So:

~~~~
const {register, remove, get} = b8r;
register('foo', {bar: 17, baz: {lurman: true}});
Test(() => {remove('foo.bar'); return get('foo.bar');}).shouldBe(null);
Test(() => {remove('foo.boris.yeltsin'); return get('foo.boris')}).shouldBe(null);
Test(() => {remove('foo.baz.lurman'); return Object.keys(get('foo.baz')).length}).shouldBe(0);
~~~~
*/
const remove = path => {
  deleteByPath(registry, path);
  touch(path);
};

module.exports = {
  get,
  getJSON,
  set,
  setJSON,
  push,
  call,
  touch,
  async_touch,
  observe,
  unobserve,
  models,
  _register,
  register,
  registered,
  remove,
  resolvePath,
  isValidPath,
};
<|endoftext|>


<commit_before>'use strict';

var debug   = require('debug')('kiss.io:namespace');
var util    = require('util');
var parser  = require('socket.io-parser');

var Socket  = require('./socket');
var Plugin  = require('./plugin');
var Router  = require('./router');

/**
 * A namespace is an individual, independent, autonomous arena
 * ..for real-time data transfer.
 *
 * It can be initialized in its own self-contained module,
 * ..and later be mounted into a kiss.io server, OR be initialized
 * ..within the server itself.
 *
 * @constructs Namespace
 * @access public
 *
 * @param {String} name (must start with '/')
 * @param {Object} [opts]
 * @returns {Namespace} self
 */
function Namespace(name, opts)
{
  if(!(this instanceof Namespace))
  {
    return new Namespace(name, opts);
  }

  this.name = Namespace.slugify(name);
  this.opts = opts || {};
  this.router = new Router(this.opts);
  this.sockets = {};
  this.middlewares = [];
  this.plugins = [];
  this.locals = {};
}

/**
 * Static function that used as helper to generate a slug for the nsp.
 * Currently just appends '/' to the beginning of the name.
 *
 * @access public
 *
 * @param name
 * @returns {String} id
 */
Namespace.slugify = function(name)
{
  if(String(name)[0] !== '/')
  {
    name = '/' + name;
  }

  return name;
};

/***
 * Plugs a plug-in to the namespace.
 *
 * @alias {Namespace.plugin}
 * @access public
 *
 * @param {Function|Plugin} plugin
 * @param {Object} [opts]: special options for the plugin
 * @returns {Namespace} self
 */
Namespace.prototype.plug =
Namespace.prototype.plugin = function(plugin, opts)
{
  if(typeof plugin == 'function')
  {
    plugin = new plugin(this, opts);
  }

  if(plugin instanceof Plugin)
  {
    var exports = plugin.exports.nsp || {};

    for(let key of Object.keys(exports))
    {
      let value = exports[key];

      if(typeof value == 'function')
      {
        value = value.bind(plugin, this);
      }
      else if(typeof value == 'object')
      {
        value = Object.create(value);
      }

      this[key] = value;
    }

    this.plugins.push(plugin);
  }
  else
  {
    debug('Namespace.plug: You can only plug `Plugin` instances.');
  }

  return this;
};

/**
 * A helper method used to configure the namespace before
 * ..mounting it on the server.
 *
 * Works synchronously, and bind the namespace to the function.
 *
 * @access public
 *
 * @param {Function} fn
 * @returns {Namespace} self
 */
Namespace.prototype.configure = function(fn)
{
  if(typeof fn == 'function')
  {
    fn.apply(this);
  }

  return this;
};

/**
 * Set/get a temporary flag.
 *
 * @access public
 *
 * @param {String} key: the name of the flag.
 * @param {*} [value]
 * @returns {*|undefined} value
 */
Namespace.prototype.flag = function(key, value)
{
  this._flags = this._flags || {};

  if(arguments.length == 2)
  {
    this._flags[key] = value;
  }

  return this._flags[key];
};

/**
 * Reset temporary flags
 *
 * @access public
 *
 * @returns {Namespace} self
 */
Namespace.prototype.reset = function()
{
  delete this._flags;
};

/**
 * Set/get a local parameter for the namespace.
 *
 * @access public
 *
 * @param {String} key
 * @param {*} value
 * @returns {*} value
 */
Namespace.prototype.local = function(key, value)
{
  if(arguments.length == 2)
  {
    this.locals[key] = value;
  }

  return this.locals[key];
};

/**
 * Mounts a middleware/plugin/route/router on the namespace.
 *
 * @access public
 *
 * @param {Function|String|Router|Route} item
 * @return {Namespace} self
 */
Namespace.prototype.use = function(item)
{
  if(typeof item == 'function')
  {
    this.middlewares.push(item);
  }
  else if(item instanceof Plugin)
  {
    this.plug(item);
  }
  else if(typeof item == 'string')
  {
    this.event.apply(this, arguments);
  }
  else if(item instanceof Router)
  {
    this.router.use(item);
  }
  else if(item instanceof Router.Route)
  {
    this.router.use(item);
  }

  return this;
};

/**
 * Registers a route for the the namespace.
 * @see Router.on
 *
 * @alias {Namespace.event}
 * @access public
 *
 * @param {String|Router|Route} event: the name of the event to register
 * @param {Function} [handler]: (if string event provided) the handler for the event
 * @returns {Namespace} self
 */
Namespace.prototype.on =
Namespace.prototype.event = function(event, handler)
{
  return this.router.on.apply(this.router, arguments);
};

/**
 * Executes the middleware for an incoming client.
 *
 * @access private
 *
 * @param {Socket} socket to execute the middleware on
 * @param {Function} fn: last fn call in the middleware
 */
Namespace.prototype.execMiddleware = function(socket, fn)
{
  var fns = this.middlewares.slice(0);

  if (!fns.length)
  {
    return fn(null);
  }

  function exec(i)
  {
    fns[i](socket, function(err)
    {
      // upon error, short-circuit
      if (err) return fn(err);

      // if no middleware left, summon callback
      if (!fns[i + 1]) return fn(null);

      // go on to next
      exec(i + 1);
    });
  }

  exec(0);
};

/**
 * Adds a new client to the namespace.
 *
 * @access public
 *
 * @param {Socket} socket
 * @param {Function} [fn]: an optional fn to execute before
 * ..connecting the socket to the namespace.
 * @return {Socket} the socket that was added to the namespace.
 */
Namespace.prototype.add = function(socket, fn)
{
  var self = this;

  socket.trigger('pre-setup', socket);

  socket.merge(this.router);

  this.plugins.forEach(function(plugin)
  {
    socket.plug(plugin);
  });

  this.execMiddleware(socket, function(err)
  {
    process.nextTick(function()
    {
      if ('open' !== socket.conn.readyState)
      {
        debug('next called after client was closed - ignoring socket');
        return;
      }
      else if (err)
      {
        return socket.error(err.data || err.message);
      }

      self.sockets[socket.id] = socket;

      if(typeof fn == 'function')
      {
        fn(err, socket);
      }

      // fire user-set events
      socket.trigger('connect', socket);
      socket.trigger('connection', socket);
    });
  });

  return socket;
};

/**
 * Removes a socket from the namespace.
 * Called by each `Socket`.
 *
 * @access private
 */
Namespace.prototype.remove = function(socket)
{
  delete this.sockets[socket.id];
};

/**
 * Sets the `except` flag to ignore sockets when broadcasting.
 *
 * @access public
 *
 * @param {Array<String>} sids: the ids of the of sockets to ignore.
 * @returns {Namespace} self
 */
Namespace.prototype.except = function(sids)
{
  if(!Array.isArray(sids))
  {
    sids = [sids];
  }

  this.flag('except', sids);
  return this;
};

/**
 * Broadcasts an event to every socket connected to the namespace,
 * ..and ignores those who are specified in the `except` flag.
 *
 * @access public
 *
 * @param {String} ev: the event to broadcast.
 * @params {...} the params to emit together with the event.
 * @returns {Namespace} self
 */
Namespace.prototype.broadcast = function(ev)
{
  var except = this.flag('except') || [];

  for(var sid of Object.keys(this.sockets))
  {
    if(except.indexOf(sid) !== -1)
    {
      continue;
    }

    var s = this.socket(sid);

    if(s instanceof Socket)
    {
      s.emit.apply(s, arguments);
    }
  }

  this.reset();
  return this;
};

/**
 * Sends a `message` event to all clients.
 *
 * @access private
 *
 * @return {Namespace} self
 */
Namespace.prototype.send =
Namespace.prototype.write = function()
{
  var args = Array.prototype.slice.call(arguments);
  args.unshift('message');

  this.broadcast.apply(this, args);
  return this;
};

/**
 * Returns a socket that's connected to the namespace,
 * ..correspondent a given id.
 *
 * @access public
 *
 * @param {String} id
 * @returns {Socket}
 */
Namespace.prototype.socket = function(id)
{
  return this.sockets[id];
};

/**
 * Sets the compress flag.
 *
 * @access public
 *
 * @param {Boolean} compress: if `true`, compresses the sending data
 * @return {Namespace} self
 */
Namespace.prototype.compress = function(compress)
{
  this.flag('compress', compress);
  return this;
};

/*!
 * Module exports.
 */
module.exports = Namespace;
<commit_msg>Fix socket.onConnect not being called<commit_after>'use strict';

var debug   = require('debug')('kiss.io:namespace');
var util    = require('util');
var parser  = require('socket.io-parser');

var Socket  = require('./socket');
var Plugin  = require('./plugin');
var Router  = require('./router');

/**
 * A namespace is an individual, independent, autonomous arena
 * ..for real-time data transfer.
 *
 * It can be initialized in its own self-contained module,
 * ..and later be mounted into a kiss.io server, OR be initialized
 * ..within the server itself.
 *
 * @constructs Namespace
 * @access public
 *
 * @param {String} name (must start with '/')
 * @param {Object} [opts]
 * @returns {Namespace} self
 */
function Namespace(name, opts)
{
  if(!(this instanceof Namespace))
  {
    return new Namespace(name, opts);
  }

  this.name = Namespace.slugify(name);
  this.opts = opts || {};
  this.router = new Router(this.opts);
  this.sockets = {};
  this.middlewares = [];
  this.plugins = [];
  this.locals = {};
}

/**
 * Static function that used as helper to generate a slug for the nsp.
 * Currently just appends '/' to the beginning of the name.
 *
 * @access public
 *
 * @param name
 * @returns {String} id
 */
Namespace.slugify = function(name)
{
  if(String(name)[0] !== '/')
  {
    name = '/' + name;
  }

  return name;
};

/***
 * Plugs a plug-in to the namespace.
 *
 * @alias {Namespace.plugin}
 * @access public
 *
 * @param {Function|Plugin} plugin
 * @param {Object} [opts]: special options for the plugin
 * @returns {Namespace} self
 */
Namespace.prototype.plug =
Namespace.prototype.plugin = function(plugin, opts)
{
  if(typeof plugin == 'function')
  {
    plugin = new plugin(this, opts);
  }

  if(plugin instanceof Plugin)
  {
    var exports = plugin.exports.nsp || {};

    for(let key of Object.keys(exports))
    {
      let value = exports[key];

      if(typeof value == 'function')
      {
        value = value.bind(plugin, this);
      }
      else if(typeof value == 'object')
      {
        value = Object.create(value);
      }

      this[key] = value;
    }

    this.plugins.push(plugin);
  }
  else
  {
    debug('Namespace.plug: You can only plug `Plugin` instances.');
  }

  return this;
};

/**
 * A helper method used to configure the namespace before
 * ..mounting it on the server.
 *
 * Works synchronously, and bind the namespace to the function.
 *
 * @access public
 *
 * @param {Function} fn
 * @returns {Namespace} self
 */
Namespace.prototype.configure = function(fn)
{
  if(typeof fn == 'function')
  {
    fn.apply(this);
  }

  return this;
};

/**
 * Set/get a temporary flag.
 *
 * @access public
 *
 * @param {String} key: the name of the flag.
 * @param {*} [value]
 * @returns {*|undefined} value
 */
Namespace.prototype.flag = function(key, value)
{
  this._flags = this._flags || {};

  if(arguments.length == 2)
  {
    this._flags[key] = value;
  }

  return this._flags[key];
};

/**
 * Reset temporary flags
 *
 * @access public
 *
 * @returns {Namespace} self
 */
Namespace.prototype.reset = function()
{
  delete this._flags;
};

/**
 * Set/get a local parameter for the namespace.
 *
 * @access public
 *
 * @param {String} key
 * @param {*} value
 * @returns {*} value
 */
Namespace.prototype.local = function(key, value)
{
  if(arguments.length == 2)
  {
    this.locals[key] = value;
  }

  return this.locals[key];
};

/**
 * Mounts a middleware/plugin/route/router on the namespace.
 *
 * @access public
 *
 * @param {Function|String|Router|Route} item
 * @return {Namespace} self
 */
Namespace.prototype.use = function(item)
{
  if(typeof item == 'function')
  {
    this.middlewares.push(item);
  }
  else if(item instanceof Plugin)
  {
    this.plug(item);
  }
  else if(typeof item == 'string')
  {
    this.event.apply(this, arguments);
  }
  else if(item instanceof Router)
  {
    this.router.use(item);
  }
  else if(item instanceof Router.Route)
  {
    this.router.use(item);
  }

  return this;
};

/**
 * Registers a route for the the namespace.
 * @see Router.on
 *
 * @alias {Namespace.event}
 * @access public
 *
 * @param {String|Router|Route} event: the name of the event to register
 * @param {Function} [handler]: (if string event provided) the handler for the event
 * @returns {Namespace} self
 */
Namespace.prototype.on =
Namespace.prototype.event = function(event, handler)
{
  return this.router.on.apply(this.router, arguments);
};

/**
 * Executes the middleware for an incoming client.
 *
 * @access private
 *
 * @param {Socket} socket to execute the middleware on
 * @param {Function} fn: last fn call in the middleware
 */
Namespace.prototype.execMiddleware = function(socket, fn)
{
  var fns = this.middlewares.slice(0);

  if (!fns.length)
  {
    return fn(null);
  }

  function exec(i)
  {
    fns[i](socket, function(err)
    {
      // upon error, short-circuit
      if (err) return fn(err);

      // if no middleware left, summon callback
      if (!fns[i + 1]) return fn(null);

      // go on to next
      exec(i + 1);
    });
  }

  exec(0);
};

/**
 * Adds a new client to the namespace.
 *
 * @access public
 *
 * @param {Socket} socket
 * @param {Function} [fn]: an optional fn to execute before
 * ..connecting the socket to the namespace.
 * @return {Socket} the socket that was added to the namespace.
 */
Namespace.prototype.add = function(socket, fn)
{
  var self = this;

  socket.trigger('pre-setup', socket);
  socket.merge(this.router);

  this.plugins.forEach(function(plugin)
  {
    socket.plug(plugin);
  });

  this.execMiddleware(socket, function(err)
  {
    process.nextTick(function()
    {
      if ('open' !== socket.conn.readyState)
      {
        debug('next called after client was closed - ignoring socket');
        return;
      }
      else if (err)
      {
        return socket.error(err.data || err.message);
      }

      self.sockets[socket.id] = socket;

      if(typeof fn == 'function')
      {
        fn(err, socket);
      }

      socket.onConnect();
      socket.trigger('connect', socket);
      socket.trigger('connection', socket);
    });
  });

  return socket;
};

/**
 * Removes a socket from the namespace.
 * Called by each `Socket`.
 *
 * @access private
 */
Namespace.prototype.remove = function(socket)
{
  delete this.sockets[socket.id];
};

/**
 * Sets the `except` flag to ignore sockets when broadcasting.
 *
 * @access public
 *
 * @param {Array<String>} sids: the ids of the of sockets to ignore.
 * @returns {Namespace} self
 */
Namespace.prototype.except = function(sids)
{
  if(!Array.isArray(sids))
  {
    sids = [sids];
  }

  this.flag('except', sids);
  return this;
};

/**
 * Broadcasts an event to every socket connected to the namespace,
 * ..and ignores those who are specified in the `except` flag.
 *
 * @access public
 *
 * @param {String} ev: the event to broadcast.
 * @params {...} the params to emit together with the event.
 * @returns {Namespace} self
 */
Namespace.prototype.broadcast = function(ev)
{
  var except = this.flag('except') || [];

  for(var sid of Object.keys(this.sockets))
  {
    if(except.indexOf(sid) !== -1)
    {
      continue;
    }

    var s = this.socket(sid);

    if(s instanceof Socket)
    {
      s.emit.apply(s, arguments);
    }
  }

  this.reset();
  return this;
};

/**
 * Sends a `message` event to all clients.
 *
 * @access private
 *
 * @return {Namespace} self
 */
Namespace.prototype.send =
Namespace.prototype.write = function()
{
  var args = Array.prototype.slice.call(arguments);
  args.unshift('message');

  this.broadcast.apply(this, args);
  return this;
};

/**
 * Returns a socket that's connected to the namespace,
 * ..correspondent a given id.
 *
 * @access public
 *
 * @param {String} id
 * @returns {Socket}
 */
Namespace.prototype.socket = function(id)
{
  return this.sockets[id];
};

/**
 * Sets the compress flag.
 *
 * @access public
 *
 * @param {Boolean} compress: if `true`, compresses the sending data
 * @return {Namespace} self
 */
Namespace.prototype.compress = function(compress)
{
  this.flag('compress', compress);
  return this;
};

/*!
 * Module exports.
 */
module.exports = Namespace;
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var React = require('react/addons');
var Combokeys = require('combokeys');
var CSSTransitionGroup = React.addons.CSSTransitionGroup;
var mui = require('material-ui');
var ThemeManager = new mui.Styles.ThemeManager();

var {Paper} = mui;
var SvgDisplay = require('./SvgDisplay');
var IconButton = require('./IconButton');

//var Actions = require('actions/xxx')

require('styles/Editor.less');

function doNothing() {}

function randomLines() {
  var lines = [];
  var limits = 900;
  var getNum = () => Math.random() * limits - 100;
  var getLineType = () => Math.floor(3 * Math.random());
  for (let i = 0; i < 40; i++) {
    lines.push([2 * getNum(), getNum(), 2 * getNum(), getNum(), getLineType()]);
  }
  return lines;
}

var LINES = randomLines();

var Editor = React.createClass({

  getInitialState() {
    return {
      debugButtons: false,
      toolbarsVisible: false,
      timeControlVisible: false,
      combokeys: new Combokeys(document)
    };
  },

  childContextTypes: {
    muiTheme: React.PropTypes.object
  },

  getChildContext() {
    return {
      muiTheme: ThemeManager.getCurrentTheme()
    };
  },

  componentWillUnmount() {
    this.state.combokeys.detach();
  },

  showToolbars() {
    this.setState({ toolbarsVisible: true });
  },

  hideToolbars() {
    this.setState({ toolbarsVisible: false });
  },

  toggleTimeControl() {
    this.setState({ timeControlVisible: !this.state.timeControlVisible });
  },

  toggleDebug() {
    this.setState({debugButtons: !this.state.debugButtons});
  },

  getStyles() {
    var styles = {
      floatCircle: { padding: '0px', width: 42, height: 42 },
      smallIcon: { padding: '6px', width: 36, height: 36, margin: '3px' },
      defaultIcon: { padding: '9px', width: 42, height: 42, margin: '3px' }
    };

    return styles;
  },

  getButtons() {
    return {
      // editing tool group
      line: { icon: 'line', hotkey: 'w', onClick: doNothing },
      pencil: { icon: 'pencil', hotkey: 'q', onClick: doNothing },
      brush: { icon: 'brush', onClick: doNothing },
      curve: { icon: 'curve', onClick: doNothing },
      multiLine: { icon: 'multi-line', onClick: doNothing },
      select: { icon: 'cursor-default', onClick: doNothing },
      eraser: { icon: 'eraser', hotkey: 'e', onClick: doNothing },
      // track state group
      save: { icon: 'content-save', hotkey: 'o', onClick: doNothing },
      undo: { icon: 'undo-variant', onClick: this.toggleDebug },
      redo: { icon: 'redo-variant', onClick: doNothing },
      // space navigation group
      pan: { icon: 'cursor-move', hotkey: 'r', onClick: doNothing },
      zoom: { icon: 'magnify', hotkey: 't', onClick: doNothing },
      viewfinder: { icon: 'viewfinder', onClick: doNothing },
      layers: { icon: 'layers', onClick: doNothing },
      // time navigation group
      play: { icon: 'play', hotkey: 'y', onClick: doNothing },
      pause: { icon: 'pause', onClick: doNothing },
      stop: { icon: 'stop', hotkey: 'u', onClick: doNothing },
      rewind: { icon: 'rewind', onClick: doNothing },
      fastFoward: { icon: 'fast-forward', onClick: doNothing },
      stepBack: { icon: 'skip-previous', onClick: doNothing },
      stepForward: { icon: 'skip-next', onClick: doNothing },
      flag: { icon: 'flag-variant', hotkey: 'i', onClick: doNothing },
      multiFlag: { icon: 'flag-outline-variant', onClick: doNothing },
      onionSkin: { icon: 'onion-skin', onClick: doNothing },
      // special feature group
      camera: { icon: 'video', onClick: doNothing },
      music: { icon: 'music-note', onClick: doNothing },
      record: { icon: 'movie', onClick: doNothing },
      // toolbar visibility group
      showToolbars: { icon: 'chevron-down', onClick: this.showToolbars },
      hideToolbars: { icon: 'chevron-up', onClick: this.hideToolbars },
      toggleTimeControl: { icon: 'chevron-down', onClick: this.toggleTimeControl },
      // misc
      chat: { icon: 'message', onClick: doNothing },
      settings: { icon: 'settings', onClick: doNothing },
      help: { icon: 'help-circle', hotkey: 'p', onClick: doNothing }
    };
  },

  getButtonGroups() {
    var b = this.getButtons();
    var styles = this.getStyles();

    b.toggleTimeControl.style = styles.defaultIcon;
    b.toggleTimeControl.style.transform = this.state.timeControlVisible ? 'rotate(0deg)' : 'rotate(180deg)';

    var floatUndo = _.clone(b.undo);
    floatUndo.style = styles.floatCircle;
    b.showToolbars.style = styles.floatCircle;

    var timeline = {
      render: (i) =>

        <input type='range' min={0} max={100} defaultValue={0} style={{width: '100%'}} />

    };

    var buttonGroups = {
      floatLeft: [
        floatUndo
      ],
      floatMiddle: [
        b.pencil, b.line, b.eraser, b.pan, b.zoom, b.play, b.stop, b.flag, b.save, b.help
      ],
      floatRight: [
        b.showToolbars
      ],
      topGroups: [
        [ b.save, b.undo, b.redo ],
        [ b.select, b.pencil, b.brush, b.line, b.curve, b.multiLine, b.eraser ],
        [ b.settings, b.chat, b.hideToolbars ]
      ],
      bottomGroups: [
        [ b.layers, b.viewfinder, b.pan, b.zoom ],
        [ b.multiFlag, b.flag, b.play, b.stop, b.pause ],
        [ b.camera, b.record, b.help, b.toggleTimeControl ]
      ],
      timeControl: [
        b.onionSkin, b.rewind, b.stepBack, timeline, b.stepForward, b.fastFoward, b.music
      ]
    };

    buttonGroups.floatMiddle = buttonGroups.floatMiddle.map(button => {
      button = _.clone(button);
      button.style = styles.smallIcon;
      return button;
    });

    return buttonGroups;
  },

  renderButtons(buttons) {
    return buttons.map((button, i) =>
      button.render ?
        button.render(i) :
      this.state.debugButtons ?
        <button key={i} style={{width: 48, height: 48, padding: 0}} {...button} /> :
        <IconButton {...button}
          key={i}
          style={button.style || this.getStyles().defaultIcon}
          combokeys={this.state.combokeys}
          selected={this.state.pressed && this.state.pressed === button.hotkey}
          onTouchTap={button.hotkey && (() => this.setState({pressed: button.hotkey}))}
        />
    );
  },

  renderButtonGroups(groups) {
    return groups.map((group, i) =>

        { this.renderButtons(group) }

    );
  },

  renderFloatBar(buttonGroups) {
    var floatPapersProps = [
      { circle: true, children: this.renderButtons(buttonGroups.floatLeft) },
      {
        className: 'float-paper-bar',
        children:
          <Toolbar className='float-toolbar'>
            {
              this.renderButtons(buttonGroups.floatMiddle)
            }
          </Toolbar>
      },
      { circle: true, children: this.renderButtons(buttonGroups.floatRight) }
    ];

    return (

        {floatPapersProps.map((props, i) =>
          <CSSTransitionGroup key={i} transitionName='float-paper'>
            {
              !this.state.toolbarsVisible ?
                <FloatPaper {...props}>
                  {props.children}
                </FloatPaper>
              : null
            }
          </CSSTransitionGroup>
        )}

    );
  },

  renderTopBar(buttonGroups) {
    return (
      <CSSTransitionGroup transitionName='top'>
        {
          this.state.toolbarsVisible ?
          <PaperBar className='top'>
            <Toolbar className='top'>
              {
                this.renderButtonGroups(buttonGroups.topGroups)
              }
            </Toolbar>
          </PaperBar>
          : null
        }
      </CSSTransitionGroup>
    );
  },

  renderBottomBar(buttonGroups) {
    var bottomPaperBarClass = this.state.timeControlVisible ? 'bottom-extended' : 'bottom';

    return (
      <CSSTransitionGroup transitionName={bottomPaperBarClass}>
        {
          this.state.toolbarsVisible ?
          <PaperBar className={bottomPaperBarClass}>
            <Toolbar>
              {
                this.renderButtonGroups(buttonGroups.bottomGroups)
              }
            </Toolbar>
            <CSSTransitionGroup
              className='toolbar time-control-toolbar'
              transitionName='time-control-toolbar'
            >
              {
                this.state.timeControlVisible ?

                  {
                    this.renderButtons(buttonGroups.timeControl)
                  }

                : null
              }
            </CSSTransitionGroup>
          </PaperBar>
          : null
        }
      </CSSTransitionGroup>
    );
  },

  render() {
    var buttonGroups = this.getButtonGroups();

    return (

        <SvgDisplay lines={LINES} />
        { this.renderFloatBar(buttonGroups) }
        { this.renderTopBar(buttonGroups) }
        { this.renderBottomBar(buttonGroups) }

    );
  }
});

var Toolbar = React.createClass({
  render() {
    return (

        {this.props.children}

    );
  }
});

var PaperBar = React.createClass({
  render() {
    return (
      <Paper
        className={'paper-bar ' + (this.props.className || '')}
        rounded={false}
        transitionEnabled={false}
      >
        {this.props.children}
      </Paper>
    );
  }
});

var FloatPaper = React.createClass({
  render() {
    return (
      <Paper
        className={'float-paper ' + (this.props.className || '')}
        circle={this.props.circle}
        style={{boxShadow: 'null'}}
        transitionEnabled={false}
      >
        {this.props.children}
      </Paper>
    );
  }
});

module.exports = Editor;

<commit_msg>move editor buttons out + change selection behavior + other stuff<commit_after>'use strict';

var _ = require('lodash');
var React = require('react/addons');
var Combokeys = require('combokeys');
var CSSTransitionGroup = React.addons.CSSTransitionGroup;
var mui = require('material-ui');
var ThemeManager = new mui.Styles.ThemeManager();

var {Paper} = mui;
var SvgDisplay = require('./SvgDisplay');
var IconButton = require('./IconButton');
var editorButtons = require('./editorButtons');

//var Actions = require('actions/xxx')

require('styles/Editor.less');

function randomLines() {
  var lines = [];
  var limits = 900;
  var getNum = () => Math.random() * limits - 100;
  var getLineType = () => Math.floor(3 * Math.random());
  for (let i = 0; i < 40; i++) {
    lines.push([2 * getNum(), getNum(), 2 * getNum(), getNum(), getLineType()]);
  }
  return lines;
}

var LINES = randomLines();

var Editor = React.createClass({

  getInitialState() {
    return {
      debugButtons: false,
      toolbarsVisible: false,
      timeControlVisible: false,
      helpEnabled: false,
      combokeys: new Combokeys(document)
    };
  },

  childContextTypes: {
    muiTheme: React.PropTypes.object
  },

  getChildContext() {
    return {
      muiTheme: ThemeManager.getCurrentTheme()
    };
  },

  componentWillUnmount() {
    this.state.combokeys.detach();
  },

  showToolbars() {
    this.setState({ toolbarsVisible: true });
  },

  hideToolbars() {
    this.setState({ toolbarsVisible: false });
  },

  toggleTimeControl() {
    this.setState({ timeControlVisible: !this.state.timeControlVisible });
  },

  toggleDebug() {
    // this.setState({debugButtons: !this.state.debugButtons});
  },

  toggleHelp() {
    this.setState({ helpEnabled: !this.state.helpEnabled });
  },

  setCursor(hotkey) {
    this.setState({cursor: hotkey});
  },

  getStyles() {
    var styles = {
      floatCircle: { padding: '0px', width: 42, height: 42 },
      smallIcon: { padding: '6px', width: 36, height: 36, margin: '3px' },
      defaultIcon: { padding: '9px', width: 42, height: 42, margin: '3px' }
    };

    return styles;
  },

  getButtons() {
    return editorButtons(this);
  },

  getButtonGroups() {
    var b = this.getButtons();
    var styles = this.getStyles();

    b.toggleTimeControl.iconStyle = {};
    b.toggleTimeControl.iconStyle.transform = this.state.timeControlVisible ? 'rotate(0deg)' : 'rotate(180deg)';

    var floatUndo = _.clone(b.undo);
    floatUndo.style = styles.floatCircle;
    b.showToolbars.style = styles.floatCircle;
    b.help.selected = this.state.helpEnabled;
    b.help.selectedColor = 'green';

    var timeline = {
      render: (i) =>

        <input type='range' min={0} max={100} defaultValue={0} style={{width: '100%'}} />

    };

    var buttonGroups = {
      floatLeft: [
        floatUndo
      ],
      floatMiddle: [
        b.pencil, b.line, b.eraser, b.pan, b.zoom, b.play, b.stop, b.flag, b.save, b.help
      ],
      floatRight: [
        b.showToolbars
      ],
      topGroups: [
        [ b.save, b.undo, b.redo ],
        [ b.select, b.pencil, b.brush, b.line, b.curve, b.multiLine, b.eraser ],
        [ b.settings, b.chat, b.hideToolbars ]
      ],
      bottomGroups: [
        [ b.layers, b.viewfinder, b.pan, b.zoom ],
        [ b.multiFlag, b.flag, b.play, b.stop, b.pause ],
        [ b.camera, b.record, b.help, b.toggleTimeControl ]
      ],
      timeControl: [
        b.onionSkin, b.rewind, b.stepBack, timeline, b.stepForward, b.fastFoward, b.music
      ]
    };

    buttonGroups.floatMiddle = buttonGroups.floatMiddle.map(button => {
      button = _.clone(button);
      button.style = styles.smallIcon;
      return button;
    });

    buttonGroups.bottomGroups.concat([buttonGroups.timeControl]).forEach(buttonGroup => {
      buttonGroup.forEach(button => {
        button.upwardsTooltip = true;
      });
    });

    return buttonGroups;
  },

  renderButtons(buttons) {
    return buttons.map((button, i) =>
      button.render ?
        button.render(i) :
      this.state.debugButtons ?
        <button key={i} style={{width: 48, height: 48, padding: 0}} {...button} /> :
        <IconButton {...button}
          key={i}
          style={button.style || this.getStyles().defaultIcon}
          combokeys={this.state.combokeys}
          showTooltip={this.state.helpEnabled}
          selected={button.selected || this.state.cursor && this.state.cursor === button.hotkey}
        />
    );
  },

  renderButtonGroups(groups) {
    return groups.map((group, i) =>

        { this.renderButtons(group) }

    );
  },

  renderFloatBar(buttonGroups) {
    var floatPapersProps = [
      { circle: true, children: this.renderButtons(buttonGroups.floatLeft) },
      {
        className: 'float-paper-bar',
        children:
          <Toolbar className='float-toolbar'>
            {
              this.renderButtons(buttonGroups.floatMiddle)
            }
          </Toolbar>
      },
      { circle: true, children: this.renderButtons(buttonGroups.floatRight) }
    ];

    return (

        {floatPapersProps.map((props, i) =>
          <CSSTransitionGroup key={i} transitionName='float-paper'>
            {
              !this.state.toolbarsVisible ?
                <FloatPaper {...props}>
                  {props.children}
                </FloatPaper>
              : null
            }
          </CSSTransitionGroup>
        )}

    );
  },

  renderTopBar(buttonGroups) {
    return (
      <CSSTransitionGroup transitionName='top'>
        {
          this.state.toolbarsVisible ?
          <PaperBar className='top'>
            <Toolbar className='top'>
              {
                this.renderButtonGroups(buttonGroups.topGroups)
              }
            </Toolbar>
          </PaperBar>
          : null
        }
      </CSSTransitionGroup>
    );
  },

  renderBottomBar(buttonGroups) {
    var bottomPaperBarClass = this.state.timeControlVisible ? 'bottom-extended' : 'bottom';

    return (
      <CSSTransitionGroup transitionName={bottomPaperBarClass}>
        {
          this.state.toolbarsVisible ?
          <PaperBar className={bottomPaperBarClass}>
            <Toolbar>
              {
                this.renderButtonGroups(buttonGroups.bottomGroups)
              }
            </Toolbar>
            <CSSTransitionGroup
              className='toolbar time-control-toolbar'
              transitionName='time-control-toolbar'
            >
              {
                this.state.timeControlVisible ?

                  {
                    this.renderButtons(buttonGroups.timeControl)
                  }

                : null
              }
            </CSSTransitionGroup>
          </PaperBar>
          : null
        }
      </CSSTransitionGroup>
    );
  },

  render() {
    var buttonGroups = this.getButtonGroups();

    return (

        <SvgDisplay lines={LINES} />
        { this.renderFloatBar(buttonGroups) }
        { this.renderTopBar(buttonGroups) }
        { this.renderBottomBar(buttonGroups) }

    );
  }
});

var Toolbar = React.createClass({
  render() {
    return (

        {this.props.children}

    );
  }
});

var PaperBar = React.createClass({
  render() {
    return (
      <Paper
        className={'paper-bar ' + (this.props.className || '')}
        rounded={false}
        transitionEnabled={false}
      >
        {this.props.children}
      </Paper>
    );
  }
});

var FloatPaper = React.createClass({
  render() {
    return (
      <Paper
        className={'float-paper ' + (this.props.className || '')}
        circle={this.props.circle}
        style={{boxShadow: 'null'}}
        transitionEnabled={false}
      >
        {this.props.children}
      </Paper>
    );
  }
});

module.exports = Editor;

<|endoftext|>