<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import debounce from 'lodash/debounce';
import { props, t } from '../utils';

const NO_SIZE_WRAPPER = 'no-size-wrapper';

export const Props = {
  children: t.ReactChildren,
  popover: t.struct({
    content: t.ReactChildren,
    attachToBody: t.maybe(t.Boolean),
    position: t.maybe(t.enums.of(['top', 'bottom', 'left', 'right'])),
    anchor: t.maybe(t.enums.of(['start', 'center', 'end'])),
    event: t.maybe(t.enums.of(['click', 'hover'])),
    onShow: t.maybe(t.Function),
    onHide: t.maybe(t.Function),
    onToggle: t.maybe(t.Function),
    dismissOnScroll: t.maybe(t.Boolean),
    dismissOnClickOutside: t.maybe(t.Boolean),
    className: t.maybe(t.String),
    style: t.maybe(t.Object),
    id: t.maybe(t.String),
    maxWidth: t.maybe(t.union([ t.Number, t.String ])),
    distance: t.maybe(t.Number),
    offsetX: t.maybe(t.Number),
    offsetY: t.maybe(t.Number),
    isOpen: t.maybe(t.Boolean),
    delay: t.maybe(t.union([
      t.Integer,
      t.struct({ whenClosed: t.maybe(t.Integer), whenOpen: t.maybe(t.Integer) })
    ]))
  }),
  id: t.maybe(t.String),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
};

/**
 * Composed of two children: trigger (children) and popover. After a particular event on the trigger (usually "hover" or "click") it renders the popover and positions it relative to it.
 * @param children - the trigger node. It's always visible
 * @param popover - popover settings. The popover is **not** always visible
 */
@props(Props)
export default class Popover extends React.Component {

  // LIFECYCLE

  state = { isOpen: false };

  componentDidMount() {
    this.updateDebouncedMousedEvents();
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.onPopoverOpenChange(this.props);
    }
  }

  componentWillReceiveProps(nextProps) {
    this.updateDebouncedMousedEvents(nextProps);
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  }

  componentDidUpdate() {
    if (this.containerNode) {
      const popover = this.getVisiblePopover();
      ReactDOM.render(popover, this.containerNode);
    }
  }

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  }

  // LISTENERS

  addOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  };

  removeOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  };

  onClickOutside = (e) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  };

  addOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  };

  removeOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  };

  onScroll = () => this.hidePopover();

  addListeners = () => {
    this.addOnScrollListener();
    this.addOnClickListener();
  };

  removeListeners = () => {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  };

  // UTILS

  // extend with default values
  getPopoverProps = (_props) => {
    const props = _props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  };

  getPopoverNode = () => {
    let popover;
    if (this.isAbsolute()) {
      popover = this.popoverNode;
    } else {
      const childrenNode = ReactDOM.findDOMNode(this.refs.children);
      popover = childrenNode.childNodes[1];
    }
    return (popover && popover.id === NO_SIZE_WRAPPER) ? popover.childNodes[0] : popover;
  };

  getOffsetRect = (target) => {
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  };

  saveValuesFromNodeTree = (cb) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.getPopoverNode();

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  };

  isStateful = (props) => typeof this.getPopoverProps(props).isOpen === 'undefined';

  isOpen = (props) => this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;

  isEventInsideTarget = (el, target) => {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  };

  // VISIBILITY CHANGE

  appendPopover = () => {
    // create container node
    this.containerNode = document.createElement('div');
    document.body.appendChild(this.containerNode);

    // render invisible popover
    const hiddenPopover = this.getHiddenPopover();
    ReactDOM.render(hiddenPopover, this.containerNode);

    // add pointer to popover node
    this.popoverNode = this.containerNode.childNodes[0];

    // save popover size (visible popover will be rendered in componentDidUpdate)
    this.saveValuesFromNodeTree();
  };

  removePopover = () => {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  };

  onPopoverOpenChange = (props) => {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  };

  onPopoverStateChange = () => {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  };

  eventWrapper = cb => e => {
    const { event } = this.getPopoverProps();
    const childrenNode = ReactDOM.findDOMNode(this.refs.children).childNodes[0];
    const el = e.target || e.srcElement;
    if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
      cb(e);
    }
  };

  getDelayWhenClosed = delay => (delay || {}).whenClosed || delay;

  getDelayWhenOpen = delay => (delay || {}).whenOpen || delay;

  updateDebouncedMousedEvents = (nextProps) => {
    const { delay } = this.getPopoverProps(nextProps);
    const delayWhenClosed = this.getDelayWhenClosed(delay);
    const delayWhenOpen = this.getDelayWhenOpen(delay);

    const { delay: previousDelay } = this.getPopoverProps();
    const previousDelayWhenClosed = this.getDelayWhenClosed(previousDelay);
    const previousDelayWhenOpen = this.getDelayWhenOpen(previousDelay);

    if (!nextProps || previousDelayWhenClosed !== delayWhenClosed) {
      this.onMouseEventDebouncedWhenClosed = debounce(this._onMouseEvent, delayWhenClosed);
    }

    if (!nextProps || previousDelayWhenOpen !== delayWhenOpen) {
      this.onMouseEventDebouncedWhenOpen = debounce(this._onMouseEvent, delayWhenOpen);
    }
  };

  _onMouseEvent = (type) => {
    if (type === 'mouseenter') {
      this.showPopover();
    } else if (type === 'mouseleave') {
      this.hidePopover();
    }
  };

  onMouseEvent = ({ type }) => {
    const { delay } = this.getPopoverProps();
    const delayWhenClosed = this.getDelayWhenClosed(delay);
    const delayWhenOpen = this.getDelayWhenOpen(delay);

    if (this.isOpen()) {
      return delayWhenOpen ? this.onMouseEventDebouncedWhenOpen(type) : this._onMouseEvent(type);
    } else {
      return delayWhenClosed ? this.onMouseEventDebouncedWhenClosed(type) : this._onMouseEvent(type);
    }
  };

  showPopover = () => this.setIsOpen(true);

  hidePopover = () => this.setIsOpen(false);

  togglePopover = () => this.setIsOpen(!this.isOpen());

  setIsOpen = (isOpen) => {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  };

  // LOCALES

  popoverTemplate = (_style) => {
    const { position, anchor, className, content, id, event, style } = this.getPopoverProps();
    const { eventWrapper, onMouseEvent, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(onMouseEvent) } : undefined;
    return (

        {content}

    );
  };

  getVisiblePopover = () => this.popoverTemplate(this.computePopoverStyle());

  getHiddenPopover = () => {
    const style = { width: '100%', height: '100%', top: 0, left: 0, position: 'absolute', overflow: 'hidden', pointerEvents: 'none' };
    return (

        {this.popoverTemplate({ position: 'absolute', visibility: 'hidden' })}

    );
  };

  isAbsolute = () => this.getPopoverProps().attachToBody === true;

  getEventCallbacks = () => {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, onMouseEvent, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(onMouseEvent) : undefined,
      onMouseLeave: onHover ? eventWrapper(onMouseEvent) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  };

  computePopoverStyle = () => {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  };

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  }

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

}
<commit_msg>use t.interface instead of t.struct for "delay" prop<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import debounce from 'lodash/debounce';
import { props, t } from '../utils';

const NO_SIZE_WRAPPER = 'no-size-wrapper';

export const Props = {
  children: t.ReactChildren,
  popover: t.struct({
    content: t.ReactChildren,
    attachToBody: t.maybe(t.Boolean),
    position: t.maybe(t.enums.of(['top', 'bottom', 'left', 'right'])),
    anchor: t.maybe(t.enums.of(['start', 'center', 'end'])),
    event: t.maybe(t.enums.of(['click', 'hover'])),
    onShow: t.maybe(t.Function),
    onHide: t.maybe(t.Function),
    onToggle: t.maybe(t.Function),
    dismissOnScroll: t.maybe(t.Boolean),
    dismissOnClickOutside: t.maybe(t.Boolean),
    className: t.maybe(t.String),
    style: t.maybe(t.Object),
    id: t.maybe(t.String),
    maxWidth: t.maybe(t.union([ t.Number, t.String ])),
    distance: t.maybe(t.Number),
    offsetX: t.maybe(t.Number),
    offsetY: t.maybe(t.Number),
    isOpen: t.maybe(t.Boolean),
    delay: t.maybe(t.union([
      t.Integer,
      t.interface({ whenClosed: t.maybe(t.Integer), whenOpen: t.maybe(t.Integer) })
    ]))
  }),
  id: t.maybe(t.String),
  className: t.maybe(t.String),
  style: t.maybe(t.Object)
};

/**
 * Composed of two children: trigger (children) and popover. After a particular event on the trigger (usually "hover" or "click") it renders the popover and positions it relative to it.
 * @param children - the trigger node. It's always visible
 * @param popover - popover settings. The popover is **not** always visible
 */
@props(Props)
export default class Popover extends React.Component {

  // LIFECYCLE

  state = { isOpen: false };

