<commit_before>// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes, Children } from 'react';
import { findDOMNode } from 'react-dom';
import classnames from 'classnames';
import Props from '../utils/Props';
import Box from './Box';
import Button from './Button';
import SpinningIcon from './icons/Spinning';
import Scroll from '../utils/Scroll';
import InfiniteScroll from '../utils/InfiniteScroll';
import Selection from '../utils/Selection';
import KeyboardAccelerators from '../utils/KeyboardAccelerators';
import Intl from '../utils/Intl';
import { announce } from '../utils/Announcer';

import LinkPreviousIcon from './icons/base/LinkPrevious';
import LinkNextIcon from './icons/base/LinkNext';
import CSSClassnames from '../utils/CSSClassnames';

const CLASS_ROOT = CSSClassnames.TILES;
const TILE = CSSClassnames.TILE;
const SELECTED_CLASS = `${TILE}--selected`;
const ACTIVE_CLASS = `${TILE}--active`;

export default class Tiles extends Component {

  constructor(props, context) {
    super(props, context);
    this._onLeft = this._onLeft.bind(this);
    this._onRight = this._onRight.bind(this);
    this._onScrollHorizontal = this._onScrollHorizontal.bind(this);
    this._onWheel = this._onWheel.bind(this);
    this._onResize = this._onResize.bind(this);
    this._layout = this._layout.bind(this);
    this._onClick = this._onClick.bind(this);
    this._fireClick = this._fireClick.bind(this);
    this._announceTile = this._announceTile.bind(this);
    this._onPreviousTile = this._onPreviousTile.bind(this);
    this._onNextTile = this._onNextTile.bind(this);
    this._onEnter = this._onEnter.bind(this);

    this.state = {
      activeTile: undefined,
      mouseActive: false,
      overflow: false,
      selected: Selection.normalizeIndexes(props.selected)
    };
  }

  componentDidMount () {
    const { direction, onMore, selectable } = this.props;
    this._setSelection();
    if (onMore) {
      this._scroll = InfiniteScroll.startListeningForScroll(this.moreRef,
        onMore);
    }
    if ('row' === direction) {
      window.addEventListener('resize', this._onResize);
      document.addEventListener('wheel', this._onWheel, { passive: true });
      this._trackHorizontalScroll();
      // give browser a chance to stabilize
      setTimeout(this._layout, 10);
    }
    if (selectable) {
      // only listen for navigation keys if the tile row can be selected
      this._keyboardHandlers = {
        left: this._onPreviousTile,
        up: this._onPreviousTile,
        right: this._onNextTile,
        down: this._onNextTile,
        enter: this._onEnter,
        space: this._onEnter
      };
      KeyboardAccelerators.startListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.selected !== undefined) {
      this.setState({
        selected: Selection.normalizeIndexes(nextProps.selected)
      });
    }
    if (this._scroll) {
      InfiniteScroll.stopListeningForScroll(this._scroll);
      this._scroll = undefined;
    }
  }

