<commit_before>import React, { Component, PropTypes } from "react";
import ReactDOM from "react-dom";
import { Motion, spring } from "react-motion";
import { omit, defer } from "lodash";

import {
  SpringSettings,
  BLACKLIST_CURRENT_ELEMENT_DESELECT
} from "../../../../constants";
import { getElementDimensions, getPointsToSnap, snap } from "../../../../utils";
import styles from "./index.css";
import ResizeNode from "../../resize-node";
import Arrange from "../arrange";

export default class ImageElement extends Component {
  static propTypes = {
    elementIndex: PropTypes.number,
    component: PropTypes.shape({
      ComponentClass: React.PropTypes.any.isRequired,
      props: PropTypes.object
    }),
    selected: PropTypes.bool,
    mousePosition: PropTypes.array,
    scale: PropTypes.number,
    showGridLine: PropTypes.func,
    hideGridLine: PropTypes.func
  };

  static contextTypes = {
    store: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);

    this.state = {
      isPressed: false,
      mouseStart: [0, 0],
      delta: [0, 0]
    };
  }

  componentDidMount() {
    defer(() => {
      const { width, height } = this.currentElementComponent.getBoundingClientRect();

      this.setState({ // eslint-disable-line react/no-did-mount-set-state
        width,
        height
      });
    });
  }

  shouldComponentUpdate() {
    // This is needed because of the way the component is passed down
    // React isn't re-rendering this when the contextual menu updates the store
    return true;
  }

  handleTouchStartResize = (ev) => {
    ev.preventDefault();
    this.handleMouseDownResize(ev.touches[0]);
  }

  handleMouseDownResize = (ev) => {
    ev.stopPropagation();
    ev.preventDefault();

    const { currentTarget, pageX, pageY } = ev;

    const isLeftSideDrag =
      currentTarget === this.leftResizeNode ||
      currentTarget === this.topLeftNode ||
      currentTarget === this.bottomLeftNode;
    const isTopDrag =
      currentTarget === this.topLeftNode ||
      currentTarget === this.topRightNode;
    const verticalResize =
      currentTarget === this.topLeftNode ||
      currentTarget === this.bottomLeftNode ||
      currentTarget === this.topRightNode ||
      currentTarget === this.bottomRightNode;

    const { width, height } = this.currentElementComponent.getBoundingClientRect();
    const componentProps = this.props.component.props;
    const componentLeft = componentProps.style && componentProps.style.left;
    const componentTop = componentProps.style && componentProps.style.top;
    const left = componentLeft || 0;
    const top = componentTop || 0;

    this.gridLines = this.context.store.gridLines;

    this.setState({
      isTopDrag,
      isLeftSideDrag,
      verticalResize,
      isResizing: true,
      width,
      height,
      top,
      left,
      resizeLastX: pageX,
      resizeLastY: pageY
    });

    window.addEventListener("mousemove", this.handleMouseMoveResize);
    window.addEventListener("touchmove", this.handleTouchMoveResize);
    window.addEventListener("mouseup", this.handleMouseUpResize);
    window.addEventListener("touchend", this.handleTouchEndResize);
  }

  handleTouchMoveResize = (ev) => {
    ev.preventDefault();
    this.handleMouseMoveResize(ev.touches[0]);
  }

  handleMouseMoveResize = (ev) => {
    ev.preventDefault();
    const { pageX, pageY } = ev;
    const {
      isLeftSideDrag,
      resizeLastX,
      width,
      height,
      resizeLastY,
      isTopDrag,
      verticalResize
    } = this.state;

    let { left, top } = this.state;
    const delta = [];
    let isSnapped;

    const snapCallback = (line, index) => {
      if (line === null) {
        this.props.hideGridLine(true);
        isSnapped = false;
        return;
      }

      let pointToAlignWithLine;

      if (index === 0) {
        pointToAlignWithLine = left;
      }

      if (index === 1) {
        pointToAlignWithLine = Math.ceil(left + width / 2);
      }

      if (index === 2) {
        pointToAlignWithLine = Math.ceil(left + width);
      }

      const distance = Math.abs(pointToAlignWithLine - line);

      if (distance <= 3) {
        isSnapped = true;
        this.props.showGridLine(line, true);
      }
    };

    snap(
      this.gridLines.vertical,
      getPointsToSnap(
        left,
        width,
        (Math.max(pageX, resizeLastX) - Math.min(pageX, resizeLastX)) / 2
      ),
      snapCallback
    );

    if (verticalResize) {
      snap(
        this.gridLines.horizontal,
        getPointsToSnap(
          top,
          height,
          (Math.max(pageY, resizeLastY) - Math.min(pageY, resizeLastY)) / 2
        ),
        snapCallback
      );
    }

    if (isSnapped) {
      return;
    }

    if (isLeftSideDrag) {
      delta[0] = resizeLastX - pageX;
      left -= delta[0];
    } else {
      delta[0] = pageX - resizeLastX;
    }

    if (isTopDrag) {
      delta[1] = resizeLastY - pageY;
      top -= delta[1];
    } else {
      delta[1] = pageY - resizeLastY;
    }

    const newWidth = delta[0] + width;
    const newHeight = delta[1] + height;
    let nextState = {};

    if (newWidth >= 0) {
      nextState = { left, width: (delta[0] + width), resizeLastX: pageX };
    }

    if (newHeight >= 0) {
      nextState = { ...nextState, top, height: (delta[0] + height), resizeLastY: pageY };
    }

    if (Object.keys(nextState).length) {
      this.setState(nextState);
    }
  }

  handleTouchEndResize = (ev) => {
    ev.preventDefault();
    this.handleMouseUpResize(ev.touches[0]);
  }

  handleMouseUpResize = (ev) => {
    ev.preventDefault();
    window.removeEventListener("mousemove", this.handleMouseMoveResize);
    window.removeEventListener("mouseup", this.handleMouseUpResize);
    window.removeEventListener("touchmove", this.handleTouchMoveResize);
    window.removeEventListener("touchend", this.handleTouchEndResize);

    this.props.hideGridLine(true);
    this.setState({
      isResizing: false
    });

    const { width, left, top, height } = this.state;
    const propStyles = { ...this.props.component.props.style, width, left, top, height };

    this.context.store.updateElementProps({ style: propStyles });
  }

  handleTouchStart = (ev) => {
    ev.preventDefault();
    this.handleMouseDown(ev.touches[0]);
  }

  handleTouchMove = (ev) => {
    ev.preventDefault();
    this.handleMouseMove(ev.touches[0]);
  }

  handleMouseMove = ({ pageX, pageY, offsetX, offsetY, target: { id } }) => {
    const {
      mouseStart: [x, y],
      mouseOffset: [mouseOffsetX, mouseOffsetY],
      originalPosition: [originalX, originalY],
      width,
      height
    } = this.state;

    const newDelta = [pageX - x, pageY - y];

    // Note: This doesn't handle the case of the mouse being off the slide and part of the element
    // still on the slide. AKA no gridlines or snapping will occur when mouse is outside of the
    // slide.
    if (id === "slide") {
      const createSnapCallback = (isVertical, length, originalPoint) => (line, index) => {
        if (line === null) {
          this.props.hideGridLine(isVertical);

          return;
        }

        this.props.showGridLine(line, /* isVertical */ isVertical);

        // Index 0 = starting edge, 1 = middle, 2 = ending edge
        const offset = originalPoint + (length / 2 * index);

        // Set either x or y
        newDelta[isVertical ? 0 : 1] = line - offset;
      };

      snap(
        this.gridLines.horizontal,
        getPointsToSnap(offsetY, height, mouseOffsetY),
        createSnapCallback(false, height, originalY)
      );

      snap(
        this.gridLines.vertical,
        getPointsToSnap(offsetX, width, mouseOffsetX),
        createSnapCallback(true, width, originalX)
      );
    } else {
      this.props.hideGridLine(true);
      this.props.hideGridLine(false);
    }

    this.setState({
      delta: newDelta
    });
  }

  handleMouseDown = (ev) => {
    ev.preventDefault();

    if (this.clickStart) {
      this.clicking = false;
      this.handleDoubleClick(ev);

      return;
    }

    this.clickStart = new Date().getTime();
    this.context.store.setCurrentElementIndex(this.props.elementIndex);

    const { pageX, pageY, target } = ev;
    const boundingBox = target.getBoundingClientRect();
    const mouseOffset = [Math.floor(boundingBox.left - pageX), Math.floor(boundingBox.top - pageY)];
    const originalPosition = [
      this.props.component.props.style.left,
      this.props.component.props.style.top
    ];

    let { width, height } = getElementDimensions(this.props.component);

    if (height === undefined) {
      height = this.currentElementComponent.getBoundingClientRect().height;
    }

    if (width === undefined) {
      width = this.currentElementComponent.getBoundingClientRect().width;
    }

    window.addEventListener("mouseup", this.handleMouseUp);
    window.addEventListener("touchend", this.handleMouseUp);

    // Do this preemptively so that dragging doesn't take the performance hit
    this.gridLines = this.context.store.gridLines;

    // Only do drag if we hold the mouse down for a bit
    this.mouseClickTimeout = setTimeout(() => {
      this.clickStart = null;
      this.mouseClickTimeout = null;

      this.context.store.updateElementDraggingState(true, true);

      // Make the cursor dragging everywhere
      document.body.style.cursor = "-webkit-grabbing";

      // TODO: handle elements that aren't absolutely positioned?
      this.setState({
        delta: [0, 0],
        mouseStart: [pageX, pageY],
        isPressed: true,
        mouseOffset,
        originalPosition,
        width,
        height
      });

      window.addEventListener("touchmove", this.handleTouchMove);
      window.addEventListener("mousemove", this.handleMouseMove);
    }, 150);
  }

  handleMouseUp = () => {
    if (this.mouseClickTimeout || this.mouseClickTimeout === 0) {
      clearTimeout(this.mouseClickTimeout);
      window.removeEventListener("mouseup", this.handleMouseUp);
      window.removeEventListener("touchend", this.handleMouseUp);

      this.mouseClickTimeout = null;

      // this.props.onDropElement(this.props.elementType);
      const timeSinceMouseDown = new Date().getTime() - this.clickStart;

      // Give the user the remainder of the 250ms to do a double click
      setTimeout(() => {
        this.clickStart = null;
      }, 250 - timeSinceMouseDown);

      return;
    }

    window.removeEventListener("touchmove", this.handleTouchMove);
    window.removeEventListener("touchend", this.handleMouseUp);
    window.removeEventListener("mousemove", this.handleMouseMove);
    window.removeEventListener("mouseup", this.handleMouseUp);

    // Reset the cursor dragging to auto
    document.body.style.cursor = "auto";

    this.props.hideGridLine(false);
    this.props.hideGridLine(true);

    this.context.store.updateElementDraggingState(false);
    this.context.store.updateElementProps({
      style: {
        left: this.state.delta[0] + this.props.component.props.style.left,
        top: this.state.delta[1] + this.props.component.props.style.top
      }
    });

    this.setState({
      delta: [0, 0],
      mouseStart: [0, 0],
      isPressed: false
    });
  }

  handleDoubleClick = () => {
    // double click
  }

  render() {
    const {
      elementIndex,
      selected,
      component: { ComponentClass, props },
      mousePosition,
      scale
    } = this.props;

    const {
      width,
      height,
      isResizing,
      isPressed,
      delta: [x, y],
      left,
      top
    } = this.state;

    const currentlySelected = selected || elementIndex === this.context.store.currentElementIndex;
    const extraClasses = currentlySelected ? ` ${styles.selected}` : "";

    const wrapperStyle = {};
    const motionStyles = {};
    let elementStyle = props.style ? { ...props.style } : {};

    if (this.context.store.isDragging) {
      wrapperStyle.pointerEvents = "none";
    }

    if (mousePosition || props.style && props.style.position === "absolute") {
      wrapperStyle.position = "absolute";

      const mouseX = mousePosition && mousePosition[0] ? mousePosition[0] : null;
      motionStyles.left = spring(
        mouseX && mouseX || props.style && props.style.left || 0,
        SpringSettings.DRAG
      );
      const mouseY = mousePosition && mousePosition[1] ? mousePosition[1] : null;
      motionStyles.top = spring(
        mouseY && mouseY || props.style && props.style.top || 0,
        SpringSettings.DRAG
      );

      motionStyles.width = spring((width && width || 0), SpringSettings.RESIZE);
      motionStyles.height = spring((height && height || 0), SpringSettings.RESIZE);

      if (mousePosition) {
        wrapperStyle.whiteSpace = "nowrap";
      }

      if (scale) {
        wrapperStyle.transform = `scale(${scale})`;
      }
    }

    elementStyle = { ...elementStyle, position: "relative", left: 0, top: 0 };

    if (isPressed) {
      motionStyles.left = spring((props.style && props.style.left || 0) + x, SpringSettings.DRAG);
      motionStyles.top = spring((props.style && props.style.top || 0) + y, SpringSettings.DRAG);
    }

    if (isResizing) {
      const componentStylesLeft = props.style && props.style.left || 0;
      const componentStylesTop = props.style && props.style.top || 0;

      motionStyles.top = spring(
        top === undefined ? componentStylesTop : top,
        SpringSettings.RESIZE
      );
      motionStyles.left = spring(
        left === undefined ? componentStylesLeft : left,
        SpringSettings.RESIZE
      );
      motionStyles.height = spring(height, SpringSettings.RESIZE);
      motionStyles.width = spring(width, SpringSettings.RESIZE);
    }
    console.log(motionStyles);
    return (
        <Motion
          style={motionStyles}
        >
          {computedStyles => {
            const computedDragStyles = omit(computedStyles, "width", "height");
            let computedResizeStyles = { ...computedStyles };

            if (!isResizing) {
              computedResizeStyles = {};
            }

            return (
              <div
                className={
                  `${styles.canvasElement} ${extraClasses} ${BLACKLIST_CURRENT_ELEMENT_DESELECT}`
                }
                ref={component => {this.currentElementComponent = component;}}
                style={{ ...wrapperStyle, ...computedDragStyles }}
                onMouseDown={this.handleMouseDown}
                onTouchStart={this.handleTouchStart}
              >
                {currentlySelected &&
                  <ResizeNode
                    cornerTopLeft
                    ref={component => {this.topLeftNode = ReactDOM.findDOMNode(component);}}
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    ref={component => {this.leftResizeNode = ReactDOM.findDOMNode(component);}}
                    alignLeft
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    ref={component => {this.bottomLeftNode = ReactDOM.findDOMNode(component);}}
                    cornerBottomLeft
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <Arrange />
                }
                  <ComponentClass
                    {...props}
                    className={styles.image}
                    style={{ ...elementStyle, ...computedResizeStyles }}
                  />
                {currentlySelected &&
                  <ResizeNode
                    cornerTopRight
                    ref={component => {this.topRightNode = ReactDOM.findDOMNode(component);}}
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    alignRight
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    cornerBottomRight
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
              </div>
          );}}
        </Motion>
    );
  }
}
<commit_msg>bug fixes for resize nodes<commit_after>import React, { Component, PropTypes } from "react";
import ReactDOM from "react-dom";
import { Motion, spring } from "react-motion";
import { omit, defer } from "lodash";

