<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import _ from 'lodash';

import {
  colors,
  typography,
  boxShadows,
  renderThemeIfPresentOrDefault,
  renderThemeKeyOrDefaultValue,
  ifThemeInPropsIsPresentUse,
  ifThemeIsPresentUse,
} from '../styles';

import { OverflowWrapper } from './SelectInputThemes';

import Checkbox from '../Checkbox';

import TextInput, { TextBox } from '../TextInput';

import ButtonBar from '../ButtonBar';

export const SelectOptionHeight = 36;

const StyledSearchInput = styled(TextInput)`
  ${TextBox} { ${ifThemeIsPresentUse({ value: 'background-color: transparent;'  })} }
`;

const SelectOptionsContainer = styled.div`
  display: flex;
  flex-direction: column;
  visibility: hidden;
  background: ${renderThemeIfPresentOrDefault({ key: 'primary05', defaultValue: colors.white })};
  position: absolute;
  top: ${(props) => {
    if (props.theme.optionListPosition) return props.theme.optionListPosition;
    return 'calc(100%)';
  }};
  ${props => {
    if (!props.theme.optionListPositionLeft) return '';
    return `left: ${props.theme.optionListPositionLeft};`;
  }}
  width: ${props => props.width ? `${props.width}px` : '100%'};
  z-index: 2;
  box-shadow: ${(props) => {
    if (props.theme.optionsListShadow && props.visible) return props.theme.optionsListShadow;
    else if (props.visible) return boxShadows.lvl8;
    return 'none';
  }};
  border-radius: 2px;
  transform-origin: center 8px 0px;
  will-change: transform, opacity, visibility;
  ${(props) => {
    if (props.visible) {
      return `
        animation-name: open;
        animation-duration: 0.16s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
      `;
    }
    if (!props.visible && props.dirty) {
      return `
        animation-name: close;
        animation-duration: 0.1s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
      `;
    }
  }}

  @keyframes open {
    0% { transform: translateY(-24px) scaleY(0.4); opacity: 0; visibility: hidden; }
    40% { opacity: 1; }
    100% { transform: translateY(0) scaleY(1); opacity: 1; visibility: visible; }
  }

  @keyframes close {
    0% { transform: translateY(0); opacity: 1; visibility: visible; }
    100% { transform: translateY(-4px); opacity: 0; visibility: hidden; }
  }
`;

const SelectOptionsWrapper = styled.div`
  width: 100%;
  max-height: ${props => props.maxHeight};
  transform: translateZ(0);
  overflow-y: auto;

  &::-webkit-scrollbar {
    background-color: ${renderThemeIfPresentOrDefault({ key: 'primary05', defaultValue: colors.white })};
    border-left: none;
    margin-right: 10px;
    width: 10px;
    height: 10px
  }
  &::-webkit-scrollbar-button {
    width: 0px;
    height: 0px;
  }
  &::-webkit-scrollbar-thumb {
    background-clip: content-box;
    background-color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
    border: 1px solid transparent;
    border-radius: 5px;
  }
  &::-webkit-scrollbar-corner {
    background: transparent;
  }

  *, *:before, *:after {
    box-sizing: border-box;
  }
`;

const SelectOption = styled.div`
  display: flex;
  align-items: center;
  ${typography.subhead1}
  width: 100%;
  line-height: ${(props) => {
    if (props.theme.optionHeight) return `${props.theme.optionHeight}px`;
    return `${SelectOptionHeight}px`;
  }};
  box-sizing: border-box;
  border-bottom: ${(props) => {
    if (props.theme.optionBorderSeparator) return ifThemeInPropsIsPresentUse({ props, value: `1px solid ${props.theme.white10}`, defaultValue: props.theme.optionBorderSeparator });
    return 'none';
  }};
  opacity: ${(props) => {
    if (props.visible) return 1;
    return 0;
  }};

  padding: ${(props) => {
    if (props.noPadding) return '0';
    if (props.lowPadding) return '0 4px';
    if (props.theme.optionPadding) return props.theme.optionPadding;
    return '0 24px';
  }};

  color:
  ${(props) => {
    if (props.disabled) return renderThemeKeyOrDefaultValue({ props, key: 'white40', defaultValue: colors.middleGray });
    else if (props.theme.optionTextColor) return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: props.theme.optionTextColor });
    else if (props.theme.textColor) return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: props.theme.textColor });
    return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: colors.selectItemColor })
  }};

  cursor: ${(props) => {
    if (props.disabled) return 'default';
    return 'pointer';
  }};

  &:first-child {
    margin-top: 8px;
  }

  &:last-child {
    margin-bottom: 8px;
    /* accounts for when a border bottom is applied on the theme */
    border-bottom: none;
  }

  ${(props) => {
    if (!props.disabled) {
      return `&:hover {
        background: ${renderThemeKeyOrDefaultValue({ props, key: 'white10', defaultValue: colors.gray90 })};
        color: ${renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: colors.selectItemColor })};
      };`;
    }
  }}
`;

const PromotedOptions = styled.div`
  border-bottom: ${props => (props.listLength === 0 || props.hideDivider) ? '0' : `1px solid ${renderThemeKeyOrDefaultValue({ props, key: 'white40', defaultValue: colors.barLightGray })}`};
  margin-bottom: ${props => props.hideDivider ? '0' : '8px'};
  width: 100%;
`;

const BottomActionAreaWrapper = styled.div`
  padding: 10px;
`;

const BottomActionAreaDivider = styled.hr`
  height: 1px;
  background-color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
  color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
`;

const OptionsTitle = styled.div`
  height: 24px;
	width: 100%;
	color: ${renderThemeIfPresentOrDefault({ key: 'white90', defaultValue: 'rgba(0,0,0,0.87)' })};
	font-family: Roboto;
	font-size: 20px;
	font-weight: 500;
	line-height: 24px;
  padding-top: 20px;
  padding-bottom: 25px;
  padding-left: 25px;
`;

class SelectOptions extends React.Component {

  constructor() {
    super();
    this.state = {
      inputUsed: false
    };
  }

  optionElement = (keyID, option) => {
    const { onOptionUpdate, options, promotedOptions, multiSelect, selectedOptions } = this.props;
    const clonedOptionItem = typeof (option.label) === 'string' ? <OverflowWrapper>{option.label}</OverflowWrapper> : option.label;
    const optionsLength = _.get(options, 'length', 0) + _.get(promotedOptions, 'length', 0);
    const delay = {
      transition: `opacity 0.3s ease-in-out ${((keyID + 1) / (optionsLength + 1)) * 0.284}s, background 0.2s ease-in-out`
    };

    const optionSelected = multiSelect && _.includes(selectedOptions, option.value);

    const disabled = option.disabled || this.props.isDisabled;

    return (
      <SelectOption
        className={'pb-option'}
        key={keyID}
        visible={this.props.visible}
        style={delay}
        disabled={disabled}
        noPadding={option.noPadding}
        lowPadding={this.props.lowPadding}
        onClick={() => {
          if (!disabled) {
            onOptionUpdate(option.value);
          }
        }}>
          {multiSelect &&
          <Checkbox
            disabled={disabled}
            checked={optionSelected}
            onClick={() => {
              if (!disabled) {
                onOptionUpdate(option.value);
              }
            }}/>
          }
          {clonedOptionItem}
          {option.showImage && this.props.image}
        </SelectOption>
    );
  };

  onPrimaryActionClick = () => {
    if (this.props.closeAfterClick) {
      this.props.closeOptionsList();
    }
    this.props.onPrimaryActionClick();
  }

  onSecondaryActionClick = () => {
    if (this.props.closeAfterClick) {
      this.props.closeOptionsList();
    }
    this.props.onSecondaryActionClick();
  }

  renderOptions = () => {
    const { options, promotedOptions } = this.props;
    if (_.isEmpty(options) && _.isEmpty(promotedOptions)) {
      return this.optionElement(undefined, { value: 'select', label: 'Select', disabled: true });
    }
    const numberOfPomoted = _.get(promotedOptions, 'length', 0);
    return (
      options.map((option, idx) => this.optionElement(idx + numberOfPomoted, option))
    );
  };

  renderPromotedOptions = () => {
    const { promotedOptions, hideDivider } = this.props;
    if (_.get(promotedOptions, 'length')) {
      return (
        <PromotedOptions listLength={_.size(promotedOptions)} hideDivider={hideDivider}>
          { promotedOptions.map((option, idx) => this.optionElement(idx, option)) }
        </PromotedOptions>
      );
    }
  }

  renderOptionsTitle = () => {
    if (!this.props.optionsTitle) return null;

    return (
      <OptionsTitle>{this.props.optionsTitle}</OptionsTitle>
    );
  }

  renderSearch = () => {
    if (!this.props.searchable) return null;

    return (
      <div style={{ padding: '0 24px' }} >
        <StyledSearchInput
          label="Label"
          name="selectSearch"
          onChange={this.props.onSearch}
          visible={this.props.visible}
          search
        />
      </div>
    );
  }

  componentDidUpdate() {
    // Check to see if user has opened input, to know if it's okay to run close animation. Not good to run close animation on load.
    if (this.props.visible && !this.state.inputUsed) {
      this.setState({
        inputUsed: true
      });
    }
  }

  render() {
    return (
      <SelectOptionsContainer
      dirty={this.state.inputUsed}
      {...this.props}
      ref={this.props.optionsRef}
      >
        {this.renderOptionsTitle()}
        <SelectOptionsWrapper {...this.props}>
          {this.renderSearch()}
          {this.renderPromotedOptions()}
          {this.renderOptions()}
        </SelectOptionsWrapper>
        {this.props.bottomActionArea &&
          <BottomActionAreaWrapper>
            <BottomActionAreaDivider/>
            {this.props.bottomActionArea}
          </BottomActionAreaWrapper>
        }
        {this.props.showButtonBar &&
          <BottomActionAreaWrapper>
            <ButtonBar
            primaryActionText={this.props.primaryActionText}
            secondaryActionText={this.props.secondaryActionText}
            onPrimaryActionClick={this.onPrimaryActionClick}
            onSecondaryActionClick={this.onSecondaryActionClick}/>
          </BottomActionAreaWrapper>
        }

      </SelectOptionsContainer>
    );
  }
}

SelectOptions.propTypes = {
  onClick: PropTypes.func.isRequired,
  promotedOption: PropTypes.objectOf(PropTypes.string),
  options: PropTypes.array.isRequired,
  hideDivider: PropTypes.bool,
  optionsCount: PropTypes.number.isRequired,
  visible: PropTypes.bool.isRequired,
  multiSelect: PropTypes.bool,
  maxHeight: PropTypes.string.isRequired,
  optionsRef: PropTypes.func,
  closeOptionsList: PropTypes.func,
  primaryActionText: PropTypes.string,
  secondaryActionText: PropTypes.string,
  onPrimaryActionClick: PropTypes.func,
  onSecondaryActionClick: PropTypes.func,
  promotedOptions: PropTypes.arrayOf(PropTypes.shape({
    value: PropTypes.any,
    label: PropTypes.string,
    disabled: PropTypes.bool
  }))
};