  componentDidUpdate (prevProps, prevState) {
    const { direction, onMore, selectable } = this.props;
    const { selected } = this.state;
    if (JSON.stringify(selected) !==
      JSON.stringify(prevState.selected)) {
      this._setSelection();
    }
    if (onMore && !this._scroll) {
      this._scroll = InfiniteScroll.startListeningForScroll(this.moreRef,
        onMore);
    }
    if ('row' === direction) {
      this._trackHorizontalScroll();
      // give browser a chance to stabilize
      setTimeout(this._layout, 10);
    }
    if (selectable) {
      // only listen for navigation keys if the list row can be selected
      this._keyboardHandlers = {
        left: this._onPreviousTile,
        up: this._onPreviousTile,
        right: this._onNextTile,
        down: this._onNextTile,
        enter: this._onEnter,
        space: this._onEnter
      };
      KeyboardAccelerators.startListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  componentWillUnmount () {
    const { direction, selectable } = this.props;
    if (this._scroll) {
      InfiniteScroll.stopListeningForScroll(this._scroll);
    }
    if ('row' === direction) {
      window.removeEventListener('resize', this._onResize);
      document.removeEventListener('wheel', this._onWheel);
      if (this._tracking) {
        const tiles = findDOMNode(this.tilesRef);
        tiles.removeEventListener('scroll', this._onScrollHorizontal);
      }
    }
    if (selectable) {
      KeyboardAccelerators.stopListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  _announceTile (label) {
    const { intl } = this.context;
    const enterSelectMessage = Intl.getMessage(intl, 'Enter Select');
    // avoid a long text to be read by the screen reader
    const labelMessage = label.length > 15 ?
      `${label.substring(0, 15)}...` : label;
    announce(`${labelMessage} ${enterSelectMessage}`);
  }

  _onPreviousTile (event) {
    if (findDOMNode(this.tilesRef).contains(document.activeElement)) {
      event.preventDefault();
      const { activeTile } = this.state;
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      if (rows && rows.length > 0) {
        if (activeTile === undefined) {
          rows[0].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: 0 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        } else if (activeTile - 1 >= 0) {
          rows[activeTile].classList.remove(ACTIVE_CLASS);
          rows[activeTile - 1].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: activeTile - 1 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        }
      }

      //stop event propagation
      return true;
    }
  }

  _onNextTile (event) {
    if (findDOMNode(this.tilesRef).contains(document.activeElement)) {
      event.preventDefault();
      const { activeTile } = this.state;
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      if (rows && rows.length > 0) {
        if (activeTile === undefined) {
          rows[0].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: 0 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        } else if (activeTile + 1 <= rows.length - 1) {
          rows[activeTile].classList.remove(ACTIVE_CLASS);
          rows[activeTile + 1].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: activeTile + 1 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        }
      }

      //stop event propagation
      return true;
    }
  }

  _fireClick (element, shiftKey) {
    let event;
    try {
      event = new MouseEvent('click', {
        'bubbles': true,
        'cancelable': true,
        'shiftKey': shiftKey
      });
    } catch (e) {
      // IE11 workaround.
      event = document.createEvent('Event');
      event.initEvent('click', true, true);
    }
    // We use dispatchEvent to have the browser fill out the event fully.
    element.dispatchEvent(event);
  }

  _onEnter (event) {
    const { activeTile } = this.state;
    const { intl } = this.context;
    if (findDOMNode(this.tilesRef).contains(document.activeElement) &&
      activeTile !== undefined) {
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      this._fireClick(rows[activeTile], event.shiftKey);
      rows[activeTile].classList.remove(ACTIVE_CLASS);
      const label = rows[activeTile].innerText;
      // avoid a long text to be read by the screen reader
      const labelMessage = label.length > 15 ?
        `${label.substring(0, 15)}...` : label;
      const selectedMessage = Intl.getMessage(intl, 'Selected');
      announce(`${labelMessage} ${selectedMessage}`);
    }
  }

  _onLeft () {
    const tiles = findDOMNode(this.tilesRef);
    Scroll.scrollBy(tiles, 'scrollLeft', - tiles.offsetWidth);
  }

  _onRight () {
    const tiles = findDOMNode(this.tilesRef);
    Scroll.scrollBy(tiles, 'scrollLeft', tiles.offsetWidth);
  }

  _onScrollHorizontal () {
    // debounce
    clearTimeout(this._scrollTimer);
    this._scrollTimer = setTimeout(this._layout, 50);
  }

  _onWheel (event) {
    if (Math.abs(event.deltaX) > 100) {
      clearInterval(this._scrollTimer);
    } else if (event.deltaX > 5) {
      this._onRight();
    } else if (event.deltaX < -5) {
      this._onLeft();
    }
  }

  _layout () {
    const { direction } = this.props;
    if ('row' === direction) {
      // determine if we have more tiles than room to fit
      const tiles = findDOMNode(this.tilesRef);
      // 20 is to allow some fuzziness as scrollbars come and go
      this.setState({
        overflow: (tiles.scrollWidth > (tiles.offsetWidth + 20)),
        overflowStart: (tiles.scrollLeft <= 20),
        overflowEnd:
          (tiles.scrollLeft >= (tiles.scrollWidth - tiles.offsetWidth))
      });

      // mark any tiles that might be clipped
      const rect = tiles.getBoundingClientRect();
      const children = tiles.querySelectorAll(`.${TILE}`);

      Array.from(children).map((child, index) => {
        const childRect = child.getBoundingClientRect();
        // 12 accounts for padding
        if ((childRect.left + 12) < rect.left ||
          (childRect.right - 12) > rect.right) {
          child.classList.add(`${TILE}--eclipsed`);
        } else {
          child.classList.remove(`${TILE}--eclipsed`);
        }
      });
    }
  }

  _renderChild (element) {
    const { flush } = this.props;

    if (element) {
      // only clone tile children
      if (element.type && element.type.displayName === 'Tile') {
        const elementClone = React.cloneElement(element, {
          hoverBorder: !flush
        });

        return elementClone;
      }
      return element;
    }

    return undefined;
  }

  _onResize () {
    // debounce
    clearTimeout(this._resizeTimer);
    this._resizeTimer = setTimeout(this._layout, 50);
  }

  _trackHorizontalScroll () {
    const { overflow } = this.state;
    if (overflow && ! this._tracking) {
      const tiles = findDOMNode(this.tilesRef);
      tiles.addEventListener('scroll', this._onScrollHorizontal);
      this._tracking = true;
    }
  }

  _setSelection () {
    Selection.setClassFromIndexes({
      containerElement: findDOMNode(this.tilesRef),
      childSelector: `.${TILE}`,
      selectedClass: SELECTED_CLASS,
      selectedIndexes: this.state.selected
    });
  }

  _onClick (event) {
    const { onSelect, selectable, selected } = this.props;
    const selection = Selection.onClick(event, {
      containerElement: findDOMNode(this.tilesRef),
      childSelector: `.${TILE}`,
      selectedClass: SELECTED_CLASS,
      multiSelect: ('multiple' === selectable),
      priorSelectedIndexes: this.state.selected
    });
    // only set the selected state and classes if the caller isn't managing it.
    if (selected === undefined) {
      this.setState({ selected: selection }, this._setSelection);
    }

    if (onSelect) {
      onSelect(selection.length === 1 ? selection[0] : selection);
    }
  }

  // children should be an array of Tile
  render () {
    const {
      a11yTitle, className, children, direction, fill, flush, onBlur, onFocus,
      onMore, onMouseDown, onMouseUp, selectable
    } = this.props;
    const {
      activeTile, focus, mouseActive, overflow, overflowEnd, overflowStart
    } = this.state;
    const { intl } = this.context;

    const classes = classnames(
      CLASS_ROOT,
      {
        [`${CLASS_ROOT}--fill`]: fill,
        [`${CLASS_ROOT}--flush`]: flush,
        [`${CLASS_ROOT}--focus`]: focus,
        [`${CLASS_ROOT}--selectable`]: selectable,
        [`${CLASS_ROOT}--moreable`]: onMore,
        [`${CLASS_ROOT}--overflowed`]: overflow
      },
      className
    );

    const other = Props.pick(this.props, Object.keys(Box.propTypes));

    let more;
    if (onMore) {
      more = (

 this.moreRef = ref} className={`${CLASS_ROOT}__more`}>
          <SpinningIcon />

      );
    }

    const tileContents = Children.map(children, (element) => {
      return this._renderChild(element);
    });

    let selectableProps;
    if (selectable) {
      const multiSelectMessage = selectable === 'multiple' ?
        `(${Intl.getMessage(intl, 'Multi Select')})` : '';
      const tilesMessage = a11yTitle || Intl.getMessage(intl, 'Tiles');
      const navigationHelpMessage = Intl.getMessage(intl, 'Navigation Help');
      selectableProps = {
        'aria-label': (
          `${tilesMessage} ${multiSelectMessage} ${navigationHelpMessage}`
        ),
        tabIndex: '0',
        onClick: this._onClick,
        onMouseDown: (event) => {
          this.setState({ mouseActive: true });
          if (onMouseDown) {
            onMouseDown(event);
          }
        },
        onMouseUp: (event) => {
          this.setState({ mouseActive: false });
          if (onMouseUp) {
            onMouseUp(event);
          }
        },
        onFocus: (event) => {
          if (mouseActive === false) {
            this.setState({ focus: true });
          }
          if (onFocus) {
            onFocus(event);
          }
        },
        onBlur: (event) => {
          if (activeTile) {
            const rows = (
              findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`)
            );
            rows[activeTile].classList.remove(ACTIVE_CLASS);
          }
          this.setState({ focus: false, activeTile: undefined });
          if (onBlur) {
            onBlur(event);
          }
        }
      };
    }

    let contents = (
      <Box ref={ref => this.tilesRef = ref} {...other}
        wrap={direction ? false : true}
        direction={direction ? direction : 'row'}
        className={classes} focusable={false} {...selectableProps}>
        {tileContents}
        {more}
      </Box>
    );

    if (overflow) {
      let left;
      let right;

      if (!overflowStart) {
        const previousTilesMessage = Intl.getMessage(intl, 'Previous Tiles');
        left = (
          <Button className={`${CLASS_ROOT}__left`} icon={<LinkPreviousIcon />}
            a11yTitle={previousTilesMessage} onClick={this._onLeft} />
        );
      }
      if (!overflowEnd) {
        const nextTilesMessage = Intl.getMessage(intl, 'Next Tiles');
        right = (
          <Button className={`${CLASS_ROOT}__right`} icon={<LinkNextIcon />}
            a11yTitle={nextTilesMessage} onClick={this._onRight} />
        );
      }

      contents = (
        <div className={`${CLASS_ROOT}__container`}>
          {left}
          {contents}
          {right}

      );
    }

    return contents;
  }

}

Tiles.contextTypes = {
  intl: PropTypes.object
};

Tiles.propTypes = {
  fill: PropTypes.bool,
  flush: PropTypes.bool,
  onMore: PropTypes.func,
  onSelect: PropTypes.func,
  selectable: PropTypes.oneOfType([
    PropTypes.bool,
    PropTypes.oneOf(['multiple'])
  ]),
  selected: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.arrayOf(PropTypes.number)
  ]),
  ...Box.propTypes
};

Tiles.defaultProps = {
  flush: true,
  justify: 'start',
  pad: 'small'
};
<commit_msg>Fixed Tiles row endless loop. (#1028)<commit_after>// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes, Children } from 'react';
import { findDOMNode } from 'react-dom';
import classnames from 'classnames';
import Props from '../utils/Props';
import Box from './Box';
import Button from './Button';
import SpinningIcon from './icons/Spinning';
import Scroll from '../utils/Scroll';
import InfiniteScroll from '../utils/InfiniteScroll';
import Selection from '../utils/Selection';
import KeyboardAccelerators from '../utils/KeyboardAccelerators';
import Intl from '../utils/Intl';
import { announce } from '../utils/Announcer';

import LinkPreviousIcon from './icons/base/LinkPrevious';
import LinkNextIcon from './icons/base/LinkNext';
import CSSClassnames from '../utils/CSSClassnames';

const CLASS_ROOT = CSSClassnames.TILES;
const TILE = CSSClassnames.TILE;
const SELECTED_CLASS = `${TILE}--selected`;
const ACTIVE_CLASS = `${TILE}--active`;

export default class Tiles extends Component {

  constructor(props, context) {
    super(props, context);
    this._onLeft = this._onLeft.bind(this);
    this._onRight = this._onRight.bind(this);
    this._onScrollHorizontal = this._onScrollHorizontal.bind(this);
    this._onWheel = this._onWheel.bind(this);
    this._onResize = this._onResize.bind(this);
    this._layout = this._layout.bind(this);
    this._onClick = this._onClick.bind(this);
    this._fireClick = this._fireClick.bind(this);
    this._announceTile = this._announceTile.bind(this);
    this._onPreviousTile = this._onPreviousTile.bind(this);
    this._onNextTile = this._onNextTile.bind(this);
    this._onEnter = this._onEnter.bind(this);

    this.state = {
      activeTile: undefined,
      mouseActive: false,
      overflow: false,
      selected: Selection.normalizeIndexes(props.selected)
    };
  }

  componentDidMount () {
    const { direction, onMore, selectable } = this.props;
    this._setSelection();
    if (onMore) {
      this._scroll = InfiniteScroll.startListeningForScroll(this.moreRef,
        onMore);
    }
    if ('row' === direction) {
      window.addEventListener('resize', this._onResize);
      document.addEventListener('wheel', this._onWheel, { passive: true });
      this._trackHorizontalScroll();
      // give browser a chance to stabilize
      setTimeout(this._layout, 10);
    }
    if (selectable) {
      // only listen for navigation keys if the tile row can be selected
      this._keyboardHandlers = {
        left: this._onPreviousTile,
        up: this._onPreviousTile,
        right: this._onNextTile,
        down: this._onNextTile,
        enter: this._onEnter,
        space: this._onEnter
      };
      KeyboardAccelerators.startListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.selected !== undefined) {
      this.setState({
        selected: Selection.normalizeIndexes(nextProps.selected)
      });
    }
    if (this._scroll) {
      InfiniteScroll.stopListeningForScroll(this._scroll);
      this._scroll = undefined;
    }
  }

  componentDidUpdate (prevProps, prevState) {
    const { direction, onMore, selectable } = this.props;
    const { selected } = this.state;
    if (JSON.stringify(selected) !==
      JSON.stringify(prevState.selected)) {
      this._setSelection();
    }
    if (onMore && !this._scroll) {
      this._scroll = InfiniteScroll.startListeningForScroll(this.moreRef,
        onMore);
    }
    if ('row' === direction) {
      this._trackHorizontalScroll();
      // give browser a chance to stabilize
      setTimeout(this._layout, 10);
    }
    if (selectable) {
      // only listen for navigation keys if the list row can be selected
      this._keyboardHandlers = {
        left: this._onPreviousTile,
        up: this._onPreviousTile,
        right: this._onNextTile,
        down: this._onNextTile,
        enter: this._onEnter,
        space: this._onEnter
      };
      KeyboardAccelerators.startListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  componentWillUnmount () {
    const { direction, selectable } = this.props;
    if (this._scroll) {
      InfiniteScroll.stopListeningForScroll(this._scroll);
    }
    if ('row' === direction) {
      window.removeEventListener('resize', this._onResize);
      document.removeEventListener('wheel', this._onWheel);
      if (this._tracking) {
        const tiles = findDOMNode(this.tilesRef);
        tiles.removeEventListener('scroll', this._onScrollHorizontal);
      }
    }
    if (selectable) {
      KeyboardAccelerators.stopListeningToKeyboard(
        this, this._keyboardHandlers
      );
    }
  }

  _announceTile (label) {
    const { intl } = this.context;
    const enterSelectMessage = Intl.getMessage(intl, 'Enter Select');
    // avoid a long text to be read by the screen reader
    const labelMessage = label.length > 15 ?
      `${label.substring(0, 15)}...` : label;
    announce(`${labelMessage} ${enterSelectMessage}`);
  }

  _onPreviousTile (event) {
    if (findDOMNode(this.tilesRef).contains(document.activeElement)) {
      event.preventDefault();
      const { activeTile } = this.state;
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      if (rows && rows.length > 0) {
        if (activeTile === undefined) {
          rows[0].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: 0 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        } else if (activeTile - 1 >= 0) {
          rows[activeTile].classList.remove(ACTIVE_CLASS);
          rows[activeTile - 1].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: activeTile - 1 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        }
      }

      //stop event propagation
      return true;
    }
  }

  _onNextTile (event) {
    if (findDOMNode(this.tilesRef).contains(document.activeElement)) {
      event.preventDefault();
      const { activeTile } = this.state;
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      if (rows && rows.length > 0) {
        if (activeTile === undefined) {
          rows[0].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: 0 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        } else if (activeTile + 1 <= rows.length - 1) {
          rows[activeTile].classList.remove(ACTIVE_CLASS);
          rows[activeTile + 1].classList.add(ACTIVE_CLASS);
          this.setState({ activeTile: activeTile + 1 }, () => {
            this._announceTile(
              rows[this.state.activeTile].innerText
            );
          });
        }
      }

      //stop event propagation
      return true;
    }
  }

  _fireClick (element, shiftKey) {
    let event;
    try {
      event = new MouseEvent('click', {
        'bubbles': true,
        'cancelable': true,
        'shiftKey': shiftKey
      });
    } catch (e) {
      // IE11 workaround.
      event = document.createEvent('Event');
      event.initEvent('click', true, true);
    }
    // We use dispatchEvent to have the browser fill out the event fully.
    element.dispatchEvent(event);
  }

  _onEnter (event) {
    const { activeTile } = this.state;
    const { intl } = this.context;
    if (findDOMNode(this.tilesRef).contains(document.activeElement) &&
      activeTile !== undefined) {
      const rows = findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`);
      this._fireClick(rows[activeTile], event.shiftKey);
      rows[activeTile].classList.remove(ACTIVE_CLASS);
      const label = rows[activeTile].innerText;
      // avoid a long text to be read by the screen reader
      const labelMessage = label.length > 15 ?
        `${label.substring(0, 15)}...` : label;
      const selectedMessage = Intl.getMessage(intl, 'Selected');
      announce(`${labelMessage} ${selectedMessage}`);
    }
  }

  _onLeft () {
    const tiles = findDOMNode(this.tilesRef);
    Scroll.scrollBy(tiles, 'scrollLeft', - tiles.offsetWidth);
  }

  _onRight () {
    const tiles = findDOMNode(this.tilesRef);
    Scroll.scrollBy(tiles, 'scrollLeft', tiles.offsetWidth);
  }

  _onScrollHorizontal () {
    // debounce
    clearTimeout(this._scrollTimer);
    this._scrollTimer = setTimeout(this._layout, 50);
  }

  _onWheel (event) {
    if (Math.abs(event.deltaX) > 100) {
      clearInterval(this._scrollTimer);
    } else if (event.deltaX > 5) {
      this._onRight();
    } else if (event.deltaX < -5) {
      this._onLeft();
    }
  }

  _layout () {
    const { direction } = this.props;

    if ('row' === direction) {
      // determine if we have more tiles than room to fit
      const tiles = findDOMNode(this.tilesRef);

      // 20 is to allow some fuzziness as scrollbars come and go
      const newState = {
        overflow: (tiles.scrollWidth > (tiles.offsetWidth + 20)),
        overflowStart: (tiles.scrollLeft <= 20),
        overflowEnd:
          (tiles.scrollLeft >= (tiles.scrollWidth - tiles.offsetWidth)),
        scrollWidth: tiles.scrollWidth
      };

      const state = {
        overflow: this.state.overflow,
        overflowStart: this.state.overflowStart,
        overflowEnd: this.state.overflowEnd,
        scrollWidth: this.state.scrollWidth
      };

      // Shallow compare states.
      if (JSON.stringify(newState) !== JSON.stringify(state)) {
        this.setState({ ...newState });
      }

      // mark any tiles that might be clipped
      const rect = tiles.getBoundingClientRect();
      const children = tiles.querySelectorAll(`.${TILE}`);

      Array.from(children).map((child, index) => {
        const childRect = child.getBoundingClientRect();
        // 12 accounts for padding
        if ((childRect.left + 12) < rect.left ||
          (childRect.right - 12) > rect.right) {
          child.classList.add(`${TILE}--eclipsed`);
        } else {
          child.classList.remove(`${TILE}--eclipsed`);
        }
      });
    }
  }

  _renderChild (element) {
    const { flush } = this.props;

    if (element) {
      // only clone tile children
      if (element.type && element.type.displayName === 'Tile') {
        const elementClone = React.cloneElement(element, {
          hoverBorder: !flush
        });

        return elementClone;
      }
      return element;
    }

    return undefined;
  }

  _onResize () {
    // debounce
    clearTimeout(this._resizeTimer);
    this._resizeTimer = setTimeout(this._layout, 50);
  }

  _trackHorizontalScroll () {
    const { overflow } = this.state;
    if (overflow && ! this._tracking) {
      const tiles = findDOMNode(this.tilesRef);
      tiles.addEventListener('scroll', this._onScrollHorizontal);
      this._tracking = true;
    }
  }

  _setSelection () {
    Selection.setClassFromIndexes({
      containerElement: findDOMNode(this.tilesRef),
      childSelector: `.${TILE}`,
      selectedClass: SELECTED_CLASS,
      selectedIndexes: this.state.selected
    });
  }

  _onClick (event) {
    const { onSelect, selectable, selected } = this.props;
    const selection = Selection.onClick(event, {
      containerElement: findDOMNode(this.tilesRef),
      childSelector: `.${TILE}`,
      selectedClass: SELECTED_CLASS,
      multiSelect: ('multiple' === selectable),
      priorSelectedIndexes: this.state.selected
    });
    // only set the selected state and classes if the caller isn't managing it.
    if (selected === undefined) {
      this.setState({ selected: selection }, this._setSelection);
    }

    if (onSelect) {
      onSelect(selection.length === 1 ? selection[0] : selection);
    }
  }

  // children should be an array of Tile
  render () {
    const {
      a11yTitle, className, children, direction, fill, flush, onBlur, onFocus,
      onMore, onMouseDown, onMouseUp, selectable
    } = this.props;
    const {
      activeTile, focus, mouseActive, overflow, overflowEnd, overflowStart
    } = this.state;
    const { intl } = this.context;

    const classes = classnames(
      CLASS_ROOT,
      {
        [`${CLASS_ROOT}--fill`]: fill,
        [`${CLASS_ROOT}--flush`]: flush,
        [`${CLASS_ROOT}--focus`]: focus,
        [`${CLASS_ROOT}--selectable`]: selectable,
        [`${CLASS_ROOT}--moreable`]: onMore,
        [`${CLASS_ROOT}--overflowed`]: overflow
      },
      className
    );

    const other = Props.pick(this.props, Object.keys(Box.propTypes));

    let more;
    if (onMore) {
      more = (

 this.moreRef = ref} className={`${CLASS_ROOT}__more`}>
          <SpinningIcon />

      );
    }

    const tileContents = Children.map(children, (element) => {
      return this._renderChild(element);
    });

    let selectableProps;
    if (selectable) {
      const multiSelectMessage = selectable === 'multiple' ?
        `(${Intl.getMessage(intl, 'Multi Select')})` : '';
      const tilesMessage = a11yTitle || Intl.getMessage(intl, 'Tiles');
      const navigationHelpMessage = Intl.getMessage(intl, 'Navigation Help');
      selectableProps = {
        'aria-label': (
          `${tilesMessage} ${multiSelectMessage} ${navigationHelpMessage}`
        ),
        tabIndex: '0',
        onClick: this._onClick,
        onMouseDown: (event) => {
          this.setState({ mouseActive: true });
          if (onMouseDown) {
            onMouseDown(event);
          }
        },
        onMouseUp: (event) => {
          this.setState({ mouseActive: false });
          if (onMouseUp) {
            onMouseUp(event);
          }
        },
        onFocus: (event) => {
          if (mouseActive === false) {
            this.setState({ focus: true });
          }
          if (onFocus) {
            onFocus(event);
          }
        },
        onBlur: (event) => {
          if (activeTile) {
            const rows = (
              findDOMNode(this.tilesRef).querySelectorAll(`.${TILE}`)
            );
            rows[activeTile].classList.remove(ACTIVE_CLASS);
          }
          this.setState({ focus: false, activeTile: undefined });
          if (onBlur) {
            onBlur(event);
          }
        }
      };
    }

    let contents = (
      <Box ref={ref => this.tilesRef = ref} {...other}
        wrap={direction ? false : true}
        direction={direction ? direction : 'row'}
        className={classes} focusable={false} {...selectableProps}>
        {tileContents}
        {more}
      </Box>
    );

    if (overflow) {
      let left;
      let right;

      if (!overflowStart) {
        const previousTilesMessage = Intl.getMessage(intl, 'Previous Tiles');
        left = (
          <Button className={`${CLASS_ROOT}__left`} icon={<LinkPreviousIcon />}
            a11yTitle={previousTilesMessage} onClick={this._onLeft} />
        );
      }
      if (!overflowEnd) {
        const nextTilesMessage = Intl.getMessage(intl, 'Next Tiles');
        right = (
          <Button className={`${CLASS_ROOT}__right`} icon={<LinkNextIcon />}
            a11yTitle={nextTilesMessage} onClick={this._onRight} />
        );
      }

      contents = (
        <div className={`${CLASS_ROOT}__container`}>
          {left}
          {contents}
          {right}

      );
    }

    return contents;
  }

}

Tiles.contextTypes = {
  intl: PropTypes.object
};

Tiles.propTypes = {
  fill: PropTypes.bool,
  flush: PropTypes.bool,
  onMore: PropTypes.func,
  onSelect: PropTypes.func,
  selectable: PropTypes.oneOfType([
    PropTypes.bool,
    PropTypes.oneOf(['multiple'])
  ]),
  selected: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.arrayOf(PropTypes.number)
  ]),
  ...Box.propTypes
};

Tiles.defaultProps = {
  flush: true,
  justify: 'start',
  pad: 'small'
};
<|endoftext|>


<commit_before>CKEDITOR.dialog.add(
    'linkDialog',
    function (editor) {
        return {
            title: 'Add link',

            minWidth: 400,
            minHeight: 200,

            contents: [
                {
                    id: 'tab',
                    label: '',
                    elements: [
                        {
                            type: 'text',
                            id: 'displayText',
                            label: 'Display Text',
                            validate: CKEDITOR.dialog.validate.notEmpty('Display cannot be empty.')
                        },
                        {
                            type: 'text',
                            id: 'url',
                            label: 'URL',
                            validate: function () {
                                return CKEDITOR.dialog.validate.notEmpty('URL cannot be empty.') &&
                                    CKEDITOR.dialog.validate.regex('https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)', 'URL is not valid.')
                            }
                        },
                        {
                            type: 'button',
                            id: 'browseServer',
                            label: 'Browse server'
                        }
                    ]
                }
            ],

            onOk: function () {
                var dialog = this;

                var anchor = editor.document.createElement('a');
                anchor.setAttribute('href', dialog.getValueOf('tab', 'url'));
                anchor.setText(dialog.getValueOf('tab', 'displayText'));

                editor.insertElement(anchor);
            }
        }
    }
)
<commit_msg>Make selected content editable.<commit_after>CKEDITOR.dialog.add(
    'linkDialog',
    function (editor) {
        var urlRegex = 'https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)';

        return {
            title: 'Add link',

            minWidth: 400,
            minHeight: 200,

            contents: [
                {
                    id: 'tab',
                    label: '',
                    elements: [
                        {
                            type: 'text',
                            id: 'displayText',
                            label: 'Display Text',
                            validate: CKEDITOR.dialog.validate.notEmpty('Display cannot be empty.'),
                            setup: function(element) {
                                this.setValue(element.getText());
                            },
                            commit: function (element) {
                                element.setText(this.getValue());
                            }
                        },
                        {
                            type: 'text',
                            id: 'url',
                            label: 'URL',
                            validate: CKEDITOR.dialog.validate.regex(new RegExp(urlRegex), 'URL is not valid.'),
                            setup: function(element) {
                                this.setValue(element.getAttribute('href'));
                            },
                            commit: function (element) {
                                element.setAttribute('href', this.getValue());
                            }
                        },
                        {
                            type: 'button',
                            id: 'browseServer',
                            label: 'Browse server'
                        }
                    ]
                }
            ],

            onOk: function () {
                // var dialog = this;

                // var anchor = editor.document.createElement('a');
                // anchor.setAttribute('href', dialog.getValueOf('tab', 'url'));
                // anchor.setText(dialog.getValueOf('tab', 'displayText'));
                //
                // editor.insertElement(anchor);

                var dialog = this,
                    anchor = dialog.element;

                dialog.commitContent(anchor);

                if (dialog.insertMode) {
                    editor.insertElement(anchor);
                }
            },

            onShow: function () {
                var dialog = this;

                var selection = editor.getSelection();
                var initialText = selection.getSelectedText();
                var element = selection.getStartElement();

                if (element) {
                    element = element.getAscendant('a', true);
                }

                if (!element || element.getName() !== 'a') {
                    element = editor.document.createElement('a');

                    if (initialText.match(urlRegex)) {
                        dialog.setValueOf('tab', 'url', initialText);
                    } else {
                        dialog.setValueOf('tab', 'displayText', initialText);
                    }

                    this.insertMode = true;
                } else {
                    this.insertMode = false;
                }

                this.element = element;

                if (!this.insertMode) {
                    this.setupContent(element)
                }
            }
        }
    }
)
<|endoftext|>


<commit_before>'use strict';

var parse = require('url').parse
  , http = require('http');

/**
 * Minimum viable WebSocket server for Node.js that works through the Primus
 * interface.
 *
 * @runat server
 * @api private
 */
module.exports = function server() {
  var WebSocketServer = require('lws').Server
    , Spark = this.Spark;

  var service = this.service = new WebSocketServer({
    perMessageDeflate: !!this.primus.options.compression
  });

  service.on('message', function (socket, message, binary) {
    var spark = service.getUserData(socket);
    spark.emit('incoming::data', binary ? message : message.toString());
  });

  service.on('close', function (socket) {
    var spark = service.getUserData(socket);
    spark.emit('incoming::end');
  });

  //
  // Listen to upgrade requests.
  //
  this.on('upgrade', function upgrade(req, soc, head) {
    var socket = this.service.handleUpgrade(soc, req, head);

    if (!socket) return;

    var spark = new Spark(
        req.headers               // HTTP request headers.
      , req                       // IP address location.
      , parse(req.url).query      // Optional query string.
      , null                      // We don't have an unique id.
      , req                       // Reference to the HTTP req.
    );

    service.setUserData(socket, spark);

    spark.on('outgoing::end', function end() {
      service.close(socket);
    }).on('outgoing::data', function write(data) {
      if ('string' === typeof data) {
        // todo: take a string directly
        service.send(socket, new Buffer(data), false);
      } else {
        service.send(socket, data, true);
      }
    });
  });

  //
  // Listen to non-upgrade requests.
  //
  this.on('request', function request(req, res) {
    res.writeHead(426, { 'content-type': 'text/plain' });
    res.end(http.STATUS_CODES[426]);
  });

  this.on('close', function close() {
    service.close();
  });
};
<commit_msg>[minor] Add deprecation warning for lws transformer<commit_after>'use strict';

var parse = require('url').parse
  , http = require('http');

[
  '',
  'The lws transformer has been deprecated in favor of its successor uws.',
  'We strongly suggest to switch to uws as lws will be removed in future',
  'versions of Primus.',
  ''
].forEach(function each(line) {
  console.error('Primus: '+ line);
});

/**
 * Minimum viable WebSocket server for Node.js that works through the Primus
 * interface.
 *
 * @runat server
 * @api private
 */
module.exports = function server() {
  var WebSocketServer = require('lws').Server
    , Spark = this.Spark;

  var service = this.service = new WebSocketServer({
    perMessageDeflate: !!this.primus.options.compression
  });

  service.on('message', function (socket, message, binary) {
    var spark = service.getUserData(socket);
    spark.emit('incoming::data', binary ? message : message.toString());
  });

  service.on('close', function (socket) {
    var spark = service.getUserData(socket);
    spark.emit('incoming::end');
  });

  //
  // Listen to upgrade requests.
  //
  this.on('upgrade', function upgrade(req, soc, head) {
    var socket = this.service.handleUpgrade(soc, req, head);

    if (!socket) return;

    var spark = new Spark(
        req.headers               // HTTP request headers.
      , req                       // IP address location.
      , parse(req.url).query      // Optional query string.
      , null                      // We don't have an unique id.
      , req                       // Reference to the HTTP req.
    );

    service.setUserData(socket, spark);

    spark.on('outgoing::end', function end() {
      service.close(socket);
    }).on('outgoing::data', function write(data) {
      if ('string' === typeof data) {
        // todo: take a string directly
        service.send(socket, new Buffer(data), false);
      } else {
        service.send(socket, data, true);
      }
    });
  });

  //
  // Listen to non-upgrade requests.
  //
  this.on('request', function request(req, res) {
    res.writeHead(426, { 'content-type': 'text/plain' });
    res.end(http.STATUS_CODES[426]);
  });

  this.on('close', function close() {
    service.close();
  });
};
<|endoftext|>


<commit_before>import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { dropTask, restartableTask } from 'ember-concurrency';
import { all } from 'rsvp';
import { ValidateIf } from 'class-validator';
import {
  validatable,
  AfterDate,
  Custom,
  IsInt,
  Gte,
  Lte,
  NotBlank,
} from 'ilios-common/decorators/validation';

@validatable
export default class CurriculumInventorySequenceBlockOverviewComponent extends Component {
  @service intl;
  @service store;
  @tracked
  @Custom('validateStartingEndingLevelCallback', 'validateStartingLevelMessageCallback')
  startingAcademicLevel;
  @tracked
  @Custom('validateStartingEndingLevelCallback', 'validateEndingLevelMessageCallback')
  endingAcademicLevel;
  @tracked academicLevels;
  @tracked childSequenceOrder;
  @tracked course;
  @tracked description;
  @tracked isEditingDatesAndDuration = false;
  @tracked isEditingMinMax = false;
  @tracked isManagingSessions = false;
  @tracked linkableCourses = [];
  @tracked @NotBlank() @IsInt() @Gte(0) minimum;
  @tracked
  @NotBlank()
  @IsInt()
  @Gte(0)
  @Custom('validateMaximumCallback', 'validateMaximumMessageCallback')
  maximum;
  @tracked isInOrderedSequence;
  @tracked orderInSequence;
  @tracked orderInSequenceOptions = [];
  @tracked parent;
  @tracked report;
  @tracked required;
  @tracked sessions = [];
  @tracked
  @NotBlank()
  @IsInt()
  @Custom('validateDurationCallback', 'validateDurationMessageCallback')
  @Lte(1200)
  duration;
  @tracked
  @ValidateIf((o) => o.hasZeroDuration || o.linkedCourseIsClerkship)
  @NotBlank()
  startDate;
  @tracked
  @ValidateIf((o) => o.startDate)
  @NotBlank()
  @AfterDate('startDate', { granularity: 'day' })
  endDate;

  get linkedCourseIsClerkship() {
    if (!this.course) {
      return false;
    }
    return !!this.course.belongsTo('clerkshipType').id();
  }

  get hasZeroDuration() {
    const num = Number(this.duration);
    if (Number.isNaN(num)) {
      return false;
    }
    return 0 === num;
  }

  @restartableTask
  *load(element, [sequenceBlock]) {
    this.report = yield sequenceBlock.report;
    this.parent = yield sequenceBlock.parent;
    this.academicLevels = (yield this.report.academicLevels).toArray();
    this.isInOrderedSequence = false;
    this.orderInSequenceOptions = [];
    if (isPresent(this.parent) && this.parent.isOrdered) {
      this.isInOrderedSequence = true;
      const siblings = yield this.parent.children;
      for (let i = 0, n = siblings.toArray().length; i < n; i++) {
        const num = i + 1;
        this.orderInSequenceOptions.push(num);
      }
    }
    this.linkedSessions = yield sequenceBlock.sessions;
    this.startingAcademicLevel = yield sequenceBlock.startingAcademicLevel;
    this.endingAcademicLevel = yield sequenceBlock.endingAcademicLevel;
    this.required = sequenceBlock.required.toString();
    this.duration = sequenceBlock.duration;
    this.startDate = sequenceBlock.startDate;
    this.endDate = sequenceBlock.endDate;
    this.childSequenceOrder = sequenceBlock.childSequenceOrder.toString();
    this.orderInSequence = sequenceBlock.orderInSequence;
    this.description = sequenceBlock.description;
    this.course = yield sequenceBlock.course;
    this.minimum = sequenceBlock.minimum;
    this.maximum = sequenceBlock.maximum;
    this.sessions = yield this.getSessions(this.course);
    this.linkableCourses = yield this.getLinkableCourses(this.report, this.course);
  }

  get requiredLabel() {
    switch (this.required) {
      case '1':
        return this.intl.t('general.required');
      case '2':
        return this.intl.t('general.optionalElective');
      case '3':
        return this.intl.t('general.requiredInTrack');
      default:
        return null;
    }
  }

  get isElective() {
    return '2' === this.required;
  }

  get isSelective() {
    if (this.isElective) {
      return false;
    }
    const minimum = parseInt(this.minimum, 10);
    const maximum = parseInt(this.maximum, 10);
    return minimum > 0 && minimum !== maximum;
  }

  get childSequenceOrderLabel() {
    switch (this.childSequenceOrder) {
      case '1':
        return this.intl.t('general.ordered');
      case '2':
        return this.intl.t('general.unordered');
      case '3':
        return this.intl.t('general.parallel');
      default:
        return null;
    }
  }

  /**
   * Returns a list of courses that can be linked to this sequence block.
   */
  async getLinkableCourses(report, course) {
    if (!report) {
      return [];
    }
    const program = await report.program;
    const schoolId = program.belongsTo('school').id();
    const allLinkableCourses = await this.store.query('course', {
      filters: {
        published: true,
        school: [schoolId],
        year: report.get('year'),
      },
    });
    const linkedCourses = await report.getLinkedCourses();
    // Filter out all courses that are linked to (sequence blocks in) this report.
    const linkableCourses = allLinkableCourses.filter((course) => {
      return !linkedCourses.includes(course);
    });
    // Always add the currently linked course to this list, if existent.
    if (isPresent(course)) {
      linkableCourses.pushObject(course);
    }

    return linkableCourses;
  }

  /**
   * Returns a list of published sessions that belong to a given course.
   */
  async getSessions(course) {
    if (!course) {
      return [];
    }
    const sessions = await this.store.query('session', {
      filters: {
        course: course.get('id'),
        published: true,
      },
    });
    return sessions.toArray();
  }

  @dropTask
  *changeRequired() {
    this.args.sequenceBlock.required = parseInt(this.required, 10);
    if ('2' === this.required) {
      this.args.sequenceBlock.minimum = 0;
    }
    yield this.args.sequenceBlock.save();
  }

  @action
  setRequired(required) {
    this.required = required;
    if ('2' === required) {
      this.minimum = 0;
    } else {
      this.minimum = this.args.sequenceBlock.minimum;
    }
  }

  @action
  revertRequiredChanges() {
    this.required = this.args.sequenceBlock.required.toString();
    this.minimum = this.args.sequenceBlock.minimum;
  }

  @action
  async saveCourse() {
    const oldCourse = await this.args.sequenceBlock.course;
    if (oldCourse !== this.course) {
      this.args.sequenceBlock.set('sessions', []);
      this.args.sequenceBlock.set('excludedSessions', []);
    }
    this.args.sequenceBlock.set('course', this.course);
    await this.args.sequenceBlock.save();
  }

  @action
  async revertCourseChanges() {
    this.course = await this.args.sequenceBlock.get('course');
  }

  @dropTask()
  *changeTrack(value) {
    this.args.sequenceBlock.set('track', value);
    yield this.args.sequenceBlock.save();
  }

  @dropTask()
  *saveDescription() {
    this.args.sequenceBlock.set('description', this.description);
    yield this.args.sequenceBlock.save();
  }

  @action
  revertDescriptionChanges() {
    this.description = this.args.sequenceBlock.get('description');
  }

  @dropTask
  *changeChildSequenceOrder() {
    this.args.sequenceBlock.set('childSequenceOrder', parseInt(this.childSequenceOrder, 10));
    const savedBlock = yield this.args.sequenceBlock.save();
    const children = yield savedBlock.get('children');
    yield children.invoke('reload');
  }

  @action
  revertChildSequenceOrderChanges() {
    this.childSequenceOrder = this.args.sequenceBlock.get('childSequenceOrder').toString();
  }

  @dropTask
  *changeStartingAcademicLevel() {
    this.addErrorDisplaysFor(['startingAcademicLevel']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.set('startingAcademicLevel', this.startingAcademicLevel);
    yield this.args.sequenceBlock.save();
  }

  @action
  setStartingAcademicLevel(event) {
    const id = event.target.value;
    this.startingAcademicLevel = this.academicLevels.findBy('id', id);
  }

  @action
  revertStartingAcademicLevelChanges() {
    this.startingAcademicLevel = this.args.sequenceBlock.startingAcademicLevel;
  }

  @dropTask
  *changeEndingAcademicLevel() {
    this.addErrorDisplaysFor(['endingAcademicLevel']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.set('endingAcademicLevel', this.endingAcademicLevel);
    yield this.args.sequenceBlock.save();
  }

  @action
  setEndingAcademicLevel(event) {
    const id = event.target.value;
    this.endingAcademicLevel = this.academicLevels.findBy('id', id);
  }

  @action
  revertEndingAcademicLevelChanges() {
    this.endingAcademicLevel = this.args.sequenceBlock.endingAcademicLevel;
  }

  @dropTask
  *changeOrderInSequence() {
    this.args.sequenceBlock.set('orderInSequence', this.orderInSequence);
    const savedBlock = yield this.args.sequenceBlock.save();
    const parent = yield savedBlock.parent;
    const children = yield parent.children;
    yield all(children.invoke('reload'));
  }

  @action
  revertOrderInSequenceChanges() {
    this.orderInSequence = this.args.sequenceBlock.orderInSequence;
  }

  @action
  editMinMax() {
    this.isEditingMinMax = true;
  }

  @action
  cancelMinMaxEditing() {
    this.minimum = this.args.sequenceBlock.minimum;
    this.maximum = this.args.sequenceBlock.maximum;
    this.isEditingMinMax = false;
  }

  @action
  toggleManagingSessions() {
    this.isManagingSessions = !this.isManagingSessions;
  }

  @action
  cancelManagingSessions() {
    this.isManagingSessions = false;
  }

  @dropTask
  *changeSessions(sessions, excludedSessions) {
    this.args.sequenceBlock.set('sessions', sessions);
    this.args.sequenceBlock.set('excludedSessions', excludedSessions);
    yield this.args.sequenceBlock.save();
    this.isManagingSessions = false;
  }

  @action
  updateCourse(event) {
    const value = event.target.value;
    if (!value) {
      this.course = null;
    } else {
      this.course = this.linkableCourses.findBy('id', value);
    }
  }

  @action
  changeDescription(event) {
    this.description = event.target.value;
  }

  @action
  updateOrderInSequence(event) {
    this.orderInSequence = event.target.value;
  }

  @action
  async saveOrCancelMinMax(ev) {
    const keyCode = ev.keyCode;
    if (13 === keyCode) {
      await this.saveMinMax.perform();
      return;
    }

    if (27 === keyCode) {
      this.isEditingMinMax = false;
    }
  }

  @action
  validateMaximumCallback() {
    const max = parseInt(this.maximum, 10) || 0;
    const min = parseInt(this.minimum, 10) || 0;
    return max >= min;
  }

  @action
  validateMaximumMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.intl.t('general.minimum'),
      description: this.intl.t('general.maximum'),
    });
  }

  @action
  validateStartingEndingLevelCallback() {
    return this.endingAcademicLevel.level >= this.startingAcademicLevel.level;
  }

  @action
  validateEndingLevelMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.intl.t('general.startLevel'),
      description: this.intl.t('general.endLevel'),
    });
  }

  @action
  validateStartingLevelMessageCallback() {
    return this.intl.t('errors.lessThanOrEqualTo', {
      lte: this.intl.t('general.endLevel'),
      description: this.intl.t('general.startLevel'),
    });
  }

  @action
  validateDurationCallback() {
    const duration = parseInt(this.duration, 10);
    return this.linkedCourseIsClerkship ? duration >= 1 : duration >= 0;
  }

  @action
  validateDurationMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.linkedCourseIsClerkship ? 1 : 0,
      description: this.intl.t('general.duration'),
    });
  }

  @dropTask
  *saveMinMax() {
    this.addErrorDisplaysFor(['minimum', 'maximum']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.minimum = this.minimum;
    this.args.sequenceBlock.maximum = this.maximum;

    yield this.args.sequenceBlock.save();
    this.isEditingMinMax = false;
  }

  @action
  changeStartDate(startDate) {
    this.startDate = startDate;
  }

  @action
  changeEndDate(endDate) {
    this.endDate = endDate;
  }

  @dropTask
  *saveDuration() {
    this.addErrorDisplaysFor(['startDate', 'endDate', 'duration']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.startDate = this.startDate;
    this.args.sequenceBlock.endDate = this.endDate;
    this.args.sequenceBlock.duration = this.duration;
    yield this.args.sequenceBlock.save();
    this.isEditingDatesAndDuration = false;
  }

  @action
  cancelDurationEditing() {
    this.startDate = this.args.sequenceBlock.startDate;
    this.endDate = this.args.sequenceBlock.endDate;
    this.duration = this.args.sequenceBlock.duration;
    this.isEditingDatesAndDuration = false;
  }

  @action
  async saveOrCancelDuration(ev) {
    const keyCode = ev.keyCode;
    if (13 === keyCode) {
      await this.saveDuration.perform();
      return;
    }

    if (27 === keyCode) {
      this.isEditingDatesAndDuration = false;
    }
  }
}
<commit_msg>prevent code from executing when component's instance has gone away.<commit_after>import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { dropTask, restartableTask } from 'ember-concurrency';
import { all } from 'rsvp';
import { ValidateIf } from 'class-validator';
import {
  validatable,
  AfterDate,
  Custom,
  IsInt,
  Gte,
  Lte,
  NotBlank,
} from 'ilios-common/decorators/validation';

@validatable
export default class CurriculumInventorySequenceBlockOverviewComponent extends Component {
  @service intl;
  @service store;
  @tracked
  @Custom('validateStartingEndingLevelCallback', 'validateStartingLevelMessageCallback')
  startingAcademicLevel;
  @tracked
  @Custom('validateStartingEndingLevelCallback', 'validateEndingLevelMessageCallback')
  endingAcademicLevel;
  @tracked academicLevels;
  @tracked childSequenceOrder;
  @tracked course;
  @tracked description;
  @tracked isEditingDatesAndDuration = false;
  @tracked isEditingMinMax = false;
  @tracked isManagingSessions = false;
  @tracked linkableCourses = [];
  @tracked @NotBlank() @IsInt() @Gte(0) minimum;
  @tracked
  @NotBlank()
  @IsInt()
  @Gte(0)
  @Custom('validateMaximumCallback', 'validateMaximumMessageCallback')
  maximum;
  @tracked isInOrderedSequence;
  @tracked orderInSequence;
  @tracked orderInSequenceOptions = [];
  @tracked parent;
  @tracked report;
  @tracked required;
  @tracked sessions = [];
  @tracked
  @NotBlank()
  @IsInt()
  @Custom('validateDurationCallback', 'validateDurationMessageCallback')
  @Lte(1200)
  duration;
  @tracked
  @ValidateIf((o) => o.hasZeroDuration || o.linkedCourseIsClerkship)
  @NotBlank()
  startDate;
  @tracked
  @ValidateIf((o) => o.startDate)
  @NotBlank()
  @AfterDate('startDate', { granularity: 'day' })
  endDate;

  get linkedCourseIsClerkship() {
    if (!this.course) {
      return false;
    }
    return !!this.course.belongsTo('clerkshipType').id();
  }

  get hasZeroDuration() {
    const num = Number(this.duration);
    if (Number.isNaN(num)) {
      return false;
    }
    return 0 === num;
  }

  @restartableTask
  *load(element, [sequenceBlock]) {
    this.report = yield sequenceBlock.report;
    this.parent = yield sequenceBlock.parent;
    this.academicLevels = (yield this.report.academicLevels).toArray();
    this.isInOrderedSequence = false;
    this.orderInSequenceOptions = [];
    if (isPresent(this.parent) && this.parent.isOrdered) {
      this.isInOrderedSequence = true;
      const siblings = yield this.parent.children;
      for (let i = 0, n = siblings.toArray().length; i < n; i++) {
        const num = i + 1;
        this.orderInSequenceOptions.push(num);
      }
    }
    this.linkedSessions = yield sequenceBlock.sessions;
    this.startingAcademicLevel = yield sequenceBlock.startingAcademicLevel;
    this.endingAcademicLevel = yield sequenceBlock.endingAcademicLevel;
    this.required = sequenceBlock.required.toString();
    this.duration = sequenceBlock.duration;
    this.startDate = sequenceBlock.startDate;
    this.endDate = sequenceBlock.endDate;
    this.childSequenceOrder = sequenceBlock.childSequenceOrder.toString();
    this.orderInSequence = sequenceBlock.orderInSequence;
    this.description = sequenceBlock.description;
    this.course = yield sequenceBlock.course;
    this.minimum = sequenceBlock.minimum;
    this.maximum = sequenceBlock.maximum;
    this.sessions = yield this.getSessions(this.course);
    this.linkableCourses = yield this.getLinkableCourses(this.report, this.course);
  }

  get requiredLabel() {
    switch (this.required) {
      case '1':
        return this.intl.t('general.required');
      case '2':
        return this.intl.t('general.optionalElective');
      case '3':
        return this.intl.t('general.requiredInTrack');
      default:
        return null;
    }
  }

  get isElective() {
    return '2' === this.required;
  }

  get isSelective() {
    if (this.isElective) {
      return false;
    }
    const minimum = parseInt(this.minimum, 10);
    const maximum = parseInt(this.maximum, 10);
    return minimum > 0 && minimum !== maximum;
  }

  get childSequenceOrderLabel() {
    switch (this.childSequenceOrder) {
      case '1':
        return this.intl.t('general.ordered');
      case '2':
        return this.intl.t('general.unordered');
      case '3':
        return this.intl.t('general.parallel');
      default:
        return null;
    }
  }

  /**
   * Returns a list of courses that can be linked to this sequence block.
   */
  async getLinkableCourses(report, course) {
    if (!report) {
      return [];
    }
    const program = await report.program;
    const schoolId = program.belongsTo('school').id();
    const allLinkableCourses = await this.store.query('course', {
      filters: {
        published: true,
        school: [schoolId],
        year: report.get('year'),
      },
    });
    const linkedCourses = await report.getLinkedCourses();
    // Filter out all courses that are linked to (sequence blocks in) this report.
    const linkableCourses = allLinkableCourses.filter((course) => {
      return !linkedCourses.includes(course);
    });
    // Always add the currently linked course to this list, if existent.
    if (isPresent(course)) {
      linkableCourses.pushObject(course);
    }

    return linkableCourses;
  }

  /**
   * Returns a list of published sessions that belong to a given course.
   */
  async getSessions(course) {
    if (!course) {
      return [];
    }
    const sessions = await this.store.query('session', {
      filters: {
        course: course.get('id'),
        published: true,
      },
    });
    return sessions.toArray();
  }

  @dropTask
  *changeRequired() {
    this.args.sequenceBlock.required = parseInt(this.required, 10);
    if ('2' === this.required) {
      this.args.sequenceBlock.minimum = 0;
    }
    yield this.args.sequenceBlock.save();
  }

  @action
  setRequired(required) {
    this.required = required;
    if ('2' === required) {
      this.minimum = 0;
    } else {
      this.minimum = this.args.sequenceBlock.minimum;
    }
  }

  @action
  revertRequiredChanges() {
    this.required = this.args.sequenceBlock.required.toString();
    this.minimum = this.args.sequenceBlock.minimum;
  }

  @action
  async saveCourse() {
    if (!this.isDestroying) {
      const oldCourse = await this.args.sequenceBlock.course;
      if (oldCourse !== this.course) {
        this.args.sequenceBlock.set('sessions', []);
        this.args.sequenceBlock.set('excludedSessions', []);
      }
      this.args.sequenceBlock.set('course', this.course);
      await this.args.sequenceBlock.save();
    }
  }

  @action
  async revertCourseChanges() {
    this.course = await this.args.sequenceBlock.get('course');
  }

  @dropTask()
  *changeTrack(value) {
    this.args.sequenceBlock.set('track', value);
    yield this.args.sequenceBlock.save();
  }

  @dropTask()
  *saveDescription() {
    this.args.sequenceBlock.set('description', this.description);
    yield this.args.sequenceBlock.save();
  }

  @action
  revertDescriptionChanges() {
    this.description = this.args.sequenceBlock.get('description');
  }

  @dropTask
  *changeChildSequenceOrder() {
    this.args.sequenceBlock.set('childSequenceOrder', parseInt(this.childSequenceOrder, 10));
    const savedBlock = yield this.args.sequenceBlock.save();
    const children = yield savedBlock.get('children');
    yield children.invoke('reload');
  }

  @action
  revertChildSequenceOrderChanges() {
    this.childSequenceOrder = this.args.sequenceBlock.get('childSequenceOrder').toString();
  }

  @dropTask
  *changeStartingAcademicLevel() {
    this.addErrorDisplaysFor(['startingAcademicLevel']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.set('startingAcademicLevel', this.startingAcademicLevel);
    yield this.args.sequenceBlock.save();
  }

  @action
  setStartingAcademicLevel(event) {
    const id = event.target.value;
    this.startingAcademicLevel = this.academicLevels.findBy('id', id);
  }

  @action
  revertStartingAcademicLevelChanges() {
    this.startingAcademicLevel = this.args.sequenceBlock.startingAcademicLevel;
  }

  @dropTask
  *changeEndingAcademicLevel() {
    this.addErrorDisplaysFor(['endingAcademicLevel']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.set('endingAcademicLevel', this.endingAcademicLevel);
    yield this.args.sequenceBlock.save();
  }

  @action
  setEndingAcademicLevel(event) {
    const id = event.target.value;
    this.endingAcademicLevel = this.academicLevels.findBy('id', id);
  }

  @action
  revertEndingAcademicLevelChanges() {
    this.endingAcademicLevel = this.args.sequenceBlock.endingAcademicLevel;
  }

  @dropTask
  *changeOrderInSequence() {
    this.args.sequenceBlock.set('orderInSequence', this.orderInSequence);
    const savedBlock = yield this.args.sequenceBlock.save();
    const parent = yield savedBlock.parent;
    const children = yield parent.children;
    yield all(children.invoke('reload'));
  }

  @action
  revertOrderInSequenceChanges() {
    this.orderInSequence = this.args.sequenceBlock.orderInSequence;
  }

  @action
  editMinMax() {
    this.isEditingMinMax = true;
  }

  @action
  cancelMinMaxEditing() {
    this.minimum = this.args.sequenceBlock.minimum;
    this.maximum = this.args.sequenceBlock.maximum;
    this.isEditingMinMax = false;
  }

  @action
  toggleManagingSessions() {
    this.isManagingSessions = !this.isManagingSessions;
  }

  @action
  cancelManagingSessions() {
    this.isManagingSessions = false;
  }

  @dropTask
  *changeSessions(sessions, excludedSessions) {
    this.args.sequenceBlock.set('sessions', sessions);
    this.args.sequenceBlock.set('excludedSessions', excludedSessions);
    yield this.args.sequenceBlock.save();
    this.isManagingSessions = false;
  }

  @action
  updateCourse(event) {
    const value = event.target.value;
    if (!value) {
      this.course = null;
    } else {
      this.course = this.linkableCourses.findBy('id', value);
    }
  }

  @action
  changeDescription(event) {
    this.description = event.target.value;
  }

  @action
  updateOrderInSequence(event) {
    this.orderInSequence = event.target.value;
  }

  @action
  async saveOrCancelMinMax(ev) {
    const keyCode = ev.keyCode;
    if (13 === keyCode) {
      await this.saveMinMax.perform();
      return;
    }

    if (27 === keyCode) {
      this.isEditingMinMax = false;
    }
  }

  @action
  validateMaximumCallback() {
    const max = parseInt(this.maximum, 10) || 0;
    const min = parseInt(this.minimum, 10) || 0;
    return max >= min;
  }

  @action
  validateMaximumMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.intl.t('general.minimum'),
      description: this.intl.t('general.maximum'),
    });
  }

  @action
  validateStartingEndingLevelCallback() {
    return this.endingAcademicLevel.level >= this.startingAcademicLevel.level;
  }

  @action
  validateEndingLevelMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.intl.t('general.startLevel'),
      description: this.intl.t('general.endLevel'),
    });
  }

  @action
  validateStartingLevelMessageCallback() {
    return this.intl.t('errors.lessThanOrEqualTo', {
      lte: this.intl.t('general.endLevel'),
      description: this.intl.t('general.startLevel'),
    });
  }

  @action
  validateDurationCallback() {
    const duration = parseInt(this.duration, 10);
    return this.linkedCourseIsClerkship ? duration >= 1 : duration >= 0;
  }

  @action
  validateDurationMessageCallback() {
    return this.intl.t('errors.greaterThanOrEqualTo', {
      gte: this.linkedCourseIsClerkship ? 1 : 0,
      description: this.intl.t('general.duration'),
    });
  }

  @dropTask
  *saveMinMax() {
    this.addErrorDisplaysFor(['minimum', 'maximum']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.minimum = this.minimum;
    this.args.sequenceBlock.maximum = this.maximum;

    yield this.args.sequenceBlock.save();
    this.isEditingMinMax = false;
  }

  @action
  changeStartDate(startDate) {
    this.startDate = startDate;
  }

  @action
  changeEndDate(endDate) {
    this.endDate = endDate;
  }

  @dropTask
  *saveDuration() {
    this.addErrorDisplaysFor(['startDate', 'endDate', 'duration']);
    const isValid = yield this.isValid();
    if (!isValid) {
      return false;
    }
    this.args.sequenceBlock.startDate = this.startDate;
    this.args.sequenceBlock.endDate = this.endDate;
    this.args.sequenceBlock.duration = this.duration;
    yield this.args.sequenceBlock.save();
    this.isEditingDatesAndDuration = false;
  }

  @action
  cancelDurationEditing() {
    this.startDate = this.args.sequenceBlock.startDate;
    this.endDate = this.args.sequenceBlock.endDate;
    this.duration = this.args.sequenceBlock.duration;
    this.isEditingDatesAndDuration = false;
  }

  @action
  async saveOrCancelDuration(ev) {
    const keyCode = ev.keyCode;
    if (13 === keyCode) {
      await this.saveDuration.perform();
      return;
    }

    if (27 === keyCode) {
      this.isEditingDatesAndDuration = false;
    }
  }
}
<|endoftext|>