  componentDidMount() {
    this.updateDebouncedMousedEvents();
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.onPopoverOpenChange(this.props);
    }
  }

  componentWillReceiveProps(nextProps) {
    this.updateDebouncedMousedEvents(nextProps);
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  }

  componentDidUpdate() {
    if (this.containerNode) {
      const popover = this.getVisiblePopover();
      ReactDOM.render(popover, this.containerNode);
    }
  }

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  }

  // LISTENERS

  addOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  };

  removeOnClickListener = () => {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  };

  onClickOutside = (e) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  };

  addOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  };

  removeOnScrollListener = () => {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  };

  onScroll = () => this.hidePopover();

  addListeners = () => {
    this.addOnScrollListener();
    this.addOnClickListener();
  };

  removeListeners = () => {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  };

  // UTILS

  // extend with default values
  getPopoverProps = (_props) => {
    const props = _props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  };

  getPopoverNode = () => {
    let popover;
    if (this.isAbsolute()) {
      popover = this.popoverNode;
    } else {
      const childrenNode = ReactDOM.findDOMNode(this.refs.children);
      popover = childrenNode.childNodes[1];
    }
    return (popover && popover.id === NO_SIZE_WRAPPER) ? popover.childNodes[0] : popover;
  };

  getOffsetRect = (target) => {
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  };

  saveValuesFromNodeTree = (cb) => {
    const childrenNode = ReactDOM.findDOMNode(this.refs.children);
    const popoverNode = this.getPopoverNode();

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  };

  isStateful = (props) => typeof this.getPopoverProps(props).isOpen === 'undefined';

  isOpen = (props) => this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;

  isEventInsideTarget = (el, target) => {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  };

  // VISIBILITY CHANGE

  appendPopover = () => {
    // create container node
    this.containerNode = document.createElement('div');
    document.body.appendChild(this.containerNode);

    // render invisible popover
    const hiddenPopover = this.getHiddenPopover();
    ReactDOM.render(hiddenPopover, this.containerNode);

    // add pointer to popover node
    this.popoverNode = this.containerNode.childNodes[0];

    // save popover size (visible popover will be rendered in componentDidUpdate)
    this.saveValuesFromNodeTree();
  };

  removePopover = () => {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  };

  onPopoverOpenChange = (props) => {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  };

  onPopoverStateChange = () => {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  };

  eventWrapper = cb => e => {
    const { event } = this.getPopoverProps();
    const childrenNode = ReactDOM.findDOMNode(this.refs.children).childNodes[0];
    const el = e.target || e.srcElement;
    if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
      cb(e);
    }
  };

  getDelayWhenClosed = delay => (delay || {}).whenClosed || delay;

  getDelayWhenOpen = delay => (delay || {}).whenOpen || delay;

  updateDebouncedMousedEvents = (nextProps) => {
    const { delay } = this.getPopoverProps(nextProps);
    const delayWhenClosed = this.getDelayWhenClosed(delay);
    const delayWhenOpen = this.getDelayWhenOpen(delay);

    const { delay: previousDelay } = this.getPopoverProps();
    const previousDelayWhenClosed = this.getDelayWhenClosed(previousDelay);
    const previousDelayWhenOpen = this.getDelayWhenOpen(previousDelay);

    if (!nextProps || previousDelayWhenClosed !== delayWhenClosed) {
      this.onMouseEventDebouncedWhenClosed = debounce(this._onMouseEvent, delayWhenClosed);
    }

    if (!nextProps || previousDelayWhenOpen !== delayWhenOpen) {
      this.onMouseEventDebouncedWhenOpen = debounce(this._onMouseEvent, delayWhenOpen);
    }
  };

  _onMouseEvent = (type) => {
    if (type === 'mouseenter') {
      this.showPopover();
    } else if (type === 'mouseleave') {
      this.hidePopover();
    }
  };

  onMouseEvent = ({ type }) => {
    const { delay } = this.getPopoverProps();
    const delayWhenClosed = this.getDelayWhenClosed(delay);
    const delayWhenOpen = this.getDelayWhenOpen(delay);

    if (this.isOpen()) {
      return delayWhenOpen ? this.onMouseEventDebouncedWhenOpen(type) : this._onMouseEvent(type);
    } else {
      return delayWhenClosed ? this.onMouseEventDebouncedWhenClosed(type) : this._onMouseEvent(type);
    }
  };

  showPopover = () => this.setIsOpen(true);

  hidePopover = () => this.setIsOpen(false);

  togglePopover = () => this.setIsOpen(!this.isOpen());

  setIsOpen = (isOpen) => {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  };

  // LOCALES

  popoverTemplate = (_style) => {
    const { position, anchor, className, content, id, event, style } = this.getPopoverProps();
    const { eventWrapper, onMouseEvent, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(onMouseEvent) } : undefined;
    return (

        {content}

    );
  };

  getVisiblePopover = () => this.popoverTemplate(this.computePopoverStyle());

  getHiddenPopover = () => {
    const style = { width: '100%', height: '100%', top: 0, left: 0, position: 'absolute', overflow: 'hidden', pointerEvents: 'none' };
    return (

        {this.popoverTemplate({ position: 'absolute', visibility: 'hidden' })}

    );
  };

  isAbsolute = () => this.getPopoverProps().attachToBody === true;

  getEventCallbacks = () => {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, onMouseEvent, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(onMouseEvent) : undefined,
      onMouseLeave: onHover ? eventWrapper(onMouseEvent) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  };

  computePopoverStyle = () => {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  };

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  }

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

}
<|endoftext|>


<commit_before>import React from 'react';
import cx from 'classnames';

const Popover = React.createClass({

  propTypes: {
    children: React.PropTypes.node.isRequired,
    popover: React.PropTypes.shape({
      content: React.PropTypes.node.isRequired,
      attachToBody: React.PropTypes.bool,
      position: React.PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
      anchor: React.PropTypes.oneOf(['start', 'center', 'end']),
      event: React.PropTypes.oneOf(['click', 'hover']),
      onShow: React.PropTypes.func,
      onHide: React.PropTypes.func,
      onToggle: React.PropTypes.func,
      dismissOnScroll: React.PropTypes.bool,
      dismissOnClickOutside: React.PropTypes.bool,
      className: React.PropTypes.string,
      id: React.PropTypes.string,
      maxWidth: React.PropTypes.oneOfType([
        React.PropTypes.number,
        React.PropTypes.string
      ]),
      distance: React.PropTypes.number,
      offsetX: React.PropTypes.number,
      offsetY: React.PropTypes.number,
      isOpen: React.PropTypes.bool
    }).isRequired,
    id: React.PropTypes.string,
    className: React.PropTypes.string,
    style: React.PropTypes.object
  },

  // LIFECYCLE

  getDefaultProps() {
    return {
      id: '',
      className: '',
      style: {}
    };
  },

  getInitialState() {
    return {
      isOpen: false
    };
  },

  componentDidMount() {
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.addListeners();
      this.forceUpdate();
    }
  },

  componentWillReceiveProps(nextProps) {
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  },

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  },

  // LISTENERS

  addOnClickListener() {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  },

  removeOnClickListener() {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  },

  onClickOutside(e) {
    const childrenNode = this.refs.children.getDOMNode();
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  },

  addOnScrollListener() {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  },

  removeOnScrollListener() {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  },

  onScroll() {
    this.hidePopover();
  },

  addListeners() {
    this.addOnScrollListener();
    this.addOnClickListener();
  },

  removeListeners() {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  },

  // UTILS

  // extend with default values
  getPopoverProps(props) {
    props = props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  },

  getOffsetRect(target) {
    target = this.refs.children.getDOMNode();
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  },

  saveValuesFromNodeTree(cb) {
    const childrenNode = this.refs.children.getDOMNode();
    const popoverNode = this.isAbsolute() ? this.popoverNode : childrenNode.childNodes[1];

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  },

  isStateful(props) {
    return typeof this.getPopoverProps(props).isOpen === 'undefined';
  },

  isOpen(props) {
    return this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;
  },

  isEventInsideTarget(el, target) {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  },

  // VISIBILITY CHANGE

  appendPopover() {
    const hiddenPopover = this.getHiddenPopover();
    this.containerNode = document.createElement('div');
    this.containerNode.innerHTML = React.renderToString(hiddenPopover);
    this.popoverNode = this.containerNode.childNodes[0];
    document.body.appendChild(this.containerNode);

    this.saveValuesFromNodeTree(() => {
      const popover = this.getVisiblePopover();
      this.containerNode.innerHTML = React.renderToString(popover);
    });
  },

  removePopover() {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  },

  onPopoverOpenChange(props) {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  },

  onPopoverStateChange() {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  },

  eventWrapper(cb) {
    return (e) => {
      const { event } = this.getPopoverProps();
      const childrenNode = this.refs.children.getDOMNode().childNodes[0];
      const el = e.target || e.srcElement;
      if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
        cb();
      }
    };
  },

  showPopover() {
    this.setIsOpen(true);
  },

  hidePopover() {
    this.setIsOpen(false);
  },

  togglePopover() {
    this.setIsOpen(!this.isOpen());
  },

  setIsOpen(isOpen) {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  },

  // LOCALES

  popoverTemplate(style) {
    const { position, anchor, className, content, id, event } = this.getPopoverProps();
    const { eventWrapper, hidePopover, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(hidePopover) } : undefined;
    return (

        {content}

    );
  },

  getVisiblePopover() {
    return this.popoverTemplate(this.computePopoverStyle());
  },

  getHiddenPopover() {
    return this.popoverTemplate({ position: 'absolute', visibility: 'hidden' });
  },

  isAbsolute() {
    return this.getPopoverProps().attachToBody === true;
  },

  getEventCallbacks() {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, showPopover, hidePopover, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(showPopover) : undefined,
      onMouseLeave: onHover ? eventWrapper(hidePopover) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  },

  computePopoverStyle() {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  },

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  },

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

});