SelectOptions.defaultProps = {
  onClick: _.noop,
  options: [],
  optionsCount: 0,
  visible: false,
  multiSelect: false,
  maxHeight: '200px',
  bottomActionArea: null,
  optionsRef: _.noop,
  primaryActionText: '',
  secondaryActionText: '',
  onPrimaryActionClick: () => {},
  onSecondaryActionClick: () => {},
};

export default SelectOptions;
<commit_msg>fix(SelectOption): try passed key first <commit_after>import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import _ from 'lodash';

import {
  colors,
  typography,
  boxShadows,
  renderThemeIfPresentOrDefault,
  renderThemeKeyOrDefaultValue,
  ifThemeInPropsIsPresentUse,
  ifThemeIsPresentUse,
} from '../styles';

import { OverflowWrapper } from './SelectInputThemes';

import Checkbox from '../Checkbox';

import TextInput, { TextBox } from '../TextInput';

import ButtonBar from '../ButtonBar';

export const SelectOptionHeight = 36;

const StyledSearchInput = styled(TextInput)`
  ${TextBox} { ${ifThemeIsPresentUse({ value: 'background-color: transparent;'  })} }
`;

const SelectOptionsContainer = styled.div`
  display: flex;
  flex-direction: column;
  visibility: hidden;
  background: ${renderThemeIfPresentOrDefault({ key: 'primary05', defaultValue: colors.white })};
  position: absolute;
  top: ${(props) => {
    if (props.theme.optionListPosition) return props.theme.optionListPosition;
    return 'calc(100%)';
  }};
  ${props => {
    if (!props.theme.optionListPositionLeft) return '';
    return `left: ${props.theme.optionListPositionLeft};`;
  }}
  width: ${props => props.width ? `${props.width}px` : '100%'};
  z-index: 2;
  box-shadow: ${(props) => {
    if (props.theme.optionsListShadow && props.visible) return props.theme.optionsListShadow;
    else if (props.visible) return boxShadows.lvl8;
    return 'none';
  }};
  border-radius: 2px;
  transform-origin: center 8px 0px;
  will-change: transform, opacity, visibility;
  ${(props) => {
    if (props.visible) {
      return `
        animation-name: open;
        animation-duration: 0.16s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
      `;
    }
    if (!props.visible && props.dirty) {
      return `
        animation-name: close;
        animation-duration: 0.1s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
      `;
    }
  }}

  @keyframes open {
    0% { transform: translateY(-24px) scaleY(0.4); opacity: 0; visibility: hidden; }
    40% { opacity: 1; }
    100% { transform: translateY(0) scaleY(1); opacity: 1; visibility: visible; }
  }

  @keyframes close {
    0% { transform: translateY(0); opacity: 1; visibility: visible; }
    100% { transform: translateY(-4px); opacity: 0; visibility: hidden; }
  }
`;

const SelectOptionsWrapper = styled.div`
  width: 100%;
  max-height: ${props => props.maxHeight};
  transform: translateZ(0);
  overflow-y: auto;

  &::-webkit-scrollbar {
    background-color: ${renderThemeIfPresentOrDefault({ key: 'primary05', defaultValue: colors.white })};
    border-left: none;
    margin-right: 10px;
    width: 10px;
    height: 10px
  }
  &::-webkit-scrollbar-button {
    width: 0px;
    height: 0px;
  }
  &::-webkit-scrollbar-thumb {
    background-clip: content-box;
    background-color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
    border: 1px solid transparent;
    border-radius: 5px;
  }
  &::-webkit-scrollbar-corner {
    background: transparent;
  }

  *, *:before, *:after {
    box-sizing: border-box;
  }
`;

const SelectOption = styled.div`
  display: flex;
  align-items: center;
  ${typography.subhead1}
  width: 100%;
  line-height: ${(props) => {
    if (props.theme.optionHeight) return `${props.theme.optionHeight}px`;
    return `${SelectOptionHeight}px`;
  }};
  box-sizing: border-box;
  border-bottom: ${(props) => {
    if (props.theme.optionBorderSeparator) return ifThemeInPropsIsPresentUse({ props, value: `1px solid ${props.theme.white10}`, defaultValue: props.theme.optionBorderSeparator });
    return 'none';
  }};
  opacity: ${(props) => {
    if (props.visible) return 1;
    return 0;
  }};

  padding: ${(props) => {
    if (props.noPadding) return '0';
    if (props.lowPadding) return '0 4px';
    if (props.theme.optionPadding) return props.theme.optionPadding;
    return '0 24px';
  }};

  color:
  ${(props) => {
    if (props.disabled) return renderThemeKeyOrDefaultValue({ props, key: 'white40', defaultValue: colors.middleGray });
    else if (props.theme.optionTextColor) return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: props.theme.optionTextColor });
    else if (props.theme.textColor) return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: props.theme.textColor });
    return renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: colors.selectItemColor })
  }};

  cursor: ${(props) => {
    if (props.disabled) return 'default';
    return 'pointer';
  }};

  &:first-child {
    margin-top: 8px;
  }

  &:last-child {
    margin-bottom: 8px;
    /* accounts for when a border bottom is applied on the theme */
    border-bottom: none;
  }

  ${(props) => {
    if (!props.disabled) {
      return `&:hover {
        background: ${renderThemeKeyOrDefaultValue({ props, key: 'white10', defaultValue: colors.gray90 })};
        color: ${renderThemeKeyOrDefaultValue({ props, key: 'white90', defaultValue: colors.selectItemColor })};
      };`;
    }
  }}
`;

const PromotedOptions = styled.div`
  border-bottom: ${props => (props.listLength === 0 || props.hideDivider) ? '0' : `1px solid ${renderThemeKeyOrDefaultValue({ props, key: 'white40', defaultValue: colors.barLightGray })}`};
  margin-bottom: ${props => props.hideDivider ? '0' : '8px'};
  width: 100%;
`;

const BottomActionAreaWrapper = styled.div`
  padding: 10px;
`;

const BottomActionAreaDivider = styled.hr`
  height: 1px;
  background-color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
  color: ${renderThemeIfPresentOrDefault({ key: 'white40', defaultValue: colors.black40 })};
`;

const OptionsTitle = styled.div`
  height: 24px;
	width: 100%;
	color: ${renderThemeIfPresentOrDefault({ key: 'white90', defaultValue: 'rgba(0,0,0,0.87)' })};
	font-family: Roboto;
	font-size: 20px;
	font-weight: 500;
	line-height: 24px;
  padding-top: 20px;
  padding-bottom: 25px;
  padding-left: 25px;
`;

class SelectOptions extends React.Component {

  constructor() {
    super();
    this.state = {
      inputUsed: false
    };
  }

  optionElement = (keyID, option) => {
    const { onOptionUpdate, options, promotedOptions, multiSelect, selectedOptions } = this.props;
    const clonedOptionItem = typeof (option.label) === 'string' ? <OverflowWrapper>{option.label}</OverflowWrapper> : option.label;
    const optionsLength = _.get(options, 'length', 0) + _.get(promotedOptions, 'length', 0);
    const delay = {
      transition: `opacity 0.3s ease-in-out ${((keyID + 1) / (optionsLength + 1)) * 0.284}s, background 0.2s ease-in-out`
    };

    const optionSelected = multiSelect && _.includes(selectedOptions, option.value);

    const disabled = option.disabled || this.props.isDisabled;

    return (
      <SelectOption
        className={'pb-option'}
        key={option.keyID || keyID}
        visible={this.props.visible}
        style={delay}
        disabled={disabled}
        noPadding={option.noPadding}
        lowPadding={this.props.lowPadding}
        onClick={() => {
          if (!disabled) {
            onOptionUpdate(option.value);
          }
        }}>
          {multiSelect &&
          <Checkbox
            disabled={disabled}
            checked={optionSelected}
            onClick={() => {
              if (!disabled) {
                onOptionUpdate(option.value);
              }
            }}/>
          }
          {clonedOptionItem}
          {option.showImage && this.props.image}
        </SelectOption>
    );
  };

  onPrimaryActionClick = () => {
    if (this.props.closeAfterClick) {
      this.props.closeOptionsList();
    }
    this.props.onPrimaryActionClick();
  }

  onSecondaryActionClick = () => {
    if (this.props.closeAfterClick) {
      this.props.closeOptionsList();
    }
    this.props.onSecondaryActionClick();
  }

  renderOptions = () => {
    const { options, promotedOptions } = this.props;
    if (_.isEmpty(options) && _.isEmpty(promotedOptions)) {
      return this.optionElement(undefined, { value: 'select', label: 'Select', disabled: true });
    }
    const numberOfPomoted = _.get(promotedOptions, 'length', 0);
    return (
      options.map((option, idx) => this.optionElement(idx + numberOfPomoted, option))
    );
  };

  renderPromotedOptions = () => {
    const { promotedOptions, hideDivider } = this.props;
    if (_.get(promotedOptions, 'length')) {
      return (
        <PromotedOptions listLength={_.size(promotedOptions)} hideDivider={hideDivider}>
          { promotedOptions.map((option, idx) => this.optionElement(idx, option)) }
        </PromotedOptions>
      );
    }
  }

  renderOptionsTitle = () => {
    if (!this.props.optionsTitle) return null;

    return (
      <OptionsTitle>{this.props.optionsTitle}</OptionsTitle>
    );
  }

  renderSearch = () => {
    if (!this.props.searchable) return null;

    return (
      <div style={{ padding: '0 24px' }} >
        <StyledSearchInput
          label="Label"
          name="selectSearch"
          onChange={this.props.onSearch}
          visible={this.props.visible}
          search
        />
      </div>
    );
  }

  componentDidUpdate() {
    // Check to see if user has opened input, to know if it's okay to run close animation. Not good to run close animation on load.
    if (this.props.visible && !this.state.inputUsed) {
      this.setState({
        inputUsed: true
      });
    }
  }

  render() {
    return (
      <SelectOptionsContainer
      dirty={this.state.inputUsed}
      {...this.props}
      ref={this.props.optionsRef}
      >
        {this.renderOptionsTitle()}
        <SelectOptionsWrapper {...this.props}>
          {this.renderSearch()}
          {this.renderPromotedOptions()}
          {this.renderOptions()}
        </SelectOptionsWrapper>
        {this.props.bottomActionArea &&
          <BottomActionAreaWrapper>
            <BottomActionAreaDivider/>
            {this.props.bottomActionArea}
          </BottomActionAreaWrapper>
        }
        {this.props.showButtonBar &&
          <BottomActionAreaWrapper>
            <ButtonBar
            primaryActionText={this.props.primaryActionText}
            secondaryActionText={this.props.secondaryActionText}
            onPrimaryActionClick={this.onPrimaryActionClick}
            onSecondaryActionClick={this.onSecondaryActionClick}/>
          </BottomActionAreaWrapper>
        }

      </SelectOptionsContainer>
    );
  }
}