<commit_before>import { loadFile, writeToFile } from '../../utility/file'

const mps = loadFile('data/mps.json')
const subjects = loadFile('data/subjects.json')
const allVotes = loadFile('data/all-votes.json')

console.log('Loaded all data needed, derp!')

const globalVoteTypes = {}
const globalWordTypes = {}

function createOccuranceMap() {
  const occuranceMap = {}

  allVotes.forEach(mpVotes => {
    occuranceMap[mpVotes.mpId] = {}

    mpVotes.votes.forEach(voteTopic => {
      const topicId = voteTopic.id
      const subjectWords = subjects[topicId].words.subjects

      voteTopic.votes.forEach(vote => {
        const voteValue = vote.vote

        if (!globalVoteTypes[voteValue]) {
          globalVoteTypes[voteValue] = true
        }

        subjectWords.forEach(word => {
          if (!globalWordTypes[word]) {
            globalWordTypes[word] = true
          }

          if (!occuranceMap[mpVotes.mpId][word]) {
            occuranceMap[mpVotes.mpId][word] = {}
          }

          if (!occuranceMap[mpVotes.mpId][word][voteValue]) {
            occuranceMap[mpVotes.mpId][word][voteValue] = 0
          }

          occuranceMap[mpVotes.mpId][word][voteValue]++
        })
      })
    })
  })

  return occuranceMap
}