import {
  SpringSettings,
  BLACKLIST_CURRENT_ELEMENT_DESELECT
} from "../../../../constants";
import { getElementDimensions, getPointsToSnap, snap } from "../../../../utils";
import styles from "./index.css";
import ResizeNode from "../../resize-node";
import Arrange from "../arrange";

export default class ImageElement extends Component {
  static propTypes = {
    elementIndex: PropTypes.number,
    component: PropTypes.shape({
      ComponentClass: React.PropTypes.any.isRequired,
      props: PropTypes.object
    }),
    selected: PropTypes.bool,
    mousePosition: PropTypes.array,
    scale: PropTypes.number,
    showGridLine: PropTypes.func,
    hideGridLine: PropTypes.func
  };

  static contextTypes = {
    store: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);

    this.state = {
      isPressed: false,
      mouseStart: [0, 0],
      delta: [0, 0]
    };
  }

  componentDidMount() {
    defer(() => {
      const { width, height } = this.currentElementComponent.getBoundingClientRect();

      this.setState({ // eslint-disable-line react/no-did-mount-set-state
        width,
        height
      });
    });
  }

  shouldComponentUpdate() {
    // This is needed because of the way the component is passed down
    // React isn't re-rendering this when the contextual menu updates the store
    return true;
  }

  handleTouchStartResize = (ev) => {
    ev.preventDefault();
    this.handleMouseDownResize(ev.touches[0]);
  }

  handleMouseDownResize = (ev) => {
    ev.stopPropagation();
    ev.preventDefault();

    const { currentTarget, pageX, pageY } = ev;

    const isLeftSideDrag =
      currentTarget === this.leftResizeNode ||
      currentTarget === this.topLeftNode ||
      currentTarget === this.bottomLeftNode;
    const isTopDrag =
      currentTarget === this.topLeftNode ||
      currentTarget === this.topRightNode;
    const verticalResize =
      currentTarget === this.topLeftNode ||
      currentTarget === this.bottomLeftNode ||
      currentTarget === this.topRightNode ||
      currentTarget === this.bottomRightNode;

    const { width, height } = this.currentElementComponent.getBoundingClientRect();
    const componentProps = this.props.component.props;
    const componentLeft = componentProps.style && componentProps.style.left;
    const componentTop = componentProps.style && componentProps.style.top;
    const left = componentLeft || 0;
    const top = componentTop || 0;

    this.gridLines = this.context.store.gridLines;

    this.setState({
      isTopDrag,
      isLeftSideDrag,
      verticalResize,
      isResizing: true,
      width,
      height,
      top,
      left,
      resizeLastX: pageX,
      resizeLastY: pageY
    });

    window.addEventListener("mousemove", this.handleMouseMoveResize);
    window.addEventListener("touchmove", this.handleTouchMoveResize);
    window.addEventListener("mouseup", this.handleMouseUpResize);
    window.addEventListener("touchend", this.handleTouchEndResize);
  }

  handleTouchMoveResize = (ev) => {
    ev.preventDefault();
    this.handleMouseMoveResize(ev.touches[0]);
  }

  handleMouseMoveResize = (ev) => {
    ev.preventDefault();
    const { pageX, pageY } = ev;
    const {
      isLeftSideDrag,
      resizeLastX,
      width,
      height,
      resizeLastY,
      isTopDrag,
      verticalResize
    } = this.state;

    let { left, top } = this.state;
    const delta = [];
    let isSnapped;

    const snapCallback = (line, index) => {
      if (line === null) {
        this.props.hideGridLine(true);
        isSnapped = false;
        return;
      }

      let pointToAlignWithLine;

      if (index === 0) {
        pointToAlignWithLine = left;
      }

      if (index === 1) {
        pointToAlignWithLine = Math.ceil(left + width / 2);
      }

      if (index === 2) {
        pointToAlignWithLine = Math.ceil(left + width);
      }

      const distance = Math.abs(pointToAlignWithLine - line);

      if (distance <= 3) {
        isSnapped = true;
        this.props.showGridLine(line, true);
      }
    };

    snap(
      this.gridLines.vertical,
      getPointsToSnap(
        left,
        width,
        (Math.max(pageX, resizeLastX) - Math.min(pageX, resizeLastX)) / 2
      ),
      snapCallback
    );

    if (isLeftSideDrag) {
      delta[0] = resizeLastX - pageX;
      left -= delta[0];
    } else {
      delta[0] = pageX - resizeLastX;
    }

    let nextState = {};
    const newWidth = delta[0] + width;

    if (newWidth >= 0) {
      nextState = { left, width: newWidth, resizeLastX: pageX };
    }

    if (verticalResize) {
      snap(
        this.gridLines.horizontal,
        getPointsToSnap(
          top,
          height,
          (Math.max(pageY, resizeLastY) - Math.min(pageY, resizeLastY)) / 2
        ),
        snapCallback
      );

      if (isTopDrag) {
        delta[1] = resizeLastY - pageY;
        top -= delta[1];
      } else {
        delta[1] = pageY - resizeLastY;
      }

      const newHeight = delta[1] + height;

      if (newHeight >= 0) {
        nextState = { ...nextState, top, height: newHeight, resizeLastY: pageY };
      }
    }

    if (isSnapped) {
      return;
    }

    if (Object.keys(nextState).length) {
      this.setState(nextState);
    }
  }

  handleTouchEndResize = (ev) => {
    ev.preventDefault();
    this.handleMouseUpResize(ev.touches[0]);
  }

  handleMouseUpResize = (ev) => {
    ev.preventDefault();
    window.removeEventListener("mousemove", this.handleMouseMoveResize);
    window.removeEventListener("mouseup", this.handleMouseUpResize);
    window.removeEventListener("touchmove", this.handleTouchMoveResize);
    window.removeEventListener("touchend", this.handleTouchEndResize);

    this.props.hideGridLine(true);
    this.setState({
      isResizing: false
    });

    const { width, left, top, height } = this.state;
    const propStyles = { ...this.props.component.props.style, width, left, top, height };

    this.context.store.updateElementProps({ style: propStyles });
  }

  handleTouchStart = (ev) => {
    ev.preventDefault();
    this.handleMouseDown(ev.touches[0]);
  }

  handleTouchMove = (ev) => {
    ev.preventDefault();
    this.handleMouseMove(ev.touches[0]);
  }

  handleMouseMove = ({ pageX, pageY, offsetX, offsetY, target: { id } }) => {
    const {
      mouseStart: [x, y],
      mouseOffset: [mouseOffsetX, mouseOffsetY],
      originalPosition: [originalX, originalY],
      width,
      height
    } = this.state;

    const newDelta = [pageX - x, pageY - y];

    // Note: This doesn't handle the case of the mouse being off the slide and part of the element
    // still on the slide. AKA no gridlines or snapping will occur when mouse is outside of the
    // slide.
    if (id === "slide") {
      const createSnapCallback = (isVertical, length, originalPoint) => (line, index) => {
        if (line === null) {
          this.props.hideGridLine(isVertical);

          return;
        }

        this.props.showGridLine(line, /* isVertical */ isVertical);

        // Index 0 = starting edge, 1 = middle, 2 = ending edge
        const offset = originalPoint + (length / 2 * index);

        // Set either x or y
        newDelta[isVertical ? 0 : 1] = line - offset;
      };

      snap(
        this.gridLines.horizontal,
        getPointsToSnap(offsetY, height, mouseOffsetY),
        createSnapCallback(false, height, originalY)
      );

      snap(
        this.gridLines.vertical,
        getPointsToSnap(offsetX, width, mouseOffsetX),
        createSnapCallback(true, width, originalX)
      );
    } else {
      this.props.hideGridLine(true);
      this.props.hideGridLine(false);
    }

    this.setState({
      delta: newDelta
    });
  }

  handleMouseDown = (ev) => {
    ev.preventDefault();

    if (this.clickStart) {
      this.clicking = false;
      this.handleDoubleClick(ev);

      return;
    }

    this.clickStart = new Date().getTime();
    this.context.store.setCurrentElementIndex(this.props.elementIndex);

    const { pageX, pageY, target } = ev;
    const boundingBox = target.getBoundingClientRect();
    const mouseOffset = [Math.floor(boundingBox.left - pageX), Math.floor(boundingBox.top - pageY)];
    const originalPosition = [
      this.props.component.props.style.left,
      this.props.component.props.style.top
    ];

    let { width, height } = getElementDimensions(this.props.component);

    if (height === undefined) {
      height = this.currentElementComponent.getBoundingClientRect().height;
    }

    if (width === undefined) {
      width = this.currentElementComponent.getBoundingClientRect().width;
    }

    window.addEventListener("mouseup", this.handleMouseUp);
    window.addEventListener("touchend", this.handleMouseUp);

    // Do this preemptively so that dragging doesn't take the performance hit
    this.gridLines = this.context.store.gridLines;

    // Only do drag if we hold the mouse down for a bit
    this.mouseClickTimeout = setTimeout(() => {
      this.clickStart = null;
      this.mouseClickTimeout = null;

      this.context.store.updateElementDraggingState(true, true);

      // Make the cursor dragging everywhere
      document.body.style.cursor = "-webkit-grabbing";

      // TODO: handle elements that aren't absolutely positioned?
      this.setState({
        delta: [0, 0],
        mouseStart: [pageX, pageY],
        isPressed: true,
        mouseOffset,
        originalPosition,
        width,
        height
      });

      window.addEventListener("touchmove", this.handleTouchMove);
      window.addEventListener("mousemove", this.handleMouseMove);
    }, 150);
  }

  handleMouseUp = () => {
    if (this.mouseClickTimeout || this.mouseClickTimeout === 0) {
      clearTimeout(this.mouseClickTimeout);
      window.removeEventListener("mouseup", this.handleMouseUp);
      window.removeEventListener("touchend", this.handleMouseUp);

      this.mouseClickTimeout = null;

      // this.props.onDropElement(this.props.elementType);
      const timeSinceMouseDown = new Date().getTime() - this.clickStart;

      // Give the user the remainder of the 250ms to do a double click
      setTimeout(() => {
        this.clickStart = null;
      }, 250 - timeSinceMouseDown);

      return;
    }

    window.removeEventListener("touchmove", this.handleTouchMove);
    window.removeEventListener("touchend", this.handleMouseUp);
    window.removeEventListener("mousemove", this.handleMouseMove);
    window.removeEventListener("mouseup", this.handleMouseUp);

    // Reset the cursor dragging to auto
    document.body.style.cursor = "auto";

    this.props.hideGridLine(false);
    this.props.hideGridLine(true);

    this.context.store.updateElementDraggingState(false);
    this.context.store.updateElementProps({
      style: {
        left: this.state.delta[0] + this.props.component.props.style.left,
        top: this.state.delta[1] + this.props.component.props.style.top
      }
    });

    this.setState({
      delta: [0, 0],
      mouseStart: [0, 0],
      isPressed: false
    });
  }

  handleDoubleClick = () => {
    // double click
  }

  render() {
    const {
      elementIndex,
      selected,
      component: { ComponentClass, props },
      mousePosition,
      scale
    } = this.props;

    const {
      width,
      height,
      isResizing,
      isPressed,
      delta: [x, y],
      left,
      top
    } = this.state;

    const currentlySelected = selected || elementIndex === this.context.store.currentElementIndex;
    const extraClasses = currentlySelected ? ` ${styles.selected}` : "";

    const wrapperStyle = {};
    const motionStyles = {};
    let elementStyle = props.style ? { ...props.style } : {};

    if (this.context.store.isDragging) {
      wrapperStyle.pointerEvents = "none";
    }

    if (mousePosition || props.style && props.style.position === "absolute") {
      wrapperStyle.position = "absolute";

      const mouseX = mousePosition && mousePosition[0] ? mousePosition[0] : null;
      motionStyles.left = spring(
        mouseX && mouseX || props.style && props.style.left || 0,
        SpringSettings.DRAG
      );
      const mouseY = mousePosition && mousePosition[1] ? mousePosition[1] : null;
      motionStyles.top = spring(
        mouseY && mouseY || props.style && props.style.top || 0,
        SpringSettings.DRAG
      );

      motionStyles.width = spring((width && width || 0), SpringSettings.RESIZE);
      motionStyles.height = spring((height && height || 0), SpringSettings.RESIZE);

      if (mousePosition) {
        wrapperStyle.whiteSpace = "nowrap";
      }

      if (scale) {
        wrapperStyle.transform = `scale(${scale})`;
      }
    }

    elementStyle = { ...elementStyle, position: "relative", left: 0, top: 0 };

    if (isPressed) {
      motionStyles.left = spring((props.style && props.style.left || 0) + x, SpringSettings.DRAG);
      motionStyles.top = spring((props.style && props.style.top || 0) + y, SpringSettings.DRAG);
    }

    if (isResizing) {
      const componentStylesLeft = props.style && props.style.left || 0;
      const componentStylesTop = props.style && props.style.top || 0;

      motionStyles.top = spring(
        top === undefined ? componentStylesTop : top,
        SpringSettings.RESIZE
      );
      motionStyles.left = spring(
        left === undefined ? componentStylesLeft : left,
        SpringSettings.RESIZE
      );
      motionStyles.height = spring(height, SpringSettings.RESIZE);
      motionStyles.width = spring(width, SpringSettings.RESIZE);
    }

    return (
        <Motion
          style={motionStyles}
        >
          {computedStyles => {
            const computedDragStyles = omit(computedStyles, "width", "height");
            let computedResizeStyles = { ...computedStyles };

            if (!isResizing) {
              computedResizeStyles = {};
            }

            return (
              <div
                className={
                  `${styles.canvasElement} ${extraClasses} ${BLACKLIST_CURRENT_ELEMENT_DESELECT}`
                }
                ref={component => {this.currentElementComponent = component;}}
                style={{ ...wrapperStyle, ...computedDragStyles }}
                onMouseDown={this.handleMouseDown}
                onTouchStart={this.handleTouchStart}
              >
                {currentlySelected &&
                  <ResizeNode
                    cornerTopLeft
                    ref={component => {this.topLeftNode = ReactDOM.findDOMNode(component);}}
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    ref={component => {this.leftResizeNode = ReactDOM.findDOMNode(component);}}
                    alignLeft
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    ref={component => {this.bottomLeftNode = ReactDOM.findDOMNode(component);}}
                    cornerBottomLeft
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <Arrange />
                }
                  <ComponentClass
                    {...props}
                    className={styles.image}
                    style={{ ...elementStyle, ...computedResizeStyles }}
                  />
                {currentlySelected &&
                  <ResizeNode
                    cornerTopRight
                    ref={component => {this.topRightNode = ReactDOM.findDOMNode(component);}}
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    alignRight
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
                {currentlySelected &&
                  <ResizeNode
                    ref={component => {this.bottomRightNode = ReactDOM.findDOMNode(component);}}
                    cornerBottomRight
                    handleMouseDownResize={this.handleMouseDownResize}
                    onTouch={this.handleTouchStartResize}
                    component={this.props.component}
                  />
                }
              </div>
          );}}
        </Motion>
    );
  }
}
<|endoftext|>