export default Popover;
<commit_msg>when closed render hidden popover inside an invisible wrapper with no size and overflow hidden<commit_after>import React from 'react';
import cx from 'classnames';

const NO_SIZE_WRAPPER = 'no-size-wrapper';

const Popover = React.createClass({

  propTypes: {
    children: React.PropTypes.node.isRequired,
    popover: React.PropTypes.shape({
      content: React.PropTypes.node.isRequired,
      attachToBody: React.PropTypes.bool,
      position: React.PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
      anchor: React.PropTypes.oneOf(['start', 'center', 'end']),
      event: React.PropTypes.oneOf(['click', 'hover']),
      onShow: React.PropTypes.func,
      onHide: React.PropTypes.func,
      onToggle: React.PropTypes.func,
      dismissOnScroll: React.PropTypes.bool,
      dismissOnClickOutside: React.PropTypes.bool,
      className: React.PropTypes.string,
      id: React.PropTypes.string,
      maxWidth: React.PropTypes.oneOfType([
        React.PropTypes.number,
        React.PropTypes.string
      ]),
      distance: React.PropTypes.number,
      offsetX: React.PropTypes.number,
      offsetY: React.PropTypes.number,
      isOpen: React.PropTypes.bool
    }).isRequired,
    id: React.PropTypes.string,
    className: React.PropTypes.string,
    style: React.PropTypes.object
  },

  // LIFECYCLE

  getDefaultProps() {
    return {
      id: '',
      className: '',
      style: {}
    };
  },

  getInitialState() {
    return {
      isOpen: false
    };
  },

  componentDidMount() {
    this.saveValuesFromNodeTree();
    this.initialized = true;
    if (this.isOpen()) {
      this.addListeners();
      this.forceUpdate();
    }
  },

  componentWillReceiveProps(nextProps) {
    this.saveValuesFromNodeTree();
    if (!this.isStateful() && this.getPopoverProps().isOpen !== this.getPopoverProps(nextProps).isOpen) {
      this.onPopoverOpenChange(nextProps);
    }
  },

  componentWillUnmount() {
    this.removePopover();
    this.removeListeners();
  },

  // LISTENERS

  addOnClickListener() {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.addEventListener('click', this.onClickOutside, false);
    }
  },

  removeOnClickListener() {
    if (this.getPopoverProps().dismissOnClickOutside) {
      window.removeEventListener('click', this.onClickOutside, false);
    }
  },

  onClickOutside(e) {
    const childrenNode = this.refs.children.getDOMNode();
    const popoverNode = this.isAbsolute() ? this.containerNode : childrenNode.childNodes[1];
    const el = e.target || e.srcElement;
    if (!this.isEventInsideTarget(el, childrenNode) && (!popoverNode || !this.isEventInsideTarget(el, popoverNode))) {
      this.hidePopover();
    }
  },

  addOnScrollListener() {
    if (this.getPopoverProps().dismissOnScroll) {
      window.addEventListener('mousewheel', this.onScroll, false);
    }
  },

  removeOnScrollListener() {
    if (this.getPopoverProps().dismissOnScroll) {
      window.removeEventListener('mousewheel', this.onScroll, false);
    }
  },

  onScroll() {
    this.hidePopover();
  },

  addListeners() {
    this.addOnScrollListener();
    this.addOnClickListener();
  },

  removeListeners() {
    this.removeOnScrollListener();
    this.removeOnClickListener();
  },

  // UTILS

  // extend with default values
  getPopoverProps(props) {
    props = props || this.props;
    return {
      type: 'relative',
      position: 'top',
      anchor: 'center',
      event: 'hover',
      onShow: () => {},
      onHide: () => {},
      onToggle: () => {},
      dismissOnClickOutside: true,
      dismissOnScroll: true,
      className: '',
      distance: 5,
      offsetX: 0,
      offsetY: 0,
      ...props.popover
    };
  },

  getPopoverNode() {
    let popover;
    if (this.isAbsolute()) {
      popover = this.popoverNode;
    } else {
      const childrenNode = this.refs.children.getDOMNode();
      popover = childrenNode.childNodes[1];
    }
    return (popover && popover.id === NO_SIZE_WRAPPER) ? popover.childNodes[0] : popover;
  },

  getOffsetRect(target) {
    target = this.refs.children.getDOMNode();
    const box = target.getBoundingClientRect();

    const body = document.body;
    const docElem = document.documentElement;

    const scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;

    const clientTop = docElem.clientTop || body.clientTop || 0;
    const clientLeft = docElem.clientLeft || body.clientLeft || 0;

    const top = Math.round(box.top + scrollTop - clientTop);
    const left = Math.round(box.left + scrollLeft - clientLeft);

    return { top, left };
  },

  saveValuesFromNodeTree(cb) {
    const childrenNode = this.refs.children.getDOMNode();
    const popoverNode = this.getPopoverNode();

    if (popoverNode) {
      const { clientWidth: childWidth, clientHeight: childHeight } = childrenNode;
      const { clientHeight: popoverHeight, clientWidth: popoverWidth } = popoverNode;
      const { top: childY, left: childX } = this.getOffsetRect(childrenNode);
      const { top: popoverY, left: popoverX } = this.getOffsetRect(popoverNode);

      this.setState({
        child: {
          width: childWidth,
          height: childHeight,
          x: childX,
          y: childY
        },
        popover: {
          width: popoverWidth,
          height: popoverHeight,
          x: popoverX,
          y: popoverY
        }
      }, cb);
    }
  },

  isStateful(props) {
    return typeof this.getPopoverProps(props).isOpen === 'undefined';
  },

  isOpen(props) {
    return this.isStateful() ? this.state.isOpen : this.getPopoverProps(props).isOpen;
  },

  isEventInsideTarget(el, target) {
    if (!el) {
      return false;
    } else if (el === target) {
      return true;
    } else if (el.parentNode) {
      return this.isEventInsideTarget(el.parentNode, target);
    } else {
      return false;
    }
  },

  // VISIBILITY CHANGE

  appendPopover() {
    const hiddenPopover = this.getHiddenPopover();
    this.containerNode = document.createElement('div');
    this.containerNode.innerHTML = React.renderToString(hiddenPopover);
    this.popoverNode = this.containerNode.childNodes[0];
    document.body.appendChild(this.containerNode);

    this.saveValuesFromNodeTree(() => {
      const popover = this.getVisiblePopover();
      this.containerNode.innerHTML = React.renderToString(popover);
    });
  },

  removePopover() {
    if (this.containerNode) {
      document.body.removeChild(this.containerNode);
      this.containerNode = null;
    }
  },

  onPopoverOpenChange(props) {
    if (this.isOpen(props)) {
      if (this.isAbsolute()) {
        this.appendPopover();
      }
      this.addListeners();
    } else {
      if (this.isAbsolute()) {
        this.removePopover();
      }
      this.removeListeners();
    }
  },

  onPopoverStateChange() {
    const { onShow, onHide, onToggle } = this.getPopoverProps();
    if (this.state.isOpen) {
      onShow();
    } else {
      onHide();
    }
    onToggle();
    this.onPopoverOpenChange();
  },

  eventWrapper(cb) {
    return (e) => {
      const { event } = this.getPopoverProps();
      const childrenNode = this.refs.children.getDOMNode().childNodes[0];
      const el = e.target || e.srcElement;
      if (this.isAbsolute() || (event === 'hover') || this.isEventInsideTarget(el, childrenNode)) {
        cb();
      }
    };
  },

  showPopover() {
    this.setIsOpen(true);
  },

  hidePopover() {
    this.setIsOpen(false);
  },

  togglePopover() {
    this.setIsOpen(!this.isOpen());
  },

  setIsOpen(isOpen) {
    if (this.isStateful()) {
      this.setState({ isOpen }, this.onPopoverStateChange);
    } else {
      const { onShow, onHide, onToggle } = this.getPopoverProps();
      const cb = isOpen ? onShow : onHide;
      cb();
      onToggle();
    }
  },

  // LOCALES

  popoverTemplate(style) {
    const { position, anchor, className, content, id, event } = this.getPopoverProps();
    const { eventWrapper, hidePopover, isAbsolute } = this;
    const positionClass = `position-${position}`;
    const anchorClass = `anchor-${anchor}`;
    const _className = `popover-content ${positionClass} ${anchorClass} ${className}`;
    const events = !isAbsolute() && event === 'hover' ? { onMouseEnter: eventWrapper(hidePopover) } : undefined;
    return (

        {content}

    );
  },

  getVisiblePopover() {
    return this.popoverTemplate(this.computePopoverStyle());
  },

  getHiddenPopover() {
    return (

        {this.popoverTemplate({ position: 'absolute', visibility: 'hidden' })}

    );
  },

  isAbsolute() {
    return this.getPopoverProps().attachToBody === true;
  },

  getEventCallbacks() {
    const { event } = this.getPopoverProps();
    const onHover = event === 'hover';
    const onClick = event === 'click';
    const { eventWrapper, showPopover, hidePopover, togglePopover } = this;
    return {
      onMouseEnter: onHover ? eventWrapper(showPopover) : undefined,
      onMouseLeave: onHover ? eventWrapper(hidePopover) : undefined,
      onClick: onClick ? eventWrapper(togglePopover) : undefined
    };
  },

  computePopoverStyle() {
    const { child, popover } = this.state;
    const { anchor, position, maxWidth, offsetX, offsetY, distance } = this.getPopoverProps();

    const isAbsolute = this.isAbsolute();
    const isHorizontal = position === 'top' || position === 'bottom';
    const isVertical = position === 'right' || position === 'left';

    const anchorOffset = { top: 0, left: 0 };
    const positionOffset = { top: 0, left: 0 };

    switch (anchor) {
      case 'start':
        // default -> { top: 0, left: 0 }
        break;
      case 'center':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) / 2 : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) / 2 : 0;
        break;
      case 'end':
        anchorOffset.left = isHorizontal ? (child.width - popover.width) : 0;
        anchorOffset.top = isVertical ? (child.height - popover.height) : 0;
        break;
    }

    switch (position) {
      case 'top':
        positionOffset.top = -(popover.height + distance);
        break;
      case 'bottom':
        positionOffset.top = child.height + distance;
        break;
      case 'left':
        positionOffset.left = -(popover.width + distance);
        break;
      case 'right':
        positionOffset.left = child.width + distance;
        break;
    }

    return {
      position: 'absolute',
      top: (isAbsolute ? child.y : 0) + (positionOffset.top + anchorOffset.top + offsetY),
      left: (isAbsolute ? child.x : 0) + (positionOffset.left + anchorOffset.left + offsetX),
      maxWidth
    };
  },

  getLocals() {
    const isRelative = !this.isAbsolute();
    const isOpen = this.isOpen();
    const popover = isRelative && (this.initialized && isOpen ? this.getVisiblePopover() : this.getHiddenPopover());
    return {
      ...this.props,
      style: {
        display: 'inline-block',
        position: isRelative ? 'relative' : undefined,
        ...this.props.style
      },
      className: cx('react-popover', this.props.className, { 'is-open': isOpen, 'is-closed': !isOpen }),
      eventCallbacks: this.getEventCallbacks(),
      popover
    };
  },

  // RENDER

  render() {
    const { children, style, className, id, eventCallbacks, popover } = this.getLocals();
    return (

        {children}
        {popover}

    );
  }

});