function getTopListOf(occuranceMap, mpId, voteType, words) {
  const wordsThatHaveType = []

  words.forEach(word => {
    if (occuranceMap[mpId][word][voteType] !== undefined) {
      wordsThatHaveType.push(word)
    }
  })

  if (wordsThatHaveType.length) {
    return wordsThatHaveType.sort((a, b) => {
      const occurancesForA = occuranceMap[mpId][a][voteType]
      const occurancesForB = occuranceMap[mpId][b][voteType]

      return occurancesForB - occurancesForA
    }).map(word => `${word}: ${occuranceMap[mpId][word][voteType]}`)
  }
  return []
}

function generateTopListsForMp(voteTypes, occuranceMap, mpId, words) {
  const topLists = voteTypes.map(voteType => {
    const topList = getTopListOf(
      occuranceMap,
      mpId,
      voteType,
      words
    )

    return {
      voteType,
      words: topList,
    }
  })

  return topLists.filter(topList => topList.words.length)
}

function process() {
  const occuranceMap = createOccuranceMap()
  const voteTypes = Object.keys(globalVoteTypes)

  console.log('Vote types')
  console.log(voteTypes)

  const uniqueWords = Object.keys(globalWordTypes)

  console.log(`\nWords that occured (${uniqueWords.length})`)
  console.log(uniqueWords)
  const summary = []
  mps.forEach(mp => {
    if (occuranceMap[mp.id] !== undefined) {
      const words = Object.keys(occuranceMap[mp.id])

      summary.push({
        name: mp.name,
        top: generateTopListsForMp(
          voteTypes,
          occuranceMap,
          mp.id,
          words
        ),
      })
    }
  })

  writeToFile(summary, 'data/mp-positions.json', true)
}