SelectOptions.propTypes = {
  onClick: PropTypes.func.isRequired,
  promotedOption: PropTypes.objectOf(PropTypes.string),
  options: PropTypes.array.isRequired,
  hideDivider: PropTypes.bool,
  optionsCount: PropTypes.number.isRequired,
  visible: PropTypes.bool.isRequired,
  multiSelect: PropTypes.bool,
  maxHeight: PropTypes.string.isRequired,
  optionsRef: PropTypes.func,
  closeOptionsList: PropTypes.func,
  primaryActionText: PropTypes.string,
  secondaryActionText: PropTypes.string,
  onPrimaryActionClick: PropTypes.func,
  onSecondaryActionClick: PropTypes.func,
  promotedOptions: PropTypes.arrayOf(PropTypes.shape({
    value: PropTypes.any,
    label: PropTypes.string,
    disabled: PropTypes.bool
  }))
};

SelectOptions.defaultProps = {
  onClick: _.noop,
  options: [],
  optionsCount: 0,
  visible: false,
  multiSelect: false,
  maxHeight: '200px',
  bottomActionArea: null,
  optionsRef: _.noop,
  primaryActionText: '',
  secondaryActionText: '',
  onPrimaryActionClick: () => {},
  onSecondaryActionClick: () => {},
};

export default SelectOptions;
<|endoftext|>


<commit_before>import { colors } from '../../../themes/common-variables'
import { content } from '../constants/section-05/records'
import { font, marginBetweenSections } from '../constants/styles'
import { replaceStorageUrlPrefix } from '@twreporter/react-components/lib/shared/utils'
import { screen } from '../utils/screen'
import { storageUrlPrefix } from '../utils/config'
import groupBy from 'lodash/groupBy'
import keys from 'lodash/keys'
import List from './list'
import orderBy from 'lodash/orderBy'
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'
import styled from 'styled-components'
import sz from '../constants/screen-size'
import Timeline from './timeline'
import Waypoint from 'react-waypoint'

const _ = {
  orderBy, groupBy, keys
}

const sortedData = _.orderBy(content, [ 'year', 'month', 'date' ], [ 'asc', 'asc', 'asc' ])
const sortedDataGroupByYear = _.groupBy(sortedData, record => record.year)
const yearList = _.keys(sortedDataGroupByYear)
const yearRangebgColor = '#cacaca'
const timelineScrollingPortion = 0.95
const defaultZIndex = 0
const containerWidth = {
  mobile: '100%',
  tablet: '706px',
  desktop: '1024px',
  overDesktop: '1440px'
}

const Container = styled.div`
  position: relative;
  background-color: ${colors.white};
  overflow: hidden;
  ${screen.overDesktop`
    margin: ${marginBetweenSections.overDesktop} 0;
  `}
  ${screen.desktop`
    margin: ${marginBetweenSections.desktop} 0;
  `}
  ${screen.tablet`
    margin: ${marginBetweenSections.tablet} 0;
  `}
  ${screen.mobile`
    margin: ${marginBetweenSections.mobile} 0;
  `}
`

const SectionWrapper = styled.section`
  position: relative;
  display: block;
  margin: 0 auto;
  ${screen.overDesktop`
    width: ${containerWidth.overDesktop};
    height: 1110px;
    padding: 98px 116px 293px 152px;
  `}
  ${screen.desktop`
    width: ${containerWidth.desktop};
    height: 920px;
    padding: 119px 100px 228px 80px;
  `}
  ${screen.tablet`
    width: ${containerWidth.tablet};
    min-height: 1024px;
    padding: 80px 49px 181px 93px;
  `}
  ${screen.mobile`
    width: ${containerWidth.mobile};
    min-height: 715px;
    padding: 76px 37px 126px 37px
  `}
`

const Title = styled.div`
  background-repeat: no-repeat;
  background-size: contain;
  margin: 0;
  span{
    display: none;
  }
  ${screen.desktopAbove`
    position: absolute;
    left: 0;
    top: 0;
    background-image: url(${replaceStorageUrlPrefix(`${storageUrlPrefix}/title-section5.png`)});
  `}
  ${screen.overDesktop`
    width: 327px;
    height: 384px;
    margin: 98px 0 0 152px;
  `}
  ${screen.desktop`
    width: 257px;
    height: 271px;
    margin: 119px 0 0 80px;
  `}
  ${screen.tabletBelow`
    background-image: url(${replaceStorageUrlPrefix(`${storageUrlPrefix}/title-section5-mob.png`)});
  `}
  ${screen.tablet`
    width: 408px;
    height: 231px;
    background-position: left top;
  `}
  ${screen.mobile`
    background-position: center top;
    width: 247px;
    height: 154px;
    float: none;
    margin: 0 auto;
  `}
`

const Content = styled.div`
  width: 100%;
  ${screen.desktopAbove`
    height: 100%;
    float: right;
  `}
  ${screen.overDesktop`
    width: 616px;
    height: calc(100% - 98.7px);
    padding-right: 95px;
    margin-top: 98.7px;
  `}
  ${screen.desktop`
    width: 432px;
    height: calc(100% - 63px);
    padding-right: 61px;
    margin-top: 63px;
  `}
  ${screen.tabletAbove`
    position: relative;
  `}
  ${screen.tablet`
    height: 572px;
    padding-right: 71px;
    margin-top: 71px;
  `}
  ${screen.mobile`
    margin-top: 67px;
  `}
`

const BorderBottom = styled.div `
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: ${props => props.zIndex};
  background: ${colors.red.liverRed};
  ${screen.desktopAbove`
    position: ${props => props.fixed ? 'fixed' : 'absolute'};
  `}
  ${screen.tabletBelow`
    position: absolute;
  `}
  ${screen.overDesktop`
    height: 8px;
  `}
  ${screen.desktop`
    height: 6px;
  `}
  ${screen.tablet`
    height: 7px;
  `}
  ${screen.mobile`
    height: 6px;
  `}
`

const RunningTimeline = styled.div`
  position: relative;
  width: 100%;
  height: 100%;
  overflow-y: hidden;
  background: ${colors.white};
  ${screen.mobile`
    display: none;
  `}
`

const AccordionTimeline = styled.div`
  ${screen.tabletAbove`
    display: none;
  `}
`

const YearTag = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-100%) translateY(-100%);
  p{
    font-family: ${font.family.english.roboto}, ${font.family.sansSerifFallback};
    font-size: 13px;
    font-weight: ${font.weight.bold};
    background: ${colors.black};
    color: ${colors.white};
    padding: 2px;
  }
  ${screen.overDesktop`
    p{
      font-size: 15px;
    }
  `}
  ${screen.mobile`
    display: none;
  `}
`

const YearRange = styled.div`
  position: absolute;
  right: 0;
  top: 0;
  width: 140px;
  height: 140px;
  margin-top: -61px;
  border-top: solid 1px ${yearRangebgColor};
  border-right: solid 1px ${yearRangebgColor};
  p{
    display: block;
    width: 100%;
    font-size: 32px;
    letter-spacing: 0.2px;
    font-family: ${font.family.english.roboto}, ${font.family.sansSerifFallback};
    font-weight: ${font.weight.thin};
    background: ${yearRangebgColor};
    span{
      padding: 0 2.5px;
    }
  }
  p:first-child{
    position: absolute;
    right: 0;
    bottom: 0;
    transform-origin: right top;
    text-align: right;
    transform: translateY(100%) rotate(90deg);
    span{
      background: ${colors.black};
      color: ${colors.white};
    }
  }
  p:nth-child(2){
    position: absolute;
    left: 0;
    top: 0;
    span{
      background: ${colors.white};
      color: ${yearRangebgColor};
      border-collapse: collapse;
      box-shadow: inset 0 0 0 1px ${yearRangebgColor};
    }
    img{
      position: absolute;
      right: 0;
      top: 50%;
      margin-right: 13.25px;
    }
  }
  ${screen.overDesktop`
    width: 230px;
    height: 230px;
    margin-top: -95px;
    p{
      font-size: 53px;
      letter-spacing: 0.5px;
      line-height: 1.35;
    }
    p:nth-child(2){
      img{
        height: 78px;
        margin-right: 22.6px;
      }
    }
  `}
  ${screen.desktop`
    border: none;
    p:nth-child(2){
      img{
        height: 45px;
      }
    }
  `}
  ${screen.tablet`
    width: 148px;
    height: 148px;
    margin-top: -71px;
    p{
      font-size: 34px;
      letter-spacing: 0.13px;
      span{
        padding: 1px 2.5px;
      }
    }
    p:nth-child(2){
      img{
        height: 48px;
      }
    }
  `}
  ${screen.mobile`
    display: none;
  `}
`

const Circle = styled.div`
  position: absolute;
  left: 0;
  bottom: 0;
  width: 218px;
  height: 218px;
  border-radius: 50%;
  background: ${props => props.color};
  ${screen.overDesktop`
    width: 277px;
    height: 277px;
  `}
  ${screen.tabletBelow`
    display: none;
  `}
`

const OuterCircle = styled(Circle)`
  margin-left: 80px;
  margin-bottom: 228px;
  ${screen.overDesktop`
    width: 277px;
    margin-left: 152px;
    margin-bottom: 293px;
  `}
`

const InnerCircle = styled(Circle)`
  margin: 0;
  transform: translateX(50%) translateY(-50%);
`

const Rect = styled.div`
  position: absolute;
  right: 0;
  bottom: 0;
  width: 30px;
  height: 30px;
  margin: 0 -38px 0 0;
  background: ${props => props.color};
  ${screen.overDesktop`
    width: 45px;
    height: 45px;
    margin: 0 -51.5px 0 0;
  `}