export default Popover;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import analytics, { updateLastActive } from 'src/analytics'
import extractQueryFilters from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { isLoggable, getPauseState } from 'src/activity-logger'
import { getTooltipState, setTooltipState } from 'src/content-tooltip/utils'
import { IndexDropdown } from 'src/common-ui/containers'
import Popup from './components/Popup'
import Button from './components/Button'
import BlacklistConfirm from './components/BlacklistConfirm'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'
import UpgradeButton from './components/UpgradeButton'
import ButtonIcon from './components/ButtonIcon'
import ToggleTooltip from './components/ToggleTooltip'
import styles from './components/Button.css'

class PopupContainer extends Component {
    static propTypes = {
        pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired,
    }

    static defaultProps = {
        pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity],
    }

    constructor(props) {
        super(props)

        this.pageLookup = remoteFunction('pageLookup')
        this.fetchBlacklist = remoteFunction('fetchBlacklist')
        this.addToBlacklist = remoteFunction('addToBlacklist')
        this.isURLBlacklisted = remoteFunction('isURLBlacklisted')
        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')
        this.deletePages = remoteFunction('delPages')
        this.deletePagesByDomain = remoteFunction('delPagesByDomain')
        this.removeBookmarkByUrl = remoteFunction('delBookmark')
        this.createBookmarkByUrl = remoteFunction('addBookmark')
    }

    state = {
        url: '',
        tabID: null,
        searchValue: '',
        pauseValue: 20,

        // Used to derive button disabled states among other states
        isLoggable: false,
        isBlacklisted: false,
        page: null, // Contains the reverse index doc, if available

        // View switching flags
        blacklistChoice: false,
        blacklistConfirm: false,
        tagMode: false,

        // Behaviour switching flags
        domainDelete: false,
        isPaused: false,

        // Tooltip Flag
        isTooltipEnabled: true,
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({
            active: true,
            currentWindow: true,
        })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) {
            return
        }

        const updateState = newState =>
            this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({
            url: currentTab.url,
            tabID: currentTab.id,
            isLoggable: isLoggable({ url: currentTab.url }),
        })

        this.getInitPageData()
            .then(updateState)
            .catch(noop)
        this.getInitPauseState()
            .then(updateState)
            .catch(noop)
        this.getInitBlacklistBtnState()
            .then(updateState)
            .catch(noop)
        this.getInitTooltipState()
            .then(updateState)
            .catch(noop)
    }

    async getInitTooltipState() {
        const isTooltipEnabled = await getTooltipState()
        return { isTooltipEnabled }
    }

    async getInitPageData() {
        return { page: await this.pageLookup(this.state.url) }
    }

    async getInitPauseState() {
        const isPaused = await getPauseState()

        return { isPaused }
    }

    async getInitBlacklistBtnState() {
        const blacklist = await this.fetchBlacklist()

        return {
            isBlacklisted: await this.isURLBlacklisted(
                this.state.url,
                blacklist,
            ),
        }
    }

    get blacklistBtnState() {
        if (this.state.isBlacklisted) {
            return constants.BLACKLIST_BTN_STATE.BLACKLISTED
        }

        return this.state.isLoggable
            ? constants.BLACKLIST_BTN_STATE.UNLISTED
            : constants.BLACKLIST_BTN_STATE.DISABLED
    }

    get isTagBtnDisabled() {
        return !this.state.isLoggable || this.state.page == null
    }

    get bookmarkBtnState() {
        // Cannot bookmark
        if (!this.state.isLoggable || this.state.isBlacklisted) {
            return constants.BOOKMARK_BTN_STATE.DISABLED
        }

        // Already a bookmark
        if (this.state.page != null && this.state.page.hasBookmark) {
            return constants.BOOKMARK_BTN_STATE.BOOKMARK
        }

        // Not yet bookmarked
        return constants.BOOKMARK_BTN_STATE.UNBOOKMARK
    }

    get pageTags() {
        // No assoc. page indexed, or tagless page
        if (this.state.page == null || this.state.page.tags == null) {
            return []
        }

        return this.state.page.tags
    }

    onBlacklistBtnClick(domainDelete = false) {
        const url = domainDelete
            ? new URL(this.state.url).hostname
            : this.state.url

        return event => {
            event.preventDefault()

            analytics.trackEvent({
                category: 'Popup',
                action: domainDelete ? 'Blacklist domain' : 'Blacklist site',
            })

            this.addToBlacklist(url)
            this.setState(state => ({
                ...state,
                blacklistChoice: false,
                blacklistConfirm: true,
                isBlacklisted: true,
                url,
                domainDelete,
            }))
        }
    }

    onPauseConfirm = event => {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        analytics.trackEvent({
            category: 'Popup',
            action: isPaused ? 'Resume indexing' : 'Pause indexing',
            value: isPaused ? undefined : pauseValue,
        })

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)
        updateLastActive() // Consider user active (analytics)

        // Do local level state toggle and reset
        this.setState(state => ({
            ...state,
            isPaused: !isPaused,
            pauseValue: 20,
        }))
    }

    onPauseChange = event => {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    onSearchChange = event => {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter = event => {
        if (event.key === 'Enter') {
            event.preventDefault()
            analytics.trackEvent({
                category: 'Search',
                action: 'Popup search',
            })

            const queryFilters = extractQueryFilters(this.state.searchValue)
            const queryParams = qs.stringify(queryFilters)

            browser.tabs.create({
                url: `${constants.OVERVIEW_URL}?${queryParams}`,
            }) // New tab with query
            window.close() // Close the popup
        }
    }

    toggleTooltip = async () => {
        const isTooltipEnabled = !this.state.isTooltipEnabled
        await setTooltipState(isTooltipEnabled)
        this.setState({
            isTooltipEnabled,
        })
    }

    // Hides full-popup confirm
    resetBlacklistConfirmState = () =>
        this.setState(state => ({ ...state, blacklistConfirm: false }))

    handleDeleteBlacklistData = () => {
        analytics.trackEvent({
            category: 'Popup',
            action: 'Delete blacklisted pages',
        })

        if (this.state.domainDelete) {
            this.deletePagesByDomain(this.state.url)
        } else {
            this.deletePages([this.state.url])
        }
        this.resetBlacklistConfirmState()
    }

    setBlacklistChoice = () =>
        this.setState(state => ({ ...state, blacklistChoice: true }))

    renderBlacklistButton() {
        if (!this.state.blacklistChoice) {
            // Standard blacklist button
            return this.blacklistBtnState ===
                constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    itemClass={styles.itemBlacklisted}
                    btnClass={styles.itemBtnBlacklisted}
                >
                    This Page is Blacklisted. Undo>>
                </LinkButton>
            ) : (
                <Button
                    onClick={this.setBlacklistChoice}
                    disabled={
                        this.blacklistBtnState ===
                        constants.BLACKLIST_BTN_STATE.DISABLED
                    }
                    btnClass={styles.blacklist}
                >
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton iconClass={styles.blacklist}>
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => (
            <option key={i} value={val}>
                {val === Infinity ? '∞' : val}
            </option>
        )

        return this.props.pauseValues.map(pauseValueToOption)
    }

    handleAddBookmark = () => {
        if (this.bookmarkBtnState === constants.BOOKMARK_BTN_STATE.UNBOOKMARK) {
            this.createBookmarkByUrl({
                url: this.state.url,
                tabId: this.state.tabID,
            })
        } else if (
            this.bookmarkBtnState === constants.BOOKMARK_BTN_STATE.BOOKMARK
        ) {
            this.removeBookmarkByUrl({ url: this.state.url })
        }

        updateLastActive() // Consider user active (analytics)
        window.close()
    }

    toggleTagPopup = () =>
        this.setState(state => ({
            ...state,
            tagMode: !state.tagMode,
        }))

    renderTagButton() {
        return (
            <Button
                onClick={this.toggleTagPopup}
                disabled={this.isTagBtnDisabled}
                btnClass={styles.tag}
            >
                Add Tag(s)
            </Button>
        )
    }

    renderChildren() {
        const { blacklistConfirm, pauseValue, isPaused, tagMode } = this.state
        if (blacklistConfirm) {
            return (
                <BlacklistConfirm
                    onConfirmClick={this.handleDeleteBlacklistData}
                    onDenyClick={this.resetBlacklistConfirmState}
                />
            )
        }

        if (tagMode) {
            return (
                <IndexDropdown
                    url={this.state.url}
                    initFilters={this.pageTags}
                    source="tag"
                />
            )
        }

        return (

                <Button
                    onClick={this.handleAddBookmark}
                    btnClass={
                        this.bookmarkBtnState ===
                        constants.BOOKMARK_BTN_STATE.BOOKMARK
                            ? styles.bmk
                            : styles.notBmk
                    }
                    disabled={
                        this.bookmarkBtnState ===
                        constants.BOOKMARK_BTN_STATE.DISABLED
                    }
                >
                    {this.bookmarkBtnState ===
                    constants.BOOKMARK_BTN_STATE.BOOKMARK
                        ? 'Unbookmark this Page'
                        : 'Bookmark this Page'}
                </Button>
                {this.renderTagButton()}

                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}

                <ToggleTooltip
                    isChecked={this.state.isTooltipEnabled}
                    handleChange={this.toggleTooltip}
                />

                <LinkButton
                    btnClass={styles.voteIcon}
                    href="https://worldbrain.io/vote_feature"
                >
                    Vote for Next Features
                </LinkButton>
                <UpgradeButton />
                <ButtonIcon
                    href={`${constants.OPTIONS_URL}#/settings`}
                    icon="settings"
                    buttonType={1}
                    btnClass={styles.settings}
                />
                <ButtonIcon
                    href="https://worldbrain.io/help"
                    icon="help"
                    btnClass={styles.help}
                />

        )
    }

    render() {
        const { searchValue, tagMode } = this.state

        return (
            <Popup
                shouldRenderSearch={!tagMode}
                searchValue={searchValue}
                onSearchChange={this.onSearchChange}
                onSearchEnter={this.onSearchEnter}
            >
                {this.renderChildren()}
            </Popup>
        )
    }
}

export default PopupContainer
<commit_msg>Close popup after turning off tooltip.<commit_after>import React, { Component } from 'react'
import PropTypes from 'prop-types'
import qs from 'query-string'

import analytics, { updateLastActive } from 'src/analytics'
import extractQueryFilters from 'src/util/nlp-time-filter'
import { remoteFunction } from 'src/util/webextensionRPC'
import { isLoggable, getPauseState } from 'src/activity-logger'
import { getTooltipState, setTooltipState } from 'src/content-tooltip/utils'
import { IndexDropdown } from 'src/common-ui/containers'
import Popup from './components/Popup'
import Button from './components/Button'
import BlacklistConfirm from './components/BlacklistConfirm'
import HistoryPauser from './components/HistoryPauser'
import LinkButton from './components/LinkButton'
import SplitButton from './components/SplitButton'
import * as constants from './constants'
import UpgradeButton from './components/UpgradeButton'
import ButtonIcon from './components/ButtonIcon'
import ToggleTooltip from './components/ToggleTooltip'
import styles from './components/Button.css'

class PopupContainer extends Component {
    static propTypes = {
        pauseValues: PropTypes.arrayOf(PropTypes.number).isRequired,
    }

    static defaultProps = {
        pauseValues: [5, 10, 20, 30, 60, 120, 180, Infinity],
    }

    constructor(props) {
        super(props)

        this.pageLookup = remoteFunction('pageLookup')
        this.fetchBlacklist = remoteFunction('fetchBlacklist')
        this.addToBlacklist = remoteFunction('addToBlacklist')
        this.isURLBlacklisted = remoteFunction('isURLBlacklisted')
        this.toggleLoggingPause = remoteFunction('toggleLoggingPause')
        this.deletePages = remoteFunction('delPages')
        this.deletePagesByDomain = remoteFunction('delPagesByDomain')
        this.removeBookmarkByUrl = remoteFunction('delBookmark')
        this.createBookmarkByUrl = remoteFunction('addBookmark')
    }

    state = {
        url: '',
        tabID: null,
        searchValue: '',
        pauseValue: 20,

        // Used to derive button disabled states among other states
        isLoggable: false,
        isBlacklisted: false,
        page: null, // Contains the reverse index doc, if available

        // View switching flags
        blacklistChoice: false,
        blacklistConfirm: false,
        tagMode: false,

        // Behaviour switching flags
        domainDelete: false,
        isPaused: false,

        // Tooltip Flag
        isTooltipEnabled: true,
    }

    async componentDidMount() {
        const [currentTab] = await browser.tabs.query({
            active: true,
            currentWindow: true,
        })

        // If we can't get the tab data, then can't init action button states
        if (!currentTab || !currentTab.url) {
            return
        }

        const updateState = newState =>
            this.setState(oldState => ({ ...oldState, ...newState }))
        const noop = f => f // Don't do anything if error; state doesn't change

        updateState({
            url: currentTab.url,
            tabID: currentTab.id,
            isLoggable: isLoggable({ url: currentTab.url }),
        })

        this.getInitPageData()
            .then(updateState)
            .catch(noop)
        this.getInitPauseState()
            .then(updateState)
            .catch(noop)
        this.getInitBlacklistBtnState()
            .then(updateState)
            .catch(noop)
        this.getInitTooltipState()
            .then(updateState)
            .catch(noop)
    }

    async getInitTooltipState() {
        const isTooltipEnabled = await getTooltipState()
        return { isTooltipEnabled }
    }

    async getInitPageData() {
        return { page: await this.pageLookup(this.state.url) }
    }

    async getInitPauseState() {
        const isPaused = await getPauseState()

        return { isPaused }
    }

    async getInitBlacklistBtnState() {
        const blacklist = await this.fetchBlacklist()

        return {
            isBlacklisted: await this.isURLBlacklisted(
                this.state.url,
                blacklist,
            ),
        }
    }

    get blacklistBtnState() {
        if (this.state.isBlacklisted) {
            return constants.BLACKLIST_BTN_STATE.BLACKLISTED
        }

        return this.state.isLoggable
            ? constants.BLACKLIST_BTN_STATE.UNLISTED
            : constants.BLACKLIST_BTN_STATE.DISABLED
    }

    get isTagBtnDisabled() {
        return !this.state.isLoggable || this.state.page == null
    }

    get bookmarkBtnState() {
        // Cannot bookmark
        if (!this.state.isLoggable || this.state.isBlacklisted) {
            return constants.BOOKMARK_BTN_STATE.DISABLED
        }

        // Already a bookmark
        if (this.state.page != null && this.state.page.hasBookmark) {
            return constants.BOOKMARK_BTN_STATE.BOOKMARK
        }

        // Not yet bookmarked
        return constants.BOOKMARK_BTN_STATE.UNBOOKMARK
    }

    get pageTags() {
        // No assoc. page indexed, or tagless page
        if (this.state.page == null || this.state.page.tags == null) {
            return []
        }

        return this.state.page.tags
    }

    onBlacklistBtnClick(domainDelete = false) {
        const url = domainDelete
            ? new URL(this.state.url).hostname
            : this.state.url

        return event => {
            event.preventDefault()

            analytics.trackEvent({
                category: 'Popup',
                action: domainDelete ? 'Blacklist domain' : 'Blacklist site',
            })

            this.addToBlacklist(url)
            this.setState(state => ({
                ...state,
                blacklistChoice: false,
                blacklistConfirm: true,
                isBlacklisted: true,
                url,
                domainDelete,
            }))
        }
    }

    onPauseConfirm = event => {
        event.preventDefault()
        const { isPaused, pauseValue } = this.state

        analytics.trackEvent({
            category: 'Popup',
            action: isPaused ? 'Resume indexing' : 'Pause indexing',
            value: isPaused ? undefined : pauseValue,
        })

        // Tell background script to do on extension level
        this.toggleLoggingPause(pauseValue)
        updateLastActive() // Consider user active (analytics)

        // Do local level state toggle and reset
        this.setState(state => ({
            ...state,
            isPaused: !isPaused,
            pauseValue: 20,
        }))
    }

    onPauseChange = event => {
        const pauseValue = event.target.value
        this.setState(state => ({ ...state, pauseValue }))
    }

    onSearchChange = event => {
        const searchValue = event.target.value
        this.setState(state => ({ ...state, searchValue }))
    }

    onSearchEnter = event => {
        if (event.key === 'Enter') {
            event.preventDefault()
            analytics.trackEvent({
                category: 'Search',
                action: 'Popup search',
            })

            const queryFilters = extractQueryFilters(this.state.searchValue)
            const queryParams = qs.stringify(queryFilters)

            browser.tabs.create({
                url: `${constants.OVERVIEW_URL}?${queryParams}`,
            }) // New tab with query
            window.close() // Close the popup
        }
    }

    toggleTooltip = async () => {
        const isTooltipEnabled = !this.state.isTooltipEnabled
        await setTooltipState(isTooltipEnabled)

        if (!isTooltipEnabled) {
            setTimeout(() => window.close(), 500)
        }

        this.setState({
            isTooltipEnabled,
        })
    }

    // Hides full-popup confirm
    resetBlacklistConfirmState = () =>
        this.setState(state => ({ ...state, blacklistConfirm: false }))

    handleDeleteBlacklistData = () => {
        analytics.trackEvent({
            category: 'Popup',
            action: 'Delete blacklisted pages',
        })

        if (this.state.domainDelete) {
            this.deletePagesByDomain(this.state.url)
        } else {
            this.deletePages([this.state.url])
        }
        this.resetBlacklistConfirmState()
    }

    setBlacklistChoice = () =>
        this.setState(state => ({ ...state, blacklistChoice: true }))

    renderBlacklistButton() {
        if (!this.state.blacklistChoice) {
            // Standard blacklist button
            return this.blacklistBtnState ===
                constants.BLACKLIST_BTN_STATE.BLACKLISTED ? (
                <LinkButton
                    href={`${constants.OPTIONS_URL}#/blacklist`}
                    itemClass={styles.itemBlacklisted}
                    btnClass={styles.itemBtnBlacklisted}
                >
                    This Page is Blacklisted. Undo>>
                </LinkButton>
            ) : (
                <Button
                    onClick={this.setBlacklistChoice}
                    disabled={
                        this.blacklistBtnState ===
                        constants.BLACKLIST_BTN_STATE.DISABLED
                    }
                    btnClass={styles.blacklist}
                >
                    Blacklist Current Page
                </Button>
            )
        }

        // Domain vs URL choice button
        return (
            <SplitButton iconClass={styles.blacklist}>
                <Button onClick={this.onBlacklistBtnClick(true)}>Domain</Button>
                <Button onClick={this.onBlacklistBtnClick(false)}>URL</Button>
            </SplitButton>
        )
    }

    renderPauseChoices() {
        const pauseValueToOption = (val, i) => (
            <option key={i} value={val}>
                {val === Infinity ? '∞' : val}
            </option>
        )

        return this.props.pauseValues.map(pauseValueToOption)
    }

    handleAddBookmark = () => {
        if (this.bookmarkBtnState === constants.BOOKMARK_BTN_STATE.UNBOOKMARK) {
            this.createBookmarkByUrl({
                url: this.state.url,
                tabId: this.state.tabID,
            })
        } else if (
            this.bookmarkBtnState === constants.BOOKMARK_BTN_STATE.BOOKMARK
        ) {
            this.removeBookmarkByUrl({ url: this.state.url })
        }

        updateLastActive() // Consider user active (analytics)
        window.close()
    }

    toggleTagPopup = () =>
        this.setState(state => ({
            ...state,
            tagMode: !state.tagMode,
        }))

    renderTagButton() {
        return (
            <Button
                onClick={this.toggleTagPopup}
                disabled={this.isTagBtnDisabled}
                btnClass={styles.tag}
            >
                Add Tag(s)
            </Button>
        )
    }

    renderChildren() {
        const { blacklistConfirm, pauseValue, isPaused, tagMode } = this.state
        if (blacklistConfirm) {
            return (
                <BlacklistConfirm
                    onConfirmClick={this.handleDeleteBlacklistData}
                    onDenyClick={this.resetBlacklistConfirmState}
                />
            )
        }

        if (tagMode) {
            return (
                <IndexDropdown
                    url={this.state.url}
                    initFilters={this.pageTags}
                    source="tag"
                />
            )
        }

        return (

                <Button
                    onClick={this.handleAddBookmark}
                    btnClass={
                        this.bookmarkBtnState ===
                        constants.BOOKMARK_BTN_STATE.BOOKMARK
                            ? styles.bmk
                            : styles.notBmk
                    }
                    disabled={
                        this.bookmarkBtnState ===
                        constants.BOOKMARK_BTN_STATE.DISABLED
                    }
                >
                    {this.bookmarkBtnState ===
                    constants.BOOKMARK_BTN_STATE.BOOKMARK
                        ? 'Unbookmark this Page'
                        : 'Bookmark this Page'}
                </Button>
                {this.renderTagButton()}

                <HistoryPauser
                    onConfirm={this.onPauseConfirm}
                    onChange={this.onPauseChange}
                    value={pauseValue}
                    isPaused={isPaused}
                >
                    {this.renderPauseChoices()}
                </HistoryPauser>
                {this.renderBlacklistButton()}

                <ToggleTooltip
                    isChecked={this.state.isTooltipEnabled}
                    handleChange={this.toggleTooltip}
                />

                <LinkButton
                    btnClass={styles.voteIcon}
                    href="https://worldbrain.io/vote_feature"
                >
                    Vote for Next Features
                </LinkButton>
                <UpgradeButton />
                <ButtonIcon
                    href={`${constants.OPTIONS_URL}#/settings`}
                    icon="settings"
                    buttonType={1}
                    btnClass={styles.settings}
                />
                <ButtonIcon
                    href="https://worldbrain.io/help"
                    icon="help"
                    btnClass={styles.help}
                />

        )
    }

    render() {
        const { searchValue, tagMode } = this.state

        return (
            <Popup
                shouldRenderSearch={!tagMode}
                searchValue={searchValue}
                onSearchChange={this.onSearchChange}
                onSearchEnter={this.onSearchEnter}
            >
                {this.renderChildren()}
            </Popup>
        )
    }
}

export default PopupContainer
<|endoftext|>


<commit_before>/**
 * HeatGridChart is the main class for generating... heatGrid charts.
 *  A heatGrid visualizes a matrix of values by a grid (matrix) of *
 *  bars, where the color of the bar represents the actual value.
 *  By default the colors are a range of green values, where
 *  light green represents low values and dark green high values.
 *  A heatGrid contains:
 *     - two categorical axis (both on x and y-axis)
 *     - no legend as series become rows on the perpendicular axis
 *  Please contact CvK if there are issues with HeatGrid at cde@vinzi.nl.
 */

pvc.HeatGridChart = pvc.CategoricalAbstract.extend({

    heatGridChartPanel : null,

    constructor: function(o){

        this.base(o);

        var _defaults = {
            showValues: true,
            originIsZero: true,
            axisOffset: 0,
            showTooltips: true,
            orientation: "vertical",
            // use a categorical here based on series labels
            orthoAxisOrdinal: true,
            scalingType: "linear",    // "normal" (distribution) or "linear"
            normPerBaseCategory: true,
            numSD: 2,                 // width (only for normal distribution)
            minColor: "white",
            maxColor: "darkgreen",
            nullColor:  "#efc5ad"  // white with a shade of orange
        };

        // Apply options
        $.extend(this.options,_defaults, o);

    },

    preRender: function(){

        this.base();

        pvc.log("Prerendering in heatGridChart");

        this.heatGridChartPanel = new pvc.HeatGridChartPanel(this, {
            stacked: this.options.stacked,
            panelSizeRatio: this.options.panelSizeRatio,
            heatGridSizeRatio: this.options.heatGridSizeRatio,
            maxHeatGridSize: this.options.maxHeatGridSize,
            showValues: this.options.showValues,
            showTooltips: this.options.showTooltips,
            orientation: this.options.orientation
        });

        this.heatGridChartPanel.appendTo(this.basePanel); // Add it

    }

}
);

/*
 * HeatGrid chart panel. Generates a heatGrid chart. Specific options are:
 * orientation - horizontal or vertical. Default: vertical
 * showValues - Show or hide heatGrid value. Default: false
 * stacked -  Stacked? Default: false
 * panelSizeRatio - Ratio of the band occupied by the pane;. Default: 0.5 (50%)
 * heatGridSizeRatio - In multiple series, percentage of inner
 * band occupied by heatGrids. Default: 0.5 (50%)
 * maxHeatGridSize - Maximum size of a heatGrid in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * chart_ - for the main chart Panel
 * heatGrid_ - for the actual heatGrid
 * heatGridPanel_ - for the panel where the heatGrids sit
 * heatGridLabel_ - for the main heatGrid label
 */

pvc.HeatGridChartPanel = pvc.BasePanel.extend({

    _parent: null,
    pvHeatGrid: null,
    pvHeatGridLabel: null,
    data: null,

    stacked: false,
    panelSizeRatio: 1,
    heatGridSizeRatio: 0.5,
    showTooltips: true,
    maxHeatGridSize: 200,
    showValues: true,
    orientation: "vertical",

    constructor: function(chart, options){

        this.base(chart,options);

    },

    create: function(){

        var myself = this;
        var opts = this.chart.options;
        this.width = this._parent.width;
        this.height = this._parent.height;

        this.pvPanel = this._parent.getPvPanel().add(this.type)
        .width(this.width)
        .height(this.height)

        var anchor = this.orientation == "vertical"?"bottom":"left";

        // reuse the existings scales
        var xScale = this.chart.xAxisPanel.scale;
        var yScale = this.chart.yAxisPanel.scale;

        var cols =  (anchor == "bottom") ? xScale.domain() : yScale.domain();

        var origData = this.chart.dataEngine.getVisibleTransposedValues();
        // create a mapping of the data that shows the columns (rows)
        data = origData.map(function(d){
            return pv.dict(cols, function(){
                return  d[this.index]
            })
        });
        data.reverse();  // the colums are build from top to bottom

        // get an array of scaling functions (one per column)
        var fill = this.getColorScale(data, cols);

        /* The cell dimensions. */
        var w = (xScale.max - xScale.min)/xScale.domain().length;
        var h = (yScale.max - yScale.min)/yScale.domain().length;

        if (anchor != "bottom") {
            var tmp = w;
            w = h;
            h = tmp;
        }

        this.pvHeatGrid = this.pvPanel.add(pv.Panel)
        .data(cols)
        [pvc.BasePanel.relativeAnchor[anchor]](function(){
            return this.index * w
            })
        [pvc.BasePanel.paralelLength[anchor]](w)
        .add(pv.Panel)
        .data(data)
        [pvc.BasePanel.oppositeAnchor[anchor]](function(){
            return this.index * h
        })
        [pvc.BasePanel.orthogonalLength[anchor]](h)
        .fillStyle(function(dat, col){
            return  (dat[col] != null) ? fill[col](dat[col]):opts.nullColor
        })
        .strokeStyle("white")
        .lineWidth(1)
        .antialias(false)
        .text(function(d,f){
          return d[f]});

        // NO SUPPORT for overflow and underflow on HeatGrids

        // NO SUPPORT for SecondAxis on HeatGrids (does not make sense)

        // Labels:

        if(this.showTooltips){
            this.pvHeatGrid
            .event("mouseover", pv.Behavior.tipsy({
                gravity: "s",
                fade: true
            }));
        }

        if (opts.clickable){
            this.pvHeatGrid
            .cursor("pointer")
            .event("click",function(d){
                var s = myself.chart.dataEngine.getSeries()[myself.stacked?this.parent.index:this.index]
                var c = myself.chart.dataEngine.getCategories()[myself.stacked?this.index:this.parent.index]
                return myself.chart.options.clickAction(s,c, d);
            });
        }

        if(this.showValues){
            this.pvHeatGridLabel = this.pvHeatGrid
            .anchor("center")
            .add(pv.Label)
            .bottom(0)
            .text(pv.identity)

            // Extend heatGridLabel
            this.extend(this.pvHeatGridLabel,"heatGridLabel_");
        }

        // Extend heatGrid and heatGridPanel
        this.extend(this.pvHeatGrid,"heatGridPanel_");
        this.extend(this.pvHeatGrid,"heatGrid_");

        // Extend body
        this.extend(this.pvPanel,"chart_");

    },

  /***********
   * compute an array of fill-functions. Each column out of "cols"
   * gets it's own scale function assigned to compute the color
   * for a value. Currently supported scales are:
   *    -  linear (from min to max
   *    -  normal distributed from   -numSD*sd to  numSD*sd
   *         (where sd is the standards deviation)
   ********/
  getColorScale: function(data, cols) {
    switch (this.chart.options.scalingType) {
    case "normal": return this.getNormalColorScale(data, cols);
    case "linear": return this.getLinearColorScale(data, cols);
    default:
      throw "Invalid option " + this.scaleType + " in HeatGrid"
    }
  },

  getLinearColorScale: function (data, cols){
    var fill;
    var opts = this.chart.options;
    // compute the mean and standard-deviation for each column
    var min = pv.dict(cols, function(f){
      return pv.min(data, function(d){
        return d[f]
      })
    });
    var max = pv.dict(cols, function(f){
      return pv.max(data, function(d){
        return d[f]
      })
    });

    if (opts.normPerBaseCategory)  //  compute a scale-function for each column (each key
      fill = pv.dict(cols, function(f){
        return pv.Scale.linear()
          .domain(min[f], max[f])
          .range(opts.minColor, opts.maxColor)
      });
     else {   // normalize over the whole array
      var theMin = min[cols[0]];
      for (var i=1; i<cols.length; i++)
        if (min[cols[i]] < theMin) theMin = min[cols[i]];

      var theMax = max[cols[0]];
      for (var i=1; i<cols.length; i++)
        if (max[cols[i]] < theMax) theMax = max[cols[i]];

      var scale = pv.Scale.linear()
        .domain(theMin, theMax)
        .range(opts.minColor, opts.maxColor);
      fill = pv.dict(cols, function(f){
        return scale
      })
    }

    return fill;  // run an array of values to compute the colors per column
  },

  getNormalColorScale: function (data, cols){
    var fill;
    var opts = this.chart.options;
    if (opts.normPerBaseCategory) {
      // compute the mean and standard-deviation for each column
      var mean = pv.dict(cols, function(f){
        return pv.mean(data, function(d){
          return d[f]
        })
      });
      var sd = pv.dict(cols, function(f){
        return pv.deviation(data, function(d){
          return d[f]
        })
      });
      //  compute a scale-function for each column (each key)
      fill = pv.dict(cols, function(f){
        return pv.Scale.linear()
          .domain(-opts.numSD * sd[f] + mean[f],
                  opts.numSD * sd[f] + mean[f])
          .range(opts.minColor, opts.maxColor)
      });
    } else {   // normalize over the whole array
      var mean = 0.0, sd = 0.0, count = 0;
      for (var i=0; i<origData.length; i++)
        for(var j=0; j<origData[i].length; j++)
          if (origData[i][j] != null){
            mean += origData[i][j];
            count++;
          }
      mean /= count;
      for (var i=0; i<origData.length; i++)
        for(var j=0; j<origData[i].length; j++)
          if (origData[i][j] != null){
            var variance = origData[i][j] - mean;
            sd += variance*variance;
          }
      sd /= count;
      sd = Math.sqrt(sd);

      var scale = pv.Scale.linear()
        .domain(-opts.numSD * sd + mean,
                opts.numSD * sd + mean)
        .range(opts.minColor, opts.maxColor);
      fill = pv.dict(cols, function(f){
        return scale
      })
    }

    return fill;  // run an array of values to compute the colors per column
}

});
<commit_msg>Fixed an issue in the HeatGrid. When using heatGrid in the pentaho-user console  the value of orthoAxisOrdinal was overwritten by false. Added a fix to remedy it. However, need to check whether this is a solid solution.<commit_after>/**
 * HeatGridChart is the main class for generating... heatGrid charts.
 *  A heatGrid visualizes a matrix of values by a grid (matrix) of *
 *  bars, where the color of the bar represents the actual value.
 *  By default the colors are a range of green values, where
 *  light green represents low values and dark green high values.
 *  A heatGrid contains:
 *     - two categorical axis (both on x and y-axis)
 *     - no legend as series become rows on the perpendicular axis
 *  Please contact CvK if there are issues with HeatGrid at cde@vinzi.nl.
 */

pvc.HeatGridChart = pvc.CategoricalAbstract.extend({

    heatGridChartPanel : null,

    constructor: function(o){

        this.base(o);

	// enforce some defaults for the HeatGridChart
        this.options.legend = false;
        this.options.orthoAxisOrdinal = true;
        this.options.orginIsZero = true;

        var _defaults = {
            showValues: true,
            //originIsZero: true,
            axisOffset: 0,
            showTooltips: true,
            orientation: "vertical",
            // use a categorical here based on series labels
            scalingType: "linear",    // "normal" (distribution) or "linear"
            normPerBaseCategory: true,
            orthoAxisOrdinal: true,
            numSD: 2,                 // width (only for normal distribution)
            minColor: "white",
            maxColor: "darkgreen",
            nullColor:  "#efc5ad"  // white with a shade of orange
        };

        // Apply options
        $.extend(this.options,_defaults, o);

	// enforce some defaults for the HeatGridChart
        this.options.orthoAxisOrdinal = true;
        this.options.legend = false;
        this.options.orginIsZero = true;

    },

    preRender: function(){

        this.base();

        pvc.log("Prerendering in heatGridChart");

        this.heatGridChartPanel = new pvc.HeatGridChartPanel(this, {
            stacked: this.options.stacked,
            panelSizeRatio: this.options.panelSizeRatio,
            heatGridSizeRatio: this.options.heatGridSizeRatio,
            maxHeatGridSize: this.options.maxHeatGridSize,
            showValues: this.options.showValues,
            showTooltips: this.options.showTooltips,
            orientation: this.options.orientation
        });

        this.heatGridChartPanel.appendTo(this.basePanel); // Add it

    }

}
);

/*
 * HeatGrid chart panel. Generates a heatGrid chart. Specific options are:
 * orientation - horizontal or vertical. Default: vertical
 * showValues - Show or hide heatGrid value. Default: false
 * stacked -  Stacked? Default: false
 * panelSizeRatio - Ratio of the band occupied by the pane;. Default: 0.5 (50%)
 * heatGridSizeRatio - In multiple series, percentage of inner
 * band occupied by heatGrids. Default: 0.5 (50%)
 * maxHeatGridSize - Maximum size of a heatGrid in pixels. Default: 2000
 *
 * Has the following protovis extension points:
 *
 * chart_ - for the main chart Panel
 * heatGrid_ - for the actual heatGrid
 * heatGridPanel_ - for the panel where the heatGrids sit
 * heatGridLabel_ - for the main heatGrid label
 */

pvc.HeatGridChartPanel = pvc.BasePanel.extend({

    _parent: null,
    pvHeatGrid: null,
    pvHeatGridLabel: null,
    data: null,

    stacked: false,
    panelSizeRatio: 1,
    heatGridSizeRatio: 0.5,
    showTooltips: true,
    maxHeatGridSize: 200,
    showValues: true,
    orientation: "vertical",

    constructor: function(chart, options){

        this.base(chart,options);

    },

    create: function(){

        var myself = this;
        var opts = this.chart.options;
        this.width = this._parent.width;
        this.height = this._parent.height;

        this.pvPanel = this._parent.getPvPanel().add(this.type)
        .width(this.width)
        .height(this.height)

        var anchor = this.orientation == "vertical"?"bottom":"left";

        // reuse the existings scales
        var xScale = this.chart.xAxisPanel.scale;
        var yScale = this.chart.yAxisPanel.scale;

        var cols =  (anchor == "bottom") ? xScale.domain() : yScale.domain();

        var origData = this.chart.dataEngine.getVisibleTransposedValues();
        // create a mapping of the data that shows the columns (rows)
        data = origData.map(function(d){
            return pv.dict(cols, function(){
                return  d[this.index]
            })
        });
        data.reverse();  // the colums are build from top to bottom

        // get an array of scaling functions (one per column)
        var fill = this.getColorScale(data, cols);

        /* The cell dimensions. */
        var w = (xScale.max - xScale.min)/xScale.domain().length;
        var h = (yScale.max - yScale.min)/yScale.domain().length;

        if (anchor != "bottom") {
            var tmp = w;
            w = h;
            h = tmp;
        }

        this.pvHeatGrid = this.pvPanel.add(pv.Panel)
        .data(cols)
        [pvc.BasePanel.relativeAnchor[anchor]](function(){
            return this.index * w
            })
        [pvc.BasePanel.paralelLength[anchor]](w)
        .add(pv.Panel)
        .data(data)
        [pvc.BasePanel.oppositeAnchor[anchor]](function(){
            return this.index * h
        })
        [pvc.BasePanel.orthogonalLength[anchor]](h)
        .fillStyle(function(dat, col){
            return  (dat[col] != null) ? fill[col](dat[col]):opts.nullColor
        })
        .strokeStyle("white")
        .lineWidth(1)
        .antialias(false)
        .text(function(d,f){
          return d[f]});

        // NO SUPPORT for overflow and underflow on HeatGrids

        // NO SUPPORT for SecondAxis on HeatGrids (does not make sense)

        // Labels:

        if(this.showTooltips){
            this.pvHeatGrid
            .event("mouseover", pv.Behavior.tipsy({
                gravity: "s",
                fade: true
            }));
        }

        if (opts.clickable){
            this.pvHeatGrid
            .cursor("pointer")
            .event("click",function(d){
                var s = myself.chart.dataEngine.getSeries()[myself.stacked?this.parent.index:this.index]
                var c = myself.chart.dataEngine.getCategories()[myself.stacked?this.index:this.parent.index]
                return myself.chart.options.clickAction(s,c, d);
            });
        }

        if(this.showValues){
            this.pvHeatGridLabel = this.pvHeatGrid
            .anchor("center")
            .add(pv.Label)
            .bottom(0)
            .text(pv.identity)

            // Extend heatGridLabel
            this.extend(this.pvHeatGridLabel,"heatGridLabel_");
        }

        // Extend heatGrid and heatGridPanel
        this.extend(this.pvHeatGrid,"heatGridPanel_");
        this.extend(this.pvHeatGrid,"heatGrid_");

        // Extend body
        this.extend(this.pvPanel,"chart_");

    },

  /***********
   * compute an array of fill-functions. Each column out of "cols"
   * gets it's own scale function assigned to compute the color
   * for a value. Currently supported scales are:
   *    -  linear (from min to max
   *    -  normal distributed from   -numSD*sd to  numSD*sd
   *         (where sd is the standards deviation)
   ********/
  getColorScale: function(data, cols) {
    switch (this.chart.options.scalingType) {
    case "normal": return this.getNormalColorScale(data, cols);
    case "linear": return this.getLinearColorScale(data, cols);
    default:
      throw "Invalid option " + this.scaleType + " in HeatGrid"
    }
  },

  getLinearColorScale: function (data, cols){
    var fill;
    var opts = this.chart.options;
    // compute the mean and standard-deviation for each column
    var min = pv.dict(cols, function(f){
      return pv.min(data, function(d){
        return d[f]
      })
    });
    var max = pv.dict(cols, function(f){
      return pv.max(data, function(d){
        return d[f]
      })
    });

    if (opts.normPerBaseCategory)  //  compute a scale-function for each column (each key
      fill = pv.dict(cols, function(f){
        return pv.Scale.linear()
          .domain(min[f], max[f])
          .range(opts.minColor, opts.maxColor)
      });
     else {   // normalize over the whole array
      var theMin = min[cols[0]];
      for (var i=1; i<cols.length; i++)
        if (min[cols[i]] < theMin) theMin = min[cols[i]];

      var theMax = max[cols[0]];
      for (var i=1; i<cols.length; i++)
        if (max[cols[i]] < theMax) theMax = max[cols[i]];

      var scale = pv.Scale.linear()
        .domain(theMin, theMax)
        .range(opts.minColor, opts.maxColor);
      fill = pv.dict(cols, function(f){
        return scale
      })
    }

    return fill;  // run an array of values to compute the colors per column
  },

  getNormalColorScale: function (data, cols){
    var fill;
    var opts = this.chart.options;
    if (opts.normPerBaseCategory) {
      // compute the mean and standard-deviation for each column
      var mean = pv.dict(cols, function(f){
        return pv.mean(data, function(d){
          return d[f]
        })
      });
      var sd = pv.dict(cols, function(f){
        return pv.deviation(data, function(d){
          return d[f]
        })
      });
      //  compute a scale-function for each column (each key)
      fill = pv.dict(cols, function(f){
        return pv.Scale.linear()
          .domain(-opts.numSD * sd[f] + mean[f],
                  opts.numSD * sd[f] + mean[f])
          .range(opts.minColor, opts.maxColor)
      });
    } else {   // normalize over the whole array
      var mean = 0.0, sd = 0.0, count = 0;
      for (var i=0; i<origData.length; i++)
        for(var j=0; j<origData[i].length; j++)
          if (origData[i][j] != null){
            mean += origData[i][j];
            count++;
          }
      mean /= count;
      for (var i=0; i<origData.length; i++)
        for(var j=0; j<origData[i].length; j++)
          if (origData[i][j] != null){
            var variance = origData[i][j] - mean;
            sd += variance*variance;
          }
      sd /= count;
      sd = Math.sqrt(sd);

      var scale = pv.Scale.linear()
        .domain(-opts.numSD * sd + mean,
                opts.numSD * sd + mean)
        .range(opts.minColor, opts.maxColor);
      fill = pv.dict(cols, function(f){
        return scale
      })
    }

    return fill;  // run an array of values to compute the colors per column
}

});
<|endoftext|>