export default process
<commit_msg>Also connect votes to tags in topics<commit_after>import { loadFile, writeToFile } from '../../utility/file'

const mps = loadFile('data/mps.json')
const subjects = loadFile('data/subjects.json')
const allVotes = loadFile('data/all-votes.json')

console.log('Loaded all data needed, derp!')

const globalVoteTypes = {}
const uniqueSubjectsMap = {}
const uniqueTagsMap = {}

function updateOccuranceCount(occuranceMap, mpId, word, vote) {
  if (!occuranceMap[mpId][word]) {
    occuranceMap[mpId][word] = {}
  }

  if (!occuranceMap[mpId][word][vote]) {
    occuranceMap[mpId][word][vote] = 0
  }

  occuranceMap[mpId][word][vote]++
}

function createOccuranceMaps() {
  const subjectOccuranceMap = {}
  const tagOccuranceMap = {}

  allVotes.forEach(mpVotes => {
    subjectOccuranceMap[mpVotes.mpId] = {}
    tagOccuranceMap[mpVotes.mpId] = {}

    mpVotes.votes.forEach(voteTopic => {
      const topicId = voteTopic.id
      const subjectWords = subjects[topicId].words.subjects
      const tagWords = subjects[topicId].words.tags

      voteTopic.votes.forEach(vote => {
        const voteValue = vote.vote

        if (!globalVoteTypes[voteValue]) {
          globalVoteTypes[voteValue] = true
        }

        subjectWords.forEach(word => {
          if (!uniqueSubjectsMap[word]) {
            uniqueSubjectsMap[word] = true
          }

          updateOccuranceCount(
            subjectOccuranceMap,
            mpVotes.mpId,
            word,
            voteValue
          )
        })

        tagWords.forEach(word => {
          if (!uniqueTagsMap[word]) {
            uniqueTagsMap[word] = true
          }

          updateOccuranceCount(
            tagOccuranceMap,
            mpVotes.mpId,
            word,
            voteValue
          )
        })
      })
    })
  })

  return {
    subjects: subjectOccuranceMap,
    tags: tagOccuranceMap,
  }
}