`

export default class Section5 extends PureComponent {
  constructor(props) {
    super(props)
    this.yearContent = []
    this.isMobile = false
    this.initalOpenUp = true
    this.state = {
      timelineScrolling: false,
      timelineScrollingHeight: 0,
      yearContentHeight: [],
      unfoldArray: yearList.map(() => false),
      currentYear: yearList[0],
      isBorderBottomfixed: true
    }
  }
  /**
   * Manages the unfoldArray which represents whether content in each year is folded
   * @param {number} index
   */
  _foldAndUnfold = (index) => {
    let newUnfoldArray = [ ...this.state.unfoldArray ]
    newUnfoldArray[index] = !newUnfoldArray[index]
    this.setState({ unfoldArray: newUnfoldArray })
  }
  _onLeave = () => {
    this.setState({ isBorderBottomfixed: true })
    this._stopTimelineAutoScrolling()
  }
  _onEnter = () => {
    // unfold the first year
    let newUnfoldArray = [ ...this.state.unfoldArray ]
    if (this.initalOpenUp) {
      newUnfoldArray[0] = true
      this.initalOpenUp = !this.initalOpenUp
    }
    this.setState({
      isBorderBottomfixed: false,
      unfoldArray: newUnfoldArray
    })

    // to start or resume auto-scrolling
    this._startTimelineAutoScrolling()
  }
  _startTimelineAutoScrolling = () => {
    if (!this.isMobile) {
      this.setState({ timelineScrolling: true })
    }
  }
  _stopTimelineAutoScrolling = () => {
    this.setState({ timelineScrolling: false })
  }
  _getBorderZIndex = () => {
    return defaultZIndex + 2
  }
  /**
   * Set year which will be displayed on title according to parameter
   * @param {number} currentYear
   */
  _getYear = (currentYearIndex) => {
    this.setState({
      currentYear: yearList[currentYearIndex]
    })
  }
  /**
   * The function is used by list.js for computing the height of children in timeline after they are mounted
   * @param {Array} contentHeightArray
   */
  _getYearContentHeight = (contentHeightArray) => {
    this.setState({ yearContentHeight: contentHeightArray })
  }
  componentDidMount() {
    const timelineScrollingHeight = ReactDOM.findDOMNode(this.scrollingContent).getBoundingClientRect().height
    this.setState({ timelineScrollingHeight: timelineScrollingHeight })
    if (window.matchMedia(`(max-width: ${sz.mediumScreenMinWidth - 1}px)`).matches) {
      this.isMobile = true
    }
  }
  render() {
    return (
      <Waypoint
        onEnter={this._onEnter}
        onLeave={this._onLeave}
        scrollableAncestor="window"
        fireOnRapidScroll
        bottomOffset="80%"
      >

          <Container>
            <SectionWrapper>
              <OuterCircle color={`${colors.black}`}>
                <InnerCircle color={`${colors.gray.gray96}`}/>
                <Rect color={`${colors.secondaryColor}`}/>
              </OuterCircle>
              <Title>大事紀</Title>
              <Content>
                <AccordionTimeline>
                  <List
                    unfoldArray={this.state.unfoldArray}
                    sortedData={sortedData}
                    sortedDataGroupByYear={sortedDataGroupByYear}
                    foldAndUnfold={this._foldAndUnfold}
                    getYearContentHeight={this._getYearContentHeight}
                  />
                </AccordionTimeline>
                <YearRange>

{yearList[yearList.length - 1]}

                    {yearList[0]}
                    <img
                      src={`${replaceStorageUrlPrefix(`${storageUrlPrefix}/section5-arrow.png`)}`}
                    />

                </YearRange>
                <RunningTimeline>
                  <Timeline
                    ref={scrollingContent => this.scrollingContent = scrollingContent}
                    childrenHeight={this.state.timelineScrollingHeight * timelineScrollingPortion}
                    autoScrolling={this.state.timelineScrolling}
                    startAutoScroll={this._startTimelineAutoScrolling}
                    stopAutoScroll={this._stopTimelineAutoScrolling}
                    yearContentHeight={this.state.yearContentHeight}
                    getYear={this._getYear}>
                    <List
                      unfoldArray={this.state.unfoldArray}
                      sortedData={sortedData}
                      sortedDataGroupByYear={sortedDataGroupByYear}
                      foldAndUnfold={this._foldAndUnfold}
                      getYearContentHeight={this._getYearContentHeight}
                    />
                  </Timeline>
                </RunningTimeline>
                <YearTag>

{this.state.currentYear}

                </YearTag>
              </Content>
            </SectionWrapper>
            <BorderBottom
              fixed={this.state.isBorderBottomfixed}
              zIndex={this._getBorderZIndex()}
            />
          </Container>

      </Waypoint>
    )
  }
}
<commit_msg>Tweak scrolling portion in the timeline on about-us page<commit_after>import { colors } from '../../../themes/common-variables'
import { content } from '../constants/section-05/records'
import { font, marginBetweenSections } from '../constants/styles'
import { replaceStorageUrlPrefix } from '@twreporter/react-components/lib/shared/utils'
import { screen } from '../utils/screen'
import { storageUrlPrefix } from '../utils/config'
import groupBy from 'lodash/groupBy'
import keys from 'lodash/keys'
import List from './list'
import orderBy from 'lodash/orderBy'
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'
import styled from 'styled-components'
import sz from '../constants/screen-size'
import Timeline from './timeline'
import Waypoint from 'react-waypoint'

const _ = {
  orderBy, groupBy, keys
}

const sortedData = _.orderBy(content, [ 'year', 'month', 'date' ], [ 'asc', 'asc', 'asc' ])
const sortedDataGroupByYear = _.groupBy(sortedData, record => record.year)
const yearList = _.keys(sortedDataGroupByYear)
const yearRangebgColor = '#cacaca'
const timelineScrollingPortion = 0.98
const defaultZIndex = 0
const containerWidth = {
  mobile: '100%',
  tablet: '706px',
  desktop: '1024px',
  overDesktop: '1440px'
}

const Container = styled.div`
  position: relative;
  background-color: ${colors.white};
  overflow: hidden;
  ${screen.overDesktop`
    margin: ${marginBetweenSections.overDesktop} 0;
  `}
  ${screen.desktop`
    margin: ${marginBetweenSections.desktop} 0;
  `}
  ${screen.tablet`
    margin: ${marginBetweenSections.tablet} 0;
  `}
  ${screen.mobile`
    margin: ${marginBetweenSections.mobile} 0;
  `}
`

const SectionWrapper = styled.section`
  position: relative;
  display: block;
  margin: 0 auto;
  ${screen.overDesktop`
    width: ${containerWidth.overDesktop};
    height: 1110px;
    padding: 98px 116px 293px 152px;
  `}
  ${screen.desktop`
    width: ${containerWidth.desktop};
    height: 920px;
    padding: 119px 100px 228px 80px;
  `}
  ${screen.tablet`
    width: ${containerWidth.tablet};
    min-height: 1024px;
    padding: 80px 49px 181px 93px;
  `}
  ${screen.mobile`
    width: ${containerWidth.mobile};
    min-height: 715px;
    padding: 76px 37px 126px 37px
  `}
`

const Title = styled.div`
  background-repeat: no-repeat;
  background-size: contain;
  margin: 0;
  span{
    display: none;
  }
  ${screen.desktopAbove`
    position: absolute;
    left: 0;
    top: 0;
    background-image: url(${replaceStorageUrlPrefix(`${storageUrlPrefix}/title-section5.png`)});
  `}
  ${screen.overDesktop`
    width: 327px;
    height: 384px;
    margin: 98px 0 0 152px;
  `}
  ${screen.desktop`
    width: 257px;
    height: 271px;
    margin: 119px 0 0 80px;
  `}
  ${screen.tabletBelow`
    background-image: url(${replaceStorageUrlPrefix(`${storageUrlPrefix}/title-section5-mob.png`)});
  `}
  ${screen.tablet`
    width: 408px;
    height: 231px;
    background-position: left top;
  `}
  ${screen.mobile`
    background-position: center top;
    width: 247px;
    height: 154px;
    float: none;
    margin: 0 auto;
  `}
`

const Content = styled.div`
  width: 100%;
  ${screen.desktopAbove`
    height: 100%;
    float: right;
  `}
  ${screen.overDesktop`
    width: 616px;
    height: calc(100% - 98.7px);
    padding-right: 95px;
    margin-top: 98.7px;
  `}
  ${screen.desktop`
    width: 432px;
    height: calc(100% - 63px);
    padding-right: 61px;
    margin-top: 63px;
  `}
  ${screen.tabletAbove`
    position: relative;
  `}
  ${screen.tablet`
    height: 572px;
    padding-right: 71px;
    margin-top: 71px;
  `}
  ${screen.mobile`
    margin-top: 67px;
  `}
`

const BorderBottom = styled.div `
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: ${props => props.zIndex};
  background: ${colors.red.liverRed};
  ${screen.desktopAbove`
    position: ${props => props.fixed ? 'fixed' : 'absolute'};
  `}
  ${screen.tabletBelow`
    position: absolute;
  `}
  ${screen.overDesktop`
    height: 8px;
  `}
  ${screen.desktop`
    height: 6px;
  `}
  ${screen.tablet`
    height: 7px;
  `}
  ${screen.mobile`
    height: 6px;
  `}
`

const RunningTimeline = styled.div`
  position: relative;
  width: 100%;
  height: 100%;
  overflow-y: hidden;
  background: ${colors.white};
  ${screen.mobile`
    display: none;
  `}
`

const AccordionTimeline = styled.div`
  ${screen.tabletAbove`
    display: none;
  `}
`

const YearTag = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  transform: translateX(-100%) translateY(-100%);
  p{
    font-family: ${font.family.english.roboto}, ${font.family.sansSerifFallback};
    font-size: 13px;
    font-weight: ${font.weight.bold};
    background: ${colors.black};
    color: ${colors.white};
    padding: 2px;
  }
  ${screen.overDesktop`
    p{
      font-size: 15px;
    }
  `}
  ${screen.mobile`
    display: none;
  `}
`

const YearRange = styled.div`
  position: absolute;
  right: 0;
  top: 0;
  width: 140px;
  height: 140px;
  margin-top: -61px;
  border-top: solid 1px ${yearRangebgColor};
  border-right: solid 1px ${yearRangebgColor};
  p{
    display: block;
    width: 100%;
    font-size: 32px;
    letter-spacing: 0.2px;
    font-family: ${font.family.english.roboto}, ${font.family.sansSerifFallback};
    font-weight: ${font.weight.thin};
    background: ${yearRangebgColor};
    span{
      padding: 0 2.5px;
    }
  }
  p:first-child{
    position: absolute;
    right: 0;
    bottom: 0;
    transform-origin: right top;
    text-align: right;
    transform: translateY(100%) rotate(90deg);
    span{
      background: ${colors.black};
      color: ${colors.white};
    }
  }
  p:nth-child(2){
    position: absolute;
    left: 0;
    top: 0;
    span{
      background: ${colors.white};
      color: ${yearRangebgColor};
      border-collapse: collapse;
      box-shadow: inset 0 0 0 1px ${yearRangebgColor};
    }
    img{
      position: absolute;
      right: 0;
      top: 50%;
      margin-right: 13.25px;
    }
  }
  ${screen.overDesktop`
    width: 230px;
    height: 230px;
    margin-top: -95px;
    p{
      font-size: 53px;
      letter-spacing: 0.5px;
      line-height: 1.35;
    }
    p:nth-child(2){
      img{
        height: 78px;
        margin-right: 22.6px;
      }
    }
  `}
  ${screen.desktop`
    border: none;
    p:nth-child(2){
      img{
        height: 45px;
      }
    }
  `}
  ${screen.tablet`
    width: 148px;
    height: 148px;
    margin-top: -71px;
    p{
      font-size: 34px;
      letter-spacing: 0.13px;
      span{
        padding: 1px 2.5px;
      }
    }
    p:nth-child(2){
      img{
        height: 48px;
      }
    }
  `}
  ${screen.mobile`
    display: none;
  `}
`