<commit_before>'use strict';

angular
	.module(
		'npQuestion',
		[ ]
	);

angular
	.module('npQuestion')

	/** @ngInject */
	.controller( 'npQuestionController',
		function( $log, $scope, ManifestService, $sce )
		{
			var cmpData = $scope.component.data;
			$log.debug( 'npQuestion::data', cmpData );

			this.id = cmpData.id;
			this.content = $sce.trustAsHtml( cmpData.content );
			this.type = cmpData.type;
			this.feedback = '';

			var feedback = cmpData.feedback;

			this.changed = function()
			{
				$log.debug( 'npQuestion::answer changed' );
				if ( feedback.immediate )
				{
					this.feedback = '';
				}
			};

			this.evaluate = function()
			{
				$log.debug('npQuestion::evaluate:', this.answer);
				var correct = true;

				switch (this.type) {
					case 'radio':
						var answer = ManifestService.getComponent( this.answer );
						if ( !answer.data.correct )
						{
							correct = false;
						}
						break;
					case 'checkbox':
						var answers = ManifestService.getAll( 'npAnswer', $scope.cmpIdx );
						var idx;
						for ( idx in answers )
						{
							if ( answers[idx].data.correct )
							{
								// confirm all correct answers were checked
								if ( ! this.answer[ answers[idx].idx ] )
								{
									correct = false;
								}
							} else {
								// confirm no incorrect answers were checked
								if ( this.answer[ answers[idx].idx ] )
								{
									correct = false;
								}
							}
						}
						break;
					case 'text':
						break;
				}
				$log.debug('npQuestion::evaluate:pass', correct );

				// set by ng-model of npAnswer's input's
				if ( feedback.immediate )
				{
					if ( correct )
					{
						this.feedback = feedback.correct;
					} else {
						this.feedback = feedback.incorrect;
					}
				}
			};
		}
	)

	/** @ngInject */
	.run(
		function( $log, $rootScope )
		{
			$log.debug('npQuestion::component loaded!');
		}
	);