function getTopListOf(occuranceMap, mpId, voteType, words) {
  const wordsThatHaveType = []

  words.forEach(word => {
    if (occuranceMap[mpId][word][voteType] !== undefined) {
      wordsThatHaveType.push(word)
    }
  })

  if (wordsThatHaveType.length) {
    return wordsThatHaveType.sort((a, b) => {
      const occurancesForA = occuranceMap[mpId][a][voteType]
      const occurancesForB = occuranceMap[mpId][b][voteType]

      return occurancesForB - occurancesForA
    }).map(word => `${word}: ${occuranceMap[mpId][word][voteType]}`)
  }
  return []
}

function generateTopListsForMp(voteTypes, occuranceMaps, mpId, words) {
  const topList = voteTypes.map(voteType => {
    const topSubjects = getTopListOf(
      occuranceMaps.subjects,
      mpId,
      voteType,
      words.subjects
    )

    const topTags = getTopListOf(
      occuranceMaps.tags,
      mpId,
      voteType,
      words.tags
    )

    return {
      voteType,
      subjects: topSubjects.slice(0, 10),
      tags: topTags.slice(0, 10),
    }
  }).filter(voteType => voteType.subjects.length || voteType.tags.length)

  return topList
}

function process() {
  const occuranceMaps = createOccuranceMaps()
  const voteTypes = Object.keys(globalVoteTypes)

  console.log('Vote types')
  console.log(voteTypes)

  const uniqueSubjects = Object.keys(uniqueSubjectsMap)
  const uniqueTags = Object.keys(uniqueTagsMap)

  console.log(`\nWords that occured (${uniqueSubjects.length} and tags: ${uniqueTags.length})`)
  console.log(uniqueSubjects)
  console.log(uniqueTags)
  const summary = []
  mps.forEach(mp => {
    if (occuranceMaps.subjects[mp.id] !== undefined) {
      const subjectWords = Object.keys(occuranceMaps.subjects[mp.id])
      const tagWords = Object.keys(occuranceMaps.tags[mp.id])

      summary.push({
        name: mp.name,
        top: generateTopListsForMp(
          voteTypes,
          occuranceMaps,
          mp.id,
          {
            subjects: subjectWords,
            tags: tagWords,
          }
        ),
      })
    }
  })

  writeToFile(summary, 'data/mp-positions.json', true)
}

export default process
<|endoftext|>