const Circle = styled.div`
  position: absolute;
  left: 0;
  bottom: 0;
  width: 218px;
  height: 218px;
  border-radius: 50%;
  background: ${props => props.color};
  ${screen.overDesktop`
    width: 277px;
    height: 277px;
  `}
  ${screen.tabletBelow`
    display: none;
  `}
`

const OuterCircle = styled(Circle)`
  margin-left: 80px;
  margin-bottom: 228px;
  ${screen.overDesktop`
    width: 277px;
    margin-left: 152px;
    margin-bottom: 293px;
  `}
`

const InnerCircle = styled(Circle)`
  margin: 0;
  transform: translateX(50%) translateY(-50%);
`

const Rect = styled.div`
  position: absolute;
  right: 0;
  bottom: 0;
  width: 30px;
  height: 30px;
  margin: 0 -38px 0 0;
  background: ${props => props.color};
  ${screen.overDesktop`
    width: 45px;
    height: 45px;
    margin: 0 -51.5px 0 0;
  `}
`

export default class Section5 extends PureComponent {
  constructor(props) {
    super(props)
    this.yearContent = []
    this.isMobile = false
    this.initalOpenUp = true
    this.state = {
      timelineScrolling: false,
      timelineScrollingHeight: 0,
      yearContentHeight: [],
      unfoldArray: yearList.map(() => false),
      currentYear: yearList[0],
      isBorderBottomfixed: true
    }
  }
  /**
   * Manages the unfoldArray which represents whether content in each year is folded
   * @param {number} index
   */
  _foldAndUnfold = (index) => {
    let newUnfoldArray = [ ...this.state.unfoldArray ]
    newUnfoldArray[index] = !newUnfoldArray[index]
    this.setState({ unfoldArray: newUnfoldArray })
  }
  _onLeave = () => {
    this.setState({ isBorderBottomfixed: true })
    this._stopTimelineAutoScrolling()
  }
  _onEnter = () => {
    // unfold the first year
    let newUnfoldArray = [ ...this.state.unfoldArray ]
    if (this.initalOpenUp) {
      newUnfoldArray[0] = true
      this.initalOpenUp = !this.initalOpenUp
    }
    this.setState({
      isBorderBottomfixed: false,
      unfoldArray: newUnfoldArray
    })

    // to start or resume auto-scrolling
    this._startTimelineAutoScrolling()
  }
  _startTimelineAutoScrolling = () => {
    if (!this.isMobile) {
      this.setState({ timelineScrolling: true })
    }
  }
  _stopTimelineAutoScrolling = () => {
    this.setState({ timelineScrolling: false })
  }
  _getBorderZIndex = () => {
    return defaultZIndex + 2
  }
  /**
   * Set year which will be displayed on title according to parameter
   * @param {number} currentYear
   */
  _getYear = (currentYearIndex) => {
    this.setState({
      currentYear: yearList[currentYearIndex]
    })
  }
  /**
   * The function is used by list.js for computing the height of children in timeline after they are mounted
   * @param {Array} contentHeightArray
   */
  _getYearContentHeight = (contentHeightArray) => {
    this.setState({ yearContentHeight: contentHeightArray })
  }
  componentDidMount() {
    const timelineScrollingHeight = ReactDOM.findDOMNode(this.scrollingContent).getBoundingClientRect().height
    this.setState({ timelineScrollingHeight: timelineScrollingHeight })
    if (window.matchMedia(`(max-width: ${sz.mediumScreenMinWidth - 1}px)`).matches) {
      this.isMobile = true
    }
  }
  render() {
    return (
      <Waypoint
        onEnter={this._onEnter}
        onLeave={this._onLeave}
        scrollableAncestor="window"
        fireOnRapidScroll
        bottomOffset="80%"
      >

          <Container>
            <SectionWrapper>
              <OuterCircle color={`${colors.black}`}>
                <InnerCircle color={`${colors.gray.gray96}`}/>
                <Rect color={`${colors.secondaryColor}`}/>
              </OuterCircle>
              <Title>大事紀</Title>
              <Content>
                <AccordionTimeline>
                  <List
                    unfoldArray={this.state.unfoldArray}
                    sortedData={sortedData}
                    sortedDataGroupByYear={sortedDataGroupByYear}
                    foldAndUnfold={this._foldAndUnfold}
                    getYearContentHeight={this._getYearContentHeight}
                  />
                </AccordionTimeline>
                <YearRange>

{yearList[yearList.length - 1]}

                    {yearList[0]}
                    <img
                      src={`${replaceStorageUrlPrefix(`${storageUrlPrefix}/section5-arrow.png`)}`}
                    />

                </YearRange>
                <RunningTimeline>
                  <Timeline
                    ref={scrollingContent => this.scrollingContent = scrollingContent}
                    childrenHeight={this.state.timelineScrollingHeight * timelineScrollingPortion}
                    autoScrolling={this.state.timelineScrolling}
                    startAutoScroll={this._startTimelineAutoScrolling}
                    stopAutoScroll={this._stopTimelineAutoScrolling}
                    yearContentHeight={this.state.yearContentHeight}
                    getYear={this._getYear}>
                    <List
                      unfoldArray={this.state.unfoldArray}
                      sortedData={sortedData}
                      sortedDataGroupByYear={sortedDataGroupByYear}
                      foldAndUnfold={this._foldAndUnfold}
                      getYearContentHeight={this._getYearContentHeight}
                    />
                  </Timeline>
                </RunningTimeline>
                <YearTag>

{this.state.currentYear}

                </YearTag>
              </Content>
            </SectionWrapper>
            <BorderBottom
              fixed={this.state.isBorderBottomfixed}
              zIndex={this._getBorderZIndex()}
            />
          </Container>

      </Waypoint>
    )
  }
}
<|endoftext|>


<commit_before>(function(){
    module.exports = EventsQueue();

    function EventsQueue(){

        var _queue       = [],
            _eventsQueue = {};

        /*========================================================================
            PUBLIC
        ========================================================================*/
        _eventsQueue.subscribe  = subscribe;
        _eventsQueue.removeAll  = removeAll;

        return _eventsQueue;

        /*========================================================================
            PRIVATE
        ========================================================================*/

        function subscribe(eventName, element, subscriberFn){
            var knownQueueItem;
            for(var i = 0; i < _queue.length; i++){
                if(_queue[i].eventName === eventName && _queue[i].element === element){
                    _queue[i].events.push(subscriberFn);
                    knownQueueItem = _queue[i];
                    break;
                }
            }
            return (knownQueueItem || new EventObject(eventName, element, subscriberFn))['publicEvents'];
        }

        function EventObject(eventName, element, subscriberFn){

            var queueObject = {
                events      : [subscriberFn],
                element     : element,
                eventName   : eventName,
                publicEvents:{
                    unSubscribe:unSubscribe
                }
            };

            _queue.push(queueObject);

            element.addEventListener(eventName, onEvent);

            return queueObject;

            function onEvent(e){
                for(var i = 0; i < queueObject.events.length; i++){
                    queueObject.events[i].call(this, e);
                }
            }

            function unSubscribe(){

                var functionIdx = queueObject.events.indexOf(subscriberFn);

                if(functionIdx > -1 && queueObject){
                    queueObject.events.splice(functionIdx, 1);
                }

                if(queueObject.events.length <= 0){
                    element.removeEventListener(eventName, onEvent);
                    _queue.splice(_queue.indexOf(queueObject), 1);
                }
            }
        }

        function removeAll(){
            var l = _queue.length;
            while(l--){
                _queue[l].publicEvents.unSubscribe();
            }
        }
    }
})();<commit_msg>Updated events queue to allow for events to be triggered on demand if their is a matching subscribe method for the element and event name.<commit_after>(function(){
    module.exports = EventsQueue();

    function EventsQueue(){

        var _queue       = [],
            _eventsQueue = {};

        /*========================================================================
            PUBLIC
        ========================================================================*/
        _eventsQueue.subscribe     = subscribe;
        _eventsQueue.removeAll     = removeAll;
        _eventsQueue.triggerEvent  = triggerEvent;

        return _eventsQueue;

        /*========================================================================
            PRIVATE
        ========================================================================*/

        function subscribe(eventName, element, subscriberFn){
            var knownQueueItem;
            for(var i = 0; i < _queue.length; i++){
                if(_queue[i].eventName === eventName && _queue[i].element === element){
                    _queue[i].events.push(subscriberFn);
                    knownQueueItem = _queue[i];
                    break;
                }
            }
            return (knownQueueItem || new EventObject(eventName, element, subscriberFn))['publicEvents'];
        }

        function triggerEvent(eventName, element){
            var l = _queue.length;
            while(l--){
                if(_queue[l].element === element && _queue[l].eventName === eventName){
                    _queue[l].publicEvents.trigger();
                    return;
                }
            }
        }

        function EventObject(eventName, element, subscriberFn){

            var queueObject = {
                events      : [subscriberFn],
                element     : element,
                eventName   : eventName,
                    publicEvents:{
                        unSubscribe : unSubscribe,
                        trigger     : trigger
                    }
                },
                customEvent = document.createEvent('Event');

            customEvent.initEvent(eventName, true, true);
            element.addEventListener(eventName, onEvent);
            _queue.push(queueObject);

            return queueObject;

            function onEvent(e){
                for(var i = 0; i < queueObject.events.length; i++){
                    queueObject.events[i].call(this, e);
                }
            }

            function trigger(){
                element.dispatchEvent(customEvent);
            }

            function unSubscribe(){

                var functionIdx = queueObject.events.indexOf(subscriberFn);

                if(functionIdx > -1 && queueObject){
                    queueObject.events.splice(functionIdx, 1);
                }

                if(queueObject.events.length <= 0){
                    element.removeEventListener(eventName, onEvent);
                    _queue.splice(_queue.indexOf(queueObject), 1);
                }
            }
        }

        function removeAll(){
            var l = _queue.length;
            while(l--){
                _queue[l].publicEvents.unSubscribe();
            }
        }
    }
})();<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow

import * as React from 'react';
import { withSize } from '../shared/WithSize';
import explicitConnect from '../../utils/connect';
import { formatBytes } from '../../utils/format-numbers';
import {
  getCommittedRange,
  getCounterSelectors,
  getProfileInterval,
} from '../../selectors/profile';
import { getThreadSelectors } from '../../selectors/per-thread';
import { ORANGE_50 } from 'photon-colors';
import Tooltip from '../tooltip/Tooltip';
import EmptyThreadIndicator from './EmptyThreadIndicator';
import bisection from 'bisection';

import type {
  CounterIndex,
  Counter,
  Thread,
  ThreadIndex,
  AccumulatedCounterSamples,
  Milliseconds,
  CssPixels,
  StartEndRange,
} from 'firefox-profiler/types';

import type { SizeProps } from '../shared/WithSize';
import type { ConnectedProps } from '../../utils/connect';

import './TrackMemory.css';

/**
 * When adding properties to these props, please consider the comment above the component.
 */