<commit_msg>Implemented text answer (manifest hold regexp string or array of acceptable strings).<commit_after>'use strict';

angular
	.module(
		'npQuestion',
		[ ]
	);

angular
	.module('npQuestion')

	/** @ngInject */
	.controller( 'npQuestionController',
		function( $log, $scope, ManifestService, $sce )
		{
			var cmpData = $scope.component.data;
			$log.debug( 'npQuestion::data', cmpData );

			this.id = cmpData.id;
			this.content = $sce.trustAsHtml( cmpData.content );
			this.type = cmpData.type;
			this.feedback = '';

			var feedback = cmpData.feedback;

			this.changed = function()
			{
				$log.debug( 'npQuestion::answer changed' );
				if ( feedback.immediate )
				{
					this.feedback = '';
				}
			};

			this.evaluate = function()
			{
				$log.debug('npQuestion::evaluate:', this.answer);
				var correct = true;

				switch (this.type) {
					case 'radio':
						var radAnswer = ManifestService.getComponent( this.answer );
						if ( !radAnswer.data.correct )
						{
							correct = false;
						}
						break;
					case 'checkbox':
						var chkAnswers = ManifestService.getAll( 'npAnswer', $scope.cmpIdx );
						var idx;
						for ( idx in chkAnswers )
						{
							if ( chkAnswers[idx].data.correct )
							{
								// confirm all correct answers were checked
								if ( ! this.answer[ chkAnswers[idx].idx ] )
								{
									correct = false;
								}
							} else {
								// confirm no incorrect answers were checked
								if ( this.answer[ chkAnswers[idx].idx ] )
								{
									correct = false;
								}
							}
						}
						break;
					case 'text':
						var txtAnswer = ManifestService.getFirst( 'npAnswer', $scope.cmpIdx );
						var key = txtAnswer.data.correct;
						var regExp,pat,mod='i';
						if ( angular.isString( key ) )
						{
							if ( key.indexOf('/') === 0 )
							{
								pat = key.substring( 1, key.lastIndexOf('/') );
								mod = key.substring( key.lastIndexOf('/')+1 );
							}
						} else
						if ( angular.isArray( key ) )
						{
							pat = '^(' + key.join('|') + ')$';
						}
						regExp = new RegExp( pat, mod );
						if ( ! regExp.test( this.answer ) )
						{
							correct = false;
						}
						break;
				}
				$log.debug('npQuestion::evaluate:pass', correct );

				// set by ng-model of npAnswer's input's
				if ( feedback.immediate )
				{
					if ( correct )
					{
						this.feedback = feedback.correct;
					} else {
						this.feedback = feedback.incorrect;
					}
				}
			};
		}
	)

	/** @ngInject */
	.run(
		function( $log, $rootScope )
		{
			$log.debug('npQuestion::component loaded!');
		}
	);

<|endoftext|>


<commit_before>HighFidelity.EpisodesController = Ember.ArrayController.extend({
    needs: ['player'],

    sortAscending: false,
    sortProperties: ['datePublished', 'episodeId'],

    actions: {
        setEpisode: function(episode) {
            this.get('controllers.player').send('setEpisode', episode);
        }
    }
});
<commit_msg>Sort only by datePublished<commit_after>HighFidelity.EpisodesController = Ember.ArrayController.extend({
    needs: ['player'],

    sortAscending: false,
    sortProperties: ['datePublished'],

    actions: {
        setEpisode: function(episode) {
            this.get('controllers.player').send('setEpisode', episode);
        }
    }
});
<|endoftext|>


<commit_before>import Underscore from 'underscore';
import d3 from 'd3';
import jQuery from 'jquery';
import ace from 'brace';
import 'brace/theme/textmate';
import Widget from '../Widget';
import Dataset from '../../../models/Dataset';
import myTemplate from './template.html';
import './style.css';

let STATUS = {
  NO_DATA: 0,
  SUCCESS: 1,
  CANT_LOAD: 2,
  CANT_PARSE: 3,
  LOADING: 4,
  NO_ATTRIBUTES: 5
};