type CanvasProps = {|
  +rangeStart: Milliseconds,
  +rangeEnd: Milliseconds,
  +counter: Counter,
  +accumulatedSamples: AccumulatedCounterSamples[],
  +interval: Milliseconds,
  +width: CssPixels,
  +height: CssPixels,
  +lineWidth: CssPixels,
|};

/**
 * This component controls the rendering of the canvas. Every render call through
 * React triggers a new canvas render. Because of this, it's important to only pass
 * in the props that are needed for the canvas draw call.
 */
class TrackMemoryCanvas extends React.PureComponent<CanvasProps> {
  _canvas: null | HTMLCanvasElement = null;
  _requestedAnimationFrame: boolean = false;

  drawCanvas(canvas: HTMLCanvasElement): void {
    const {
      rangeStart,
      rangeEnd,
      counter,
      height,
      width,
      lineWidth,
      interval,
      accumulatedSamples,
    } = this.props;
    if (width === 0) {
      // This is attempting to draw before the canvas was laid out.
      return;
    }

    const ctx = canvas.getContext('2d');
    const devicePixelRatio = window.devicePixelRatio;
    const deviceWidth = width * devicePixelRatio;
    const deviceHeight = height * devicePixelRatio;
    const deviceLineWidth = lineWidth * devicePixelRatio;
    const deviceLineHalfWidth = deviceLineWidth * 0.5;
    const innerDeviceHeight = deviceHeight - deviceLineWidth;
    const rangeLength = rangeEnd - rangeStart;
    const millisecondWidth = deviceWidth / rangeLength;
    const intervalWidth = interval * millisecondWidth;

    // Resize and clear the canvas.
    canvas.width = Math.round(deviceWidth);
    canvas.height = Math.round(deviceHeight);
    ctx.clearRect(0, 0, deviceWidth, deviceHeight);

    const sampleGroups = counter.sampleGroups;
    if (sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and do not draw anything.
      throw new Error('No sample group found for memory counter');
    }

    const samples = counter.sampleGroups[0].samples;
    if (samples.length === 0) {
      // There's no reason to draw the samples, there are none.
      return;
    }

    // Take the sample information, and convert it into chart coordinates. Use a slightly
    // smaller space than the deviceHeight, so that the stroke will be fully visible
    // both at the top and bottom of the chart.
    if (accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const { minCount, countRange, accumulatedCounts } = accumulatedSamples[0];

    {
      // Draw the chart.
      //
      //                 ...--`
      //  1 ...---```..--      `--. 2
      //    |_____________________|
      //  4                        3
      //
      // Start by drawing from 1 - 2. This will be the top of all the peaks of the
      // memory graph.

      ctx.lineWidth = deviceLineWidth;
      ctx.strokeStyle = ORANGE_50;
      ctx.fillStyle = '#ff940088'; // Orange 50 with transparency.
      ctx.beginPath();

      // The x and y are used after the loop.
      let x = 0;
      let y = 0;
      let firstX = 0;
      for (let i = 0; i < samples.length; i++) {
        // Create a path for the top of the chart. This is the line that will have
        // a stroke applied to it.
        x = (samples.time[i] - rangeStart) * millisecondWidth;
        // Add on half the stroke's line width so that it won't be cut off the edge
        // of the graph.
        const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange;
        y =
          innerDeviceHeight -
          innerDeviceHeight * unitGraphCount +
          deviceLineHalfWidth;
        if (i === 0) {
          // This is the first iteration, only move the line, do not draw it. Also
          // remember this first X, as the bottom of the graph will need to connect
          // back up to it.
          firstX = x;
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }
      // The samples range ends at the time of the last sample, plus the interval.
      // Draw this last bit.
      ctx.lineTo(x + intervalWidth, y);

      // Don't do the fill yet, just stroke the top line. This will draw a line from
      // point 1 to 2 in the diagram above.
      ctx.stroke();

      // After doing the stroke, continue the path to complete the fill to the bottom
      // of the canvas. This continues the path to point 3 and then 4.

      // Create a line from 2 to 3.
      ctx.lineTo(x + intervalWidth, deviceHeight);

      // Create a line from 3 to 4.
      ctx.lineTo(firstX, deviceHeight);

      // The line from 4 to 1 will be implicitly filled in.
      ctx.fill();
    }
  }

  _scheduleDraw() {
    if (!this._requestedAnimationFrame) {
      this._requestedAnimationFrame = true;
      window.requestAnimationFrame(() => {
        this._requestedAnimationFrame = false;
        const canvas = this._canvas;
        if (canvas) {
          this.drawCanvas(canvas);
        }
      });
    }
  }

  _takeCanvasRef = (canvas: HTMLCanvasElement | null) => {
    this._canvas = canvas;
  };

  render() {
    this._scheduleDraw();

    return (
      <canvas className="timelineTrackMemoryCanvas" ref={this._takeCanvasRef} />
    );
  }
}

type OwnProps = {|
  +counterIndex: CounterIndex,
  +lineWidth: CssPixels,
  +graphHeight: CssPixels,
|};

type StateProps = {|
  +threadIndex: ThreadIndex,
  +rangeStart: Milliseconds,
  +rangeEnd: Milliseconds,
  +counter: Counter,
  +accumulatedSamples: AccumulatedCounterSamples[],
  +interval: Milliseconds,
  +filteredThread: Thread,
  +unfilteredSamplesRange: StartEndRange | null,
|};

type DispatchProps = {||};

type Props = {|
  ...SizeProps,
  ...ConnectedProps<OwnProps, StateProps, DispatchProps>,
|};

type State = {|
  hoveredCounter: null | number,
  mouseX: CssPixels,
  mouseY: CssPixels,
|};

/**
 * The memory track graph takes memory information from counters, and renders it as a
 * graph in the timeline.
 */
class TrackMemoryGraphImpl extends React.PureComponent<Props, State> {
  state = {
    hoveredCounter: null,
    mouseX: 0,
    mouseY: 0,
  };

  _onMouseLeave = () => {
    this.setState({ hoveredCounter: null });
  };

  _onMouseMove = (event: SyntheticMouseEvent<HTMLDivElement>) => {
    const { pageX: mouseX, pageY: mouseY } = event;
    // Get the offset from here, and apply it to the time lookup.
    const { left } = event.currentTarget.getBoundingClientRect();
    const { width, rangeStart, rangeEnd, counter, interval } = this.props;
    const rangeLength = rangeEnd - rangeStart;
    const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength;

    if (counter.sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No sample group found for memory counter');
    }
    const { samples } = counter.sampleGroups[0];

    if (
      timeAtMouse < samples.time[0] ||
      timeAtMouse > samples.time[samples.length - 1] + interval
    ) {
      // We are outside the range of the samples, do not display hover information.
      this.setState({ hoveredCounter: null });
    } else {
      let hoveredCounter;
      if (
        bisection.right(samples.time, timeAtMouse) > 0 &&
        bisection.right(samples.time, timeAtMouse) < samples.time.length
      ) {
        const leftDistance =
          timeAtMouse -
          samples.time[bisection.right(samples.time, timeAtMouse) - 1];
        const rightDistance =
          samples.time[bisection.right(samples.time, timeAtMouse)] -
          timeAtMouse;
        if (leftDistance < rightDistance) {
          hoveredCounter = bisection.right(samples.time, timeAtMouse) - 1;
        } else {
          hoveredCounter = bisection.right(samples.time, timeAtMouse);
        }
      } else {
        hoveredCounter = bisection.right(samples.time, timeAtMouse);
      }

      if (hoveredCounter === samples.length) {
        // When hovering the last sample, it's possible the mouse is past the time.
        // In this case, hover over the last sample. This happens because of the
        // ` + interval` line in the `if` condition above.
        hoveredCounter = samples.time.length - 1;
      }

      this.setState({
        mouseX,
        mouseY,
        hoveredCounter,
      });
    }
  };

  _renderTooltip(counterIndex: number): React.Node {
    if (this.props.accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const {
      minCount,
      countRange,
      accumulatedCounts,
    } = this.props.accumulatedSamples[0];
    const bytes = accumulatedCounts[counterIndex] - minCount;
    return (
      <div className="timelineTrackMemoryTooltip">
        <div className="timelineTrackMemoryTooltipLine">
          <span className="timelineTrackMemoryTooltipNumber">
            {formatBytes(bytes)}
          </span>
          {' relative memory at this time'}
        </div>
        <div className="timelineTrackMemoryTooltipLine">
          <span className="timelineTrackMemoryTooltipNumber">
            {formatBytes(countRange)}
          </span>
          {' memory range in graph'}
        </div>
      </div>
    );
  }

  /**
   * Create a div that is a dot on top of the graph representing the current
   * height of the graph.
   */
  _renderMemoryDot(counterIndex: number): React.Node {
    const {
      counter,
      rangeStart,
      rangeEnd,
      graphHeight,
      width,
      lineWidth,
      accumulatedSamples,
    } = this.props;

    if (counter.sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No sample group found for memory counter');
    }
    const { samples } = counter.sampleGroups[0];
    const rangeLength = rangeEnd - rangeStart;
    const left =
      (width * (samples.time[counterIndex] - rangeStart)) / rangeLength;

    if (accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const { minCount, countRange, accumulatedCounts } = accumulatedSamples[0];
    const unitSampleCount =
      (accumulatedCounts[counterIndex] - minCount) / countRange;
    const innerTrackHeight = graphHeight - lineWidth / 2;
    const top =
      innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2;

    return (
      <div style={{ left, top }} className="timelineTrackMemoryGraphDot" />
    );
  }

  render() {
    const { hoveredCounter, mouseX, mouseY } = this.state;
    const {
      filteredThread,
      interval,
      rangeStart,
      rangeEnd,
      unfilteredSamplesRange,
      counter,
      graphHeight,
      width,
      lineWidth,
      accumulatedSamples,
    } = this.props;

    return (
      <div
        className="timelineTrackMemoryGraph"
        onMouseMove={this._onMouseMove}
        onMouseLeave={this._onMouseLeave}
      >
        <TrackMemoryCanvas
          rangeStart={rangeStart}
          rangeEnd={rangeEnd}
          counter={counter}
          height={graphHeight}
          width={width}
          lineWidth={lineWidth}
          interval={interval}
          accumulatedSamples={accumulatedSamples}
        />
        {hoveredCounter === null ? null : (
          <>
            {this._renderMemoryDot(hoveredCounter)}
            <Tooltip mouseX={mouseX} mouseY={mouseY}>
              {this._renderTooltip(hoveredCounter)}
            </Tooltip>
          </>
        )}
        <EmptyThreadIndicator
          thread={filteredThread}
          interval={interval}
          rangeStart={rangeStart}
          rangeEnd={rangeEnd}
          unfilteredSamplesRange={unfilteredSamplesRange}
        />
      </div>
    );
  }
}

export const TrackMemoryGraph = explicitConnect<
  OwnProps,
  StateProps,
  DispatchProps
>({
  mapStateToProps: (state, ownProps) => {
    const { counterIndex } = ownProps;
    const counterSelectors = getCounterSelectors(counterIndex);
    const counter = counterSelectors.getCommittedRangeFilteredCounter(state);
    const { start, end } = getCommittedRange(state);
    const selectors = getThreadSelectors(counter.mainThreadIndex);
    return {
      counter,
      threadIndex: counter.mainThreadIndex,
      accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state),
      rangeStart: start,
      rangeEnd: end,
      interval: getProfileInterval(state),
      filteredThread: selectors.getFilteredThread(state),
      unfilteredSamplesRange: selectors.unfilteredSamplesRange(state),
    };
  },
  component: withSize<Props>(TrackMemoryGraphImpl),
});
<commit_msg>Avoid multiple calls to bisection.right<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow

import * as React from 'react';
import { withSize } from '../shared/WithSize';
import explicitConnect from '../../utils/connect';
import { formatBytes } from '../../utils/format-numbers';
import {
  getCommittedRange,
  getCounterSelectors,
  getProfileInterval,
} from '../../selectors/profile';
import { getThreadSelectors } from '../../selectors/per-thread';
import { ORANGE_50 } from 'photon-colors';
import Tooltip from '../tooltip/Tooltip';
import EmptyThreadIndicator from './EmptyThreadIndicator';
import bisection from 'bisection';

import type {
  CounterIndex,
  Counter,
  Thread,
  ThreadIndex,
  AccumulatedCounterSamples,
  Milliseconds,
  CssPixels,
  StartEndRange,
} from 'firefox-profiler/types';

import type { SizeProps } from '../shared/WithSize';
import type { ConnectedProps } from '../../utils/connect';

import './TrackMemory.css';

/**
 * When adding properties to these props, please consider the comment above the component.
 */
type CanvasProps = {|
  +rangeStart: Milliseconds,
  +rangeEnd: Milliseconds,
  +counter: Counter,
  +accumulatedSamples: AccumulatedCounterSamples[],
  +interval: Milliseconds,
  +width: CssPixels,
  +height: CssPixels,
  +lineWidth: CssPixels,
|};

/**
 * This component controls the rendering of the canvas. Every render call through
 * React triggers a new canvas render. Because of this, it's important to only pass
 * in the props that are needed for the canvas draw call.
 */
class TrackMemoryCanvas extends React.PureComponent<CanvasProps> {
  _canvas: null | HTMLCanvasElement = null;
  _requestedAnimationFrame: boolean = false;

  drawCanvas(canvas: HTMLCanvasElement): void {
    const {
      rangeStart,
      rangeEnd,
      counter,
      height,
      width,
      lineWidth,
      interval,
      accumulatedSamples,
    } = this.props;
    if (width === 0) {
      // This is attempting to draw before the canvas was laid out.
      return;
    }

    const ctx = canvas.getContext('2d');
    const devicePixelRatio = window.devicePixelRatio;
    const deviceWidth = width * devicePixelRatio;
    const deviceHeight = height * devicePixelRatio;
    const deviceLineWidth = lineWidth * devicePixelRatio;
    const deviceLineHalfWidth = deviceLineWidth * 0.5;
    const innerDeviceHeight = deviceHeight - deviceLineWidth;
    const rangeLength = rangeEnd - rangeStart;
    const millisecondWidth = deviceWidth / rangeLength;
    const intervalWidth = interval * millisecondWidth;

    // Resize and clear the canvas.
    canvas.width = Math.round(deviceWidth);
    canvas.height = Math.round(deviceHeight);
    ctx.clearRect(0, 0, deviceWidth, deviceHeight);

    const sampleGroups = counter.sampleGroups;
    if (sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and do not draw anything.
      throw new Error('No sample group found for memory counter');
    }

    const samples = counter.sampleGroups[0].samples;
    if (samples.length === 0) {
      // There's no reason to draw the samples, there are none.
      return;
    }

    // Take the sample information, and convert it into chart coordinates. Use a slightly
    // smaller space than the deviceHeight, so that the stroke will be fully visible
    // both at the top and bottom of the chart.
    if (accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const { minCount, countRange, accumulatedCounts } = accumulatedSamples[0];

    {
      // Draw the chart.
      //
      //                 ...--`
      //  1 ...---```..--      `--. 2
      //    |_____________________|
      //  4                        3
      //
      // Start by drawing from 1 - 2. This will be the top of all the peaks of the
      // memory graph.

      ctx.lineWidth = deviceLineWidth;
      ctx.strokeStyle = ORANGE_50;
      ctx.fillStyle = '#ff940088'; // Orange 50 with transparency.
      ctx.beginPath();

      // The x and y are used after the loop.
      let x = 0;
      let y = 0;
      let firstX = 0;
      for (let i = 0; i < samples.length; i++) {
        // Create a path for the top of the chart. This is the line that will have
        // a stroke applied to it.
        x = (samples.time[i] - rangeStart) * millisecondWidth;
        // Add on half the stroke's line width so that it won't be cut off the edge
        // of the graph.
        const unitGraphCount = (accumulatedCounts[i] - minCount) / countRange;
        y =
          innerDeviceHeight -
          innerDeviceHeight * unitGraphCount +
          deviceLineHalfWidth;
        if (i === 0) {
          // This is the first iteration, only move the line, do not draw it. Also
          // remember this first X, as the bottom of the graph will need to connect
          // back up to it.
          firstX = x;
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }
      // The samples range ends at the time of the last sample, plus the interval.
      // Draw this last bit.
      ctx.lineTo(x + intervalWidth, y);

      // Don't do the fill yet, just stroke the top line. This will draw a line from
      // point 1 to 2 in the diagram above.
      ctx.stroke();

      // After doing the stroke, continue the path to complete the fill to the bottom
      // of the canvas. This continues the path to point 3 and then 4.

      // Create a line from 2 to 3.
      ctx.lineTo(x + intervalWidth, deviceHeight);

      // Create a line from 3 to 4.
      ctx.lineTo(firstX, deviceHeight);

      // The line from 4 to 1 will be implicitly filled in.
      ctx.fill();
    }
  }

  _scheduleDraw() {
    if (!this._requestedAnimationFrame) {
      this._requestedAnimationFrame = true;
      window.requestAnimationFrame(() => {
        this._requestedAnimationFrame = false;
        const canvas = this._canvas;
        if (canvas) {
          this.drawCanvas(canvas);
        }
      });
    }
  }

  _takeCanvasRef = (canvas: HTMLCanvasElement | null) => {
    this._canvas = canvas;
  };

  render() {
    this._scheduleDraw();

    return (
      <canvas className="timelineTrackMemoryCanvas" ref={this._takeCanvasRef} />
    );
  }
}

type OwnProps = {|
  +counterIndex: CounterIndex,
  +lineWidth: CssPixels,
  +graphHeight: CssPixels,
|};

type StateProps = {|
  +threadIndex: ThreadIndex,
  +rangeStart: Milliseconds,
  +rangeEnd: Milliseconds,
  +counter: Counter,
  +accumulatedSamples: AccumulatedCounterSamples[],
  +interval: Milliseconds,
  +filteredThread: Thread,
  +unfilteredSamplesRange: StartEndRange | null,
|};

type DispatchProps = {||};

type Props = {|
  ...SizeProps,
  ...ConnectedProps<OwnProps, StateProps, DispatchProps>,
|};

type State = {|
  hoveredCounter: null | number,
  mouseX: CssPixels,
  mouseY: CssPixels,
|};

/**
 * The memory track graph takes memory information from counters, and renders it as a
 * graph in the timeline.
 */
class TrackMemoryGraphImpl extends React.PureComponent<Props, State> {
  state = {
    hoveredCounter: null,
    mouseX: 0,
    mouseY: 0,
  };

  _onMouseLeave = () => {
    this.setState({ hoveredCounter: null });
  };

  _onMouseMove = (event: SyntheticMouseEvent<HTMLDivElement>) => {
    const { pageX: mouseX, pageY: mouseY } = event;
    // Get the offset from here, and apply it to the time lookup.
    const { left } = event.currentTarget.getBoundingClientRect();
    const { width, rangeStart, rangeEnd, counter, interval } = this.props;
    const rangeLength = rangeEnd - rangeStart;
    const timeAtMouse = rangeStart + ((mouseX - left) / width) * rangeLength;

    if (counter.sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No sample group found for memory counter');
    }
    const { samples } = counter.sampleGroups[0];

    if (
      timeAtMouse < samples.time[0] ||
      timeAtMouse > samples.time[samples.length - 1] + interval
    ) {
      // We are outside the range of the samples, do not display hover information.
      this.setState({ hoveredCounter: null });
    } else {
      let hoveredCounter;
      const bisectionCounter = bisection.right(samples.time, timeAtMouse);
      if (bisectionCounter > 0 && bisectionCounter < samples.time.length) {
        const leftDistance = timeAtMouse - samples.time[bisectionCounter - 1];
        const rightDistance = samples.time[bisectionCounter] - timeAtMouse;
        if (leftDistance < rightDistance) {
          hoveredCounter = bisectionCounter - 1;
        } else {
          hoveredCounter = bisectionCounter;
        }
      } else {
        hoveredCounter = bisectionCounter;
      }

      if (hoveredCounter === samples.length) {
        // When hovering the last sample, it's possible the mouse is past the time.
        // In this case, hover over the last sample. This happens because of the
        // ` + interval` line in the `if` condition above.
        hoveredCounter = samples.time.length - 1;
      }

      this.setState({
        mouseX,
        mouseY,
        hoveredCounter,
      });
    }
  };

  _renderTooltip(counterIndex: number): React.Node {
    if (this.props.accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const {
      minCount,
      countRange,
      accumulatedCounts,
    } = this.props.accumulatedSamples[0];
    const bytes = accumulatedCounts[counterIndex] - minCount;
    return (
      <div className="timelineTrackMemoryTooltip">
        <div className="timelineTrackMemoryTooltipLine">
          <span className="timelineTrackMemoryTooltipNumber">
            {formatBytes(bytes)}
          </span>
          {' relative memory at this time'}
        </div>
        <div className="timelineTrackMemoryTooltipLine">
          <span className="timelineTrackMemoryTooltipNumber">
            {formatBytes(countRange)}
          </span>
          {' memory range in graph'}
        </div>
      </div>
    );
  }

  /**
   * Create a div that is a dot on top of the graph representing the current
   * height of the graph.
   */
  _renderMemoryDot(counterIndex: number): React.Node {
    const {
      counter,
      rangeStart,
      rangeEnd,
      graphHeight,
      width,
      lineWidth,
      accumulatedSamples,
    } = this.props;

    if (counter.sampleGroups.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No sample group found for memory counter');
    }
    const { samples } = counter.sampleGroups[0];
    const rangeLength = rangeEnd - rangeStart;
    const left =
      (width * (samples.time[counterIndex] - rangeStart)) / rangeLength;

    if (accumulatedSamples.length === 0) {
      // Gecko failed to capture samples for some reason and it shouldn't happen for
      // malloc counter. Print an error and bail out early.
      throw new Error('No accumulated sample found for memory counter');
    }
    const { minCount, countRange, accumulatedCounts } = accumulatedSamples[0];
    const unitSampleCount =
      (accumulatedCounts[counterIndex] - minCount) / countRange;
    const innerTrackHeight = graphHeight - lineWidth / 2;
    const top =
      innerTrackHeight - unitSampleCount * innerTrackHeight + lineWidth / 2;

    return (
      <div style={{ left, top }} className="timelineTrackMemoryGraphDot" />
    );
  }

  render() {
    const { hoveredCounter, mouseX, mouseY } = this.state;
    const {
      filteredThread,
      interval,
      rangeStart,
      rangeEnd,
      unfilteredSamplesRange,
      counter,
      graphHeight,
      width,
      lineWidth,
      accumulatedSamples,
    } = this.props;

    return (
      <div
        className="timelineTrackMemoryGraph"
        onMouseMove={this._onMouseMove}
        onMouseLeave={this._onMouseLeave}
      >
        <TrackMemoryCanvas
          rangeStart={rangeStart}
          rangeEnd={rangeEnd}
          counter={counter}
          height={graphHeight}
          width={width}
          lineWidth={lineWidth}
          interval={interval}
          accumulatedSamples={accumulatedSamples}
        />
        {hoveredCounter === null ? null : (
          <>
            {this._renderMemoryDot(hoveredCounter)}
            <Tooltip mouseX={mouseX} mouseY={mouseY}>
              {this._renderTooltip(hoveredCounter)}
            </Tooltip>
          </>
        )}
        <EmptyThreadIndicator
          thread={filteredThread}
          interval={interval}
          rangeStart={rangeStart}
          rangeEnd={rangeEnd}
          unfilteredSamplesRange={unfilteredSamplesRange}
        />
      </div>
    );
  }
}

export const TrackMemoryGraph = explicitConnect<
  OwnProps,
  StateProps,
  DispatchProps
>({
  mapStateToProps: (state, ownProps) => {
    const { counterIndex } = ownProps;
    const counterSelectors = getCounterSelectors(counterIndex);
    const counter = counterSelectors.getCommittedRangeFilteredCounter(state);
    const { start, end } = getCommittedRange(state);
    const selectors = getThreadSelectors(counter.mainThreadIndex);
    return {
      counter,
      threadIndex: counter.mainThreadIndex,
      accumulatedSamples: counterSelectors.getAccumulateCounterSamples(state),
      rangeStart: start,
      rangeEnd: end,
      interval: getProfileInterval(state),
      filteredThread: selectors.getFilteredThread(state),
      unfilteredSamplesRange: selectors.unfilteredSamplesRange(state),
    };
  },
  component: withSize<Props>(TrackMemoryGraphImpl),
});
<|endoftext|>


<commit_before>import { ObjectID as objectId } from 'mongodb';
import omit from 'lodash/omit';
import { fromMongoCursor, fromMongo } from './helpers';

export const createCRUDResolvers = ({
  getRepository,
  getLoaders,
  canDelete = () => true,
  canUpdate = () => true,
  findManyQuery = () => ({}),
}) => ({
  findOne: (_, { id }, ctx) => getLoaders(ctx).findById.load(id),
  findMany: (_, args, ctx) => getLoaders(ctx).findMany.load(findManyQuery(args, ctx)),
  createOne: async (_, { input }, ctx) => fromMongo(await getRepository(ctx).createOne(input)),
  updateOne: async (_, { id, input, item: loadedItem }, ctx) => {
    const Repository = getRepository(ctx);
    const item = loadedItem || (await Repository.findById(objectId(id)));

    if (!item) {
      throw new Error(`Unable to find item with id: ${id}!`);
    }

    if (!canUpdate(ctx.user, item)) {
      throw new Error('Not Allowed!');
    }

    await Repository.validate(
      omit(
        {
          ...item,
          ...input,
        },
        ['_id'],
      ),
    );

    return fromMongo(
      await Repository.updateOne({
        query: {
          _id: item._id,
        },
        update: {
          $set: input,
        },
      }),
    );
  },
  deleteOne: async (_, { id, item: loadedItem }, ctx) => {
    const Repository = getRepository(ctx);
    const item = loadedItem || (await Repository.findById(objectId(id)));

    if (!item) {
      throw new Error(`Unable to find item with id: ${id}!`);
    }

    if (!canDelete(ctx.user, item)) {
      throw new Error('Not Allowed!');
    }

    return fromMongo(await Repository.deleteOne({ query: { _id: item._id } }));
  },
});

export const createLoaders = Repository => ({
  findById: {
    loader: async ids => {
      const items = await Repository.findMany({
        query: {
          _id: {
            $in: ids.map(objectId),
          },
        },
      }).then(fromMongoCursor);

      return ids.map(id => items.find(item => item.id.toString() === id.toString()) || null);
    },
    options: {
      cacheKeyFn: key => key.toString(),
    },
  },
  findOne: {
    loader: queries =>
      Promise.all(
        queries.map(query =>
          Repository.findOne({
            query,
          }).then(fromMongo),
        ),
      ),
    options: {
      cacheKeyFn: key => JSON.stringify(key),
    },
  },
  findMany: {
    loader: queries =>
      Promise.all(queries.map((query = {}) => Repository.findMany(query).then(fromMongoCursor))),
    options: {
      cacheKeyFn: key => JSON.stringify(key),
    },
  },
});
<commit_msg>[oors-mongodb] CRUD resolvers improvements<commit_after>import { ObjectID as objectId } from 'mongodb';
import omit from 'lodash/omit';
import { fromMongoCursor, fromMongo } from './helpers';

export const createCRUDResolvers = ({
  getRepository,
  getLoaders,
  canDelete = () => true,
  canUpdate = () => true,
  findManyQuery = () => ({}),
  findOneQuery = ({ id }) => ({ query: { _id: id } }),
}) => ({
  findById: (_, { id }, ctx) => getLoaders(ctx).findById.load(id),
  findOne: (_, args, ctx) => getLoaders(ctx).findOne.load(findOneQuery(args, ctx)),
  findMany: (_, args, ctx) => getLoaders(ctx).findMany.load(findManyQuery(args, ctx)),
  createOne: async (_, { input }, ctx) => fromMongo(await getRepository(ctx).createOne(input)),
  updateOne: async (_, { id, input, item: loadedItem }, ctx) => {
    const Repository = getRepository(ctx);
    const item = loadedItem || (await Repository.findById(objectId(id)));

    if (!item) {
      throw new Error(`Unable to find item with id: ${id}!`);
    }

    if (!canUpdate(ctx.user, item)) {
      throw new Error('Not Allowed!');
    }

    await Repository.validate(
      omit(
        {
          ...item,
          ...input,
        },
        ['_id'],
      ),
    );

    return fromMongo(
      await Repository.updateOne({
        query: {
          _id: item._id,
        },
        update: {
          $set: input,
        },
      }),
    );
  },
  deleteOne: async (_, { id, item: loadedItem }, ctx) => {
    const Repository = getRepository(ctx);
    const item = loadedItem || (await Repository.findById(objectId(id)));

    if (!item) {
      throw new Error(`Unable to find item with id: ${id}!`);
    }

    if (!canDelete(ctx.user, item)) {
      throw new Error('Not Allowed!');
    }

    return fromMongo(await Repository.deleteOne({ query: { _id: item._id } }));
  },
});

export const createLoaders = Repository => ({
  findById: {
    loader: async ids => {
      const items = await Repository.findMany({
        query: {
          _id: {
            $in: ids.map(objectId),
          },
        },
      }).then(fromMongoCursor);

      return ids.map(id => items.find(item => item.id.toString() === id.toString()) || null);
    },
    options: {
      cacheKeyFn: key => key.toString(),
    },
  },
  findOne: {
    loader: queries =>
      Promise.all(
        queries.map(query =>
          Repository.findOne({
            query,
          }).then(fromMongo),
        ),
      ),
    options: {
      cacheKeyFn: key => JSON.stringify(key),
    },
  },
  findMany: {
    loader: queries =>
      Promise.all(queries.map((query = {}) => Repository.findMany(query).then(fromMongoCursor))),
    options: {
      cacheKeyFn: key => JSON.stringify(key),
    },
  },
});
<|endoftext|>


<commit_before>/*
Copyright Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';

export default React.createClass({
    displayName: 'RoleButton',

    propTypes: {
        role: PropTypes.string.isRequired,
        size: PropTypes.string,
    },

    getDefaultProps: function() {
        return {
            size: 25,
        };
    },

    _onClick: function(ev) {
        ev.stopPropagation();

        let action;
        switch(this.props.role) {
            case 'start_chat':
                action = 'view_create_chat';
                break;
            case 'room_directory':
                action = 'view_room_directory';
                break;
            case 'create_room':
                action = 'view_create_room';
                break;
        }
        if (action) dis.dispatch({action: action});
    },

    _getIconPath() {
        switch(this.props.role) {
            case 'start_chat':
                return 'img/icons-people.svg';
            case 'room_directory':
                return 'img/icons-directory.svg';
            case 'create_room':
                return 'img/icons-create-room.svg';
        }
    },

    render: function() {
        const TintableSvg = sdk.getComponent("elements.TintableSvg");

        return (
            <AccessibleButton className="mx_RoleButton" onClick={ this._onClick }>
                <TintableSvg src={this._getIconPath()} width={this.props.size} height={this.props.size} />
            </AccessibleButton>
        );
    }
});
<commit_msg>What year is it? Who's the president?<commit_after>/*
Copyright 2017 Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';

export default React.createClass({
    displayName: 'RoleButton',

    propTypes: {
        role: PropTypes.string.isRequired,
        size: PropTypes.string,
    },

    getDefaultProps: function() {
        return {
            size: 25,
        };
    },

    _onClick: function(ev) {
        ev.stopPropagation();

        let action;
        switch(this.props.role) {
            case 'start_chat':
                action = 'view_create_chat';
                break;
            case 'room_directory':
                action = 'view_room_directory';
                break;
            case 'create_room':
                action = 'view_create_room';
                break;
        }
        if (action) dis.dispatch({action: action});
    },

    _getIconPath() {
        switch(this.props.role) {
            case 'start_chat':
                return 'img/icons-people.svg';
            case 'room_directory':
                return 'img/icons-directory.svg';
            case 'create_room':
                return 'img/icons-create-room.svg';
        }
    },

    render: function() {
        const TintableSvg = sdk.getComponent("elements.TintableSvg");

        return (
            <AccessibleButton className="mx_RoleButton" onClick={ this._onClick }>
                <TintableSvg src={this._getIconPath()} width={this.props.size} height={this.props.size} />
            </AccessibleButton>
        );
    }
});
<|endoftext|>