let DatasetView = Widget.extend({
  initialize: function () {
    Widget.prototype.initialize.apply(this, arguments);
    this.friendlyName = 'Dataset';

    this.statusText.onclick = () => {
      window.mainPage.overlay.render('DatasetLibrary');
    };
    this.statusText.title = 'Select a different dataset.';

    this.icons.splice(0, 0, {
      src: () => {
        return window.mainPage.currentUser.preferences
          .hasSeenAllTips(this.getDefaultTips()) ? Widget.infoIcon : Widget.newInfoIcon;
      },
      title: () => {
        return 'About this panel';
      },
      onclick: () => {
        this.renderInfoScreen();
      }
    });

    this.status = STATUS.NO_DATA;
    this.icons.splice(0, 0, {
      src: () => {
        if (this.status === STATUS.LOADING) {
          return Widget.spinnerIcon;
        } else if (this.status === STATUS.SUCCESS) {
          return Widget.okayIcon;
        } else {
          return Widget.warningIcon;
        }
      },
      title: () => {
        if (this.status === STATUS.LOADING) {
          return 'The dataset hasn\'t finished loading yet';
        } else if (this.status === STATUS.SUCCESS) {
          return 'The dataset appears to have loaded correctly';
        } else {
          return 'Something isn\'t quite right; click for details';
        }
      },
      onclick: () => {
        this.renderHelpScreen();
      }
    });

    this.listenTo(window.mainPage, 'rl:changeProject',
      this.handleNewProject);
    this.handleNewProject();
  },
  handleNewProject: function () {
    this.$el.html('');
    this.status = STATUS.NO_DATA;

    this.listenTo(window.mainPage.project, 'rl:changeDatasets',
      this.render);
    /* this.listenTo(window.mainPage.project, 'rl:changeMatchings',
      this.render);*/
  },
  renderInfoScreen: function () {
    window.mainPage.helpLayer.showTips(this.getDefaultTips());
  },
  renderHelpScreen: function () {
    if (this.status === STATUS.NO_DATA) {
      window.mainPage.overlay.renderUserErrorScreen('You have not chosen a dataset yet. Click here to choose one.');
    } else if (this.status === STATUS.SUCCESS) {
      window.mainPage.overlay.renderSuccessScreen('The dataset appears to have loaded correctly.');
    } else if (this.status === STATUS.CANT_LOAD) {
      window.mainPage.overlay.renderErrorScreen('The dataset could not be loaded; there is a good chance that there is a permissions problem.');
    } else if (this.status === STATUS.CANT_PARSE) {
      window.mainPage.overlay.renderUserErrorScreen('There was a problem parsing the data; you\'ll probably need to edit or reshape the data in order to use it.');
    } else if (this.status === STATUS.NO_ATTRIBUTES) {
      window.mainPage.overlay.renderUserErrorScreen('There was a problem parsing the data. Specifically, we\'re having trouble understanding the dataset attributes (usually column headers); you\'ll probably need to edit or reshape the data in order to use it.');
    }
  },
  renderAttributeSettings: function () {
    let datasets = window.mainPage.project.getMeta('datasets');
    let dataset;
    let attrs;
    if (datasets && datasets[0] && window.mainPage.loadedDatasets[datasets[0]]) {
      dataset = window.mainPage.loadedDatasets[datasets[0]];
      attrs = dataset.getSpec().attributes;
    } else {
      attrs = {};
    }
    let attrOrder = Object.keys(attrs);
    // TODO: this is technically cheating; relying on the order
    // of the dict entries to preserve the order on screen

    let cells = d3.select(this.el).select('#attributeSettings')
      .selectAll('div.cell')
      .data(attrOrder, d => d + attrs[d]);
    let cellsEnter = cells.enter().append('div')
      .attr('class', 'cell');
    cells.exit().remove();

    cellsEnter.append('span');
    cells.selectAll('span').text(d => d);

    cellsEnter.append('select');
    let typeMenuOptions = cells.selectAll('select').selectAll('option')
      .data(d3.keys(Dataset.COMPATIBLE_TYPES));
    typeMenuOptions.enter().append('option');
    typeMenuOptions.attr('value', d => d)
      .text(d => d);

    cells.selectAll('select')
      .property('value', d => attrs[d])
      .on('change', d => {
        let newType = jQuery(d3.event.target).val();
        dataset.setAttribute(d, newType);
      });
  },
  render: Underscore.debounce(function () {
    let widgetIsShowing = Widget.prototype.render.apply(this, arguments);

    // Get the dataset in the project (if there is one)
    let dataset = window.mainPage.project.getMeta('datasets');
    if (dataset) {
      dataset = window.mainPage.loadedDatasets[dataset[0]];
    }

    let editor;
    if (widgetIsShowing) {
      this.$el.html(myTemplate);
      this.renderAttributeSettings();

      editor = ace.edit('editor');
      editor.setOptions({
        fontFamily: 'Cutive Mono, Courier, Monospace',
        fontSize: '10pt'
      });
      editor.setTheme('ace/theme/textmate');
      editor.$blockScrolling = Infinity;
    } else {
      // dummy "editor" that does nothing
      // if the widget is collapsed
      editor = {
        setValue: () => {},
        setReadOnly: () => {}
      };
    }

    if (!dataset) {
      editor.setValue('');
      this.status = STATUS.NO_DATA;
      this.statusText.text = 'No file loaded';
      this.renderIndicators();
    } else {
      this.status = STATUS.LOADING;
      this.statusText.text = 'Loading...';
      this.renderIndicators();

      dataset.parse().then(parsedData => {
        let rawData = dataset.rawCache;
        let spec = dataset.getSpec();
        if (rawData === null) {
          editor.setValue('');
          this.status = STATUS.CANT_LOAD;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else if (parsedData === null) {
          editor.setValue(rawData);
          this.status = STATUS.CANT_PARSE;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else if (Object.keys(spec.attributes).length === 0) {
          editor.setValue(rawData);
          this.status = STATUS.NO_ATTRIBUTES;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else {
          editor.setValue(rawData);
          this.status = STATUS.SUCCESS;
          this.statusText.text = dataset.get('name');
          this.renderIndicators();
        }
      });
    }
    // TODO: allow the user to edit the data (convert
    // to in-browser dataset)... for now, always disable
    // the textarea
    editor.setReadOnly(true);
  }, 200)
});

export default DatasetView;
<commit_msg>Fix rendering error when no project is loaded<commit_after>import Underscore from 'underscore';
import d3 from 'd3';
import jQuery from 'jquery';
import ace from 'brace';
import 'brace/theme/textmate';
import Widget from '../Widget';
import Dataset from '../../../models/Dataset';
import myTemplate from './template.html';
import './style.css';

let STATUS = {
  NO_DATA: 0,
  SUCCESS: 1,
  CANT_LOAD: 2,
  CANT_PARSE: 3,
  LOADING: 4,
  NO_ATTRIBUTES: 5
};

let DatasetView = Widget.extend({
  initialize: function () {
    Widget.prototype.initialize.apply(this, arguments);
    this.friendlyName = 'Dataset';

    this.statusText.onclick = () => {
      window.mainPage.overlay.render('DatasetLibrary');
    };
    this.statusText.title = 'Select a different dataset.';

    this.icons.splice(0, 0, {
      src: () => {
        return window.mainPage.currentUser.preferences
          .hasSeenAllTips(this.getDefaultTips()) ? Widget.infoIcon : Widget.newInfoIcon;
      },
      title: () => {
        return 'About this panel';
      },
      onclick: () => {
        this.renderInfoScreen();
      }
    });

    this.status = STATUS.NO_DATA;
    this.icons.splice(0, 0, {
      src: () => {
        if (this.status === STATUS.LOADING) {
          return Widget.spinnerIcon;
        } else if (this.status === STATUS.SUCCESS) {
          return Widget.okayIcon;
        } else {
          return Widget.warningIcon;
        }
      },
      title: () => {
        if (this.status === STATUS.LOADING) {
          return 'The dataset hasn\'t finished loading yet';
        } else if (this.status === STATUS.SUCCESS) {
          return 'The dataset appears to have loaded correctly';
        } else {
          return 'Something isn\'t quite right; click for details';
        }
      },
      onclick: () => {
        this.renderHelpScreen();
      }
    });

    this.listenTo(window.mainPage, 'rl:changeProject',
      this.handleNewProject);
    this.handleNewProject();
  },
  handleNewProject: function () {
    this.$el.html('');
    this.status = STATUS.NO_DATA;

    this.listenTo(window.mainPage.project, 'rl:changeDatasets',
      this.render);
    /* this.listenTo(window.mainPage.project, 'rl:changeMatchings',
      this.render);*/
  },
  renderInfoScreen: function () {
    window.mainPage.helpLayer.showTips(this.getDefaultTips());
  },
  renderHelpScreen: function () {
    if (this.status === STATUS.NO_DATA) {
      window.mainPage.overlay.renderUserErrorScreen('You have not chosen a dataset yet. Click here to choose one.');
    } else if (this.status === STATUS.SUCCESS) {
      window.mainPage.overlay.renderSuccessScreen('The dataset appears to have loaded correctly.');
    } else if (this.status === STATUS.CANT_LOAD) {
      window.mainPage.overlay.renderErrorScreen('The dataset could not be loaded; there is a good chance that there is a permissions problem.');
    } else if (this.status === STATUS.CANT_PARSE) {
      window.mainPage.overlay.renderUserErrorScreen('There was a problem parsing the data; you\'ll probably need to edit or reshape the data in order to use it.');
    } else if (this.status === STATUS.NO_ATTRIBUTES) {
      window.mainPage.overlay.renderUserErrorScreen('There was a problem parsing the data. Specifically, we\'re having trouble understanding the dataset attributes (usually column headers); you\'ll probably need to edit or reshape the data in order to use it.');
    }
  },
  renderAttributeSettings: function () {
    let datasets = window.mainPage.project.getMeta('datasets');
    let dataset;
    let attrs;
    if (datasets && datasets[0] && window.mainPage.loadedDatasets[datasets[0]]) {
      dataset = window.mainPage.loadedDatasets[datasets[0]];
      attrs = dataset.getSpec().attributes;
    } else {
      attrs = {};
    }
    let attrOrder = Object.keys(attrs);
    // TODO: this is technically cheating; relying on the order
    // of the dict entries to preserve the order on screen

    let cells = d3.select(this.el).select('#attributeSettings')
      .selectAll('div.cell')
      .data(attrOrder, d => d + attrs[d]);
    let cellsEnter = cells.enter().append('div')
      .attr('class', 'cell');
    cells.exit().remove();

    cellsEnter.append('span');
    cells.selectAll('span').text(d => d);

    cellsEnter.append('select');
    let typeMenuOptions = cells.selectAll('select').selectAll('option')
      .data(d3.keys(Dataset.COMPATIBLE_TYPES));
    typeMenuOptions.enter().append('option');
    typeMenuOptions.attr('value', d => d)
      .text(d => d);

    cells.selectAll('select')
      .property('value', d => attrs[d])
      .on('change', d => {
        let newType = jQuery(d3.event.target).val();
        dataset.setAttribute(d, newType);
      });
  },
  render: Underscore.debounce(function () {
    let widgetIsShowing = Widget.prototype.render.apply(this, arguments);

    // Get the dataset in the project (if there is one)

    let dataset;
    if (window.mainPage.project) {
      let datasets = window.mainPage.project.getMeta('datasets');
      dataset = window.mainPage.loadedDatasets[datasets[0]];
    }

    let editor;
    if (widgetIsShowing) {
      this.$el.html(myTemplate);
      this.renderAttributeSettings();

      editor = ace.edit('editor');
      editor.setOptions({
        fontFamily: 'Cutive Mono, Courier, Monospace',
        fontSize: '10pt'
      });
      editor.setTheme('ace/theme/textmate');
      editor.$blockScrolling = Infinity;
    } else {
      // dummy "editor" that does nothing
      // if the widget is collapsed
      editor = {
        setValue: () => {},
        setReadOnly: () => {}
      };
    }

    if (!dataset) {
      editor.setValue('');
      this.status = STATUS.NO_DATA;
      this.statusText.text = 'No file loaded';
      this.renderIndicators();
    } else {
      this.status = STATUS.LOADING;
      this.statusText.text = 'Loading...';
      this.renderIndicators();

      dataset.parse().then(parsedData => {
        let rawData = dataset.rawCache;
        let spec = dataset.getSpec();
        if (rawData === null) {
          editor.setValue('');
          this.status = STATUS.CANT_LOAD;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else if (parsedData === null) {
          editor.setValue(rawData);
          this.status = STATUS.CANT_PARSE;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else if (Object.keys(spec.attributes).length === 0) {
          editor.setValue(rawData);
          this.status = STATUS.NO_ATTRIBUTES;
          this.statusText.text = 'ERROR';
          this.renderIndicators();
        } else {
          editor.setValue(rawData);
          this.status = STATUS.SUCCESS;
          this.statusText.text = dataset.get('name');
          this.renderIndicators();
        }
      });
    }
    // TODO: allow the user to edit the data (convert
    // to in-browser dataset)... for now, always disable
    // the textarea
    editor.setReadOnly(true);
  }, 200)
});

export default DatasetView;
<|endoftext|>


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

    angular.module('dimApp')
      .factory('dimItemService', ItemService);

    ItemService.$inject = ['dimStoreService', 'dimBungieService', 'dimItemTier', 'dimCategory', '$q'];

    function ItemService(dimStoreService, dimBungieService, dimItemTier, dimCategory, $q) {
      return {
        getSimilarItem: getSimilarItem,
        getItem: getItem,
        getItems: getItems,
        moveTo: moveTo,
        setLockState: setLockState
      };

      function setLockState(item, store, lockState) {
        return dimBungieService.setLockState(item, store, lockState)
          .then(function() {
            return lockState;
          });
      }

      // Returns the new or updated item (it may create a new item!)
      function updateItemModel(item, source, target, equip, amount) {
        // If we've moved to a new place
        if (source.id !== target.id) {
          // We handle moving stackable and nonstackable items almost exactly the same!
          var stackable = item.maxStackSize > 1;
          // Items to be decremented
          var sourceItems = stackable ?
                _.sortBy(_.select(source.items, function(i) {
                  return i.hash === item.hash &&
                    i.id === item.id;
                }), 'amount') : [item];
          // Items to be incremented. There's really only ever at most one of these, but
          // it's easier to deal with as a list.
          var targetItems = stackable ?
                _.sortBy(_.select(target.items, function(i) {
                  return i.hash === item.hash &&
                    i.id === item.id &&
                    // Don't consider full stacks as targets
                    i.amount !== i.maxStackSize;
                }), 'amount') : [];
          // moveAmount could be more than maxStackSize if there is more than one stack on a character!
          var moveAmount = amount || item.amount;
          var addAmount = moveAmount;
          var removeAmount = moveAmount;
          var removedSourceItem = false;

          // Remove inventory from the source
          while (removeAmount > 0) {
            var sourceItem = sourceItems.shift();
            if (!sourceItem) {
              throw new Error("Looks like you requested to move more of this item than exists in the source!");
            }

            var amountToRemove = Math.min(removeAmount, sourceItem.amount);
            if (amountToRemove === sourceItem.amount) {
              // Completely remove the source item
              var sourceIndex = _.findIndex(source.items, function(i) {
                return sourceItem.index === i.index;
              });
              if (sourceIndex >= 0) {
                source.items.splice(sourceIndex, 1);
                removedSourceItem = sourceItem.index === item.index;
              }
            } else {
              sourceItem.amount -= amountToRemove;
            }

            removeAmount -= amountToRemove;
          }

          // Add inventory to the target (destination)
          var targetItem;
          while (addAmount > 0) {
            targetItem = targetItems.shift();

            if (!targetItem) {
              targetItem = item;
              if (!removedSourceItem) {
                targetItem = angular.copy(item);
                targetItem.index = dimStoreService.createItemIndex(targetItem);
              }
              removedSourceItem = false; // only move without cloning once
              targetItem.amount = 0; // We'll increment amount below
              target.items.push(targetItem);
              targetItem.owner = target.id;
            }

            var amountToAdd = Math.min(addAmount, targetItem.maxStackSize - targetItem.amount);
            targetItem.amount += amountToAdd;
            addAmount -= amountToAdd;
          }
          item = targetItem; // The item we're operating on switches to the last target
        }

        if (equip) {
          var equipped = _.findWhere(target.items, {
            equipped: true,
            type: item.type
          });
          equipped.equipped = false;
          item.equipped = true;
        }

        return item;
      }

      function getSimilarItem(item) {
        return $q.when(dimStoreService.getStores())
          .then(function(stores) {
            var result = null;
            var source = _.find(stores, function(i) {
              return i.id === item.owner;
            });
            var sortedStores = _.sortBy(stores, function(store) {
              if (source.id === store.id) {
                return 0;
              } else if (store.id === 'vault') {
                return 1;
              } else {
                return 2;
              }
            });

            _.each(sortedStores, function(store) {
              if (_.isNull(result)) {
                result = searchForSimilarItem(item, store);
              }
            });

            return result;
          });
      }

      function searchForSimilarItem(item, store) {
        var sortType = {
          Legendary: 0,
          Rare: 1,
          Uncommon: 2,
          Common: 3,
          Exotic: 5
        };

        var result = _.chain(store.items)
          .filter(function(i) {
            return i.equipment &&
              i.type === item.type &&
              !i.equipped &&
              // Compatible with this class
              (i.classTypeName === 'unknown' || i.classTypeName === store.class) &&
              // Not the same item
              i.id !== item.id;
          })
          .sortBy(function(i) {
            return sortType[i.tier];
          })
          .first()
          .value();

        if (result && result.tier === dimItemTier.exotic) {
          var prefix = _.filter(store.items, function(i) {
            return i.equipped &&
              i.sort === item.sort &&
              i.tier === dimItemTier.exotic;
          });

          if (prefix.length === 0) {
            return result;
          } else {
            return null;
          }
        }

        return (result) ? result : null;
      }

      function equipItem(item) {
        return dimBungieService.equip(item)
          .then(function() {
            var store = dimStoreService.getStore(item.owner);
            return updateItemModel(item, store, store, true);
          });
      }

      function dequipItem(item, equipExotic) {
        if (_.isUndefined(equipExotic)) {
          equipExotic = false;
        }

        var scope = {
          source: null,
          target: null,
          similarItem: null
        };

        var updateEquipped;

        return getSimilarItem(item)
          .then(function(similarItem) {
            scope.similarItem = similarItem;

            // TODO: move something in from the vault to equip!
            // TODO: do we need this exotic logic?
            // could this be removed now, along with all refrences to `equipExotic` that are passed in?
            if ((!equipExotic && similarItem && similarItem.tier === 'Exotic') || !similarItem) {
              return $q.reject(new Error('There are no items to equip in the \'' + item.type + '\' slot.'));
            }

            return dimStoreService.getStore(item.owner);
          })
          .then(function(source) {
            scope.source = source;

            return dimStoreService.getStore(scope.similarItem.owner);
          })
          .then(function(target) {
            scope.target = target;

            if (scope.source.id === scope.target.id) {
              return null;
            } else {
              var p = $q.when();
              var vault;

              if (scope.similarItem.owner !== 'vault') {
                vault = dimStoreService.getVault();
                p = dimBungieService.transfer(scope.similarItem, vault)
                  .then(function() {
                    return updateItemModel(scope.similarItem, vault, scope.source, false);
                  });
              }

              return p.then(function() {
                  return dimBungieService.transfer(scope.similarItem, scope.source);
                })
                .then(function() {
                  return updateItemModel(scope.similarItem, (vault) ? vault : scope.target, scope.source, false);
                });
            }
          })
          .then(function() {
            return dimBungieService.equip(scope.similarItem);
          })
          .then(function() {
            return updateItemModel(scope.similarItem, scope.source, scope.source, true);
          })
          .catch(function(e) {
            return $q.reject(e);
          });
      }

      function moveToVault(item, amount) {
        return moveToStore(item, dimStoreService.getVault(), false, amount);
      }

      function moveToStore(item, store, equip, amount) {
        var scope = {
          source: dimStoreService.getStore(item.owner),
          target: store
        };

        return dimBungieService.transfer(item, scope.target, amount)
          .then(function() {
            var newItem = updateItemModel(item, scope.source, scope.target, false, amount);
            if ((newItem.owner !== 'vault') && equip) {
              return equipItem(newItem);
            } else {
              return newItem;
            }
          });
      }

      function canEquipExotic(item, store) {
        var deferred = $q.defer();
        var promise = deferred.promise;

        var equippedExotics = _.filter(store.items, function(i) {
            return (i.equipped &&
                    i.type !== item.type &&
                    i.sort === item.sort &&
                    i.tier === 'Exotic');
          });

        // Fix for "The Life Exotic" Perk on Exotic Items
        // Can equip multiples
        function hasLifeExotic(item) {
          return _.find(item.talentGrid.nodes, { name: 'The Life Exotic' }) !== undefined;
        }

        if (equippedExotics.length === 0) {
          deferred.resolve(true);
        } else if (equippedExotics.length === 1) {
          var equippedExotic = equippedExotics[0];

          if (hasLifeExotic(item) || hasLifeExotic(equippedExotic)) {
            deferred.resolve(true);
          } else {
            dequipItem(equippedExotic)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          }
        } else if (equippedExotics.length === 2) {
          // Assume that only one of the equipped items has 'The Life Exotic' perk
          if (hasLifeExotic(item)) {
            var exoticItemWithPerk = _.find(equippedExotics, hasLifeExotic(item));
            dequipItem(exoticItemWithPerk)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          } else {
            var equippedExoticWithoutPerk = _.find(equippedExotics, function(item) {
              return !hasLifeExotic(item);
            });

            dequipItem(equippedExoticWithoutPerk)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          }
        }

        return promise;
      }

      function canMoveToStore(item, store, triedFallback) {
        var stackAmount = 0;
        var slotsNeededForTransfer = 0;
        var predicate = (store.id === 'vault') ? {
          sort: item.sort
        } : {
          type: item.type
        };

        var itemsInStore = _(store.items)
          .chain()
          .where(predicate)
          .size()
          .value();

        if (item.maxStackSize > 1) {
          stackAmount = store.amountOfItem(item);
          slotsNeededForTransfer = Math.ceil((stackAmount + item.amount) / item.maxStackSize) - Math.ceil((stackAmount) / item.maxStackSize);
        } else {
          if (item.owner === store.id) {
            slotsNeededForTransfer = 0;
          } else {
            slotsNeededForTransfer = 1;
          }
        }

        var typeQtyCap = store.capacityForItem(item);

        if ((itemsInStore + slotsNeededForTransfer) <= typeQtyCap) {
          if ((item.owner !== store.id) && (store.id !== 'vault') && (item.owner !== 'vault')) {
            // It's a guardian-to-guardian move, so we need to check
            // if there's space in the vault since the item has to go
            // through there.
            return canMoveToStore(item, dimStoreService.getVault());
          } else {
            return $q.resolve(true);
          }
        } else {
          // Not enough space!
          if (!triedFallback) {
            // Refresh the store
            return dimStoreService.reloadStores()
              .then(function(stores) {
                store = _.find(stores, { id: store.id });
                return canMoveToStore(item, store, true);
              });
          } else {
            return $q.reject(new Error('There are too many \'' + (store.id === 'vault' ? item.sort : item.type) + '\' items in the ' + (store.id === 'vault' ? 'vault' : 'guardian') + '.'));
          }
        }
      }

      function createSpace(store, item, target) {
        var targetIsSource = (store.id === target.id);
        var scope = {};

        var promise = $q.when(dimStoreService.getStores())
          .then(function(stores) {
            return $q.reject('woopsie');
          });

        return promise;
      }

      function isVaultToVault(item, store) {
        var deferred = $q.defer();
        var promise = deferred.promise;
        var result = ((item.owner === 'vault') && (store.id === 'vault'));

        deferred.resolve(result ? deferred.reject(new Error('Cannot process vault-to-vault transfers.')) : false);

        return promise;
      }

      function canEquip(item, store) {
        return $q(function(resolve, reject) {
          if (item.classTypeName === 'unknown' || item.classTypeName === store.class) {
            resolve(true);
          } else {
            reject(new Error("This can only be equipped on " + item.classTypeName + "s."));
          }
        });
      }

      function isValidTransfer(equip, store, item) {
        var promises = [];

        promises.push(isVaultToVault(item, store));
        promises.push(canMoveToStore(item, store));

        if (equip) {
          promises.push(canEquip(item, store));
        }

        if ((item.tier === 'Exotic') && equip) {
          promises.push(canEquipExotic(item, store));
        }

        return $q.all(promises);
      }

      function moveTo(item, target, equip, amount) {
        var data = {
          item: item,
          source: dimStoreService.getStore(item.owner),
          target: target,
          sameSource: (item.owner === target.id),
          isVault: {
            source: item.owner === 'vault',
            target: target.id === 'vault'
          }
        };

        var movePlan = isValidTransfer(equip, target, item)
          .then(function(targetStore) {
            // Replace the target store - isValidTransfer may have replaced it
            data.target = dimStoreService.getStore(target.id);
          })
          .then(function(a) {
            var promise = $q.when(item);

            if (!data.isVault.source && !data.isVault.target) { // Guardian to Guardian
              if (data.source.id != data.target.id) { // Different Guardian
                if (item.equipped) {
                  promise = promise.then(function() {
                    return dequipItem(item);
                  });
                }

                promise = promise
                  .then(function() {
                    return moveToVault(item, amount);
                  })
                  .then(function(item) {
                    return moveToStore(item, data.target, equip, amount);
                  });
              }

              if (equip) {
                promise = promise.then(function() {
                  if (!item.equipped) {
                    return equipItem(item);
                  } else {
                    return $q.when(item);
                  }
                });
              } else if (!equip) {
                promise = promise.then(function() {
                  if (item.equipped) {
                    return dequipItem(item);
                  } else {
                    return $q.when(item);
                  }
                });
              }
            } else if (data.isVault.source && data.isVault.target) { // Vault to Vault
              // Do Nothing.
              //console.log('vault-to-vault');
            } else if (data.isVault.source || data.isVault.target) { // Guardian to Vault
              if (item.equipped) {
                promise = promise.then(function() {
                  return dequipItem(item);
                });
              }

              promise = promise.then(function() {
                return moveToStore(item, data.target, equip, amount);
              });
            }

            return promise;
          })
          .catch(function(e) {
            return $q.reject(e);
          });

        return movePlan;
      }

      function getItems() {
        var returnValue = [];
        dimStoreService.getStores().forEach(function(store) {
          returnValue = returnValue.concat(store.items);
        });
        return returnValue;
      }

      function getItem(params, store) {
        var items = store ? store.items : getItems();
        return _.findWhere(items, { id: params.id, hash: params.hash });
      }

      return service;
    }
  })();
<commit_msg>Vault-to-vault transfers are no-ops.<commit_after>  (function() {
    'use strict';

    angular.module('dimApp')
      .factory('dimItemService', ItemService);

    ItemService.$inject = ['dimStoreService', 'dimBungieService', 'dimItemTier', 'dimCategory', '$q'];

    function ItemService(dimStoreService, dimBungieService, dimItemTier, dimCategory, $q) {
      return {
        getSimilarItem: getSimilarItem,
        getItem: getItem,
        getItems: getItems,
        moveTo: moveTo,
        setLockState: setLockState
      };

      function setLockState(item, store, lockState) {
        return dimBungieService.setLockState(item, store, lockState)
          .then(function() {
            return lockState;
          });
      }

      // Returns the new or updated item (it may create a new item!)
      function updateItemModel(item, source, target, equip, amount) {
        // If we've moved to a new place
        if (source.id !== target.id) {
          // We handle moving stackable and nonstackable items almost exactly the same!
          var stackable = item.maxStackSize > 1;
          // Items to be decremented
          var sourceItems = stackable ?
                _.sortBy(_.select(source.items, function(i) {
                  return i.hash === item.hash &&
                    i.id === item.id;
                }), 'amount') : [item];
          // Items to be incremented. There's really only ever at most one of these, but
          // it's easier to deal with as a list.
          var targetItems = stackable ?
                _.sortBy(_.select(target.items, function(i) {
                  return i.hash === item.hash &&
                    i.id === item.id &&
                    // Don't consider full stacks as targets
                    i.amount !== i.maxStackSize;
                }), 'amount') : [];
          // moveAmount could be more than maxStackSize if there is more than one stack on a character!
          var moveAmount = amount || item.amount;
          var addAmount = moveAmount;
          var removeAmount = moveAmount;
          var removedSourceItem = false;

          // Remove inventory from the source
          while (removeAmount > 0) {
            var sourceItem = sourceItems.shift();
            if (!sourceItem) {
              throw new Error("Looks like you requested to move more of this item than exists in the source!");
            }

            var amountToRemove = Math.min(removeAmount, sourceItem.amount);
            if (amountToRemove === sourceItem.amount) {
              // Completely remove the source item
              var sourceIndex = _.findIndex(source.items, function(i) {
                return sourceItem.index === i.index;
              });
              if (sourceIndex >= 0) {
                source.items.splice(sourceIndex, 1);
                removedSourceItem = sourceItem.index === item.index;
              }
            } else {
              sourceItem.amount -= amountToRemove;
            }

            removeAmount -= amountToRemove;
          }

          // Add inventory to the target (destination)
          var targetItem;
          while (addAmount > 0) {
            targetItem = targetItems.shift();

            if (!targetItem) {
              targetItem = item;
              if (!removedSourceItem) {
                targetItem = angular.copy(item);
                targetItem.index = dimStoreService.createItemIndex(targetItem);
              }
              removedSourceItem = false; // only move without cloning once
              targetItem.amount = 0; // We'll increment amount below
              target.items.push(targetItem);
              targetItem.owner = target.id;
            }

            var amountToAdd = Math.min(addAmount, targetItem.maxStackSize - targetItem.amount);
            targetItem.amount += amountToAdd;
            addAmount -= amountToAdd;
          }
          item = targetItem; // The item we're operating on switches to the last target
        }

        if (equip) {
          var equipped = _.findWhere(target.items, {
            equipped: true,
            type: item.type
          });
          equipped.equipped = false;
          item.equipped = true;
        }

        return item;
      }

      function getSimilarItem(item) {
        return $q.when(dimStoreService.getStores())
          .then(function(stores) {
            var result = null;
            var source = _.find(stores, function(i) {
              return i.id === item.owner;
            });
            var sortedStores = _.sortBy(stores, function(store) {
              if (source.id === store.id) {
                return 0;
              } else if (store.id === 'vault') {
                return 1;
              } else {
                return 2;
              }
            });

            _.each(sortedStores, function(store) {
              if (_.isNull(result)) {
                result = searchForSimilarItem(item, store);
              }
            });

            return result;
          });
      }

      function searchForSimilarItem(item, store) {
        var sortType = {
          Legendary: 0,
          Rare: 1,
          Uncommon: 2,
          Common: 3,
          Exotic: 5
        };

        var result = _.chain(store.items)
          .filter(function(i) {
            return i.equipment &&
              i.type === item.type &&
              !i.equipped &&
              // Compatible with this class
              (i.classTypeName === 'unknown' || i.classTypeName === store.class) &&
              // Not the same item
              i.id !== item.id;
          })
          .sortBy(function(i) {
            return sortType[i.tier];
          })
          .first()
          .value();

        if (result && result.tier === dimItemTier.exotic) {
          var prefix = _.filter(store.items, function(i) {
            return i.equipped &&
              i.sort === item.sort &&
              i.tier === dimItemTier.exotic;
          });

          if (prefix.length === 0) {
            return result;
          } else {
            return null;
          }
        }

        return (result) ? result : null;
      }

      function equipItem(item) {
        return dimBungieService.equip(item)
          .then(function() {
            var store = dimStoreService.getStore(item.owner);
            return updateItemModel(item, store, store, true);
          });
      }

      function dequipItem(item, equipExotic) {
        if (_.isUndefined(equipExotic)) {
          equipExotic = false;
        }

        var scope = {
          source: null,
          target: null,
          similarItem: null
        };

        var updateEquipped;

        return getSimilarItem(item)
          .then(function(similarItem) {
            scope.similarItem = similarItem;

            // TODO: move something in from the vault to equip!
            // TODO: do we need this exotic logic?
            // could this be removed now, along with all refrences to `equipExotic` that are passed in?
            if ((!equipExotic && similarItem && similarItem.tier === 'Exotic') || !similarItem) {
              return $q.reject(new Error('There are no items to equip in the \'' + item.type + '\' slot.'));
            }

            return dimStoreService.getStore(item.owner);
          })
          .then(function(source) {
            scope.source = source;

            return dimStoreService.getStore(scope.similarItem.owner);
          })
          .then(function(target) {
            scope.target = target;

            if (scope.source.id === scope.target.id) {
              return null;
            } else {
              var p = $q.when();
              var vault;

              if (scope.similarItem.owner !== 'vault') {
                vault = dimStoreService.getVault();
                p = dimBungieService.transfer(scope.similarItem, vault)
                  .then(function() {
                    return updateItemModel(scope.similarItem, vault, scope.source, false);
                  });
              }

              return p.then(function() {
                  return dimBungieService.transfer(scope.similarItem, scope.source);
                })
                .then(function() {
                  return updateItemModel(scope.similarItem, (vault) ? vault : scope.target, scope.source, false);
                });
            }
          })
          .then(function() {
            return dimBungieService.equip(scope.similarItem);
          })
          .then(function() {
            return updateItemModel(scope.similarItem, scope.source, scope.source, true);
          })
          .catch(function(e) {
            return $q.reject(e);
          });
      }

      function moveToVault(item, amount) {
        return moveToStore(item, dimStoreService.getVault(), false, amount);
      }

      function moveToStore(item, store, equip, amount) {
        var scope = {
          source: dimStoreService.getStore(item.owner),
          target: store
        };

        return dimBungieService.transfer(item, scope.target, amount)
          .then(function() {
            var newItem = updateItemModel(item, scope.source, scope.target, false, amount);
            if ((newItem.owner !== 'vault') && equip) {
              return equipItem(newItem);
            } else {
              return newItem;
            }
          });
      }

      function canEquipExotic(item, store) {
        var deferred = $q.defer();
        var promise = deferred.promise;

        var equippedExotics = _.filter(store.items, function(i) {
            return (i.equipped &&
                    i.type !== item.type &&
                    i.sort === item.sort &&
                    i.tier === 'Exotic');
          });

        // Fix for "The Life Exotic" Perk on Exotic Items
        // Can equip multiples
        function hasLifeExotic(item) {
          return _.find(item.talentGrid.nodes, { name: 'The Life Exotic' }) !== undefined;
        }

        if (equippedExotics.length === 0) {
          deferred.resolve(true);
        } else if (equippedExotics.length === 1) {
          var equippedExotic = equippedExotics[0];

          if (hasLifeExotic(item) || hasLifeExotic(equippedExotic)) {
            deferred.resolve(true);
          } else {
            dequipItem(equippedExotic)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          }
        } else if (equippedExotics.length === 2) {
          // Assume that only one of the equipped items has 'The Life Exotic' perk
          if (hasLifeExotic(item)) {
            var exoticItemWithPerk = _.find(equippedExotics, hasLifeExotic(item));
            dequipItem(exoticItemWithPerk)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          } else {
            var equippedExoticWithoutPerk = _.find(equippedExotics, function(item) {
              return !hasLifeExotic(item);
            });

            dequipItem(equippedExoticWithoutPerk)
              .then(function(result) {
                deferred.resolve(true);
              })
              .catch(function(err) {
                deferred.reject(new Error('\'' + item.name + '\' cannot be equipped because the exotic in the ' + equippedExotic.type + ' slot cannot be unequipped.'));
              });
          }
        }

        return promise;
      }

      function canMoveToStore(item, store, triedFallback) {
        var stackAmount = 0;
        var slotsNeededForTransfer = 0;
        var predicate = (store.id === 'vault') ? {
          sort: item.sort
        } : {
          type: item.type
        };

        var itemsInStore = _(store.items)
          .chain()
          .where(predicate)
          .size()
          .value();

        if (item.maxStackSize > 1) {
          stackAmount = store.amountOfItem(item);
          slotsNeededForTransfer = Math.ceil((stackAmount + item.amount) / item.maxStackSize) - Math.ceil((stackAmount) / item.maxStackSize);
        } else {
          if (item.owner === store.id) {
            slotsNeededForTransfer = 0;
          } else {
            slotsNeededForTransfer = 1;
          }
        }

        var typeQtyCap = store.capacityForItem(item);

        if ((itemsInStore + slotsNeededForTransfer) <= typeQtyCap) {
          if ((item.owner !== store.id) && (store.id !== 'vault') && (item.owner !== 'vault')) {
            // It's a guardian-to-guardian move, so we need to check
            // if there's space in the vault since the item has to go
            // through there.
            return canMoveToStore(item, dimStoreService.getVault());
          } else {
            return $q.resolve(true);
          }
        } else {
          // Not enough space!
          if (!triedFallback) {
            // Refresh the store
            return dimStoreService.reloadStores()
              .then(function(stores) {
                store = _.find(stores, { id: store.id });
                return canMoveToStore(item, store, true);
              });
          } else {
            return $q.reject(new Error('There are too many \'' + (store.id === 'vault' ? item.sort : item.type) + '\' items in the ' + (store.id === 'vault' ? 'vault' : 'guardian') + '.'));
          }
        }
      }

      function createSpace(store, item, target) {
        var targetIsSource = (store.id === target.id);
        var scope = {};

        var promise = $q.when(dimStoreService.getStores())
          .then(function(stores) {
            return $q.reject('woopsie');
          });

        return promise;
      }

      function canEquip(item, store) {
        return $q(function(resolve, reject) {
          if (item.classTypeName === 'unknown' || item.classTypeName === store.class) {
            resolve(true);
          } else {
            reject(new Error("This can only be equipped on " + item.classTypeName + "s."));
          }
        });
      }

      function isValidTransfer(equip, store, item) {
        var promises = [];

        promises.push(canMoveToStore(item, store));

        if (equip) {
          promises.push(canEquip(item, store));
        }

        if ((item.tier === 'Exotic') && equip) {
          promises.push(canEquipExotic(item, store));
        }

        return $q.all(promises);
      }

      function moveTo(item, target, equip, amount) {
        var data = {
          item: item,
          source: dimStoreService.getStore(item.owner),
          target: target,
          isVault: {
            source: item.owner === 'vault',
            target: target.id === 'vault'
          }
        };

        var movePlan = isValidTransfer(equip, target, item)
          .then(function(targetStore) {
            // Replace the target store - isValidTransfer may have replaced it
            data.target = dimStoreService.getStore(target.id);
          })
          .then(function(a) {
            var promise = $q.when(item);

            if (!data.isVault.source && !data.isVault.target) { // Guardian to Guardian
              if (data.source.id != data.target.id) { // Different Guardian
                if (item.equipped) {
                  promise = promise.then(function() {
                    return dequipItem(item);
                  });
                }

                promise = promise
                  .then(function() {
                    return moveToVault(item, amount);
                  })
                  .then(function(item) {
                    return moveToStore(item, data.target, equip, amount);
                  });
              }

              if (equip) {
                promise = promise.then(function() {
                  if (!item.equipped) {
                    return equipItem(item);
                  } else {
                    return $q.when(item);
                  }
                });
              } else if (!equip) {
                promise = promise.then(function() {
                  if (item.equipped) {
                    return dequipItem(item);
                  } else {
                    return $q.when(item);
                  }
                });
              }
            } else if (data.isVault.source && data.isVault.target) { // Vault to Vault
              // Do Nothing.
              //console.log('vault-to-vault');
            } else if (data.isVault.source || data.isVault.target) { // Guardian to Vault
              if (item.equipped) {
                promise = promise.then(function() {
                  return dequipItem(item);
                });
              }

              promise = promise.then(function() {
                return moveToStore(item, data.target, equip, amount);
              });
            }

            return promise;
          })
          .catch(function(e) {
            return $q.reject(e);
          });

        return movePlan;
      }

      function getItems() {
        var returnValue = [];
        dimStoreService.getStores().forEach(function(store) {
          returnValue = returnValue.concat(store.items);
        });
        return returnValue;
      }

      function getItem(params, store) {
        var items = store ? store.items : getItems();
        return _.findWhere(items, { id: params.id, hash: params.hash });
      }

      return service;
    }
  })();
<|endoftext|>