<commit_before>import { applyChildNodes } from "../core/utilities.js";
import * as internal from "./internal.js";
import * as template from "../core/template.js";
import LanguageDirectionMixin from "./LanguageDirectionMixin.js";
import ListBox from "./ListBox.js";
import Modes from "./Modes.js";
import ReactiveElement from "../core/ReactiveElement.js";
import SingleSelectionMixin from "./SingleSelectionMixin.js";
import SlotItemsMixin from "./SlotItemsMixin.js";

// Does a list position imply a lateral arrangement of list and stage?
/** @type {IndexedObject<boolean>} */
const lateralPositions = {
  end: true,
  left: true,
  right: true,
  start: true
};

const Base = LanguageDirectionMixin(
  SingleSelectionMixin(SlotItemsMixin(ReactiveElement))
);

/**
 * Combines a list with an area focusing on a single selected item.
 *
 * @inherits ReactiveElement
 * @mixes LanguageDirectionMixin
 * @mixes SingleSelectionMixin
 * @mixes SlotItemsMixin
 * @part {div} proxy - an element representing an item in the list
 * @part {ListBox} proxy-list - the container for the list of proxies
 * @part {Modes} stage - the main element showing a single item from the list
 */
class Explorer extends Base {
  [internal.checkSize]() {
    if (super[internal.checkSize]) {
      super[internal.checkSize]();
    }
    if (this[internal.ids].stage[internal.checkSize]) {
      this[internal.ids].stage[internal.checkSize]();
    }
    if (this[internal.ids].proxyList[internal.checkSize]) {
      this[internal.ids].proxyList[internal.checkSize]();
    }
  }

  [internal.componentDidMount]() {
    super[internal.componentDidMount]();

    // When proxy slot's assigned nodes change, determine whether we need to
    // generate default proxies or (if assigned nodes are present) treat the
    // assigned nodes as the proxies.
    this[internal.ids].proxySlot.addEventListener("slotchange", () => {
      const proxySlot = /** @type {any} */ (this[internal.ids].proxySlot);
      const proxies = proxySlot.assignedNodes({ flatten: true });
      const proxiesAssigned = proxies.length > 0;
      if (proxiesAssigned) {
        // Nodes assigned to slot become proxies.
        this[internal.setState]({
          proxiesAssigned,
          proxies
        });
      } else {
        // No nodes assigned -- we'll need to generate proxies.
        this[internal.setState]({ proxiesAssigned });
      }
    });
  }

  get [internal.defaultState]() {
    return Object.assign(super[internal.defaultState], {
      proxies: [],
      proxiesAssigned: false,
      proxyPartType: "div",
      proxyListOverlap: false,
      proxyListPosition: "top",
      proxyListPartType: ListBox,
      selectionRequired: true,
      stagePartType: Modes
    });
  }

  [internal.render](/** @type {PlainObject} */ changed) {
    super[internal.render](changed);
    /** @type {any} */
    const handleSelectedIndexChanged = (/** @type {CustomEvent} */ event) => {
      // The proxy list and stage may raise events before they've actually
      // had a chance to sync up their items to reflect the current state
      // of the explorer component, so we only handle their events if
      // their count of items matches ours.
      /** @type {any} */ const cast = event.target;
      if (cast && this.items.length === cast.items.length) {
        const selectedIndex = event.detail.selectedIndex;
        if (this.selectedIndex !== selectedIndex) {
          this[internal.raiseChangeEvents] = true;
          this.selectedIndex = selectedIndex;
          this[internal.raiseChangeEvents] = false;
        }
      }
    };
    if (changed.proxyListPartType) {
      template.transmute(
        this[internal.ids].proxyList,
        this[internal.state].proxyListPartType
      );
      this[internal.ids].proxyList.addEventListener(
        "selected-index-changed",
        handleSelectedIndexChanged
      );
    }
    if (changed.stagePartType) {
      template.transmute(
        this[internal.ids].stage,
        this[internal.state].stagePartType
      );
      this[internal.ids].stage.addEventListener(
        "selected-index-changed",
        handleSelectedIndexChanged
      );
      this[internal.ids].stage.addEventListener(
        "selection-effect-finished",
        event => {
          const { selectedIndex } = /** @type {any} */ (event).detail;
          /**
           * This event is raised if the current `stage` applies a transition
           * effect when changing the selection, and the selection effect has
           * completed. [CrossfadeStage](CrossfadeStage) applies such an effect,
           * for example.
           *
           * The order of events when the `selectedIndex` property changes is
           * therefore: `selected-index-changed` (occurs immediately when the
           * index changes), followed by `selection-effect-finished` (occurs
           * some time later).
           *
           * @event selection-effect-finished
           */
          const finishedEvent = new CustomEvent("selection-effect-finished", {
            detail: { selectedIndex }
          });
          this.dispatchEvent(finishedEvent);
        }
      );
    }
    const proxyList = this[internal.ids].proxyList;
    const stage = this[internal.ids].stage;
    if (changed.proxies || changed.proxiesAssigned) {
      // Render the default proxies.
      const { proxies, proxiesAssigned } = this[internal.state];
      const childNodes = proxiesAssigned
        ? [this[internal.ids].proxySlot]
        : [this[internal.ids].proxySlot, ...proxies];
      applyChildNodes(this[internal.ids].proxyList, childNodes);
    }
    if (
      changed.proxyListOverlap ||
      changed.proxyListPosition ||
      changed.proxyListPartType
    ) {
      const { proxyListOverlap, proxyListPosition } = this[internal.state];
      const lateralPosition = lateralPositions[proxyListPosition];
      Object.assign(proxyList.style, {
        height: lateralPosition ? "100%" : null,
        position: proxyListOverlap ? "absolute" : null,
        width: lateralPosition ? null : "100%",
        zIndex: proxyListOverlap ? "1" : null
      });
    }
    if (changed.proxyListPosition || changed.rightToLeft) {
      // Map the relative position of the list vis-a-vis the stage to a position
      // from the perspective of the list.
      const cast = /** @type {any} */ (proxyList);
      if ("position" in cast) {
        const { proxyListPosition, rightToLeft } = this[internal.state];
        let position;
        switch (proxyListPosition) {
          case "end":
            position = rightToLeft ? "left" : "right";
            break;
          case "start":
            position = rightToLeft ? "right" : "left";
            break;
          default:
            position = proxyListPosition;
            break;
        }
        cast.position = position;
      }
    }
    if (changed.proxyListPosition || changed.proxyListPartType) {
      setListAndStageOrder(this, this[internal.state]);
      const { proxyListPosition } = this[internal.state];
      const lateralPosition = lateralPositions[proxyListPosition];
      this[internal.ids].explorerContainer.style.flexDirection = lateralPosition
        ? "row"
        : "column";
      Object.assign(proxyList.style, {
        bottom: proxyListPosition === "bottom" ? "0" : null,
        left: proxyListPosition === "left" ? "0" : null,
        right: proxyListPosition === "right" ? "0" : null,
        top: proxyListPosition === "top" ? "0" : null
      });
    }
    if (changed.selectedIndex || changed.proxyListPartType) {
      if ("selectedIndex" in proxyList) {
        const { selectedIndex } = this[internal.state];
        /** @type {any} */ (proxyList).selectedIndex = selectedIndex;
      }
    }
    if (changed.selectedIndex || changed.stagePartType) {
      if ("selectedIndex" in stage) {
        const { selectedIndex } = this[internal.state];
        /** @type {any} */ (stage).selectedIndex = selectedIndex;
      }
    }
    if (changed.selectionRequired || changed.proxyListPartType) {
      if ("selectionRequired" in proxyList) {
        const { selectionRequired } = this[internal.state];
        /** @type {any} */ (proxyList).selectionRequired = selectionRequired;
      }
    }
    if (changed.swipeFraction || changed.proxyListPartType) {
      if ("swipeFraction" in proxyList) {
        const { swipeFraction } = this[internal.state];
        /** @type {any} */ (proxyList).swipeFraction = swipeFraction;
      }
    }
    if (changed.swipeFraction || changed.stagePartType) {
      if ("swipeFraction" in stage) {
        const { swipeFraction } = this[internal.state];
        /** @type {any} */ (stage).swipeFraction = swipeFraction;
      }
    }
  }

  /**
   * The current set of proxy elements that correspond to the component's
   * main `items`. If you have assigned elements to the `proxy` slot, this
   * returns the collection of those elements. Otherwise, this will return
   * a collection of default proxies generated by the component, one for
   * each item.
   *
   * @type {Element[]}
   */
  get proxies() {
    return this[internal.state].proxies;
  }

  /**
   * True if the list of proxies should overlap the stage, false if not.
   *
   * @type {boolean}
   * @default false
   */
  get proxyListOverlap() {
    return this[internal.state].proxyListOverlap;
  }
  set proxyListOverlap(proxyListOverlap) {
    const parsed = String(proxyListOverlap) === "true";
    this[internal.setState]({
      proxyListOverlap: parsed
    });
  }

  /**
   * The position of the proxy list relative to the stage.
   *
   * The `start` and `end` values refer to text direction: in left-to-right
   * languages such as English, these are equivalent to `left` and `right`,
   * respectively.
   *
   * @type {('bottom'|'end'|'left'|'right'|'start'|'top')}
   * @default 'start'
   */
  get proxyListPosition() {
    return this[internal.state].proxyListPosition;
  }
  set proxyListPosition(proxyListPosition) {
    this[internal.setState]({ proxyListPosition });
  }

  /**
   * The class, tag, or template used to create the `proxy-list` part - the list
   * of selectable proxies representing the items in the list.
   *
   * @type {PartDescriptor}
   * @default ListBox
   */
  get proxyListPartType() {
    return this[internal.state].proxyListPartType;
  }
  set proxyListPartType(proxyListPartType) {
    this[internal.setState]({ proxyListPartType });
  }

  /**
   * The class, tag, or template used to create the `proxy` parts - the default
   * representations for the list's items.
   *
   * @type {PartDescriptor}
   * @default 'div'
   */
  get proxyPartType() {
    return this[internal.state].proxyPartType;
  }
  set proxyPartType(proxyPartType) {
    this[internal.setState]({ proxyPartType });
  }

  /**
   * The class, tag, or template used for the main "stage" element that shows a
   * single item at a time.
   *
   * @type {PartDescriptor}
   * @default Modes
   */
  get stagePartType() {
    return this[internal.state].stagePartType;
  }
  set stagePartType(stagePartType) {
    this[internal.setState]({ stagePartType });
  }

  [internal.stateEffects](state, changed) {
    const effects = super[internal.stateEffects](state, changed);

    // If items for default proxies have changed, recreate the proxies.
    // If nodes have been assigned to the proxy slot, use those instead.
    if (changed.items || changed.proxiesAssigned || changed.proxyPartType) {
      const { items, proxiesAssigned, proxyPartType } = state;
      if ((changed.items || changed.proxyPartType) && !proxiesAssigned) {
        // Generate sufficient default proxies.
        Object.assign(effects, {
          proxies: createDefaultProxies(items, proxyPartType)
        });
      }
    }

    return effects;
  }

  get [internal.template]() {
    return template.html`

<slot id="proxySlot" name="proxy"></slot>

<slot></slot>

    `;
  }
}

/**
 * Return the default list generated for the given items.
 *
 * @private
 * @param {ListItemElement[]} items
 * @param {PartDescriptor} proxyPartType
 */
function createDefaultProxies(items, proxyPartType) {
  const proxies = items
    ? items.map(() => template.createElement(proxyPartType))
    : [];
  proxies.forEach(proxy => {
    if ("part" in proxy) {
      /** @type {any} */ (proxy).part = "proxy";
    }
  });
  // Make the array immutable to help update performance.
  Object.freeze(proxies);
  return proxies;
}

/**
 * Find the child of root that is or contains the given node.
 *
 * @private
 * @param {Node} root
 * @param {Node} node
 * @returns {Node|null}
 */
function findChildContainingNode(root, node) {
  const parentNode = node.parentNode;
  return parentNode === root
    ? node
    : parentNode
    ? findChildContainingNode(root, parentNode)
    : null;
}

/**
 * Physically reorder the list and stage to reflect the desired arrangement. We
 * could change the visual appearance by reversing the order of the flex box,
 * but then the visual order wouldn't reflect the document order, which
 * determines focus order. That would surprise a user trying to tab through the
 * controls.
 *
 * @private
 * @param {Explorer} element
 * @param {PlainObject} state
 */
function setListAndStageOrder(element, state) {
  const { proxyListPosition, rightToLeft } = state;
  const listInInitialPosition =
    proxyListPosition === "top" ||
    proxyListPosition === "start" ||
    (proxyListPosition === "left" && !rightToLeft) ||
    (proxyListPosition === "right" && rightToLeft);
  const container = element[internal.ids].explorerContainer;
  const stage = findChildContainingNode(container, element[internal.ids].stage);
  const list = findChildContainingNode(
    container,
    element[internal.ids].proxyList
  );
  const firstElement = listInInitialPosition ? list : stage;
  const lastElement = listInInitialPosition ? stage : list;
  if (firstElement && lastElement) {
    const nextElementSibling =
      /** @type {any} */ (firstElement).nextElementSibling;
    if (nextElementSibling !== lastElement) {
      element[internal.ids].explorerContainer.insertBefore(
        firstElement,
        lastElement
      );
    }
  }
}

export default Explorer;
<commit_msg>Set `proxy` as an attribute instead of a property since Safari doesn't support the property yet.<commit_after>import { applyChildNodes } from "../core/utilities.js";
import * as internal from "./internal.js";
import * as template from "../core/template.js";
import LanguageDirectionMixin from "./LanguageDirectionMixin.js";
import ListBox from "./ListBox.js";
import Modes from "./Modes.js";
import ReactiveElement from "../core/ReactiveElement.js";
import SingleSelectionMixin from "./SingleSelectionMixin.js";
import SlotItemsMixin from "./SlotItemsMixin.js";

// Does a list position imply a lateral arrangement of list and stage?
/** @type {IndexedObject<boolean>} */
const lateralPositions = {
  end: true,
  left: true,
  right: true,
  start: true
};

const Base = LanguageDirectionMixin(
  SingleSelectionMixin(SlotItemsMixin(ReactiveElement))
);

/**
 * Combines a list with an area focusing on a single selected item.
 *
 * @inherits ReactiveElement
 * @mixes LanguageDirectionMixin
 * @mixes SingleSelectionMixin
 * @mixes SlotItemsMixin
 * @part {div} proxy - an element representing an item in the list
 * @part {ListBox} proxy-list - the container for the list of proxies
 * @part {Modes} stage - the main element showing a single item from the list
 */
class Explorer extends Base {
  [internal.checkSize]() {
    if (super[internal.checkSize]) {
      super[internal.checkSize]();
    }
    if (this[internal.ids].stage[internal.checkSize]) {
      this[internal.ids].stage[internal.checkSize]();
    }
    if (this[internal.ids].proxyList[internal.checkSize]) {
      this[internal.ids].proxyList[internal.checkSize]();
    }
  }

  [internal.componentDidMount]() {
    super[internal.componentDidMount]();

    // When proxy slot's assigned nodes change, determine whether we need to
    // generate default proxies or (if assigned nodes are present) treat the
    // assigned nodes as the proxies.
    this[internal.ids].proxySlot.addEventListener("slotchange", () => {
      const proxySlot = /** @type {any} */ (this[internal.ids].proxySlot);
      const proxies = proxySlot.assignedNodes({ flatten: true });
      const proxiesAssigned = proxies.length > 0;
      if (proxiesAssigned) {
        // Nodes assigned to slot become proxies.
        this[internal.setState]({
          proxiesAssigned,
          proxies
        });
      } else {
        // No nodes assigned -- we'll need to generate proxies.
        this[internal.setState]({ proxiesAssigned });
      }
    });
  }

  get [internal.defaultState]() {
    return Object.assign(super[internal.defaultState], {
      proxies: [],
      proxiesAssigned: false,
      proxyPartType: "div",
      proxyListOverlap: false,
      proxyListPosition: "top",
      proxyListPartType: ListBox,
      selectionRequired: true,
      stagePartType: Modes
    });
  }

  [internal.render](/** @type {PlainObject} */ changed) {
    super[internal.render](changed);
    /** @type {any} */
    const handleSelectedIndexChanged = (/** @type {CustomEvent} */ event) => {
      // The proxy list and stage may raise events before they've actually
      // had a chance to sync up their items to reflect the current state
      // of the explorer component, so we only handle their events if
      // their count of items matches ours.
      /** @type {any} */ const cast = event.target;
      if (cast && this.items.length === cast.items.length) {
        const selectedIndex = event.detail.selectedIndex;
        if (this.selectedIndex !== selectedIndex) {
          this[internal.raiseChangeEvents] = true;
          this.selectedIndex = selectedIndex;
          this[internal.raiseChangeEvents] = false;
        }
      }
    };
    if (changed.proxyListPartType) {
      template.transmute(
        this[internal.ids].proxyList,
        this[internal.state].proxyListPartType
      );
      this[internal.ids].proxyList.addEventListener(
        "selected-index-changed",
        handleSelectedIndexChanged
      );
    }
    if (changed.stagePartType) {
      template.transmute(
        this[internal.ids].stage,
        this[internal.state].stagePartType
      );
      this[internal.ids].stage.addEventListener(
        "selected-index-changed",
        handleSelectedIndexChanged
      );
      this[internal.ids].stage.addEventListener(
        "selection-effect-finished",
        event => {
          const { selectedIndex } = /** @type {any} */ (event).detail;
          /**
           * This event is raised if the current `stage` applies a transition
           * effect when changing the selection, and the selection effect has
           * completed. [CrossfadeStage](CrossfadeStage) applies such an effect,
           * for example.
           *
           * The order of events when the `selectedIndex` property changes is
           * therefore: `selected-index-changed` (occurs immediately when the
           * index changes), followed by `selection-effect-finished` (occurs
           * some time later).
           *
           * @event selection-effect-finished
           */
          const finishedEvent = new CustomEvent("selection-effect-finished", {
            detail: { selectedIndex }
          });
          this.dispatchEvent(finishedEvent);
        }
      );
    }
    const proxyList = this[internal.ids].proxyList;
    const stage = this[internal.ids].stage;
    if (changed.proxies || changed.proxiesAssigned) {
      // Render the default proxies.
      const { proxies, proxiesAssigned } = this[internal.state];
      const childNodes = proxiesAssigned
        ? [this[internal.ids].proxySlot]
        : [this[internal.ids].proxySlot, ...proxies];
      applyChildNodes(this[internal.ids].proxyList, childNodes);
    }
    if (
      changed.proxyListOverlap ||
      changed.proxyListPosition ||
      changed.proxyListPartType
    ) {
      const { proxyListOverlap, proxyListPosition } = this[internal.state];
      const lateralPosition = lateralPositions[proxyListPosition];
      Object.assign(proxyList.style, {
        height: lateralPosition ? "100%" : null,
        position: proxyListOverlap ? "absolute" : null,
        width: lateralPosition ? null : "100%",
        zIndex: proxyListOverlap ? "1" : null
      });
    }
    if (changed.proxyListPosition || changed.rightToLeft) {
      // Map the relative position of the list vis-a-vis the stage to a position
      // from the perspective of the list.
      const cast = /** @type {any} */ (proxyList);
      if ("position" in cast) {
        const { proxyListPosition, rightToLeft } = this[internal.state];
        let position;
        switch (proxyListPosition) {
          case "end":
            position = rightToLeft ? "left" : "right";
            break;
          case "start":
            position = rightToLeft ? "right" : "left";
            break;
          default:
            position = proxyListPosition;
            break;
        }
        cast.position = position;
      }
    }
    if (changed.proxyListPosition || changed.proxyListPartType) {
      setListAndStageOrder(this, this[internal.state]);
      const { proxyListPosition } = this[internal.state];
      const lateralPosition = lateralPositions[proxyListPosition];
      this[internal.ids].explorerContainer.style.flexDirection = lateralPosition
        ? "row"
        : "column";
      Object.assign(proxyList.style, {
        bottom: proxyListPosition === "bottom" ? "0" : null,
        left: proxyListPosition === "left" ? "0" : null,
        right: proxyListPosition === "right" ? "0" : null,
        top: proxyListPosition === "top" ? "0" : null
      });
    }
    if (changed.selectedIndex || changed.proxyListPartType) {
      if ("selectedIndex" in proxyList) {
        const { selectedIndex } = this[internal.state];
        /** @type {any} */ (proxyList).selectedIndex = selectedIndex;
      }
    }
    if (changed.selectedIndex || changed.stagePartType) {
      if ("selectedIndex" in stage) {
        const { selectedIndex } = this[internal.state];
        /** @type {any} */ (stage).selectedIndex = selectedIndex;
      }
    }
    if (changed.selectionRequired || changed.proxyListPartType) {
      if ("selectionRequired" in proxyList) {
        const { selectionRequired } = this[internal.state];
        /** @type {any} */ (proxyList).selectionRequired = selectionRequired;
      }
    }
    if (changed.swipeFraction || changed.proxyListPartType) {
      if ("swipeFraction" in proxyList) {
        const { swipeFraction } = this[internal.state];
        /** @type {any} */ (proxyList).swipeFraction = swipeFraction;
      }
    }
    if (changed.swipeFraction || changed.stagePartType) {
      if ("swipeFraction" in stage) {
        const { swipeFraction } = this[internal.state];
        /** @type {any} */ (stage).swipeFraction = swipeFraction;
      }
    }
  }

  /**
   * The current set of proxy elements that correspond to the component's
   * main `items`. If you have assigned elements to the `proxy` slot, this
   * returns the collection of those elements. Otherwise, this will return
   * a collection of default proxies generated by the component, one for
   * each item.
   *
   * @type {Element[]}
   */
  get proxies() {
    return this[internal.state].proxies;
  }

  /**
   * True if the list of proxies should overlap the stage, false if not.
   *
   * @type {boolean}
   * @default false
   */
  get proxyListOverlap() {
    return this[internal.state].proxyListOverlap;
  }
  set proxyListOverlap(proxyListOverlap) {
    const parsed = String(proxyListOverlap) === "true";
    this[internal.setState]({
      proxyListOverlap: parsed
    });
  }

  /**
   * The position of the proxy list relative to the stage.
   *
   * The `start` and `end` values refer to text direction: in left-to-right
   * languages such as English, these are equivalent to `left` and `right`,
   * respectively.
   *
   * @type {('bottom'|'end'|'left'|'right'|'start'|'top')}
   * @default 'start'
   */
  get proxyListPosition() {
    return this[internal.state].proxyListPosition;
  }
  set proxyListPosition(proxyListPosition) {
    this[internal.setState]({ proxyListPosition });
  }

  /**
   * The class, tag, or template used to create the `proxy-list` part - the list
   * of selectable proxies representing the items in the list.
   *
   * @type {PartDescriptor}
   * @default ListBox
   */
  get proxyListPartType() {
    return this[internal.state].proxyListPartType;
  }
  set proxyListPartType(proxyListPartType) {
    this[internal.setState]({ proxyListPartType });
  }

  /**
   * The class, tag, or template used to create the `proxy` parts - the default
   * representations for the list's items.
   *
   * @type {PartDescriptor}
   * @default 'div'
   */
  get proxyPartType() {
    return this[internal.state].proxyPartType;
  }
  set proxyPartType(proxyPartType) {
    this[internal.setState]({ proxyPartType });
  }

  /**
   * The class, tag, or template used for the main "stage" element that shows a
   * single item at a time.
   *
   * @type {PartDescriptor}
   * @default Modes
   */
  get stagePartType() {
    return this[internal.state].stagePartType;
  }
  set stagePartType(stagePartType) {
    this[internal.setState]({ stagePartType });
  }

  [internal.stateEffects](state, changed) {
    const effects = super[internal.stateEffects](state, changed);

    // If items for default proxies have changed, recreate the proxies.
    // If nodes have been assigned to the proxy slot, use those instead.
    if (changed.items || changed.proxiesAssigned || changed.proxyPartType) {
      const { items, proxiesAssigned, proxyPartType } = state;
      if ((changed.items || changed.proxyPartType) && !proxiesAssigned) {
        // Generate sufficient default proxies.
        Object.assign(effects, {
          proxies: createDefaultProxies(items, proxyPartType)
        });
      }
    }

    return effects;
  }

  get [internal.template]() {
    return template.html`

<slot id="proxySlot" name="proxy"></slot>

<slot></slot>

    `;
  }
}

/**
 * Return the default list generated for the given items.
 *
 * @private
 * @param {ListItemElement[]} items
 * @param {PartDescriptor} proxyPartType
 */
function createDefaultProxies(items, proxyPartType) {
  const proxies = items
    ? items.map(() => template.createElement(proxyPartType))
    : [];
  proxies.forEach(proxy => {
    // As of February 2020, the `part` property is not available in all
    // browsers, so we set it as an attribute instead.
    /** @type {any} */ (proxy).setAttribute("part", "proxy");
  });
  // Make the array immutable to avoid accidental mutation.
  Object.freeze(proxies);
  return proxies;
}

/**
 * Find the child of root that is or contains the given node.
 *
 * @private
 * @param {Node} root
 * @param {Node} node
 * @returns {Node|null}
 */
function findChildContainingNode(root, node) {
  const parentNode = node.parentNode;
  return parentNode === root
    ? node
    : parentNode
    ? findChildContainingNode(root, parentNode)
    : null;
}

/**
 * Physically reorder the list and stage to reflect the desired arrangement. We
 * could change the visual appearance by reversing the order of the flex box,
 * but then the visual order wouldn't reflect the document order, which
 * determines focus order. That would surprise a user trying to tab through the
 * controls.
 *
 * @private
 * @param {Explorer} element
 * @param {PlainObject} state
 */
function setListAndStageOrder(element, state) {
  const { proxyListPosition, rightToLeft } = state;
  const listInInitialPosition =
    proxyListPosition === "top" ||
    proxyListPosition === "start" ||
    (proxyListPosition === "left" && !rightToLeft) ||
    (proxyListPosition === "right" && rightToLeft);
  const container = element[internal.ids].explorerContainer;
  const stage = findChildContainingNode(container, element[internal.ids].stage);
  const list = findChildContainingNode(
    container,
    element[internal.ids].proxyList
  );
  const firstElement = listInInitialPosition ? list : stage;
  const lastElement = listInInitialPosition ? stage : list;
  if (firstElement && lastElement) {
    const nextElementSibling =
      /** @type {any} */ (firstElement).nextElementSibling;
    if (nextElementSibling !== lastElement) {
      element[internal.ids].explorerContainer.insertBefore(
        firstElement,
        lastElement
      );
    }
  }
}

export default Explorer;
<|endoftext|>


<commit_before>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Plotly = require('../../plotly');
var scatterSubTypes = require('../../traces/scatter/subtypes');

var createModeBar = require('./');
var modeBarButtons = require('./buttons');

/**
 * ModeBar wrapper around 'create' and 'update',
 * chooses buttons to pass to ModeBar constructor based on
 * plot type and plot config.
 *
 * @param {object} gd main plot object
 *
 */
module.exports = function manageModeBar(gd) {
    var fullLayout = gd._fullLayout,
        context = gd._context,
        modeBar = fullLayout._modeBar;

    if(!context.displayModeBar) {
        if(modeBar) {
            modeBar.destroy();
            delete fullLayout._modeBar;
        }
        return;
    }

    if(!Array.isArray(context.modeBarButtonsToRemove)) {
        throw new Error([
            '*modeBarButtonsToRemove* configuration options',
            'must be an array.'
        ].join(' '));
    }

    if(!Array.isArray(context.modeBarButtonsToAdd)) {
        throw new Error([
            '*modeBarButtonsToAdd* configuration options',
            'must be an array.'
        ].join(' '));
    }

    var customButtons = context.modeBarButtons;
    var buttonGroups;

    if(Array.isArray(customButtons) && customButtons.length) {
        buttonGroups = fillCustomButton(customButtons);
    }
    else {
        buttonGroups = getButtonGroups(
            gd,
            context.modeBarButtonsToRemove,
            context.modeBarButtonsToAdd
        );
    }

    if(modeBar) modeBar.update(gd, buttonGroups);
    else fullLayout._modeBar = createModeBar(gd, buttonGroups);
};

// logic behind which buttons are displayed by default
function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
    var fullLayout = gd._fullLayout,
        fullData = gd._fullData,
        groups = [],
        i,
        trace;

    function addGroup(newGroup) {
        var out = [];

        for(var i = 0; i < newGroup.length; i++) {
            var button = newGroup[i];
            if(buttonsToRemove.indexOf(button) !== -1) continue;
            out.push(modeBarButtons[button]);
        }

        groups.push(out);
    }

    // buttons common to all plot types
    addGroup(['toImage', 'sendDataToCloud']);

    if(fullLayout._hasGL3D) {
        addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']);
        addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']);
        addGroup(['hoverClosest3d']);
    }

    if(fullLayout._hasGeo) {
        addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']);
        addGroup(['hoverClosestGeo']);
    }

    var hasCartesian = fullLayout._hasCartesian,
        hasGL2D = fullLayout._hasGL2D,
        allAxesFixed = areAllAxesFixed(fullLayout),
        dragModeGroup = [];

    if((hasCartesian || hasGL2D) && !allAxesFixed) {
        dragModeGroup = ['zoom2d', 'pan2d'];
    }
    if(hasCartesian) {
        // look for traces that support selection
        // to be updated as we add more selectPoints handlers
        var selectable = false;
        for(i = 0; i < fullData.length; i++) {
            if(selectable) break;
            trace = fullData[i];
            if(!trace._module || !trace._module.selectPoints) continue;

            if(trace.type === 'scatter') {
                if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
                    selectable = true;
                }
            }
            // assume that in general if the trace module has selectPoints,
            // then it's selectable. Scatter is an exception to this because it must
            // have markers or text, not just be a scatter type.
            else selectable = true;
        }

        if(selectable) {
            dragModeGroup.push('select2d');
            dragModeGroup.push('lasso2d');
        }
    }
    if(dragModeGroup.length) addGroup(dragModeGroup);

    if((hasCartesian || hasGL2D) && !allAxesFixed) {
        addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']);
    }

    if(hasCartesian) {
        addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']);
    }
    if(hasGL2D) {
        addGroup(['hoverClosestGl2d']);
    }
    if(fullLayout._hasPie) {
        addGroup(['hoverClosestPie']);
    }

    // append buttonsToAdd to the groups
    if(buttonsToAdd.length) {
        if(Array.isArray(buttonsToAdd[0])) {
            for(i = 0; i < buttonsToAdd.length; i++) {
                groups.push(buttonsToAdd[i]);
            }
        }
        else groups.push(buttonsToAdd);
    }

    return groups;
}

function areAllAxesFixed(fullLayout) {
    var axList = Plotly.Axes.list({_fullLayout: fullLayout}, null, true);
    var allFixed = true;

    for(var i = 0; i < axList.length; i++) {
        if(!axList[i].fixedrange) {
            allFixed = false;
            break;
        }
    }

    return allFixed;
}

// fill in custom buttons referring to default mode bar buttons
function fillCustomButton(customButtons) {
    for(var i = 0; i < customButtons.length; i++) {
        var buttonGroup = customButtons[i];

        for(var j = 0; j < buttonGroup.length; j++) {
            var button = buttonGroup[j];

            if(typeof button === 'string') {
                if(modeBarButtons[button] !== undefined) {
                    customButtons[i][j] = modeBarButtons[button];
                }
                else {
                    throw new Error([
                        '*modeBarButtons* configuration options',
                        'invalid button name'
                    ].join(' '));
                }
            }
        }
    }

    return customButtons;
}
<commit_msg>factor out isSelectable routine<commit_after>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Plotly = require('../../plotly');
var scatterSubTypes = require('../../traces/scatter/subtypes');

var createModeBar = require('./');
var modeBarButtons = require('./buttons');

/**
 * ModeBar wrapper around 'create' and 'update',
 * chooses buttons to pass to ModeBar constructor based on
 * plot type and plot config.
 *
 * @param {object} gd main plot object
 *
 */
module.exports = function manageModeBar(gd) {
    var fullLayout = gd._fullLayout,
        context = gd._context,
        modeBar = fullLayout._modeBar;

    if(!context.displayModeBar) {
        if(modeBar) {
            modeBar.destroy();
            delete fullLayout._modeBar;
        }
        return;
    }

    if(!Array.isArray(context.modeBarButtonsToRemove)) {
        throw new Error([
            '*modeBarButtonsToRemove* configuration options',
            'must be an array.'
        ].join(' '));
    }

    if(!Array.isArray(context.modeBarButtonsToAdd)) {
        throw new Error([
            '*modeBarButtonsToAdd* configuration options',
            'must be an array.'
        ].join(' '));
    }

    var customButtons = context.modeBarButtons;
    var buttonGroups;

    if(Array.isArray(customButtons) && customButtons.length) {
        buttonGroups = fillCustomButton(customButtons);
    }
    else {
        buttonGroups = getButtonGroups(
            gd,
            context.modeBarButtonsToRemove,
            context.modeBarButtonsToAdd
        );
    }

    if(modeBar) modeBar.update(gd, buttonGroups);
    else fullLayout._modeBar = createModeBar(gd, buttonGroups);
};

// logic behind which buttons are displayed by default
function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) {
    var fullLayout = gd._fullLayout,
        fullData = gd._fullData;

    var hasCartesian = fullLayout._hasCartesian,
        hasGL3D = fullLayout._hasGL3D,
        hasGeo = fullLayout._hasGeo,
        hasPie = fullLayout._hasPie,
        hasGL2D = fullLayout._hasGL2D;

    var groups = [];

    function addGroup(newGroup) {
        var out = [];

        for(var i = 0; i < newGroup.length; i++) {
            var button = newGroup[i];
            if(buttonsToRemove.indexOf(button) !== -1) continue;
            out.push(modeBarButtons[button]);
        }

        groups.push(out);
    }

    function appendButtonsToAdd(groups) {
        if(buttonsToAdd.length) {
            if(Array.isArray(buttonsToAdd[0])) {
                for(var i = 0; i < buttonsToAdd.length; i++) {
                    groups.push(buttonsToAdd[i]);
                }
            }
            else groups.push(buttonsToAdd);
        }

        return groups;
    }

    // buttons common to all plot types
    addGroup(['toImage', 'sendDataToCloud']);

    if(hasGL3D) {
        addGroup(['zoom3d', 'pan3d', 'orbitRotation', 'tableRotation']);
        addGroup(['resetCameraDefault3d', 'resetCameraLastSave3d']);
        addGroup(['hoverClosest3d']);
    }

    if(hasGeo) {
        addGroup(['zoomInGeo', 'zoomOutGeo', 'resetGeo']);
        addGroup(['hoverClosestGeo']);
    }

    var allAxesFixed = areAllAxesFixed(fullLayout),
        dragModeGroup = [];

    if((hasCartesian || hasGL2D) && !allAxesFixed) {
        dragModeGroup = ['zoom2d', 'pan2d'];
    }
    if(hasCartesian && isSelectable(fullData)) {
        dragModeGroup.push('select2d');
        dragModeGroup.push('lasso2d');
    }
    if(dragModeGroup.length) addGroup(dragModeGroup);

    if((hasCartesian || hasGL2D) && !allAxesFixed) {
        addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']);
    }

    if(hasCartesian) {
        addGroup(['hoverClosestCartesian', 'hoverCompareCartesian']);
    }
    if(hasGL2D) {
        addGroup(['hoverClosestGl2d']);
    }
    if(hasPie) {
        addGroup(['hoverClosestPie']);
    }

    return appendButtonsToAdd(groups);
}

function areAllAxesFixed(fullLayout) {
    var axList = Plotly.Axes.list({_fullLayout: fullLayout}, null, true);
    var allFixed = true;

    for(var i = 0; i < axList.length; i++) {
        if(!axList[i].fixedrange) {
            allFixed = false;
            break;
        }
    }

    return allFixed;
}

// look for traces that support selection
// to be updated as we add more selectPoints handlers
function isSelectable(fullData) {
    var selectable = false;

    for(var i = 0; i < fullData.length; i++) {
        if(selectable) break;

        var trace = fullData[i];

        if(!trace._module || !trace._module.selectPoints) continue;

        if(trace.type === 'scatter') {
            if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
                selectable = true;
            }
        }
        // assume that in general if the trace module has selectPoints,
        // then it's selectable. Scatter is an exception to this because it must
        // have markers or text, not just be a scatter type.
        else selectable = true;
    }

    return selectable;
}

// fill in custom buttons referring to default mode bar buttons
function fillCustomButton(customButtons) {
    for(var i = 0; i < customButtons.length; i++) {
        var buttonGroup = customButtons[i];

        for(var j = 0; j < buttonGroup.length; j++) {
            var button = buttonGroup[j];

            if(typeof button === 'string') {
                if(modeBarButtons[button] !== undefined) {
                    customButtons[i][j] = modeBarButtons[button];
                }
                else {
                    throw new Error([
                        '*modeBarButtons* configuration options',
                        'invalid button name'
                    ].join(' '));
                }
            }
        }
    }

    return customButtons;
}
<|endoftext|>


<commit_before>'use strict';

angular.module('classBrowserUHApp.offeredClass', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/offeredClass', {
        templateUrl: 'views/offeredClass/offeredClass.html',
        controller: 'OfferedClassCtrl'
    });
}])
.controller('OfferedClassCtrl', ['$scope', '$http', '$q', '$rootScope', '$parse', '$window', function ($scope, $http, $q, $rootScope, $parse, $window) {
    var setScopeVariableFromJSON = function(filePath, scopeVariable) {
        $rootScope.httpService.getData(filePath).then(function(result) {
            $parse(scopeVariable).assign($scope, result);
        });
    };

    var initializeAllJSONAndScopeNames = function(name) {
        $rootScope.httpService.getData('resources/allScopeVariables.json').then(function(result) {
            $scope.allJSONAndScopeNames = result[name];
            $scope.allJSONAndScopeNames.forEach(function(e) {
                setScopeVariableFromJSON(e.path, e.scopeName);
            });
        });
    };

    initializeAllJSONAndScopeNames("offeredClass");

    $scope.validValues = ['A','1','2', '3', '4', '5', '6', '7', '8', '9', '0'];

    $scope.deleteModel = function() {
        [].slice.call(arguments).forEach(function(arg) {
            delete $scope[arg];
        });
    };

    $scope.clearForms = function() {
        var clearFormDialogConfirmation = $window.confirm('Are you sure you want to clear the entire form?');

        if (clearFormDialogConfirmation) {
            clearAllModels();
            $scope.deleteModel('warningMessage', 'hasNoResults', 'isError');
        }
    };

    $scope.findClasses = function(isValidForm) {
        if(isValidForm) {
            $scope.isDataLoading = true;
            $scope.hasNoResults = false;
            $scope.isError = false;
            $scope.parametersMessage = generateMessageForAllModels();

            var setRowCollectionFromData = function(data) {
                $scope.rowCollection = [];
                $scope.numberOfRows = 0;
                data.forEach(function(aRow) {
                    $scope.numberOfRows += aRow.numberOfRows;
                    $scope.rowCollection = $scope.rowCollection.concat(aRow.result);
                });
            };

            var onError = function(err) {
                console.log('Unable to get the data given error: ' + err);
                $scope.isError = true;
            };

            var finallyDo = function() {
                $scope.isDataLoading = false;

                if($scope.numberOfRows == 0 && !$scope.isError) {
                    $scope.hasNoResults = true;
                    $scope.warningMessage = "There are no classes found with the categories you have selected. Please try again.";
                }
                else if(!$scope.isError) {
                    $scope.showResults = true;
                }
            };

            getAllData()
                .then(setRowCollectionFromData)
                .catch(onError)
                .finally(finallyDo);
        }
        else {
            alert("Please select a class term.");
        }
    };

    var getAllData = function() {
        var apiURLs = getAllAPIUrls();
        var promises = [];

        _.each(apiURLs, function(apiURL) {
            promises.push($rootScope.httpService.getData(apiURL));
        });

        return $q.all(promises);
    };

    var generateBaseURL = function() {
        var baseURLWithoutTerm = $scope.apiUrl + "/classes/term=";
        baseURLWithoutTerm += $scope.$eval('termModel')['term'];
        baseURLWithoutTerm += "?";
        return baseURLWithoutTerm;
    };

    var generateAllParameters = function() {
        var allParameters = [];
        $scope.allJSONAndScopeNames.forEach(function(aScopeVariable) {
            if(aScopeVariable.modelName != 'termModel') {
                var modelValue = $scope.$eval(aScopeVariable.modelName);
                if (modelValue != undefined) {
                    if (isNotArray(modelValue)) {
                        modelValue = [modelValue];
                    }

                    var currentParameters = [];
                    modelValue.forEach(function (aModel) {
                        if (aModel["isOrNot"] != undefined) {
                            currentParameters.push(aModel[aScopeVariable.apiParameterValueFromModel].split(" ").join("-") +
                                "=" + (aModel["isOrNot"] == "Is"));
                        }
                        else {
                            currentParameters.push(aScopeVariable.apiParameterInEndpoint + "=" + aModel[aScopeVariable.apiParameterValueFromModel]);
                        }
                    });
                    allParameters.push(currentParameters);
                }
            }
        });
        return allParameters;
    };

    var getAllAPIUrls = function() {
        var baseURL = generateBaseURL();
        var allParameters = generateAllParameters();
        var allAPIUrls = allParameters
            .reduce($rootScope.cartesianProductService.productAdd, [""]);

        return $rootScope.apiURLService.appendParametersAndReturnAPIUrls(allAPIUrls, baseURL);
    };

    var generateMessageForAllModels = function() {
        var message = "";

        $scope.allJSONAndScopeNames.forEach(function(aScopeVariable) {
            if($scope.$eval(aScopeVariable.modelName) != undefined) {
                var modelValue = $scope.$eval(aScopeVariable.modelName);
                var allDisplayParameters = aScopeVariable.displayParameters.split(", ");
                var allMessagesForModel = getAllMessagesForModelAndParameters(modelValue, allDisplayParameters);

                message += "" + aScopeVariable.uiParameter + ": ";
                message += allMessagesForModel.join(", ");
                message += "
";
            }
        });

        return message;
    };

    var isNotArray = function(models) {
        return $rootScope.types.get(models) !== $rootScope.types.array;
    };

    var getAllMessagesForModelAndParameters = function(models, parameters) {
        if(isNotArray(models)) {
            models = [models];
        }

        var allMessages = [];
        models.forEach(function(model) {
            var aMessageForModel = "";
            parameters.forEach(function(parameter) {
                aMessageForModel = aMessageForModel + model[parameter] + " ";
            });
            allMessages.push(aMessageForModel.trim());
        });
        return allMessages;
    };

    $scope.goBack = function() {
        console.log('Going back to offered class search page.');
        $scope.showResults = false;
    };

    $scope.selectTestModels = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.formatModel = [{"format": "Hybrid", "isOrNot": "Is"}, {"format": "Face to Face", "isOrNot": "Not"}];
        $scope.statusModel = {"status": "Closed"};
        $scope.sessionModel = [{"session": 4, "sessionTitle": "Session 4"}, {
            "session": 2,
            "sessionTitle": "Session 2"
        }];
        $scope.subjectModel = [{"subject": "AFSC", "subjectFullName": "Air Force Science"}, {
            "subject": "ARAB",
            "subjectFullName": "Arabic"
        }];
        $scope.buildingModel = [{"buildingID": 517, "building": "A", "buildingName": "Cullen Performance Hall"}];
        $scope.locationModel = [{"location": "UH-Sugar Land"}, {"location": "UH-San Antonio-Conrad Hilton"}, {"location": "UH"}];
        $scope.componentModel = [{"component": "LEC", "componentName": "Lecture"}];
        $scope.creditHourModel = [{"credit-hours": 4}];
        $scope.weekendUModel = {"weekendU": "No"};
        $scope.coreCategoriesModel = [{"categoryNumber": 2, "categoryName": "Mathematics"}];
        $scope.isCoreModel = {"is-core": "No"};
        $scope.syllabusModel = {"syllabus": "No"};
        $scope.classDaysModel = [{"day": "Monday", "isOrNot": "Is"}, {
            "day": "Sunday",
            "isOrNot": "Is"
        }, {"day": "Tuesday", "isOrNot": "Not"}];
        $scope.courseNumberModel = {"courseNumber": 2410};
    };

    $scope.selectTestModelsTwo = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.weekendUModel = {"weekendU": "Yes"};

    };

    $scope.selectTestModelsThree = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.isCoreModel = {"is-core": "Yes"};
        $scope.formatModel = [{"format": "Online", "isOrNot": "Is"}];
    };

    $scope.selectTestModelsOneClassResult = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.formatModel = [{"format": "Online", "isOrNot": "Is"}];
        $scope.sessionModel = [{"session": 6, "sessionTitle": "Session 6"}];
        $scope.subjectModel = [{"subject": "SOC", "subjectFullName": "Sociology"}];

    };

    var clearAllModels = function() {
        $scope.allJSONAndScopeNames.forEach(function(e) {
            $scope.deleteModel(e.modelName);
        });
    };

    $scope.getSyllabus = function(syllabus) {
        if(syllabus == 'Unavailable') {
            return "Syllabus Not Available.";
        }
        else {
            return "Syllabus";
        }
    };

    $scope.getClassDaysMessage = function(classDaysForAClass) {
        var allClassDays = $scope.classDays.dayAbbreviations;

        return allClassDays
                .filter(function(aClassDay) {
                    return classDaysForAClass[aClassDay.classDay];
                })
                .map(function(aClassDay) {
                    return aClassDay.classDayAbbreviation;
                })
                .join("") || "No Class Days";
    };

    $scope.getCoreCategoriesMessage = function(coreCategories) {
        return coreCategories
                .filter(function(aCoreCategory) {
                    return aCoreCategory.length != 0;
                })
                .map(function(aCoreCategory) {
                    return $scope.coreCategories.availableOptions[aCoreCategory - 1].categoryName;
                })
                .join(", ") || "Not a core class.";
    };

    $scope.displayDetailedOfferedClass = function(aClass) {
        $scope.displayDetailed = true;
        $scope.detailedClass = aClass;
        $scope.detailedOfferedClassMessage = aClass.classTitle;
    };

    $scope.exitDisplayDetailedOfferedClass = function() {
        $scope.displayDetailed = false;
        $scope.detailedClass = undefined;
    };

    $scope.getSeatInformationMessage = function(status, seatInformation) {
        return status + " (" + seatInformation.seatsTaken + "/" +
            seatInformation.seatsTotal + ")";
    };

    $scope.getMeetingTimesMessage = function(startTime, endTime) {
        console.log(startTime);
        return startTime + " " + endTime;
    };

    $scope.getMeetingDatesMessage = function(startDate, endDate) {
        return startDate.replace("-", "/") + " to " + endDate.replace("-", "/");
    };

}]);
<commit_msg>Implemented getMeetingTimesMessage which turns HH:MM:SS to HH:MM AM.<commit_after>'use strict';

angular.module('classBrowserUHApp.offeredClass', ['ngRoute'])

.config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/offeredClass', {
        templateUrl: 'views/offeredClass/offeredClass.html',
        controller: 'OfferedClassCtrl'
    });
}])
.controller('OfferedClassCtrl', ['$scope', '$http', '$q', '$rootScope', '$parse', '$window', function ($scope, $http, $q, $rootScope, $parse, $window) {
    var setScopeVariableFromJSON = function(filePath, scopeVariable) {
        $rootScope.httpService.getData(filePath).then(function(result) {
            $parse(scopeVariable).assign($scope, result);
        });
    };

    var initializeAllJSONAndScopeNames = function(name) {
        $rootScope.httpService.getData('resources/allScopeVariables.json').then(function(result) {
            $scope.allJSONAndScopeNames = result[name];
            $scope.allJSONAndScopeNames.forEach(function(e) {
                setScopeVariableFromJSON(e.path, e.scopeName);
            });
        });
    };

    initializeAllJSONAndScopeNames("offeredClass");

    $scope.validValues = ['A','1','2', '3', '4', '5', '6', '7', '8', '9', '0'];

    $scope.deleteModel = function() {
        [].slice.call(arguments).forEach(function(arg) {
            delete $scope[arg];
        });
    };

    $scope.clearForms = function() {
        var clearFormDialogConfirmation = $window.confirm('Are you sure you want to clear the entire form?');

        if (clearFormDialogConfirmation) {
            clearAllModels();
            $scope.deleteModel('warningMessage', 'hasNoResults', 'isError');
        }
    };

    $scope.findClasses = function(isValidForm) {
        if(isValidForm) {
            $scope.isDataLoading = true;
            $scope.hasNoResults = false;
            $scope.isError = false;
            $scope.parametersMessage = generateMessageForAllModels();

            var setRowCollectionFromData = function(data) {
                $scope.rowCollection = [];
                $scope.numberOfRows = 0;
                data.forEach(function(aRow) {
                    $scope.numberOfRows += aRow.numberOfRows;
                    $scope.rowCollection = $scope.rowCollection.concat(aRow.result);
                });
            };

            var onError = function(err) {
                console.log('Unable to get the data given error: ' + err);
                $scope.isError = true;
            };

            var finallyDo = function() {
                $scope.isDataLoading = false;

                if($scope.numberOfRows == 0 && !$scope.isError) {
                    $scope.hasNoResults = true;
                    $scope.warningMessage = "There are no classes found with the categories you have selected. Please try again.";
                }
                else if(!$scope.isError) {
                    $scope.showResults = true;
                }
            };

            getAllData()
                .then(setRowCollectionFromData)
                .catch(onError)
                .finally(finallyDo);
        }
        else {
            alert("Please select a class term.");
        }
    };

    var getAllData = function() {
        var apiURLs = getAllAPIUrls();
        var promises = [];

        _.each(apiURLs, function(apiURL) {
            promises.push($rootScope.httpService.getData(apiURL));
        });

        return $q.all(promises);
    };

    var generateBaseURL = function() {
        var baseURLWithoutTerm = $scope.apiUrl + "/classes/term=";
        baseURLWithoutTerm += $scope.$eval('termModel')['term'];
        baseURLWithoutTerm += "?";
        return baseURLWithoutTerm;
    };

    var generateAllParameters = function() {
        var allParameters = [];
        $scope.allJSONAndScopeNames.forEach(function(aScopeVariable) {
            if(aScopeVariable.modelName != 'termModel') {
                var modelValue = $scope.$eval(aScopeVariable.modelName);
                if (modelValue != undefined) {
                    if (isNotArray(modelValue)) {
                        modelValue = [modelValue];
                    }

                    var currentParameters = [];
                    modelValue.forEach(function (aModel) {
                        if (aModel["isOrNot"] != undefined) {
                            currentParameters.push(aModel[aScopeVariable.apiParameterValueFromModel].split(" ").join("-") +
                                "=" + (aModel["isOrNot"] == "Is"));
                        }
                        else {
                            currentParameters.push(aScopeVariable.apiParameterInEndpoint + "=" + aModel[aScopeVariable.apiParameterValueFromModel]);
                        }
                    });
                    allParameters.push(currentParameters);
                }
            }
        });
        return allParameters;
    };

    var getAllAPIUrls = function() {
        var baseURL = generateBaseURL();
        var allParameters = generateAllParameters();
        var allAPIUrls = allParameters
            .reduce($rootScope.cartesianProductService.productAdd, [""]);

        return $rootScope.apiURLService.appendParametersAndReturnAPIUrls(allAPIUrls, baseURL);
    };

    var generateMessageForAllModels = function() {
        var message = "";

        $scope.allJSONAndScopeNames.forEach(function(aScopeVariable) {
            if($scope.$eval(aScopeVariable.modelName) != undefined) {
                var modelValue = $scope.$eval(aScopeVariable.modelName);
                var allDisplayParameters = aScopeVariable.displayParameters.split(", ");
                var allMessagesForModel = getAllMessagesForModelAndParameters(modelValue, allDisplayParameters);

                message += "" + aScopeVariable.uiParameter + ": ";
                message += allMessagesForModel.join(", ");
                message += "
";
            }
        });

        return message;
    };

    var isNotArray = function(models) {
        return $rootScope.types.get(models) !== $rootScope.types.array;
    };

    var getAllMessagesForModelAndParameters = function(models, parameters) {
        if(isNotArray(models)) {
            models = [models];
        }

        var allMessages = [];
        models.forEach(function(model) {
            var aMessageForModel = "";
            parameters.forEach(function(parameter) {
                aMessageForModel = aMessageForModel + model[parameter] + " ";
            });
            allMessages.push(aMessageForModel.trim());
        });
        return allMessages;
    };

    $scope.goBack = function() {
        console.log('Going back to offered class search page.');
        $scope.showResults = false;
    };

    $scope.selectTestModels = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.formatModel = [{"format": "Hybrid", "isOrNot": "Is"}, {"format": "Face to Face", "isOrNot": "Not"}];
        $scope.statusModel = {"status": "Closed"};
        $scope.sessionModel = [{"session": 4, "sessionTitle": "Session 4"}, {
            "session": 2,
            "sessionTitle": "Session 2"
        }];
        $scope.subjectModel = [{"subject": "AFSC", "subjectFullName": "Air Force Science"}, {
            "subject": "ARAB",
            "subjectFullName": "Arabic"
        }];
        $scope.buildingModel = [{"buildingID": 517, "building": "A", "buildingName": "Cullen Performance Hall"}];
        $scope.locationModel = [{"location": "UH-Sugar Land"}, {"location": "UH-San Antonio-Conrad Hilton"}, {"location": "UH"}];
        $scope.componentModel = [{"component": "LEC", "componentName": "Lecture"}];
        $scope.creditHourModel = [{"credit-hours": 4}];
        $scope.weekendUModel = {"weekendU": "No"};
        $scope.coreCategoriesModel = [{"categoryNumber": 2, "categoryName": "Mathematics"}];
        $scope.isCoreModel = {"is-core": "No"};
        $scope.syllabusModel = {"syllabus": "No"};
        $scope.classDaysModel = [{"day": "Monday", "isOrNot": "Is"}, {
            "day": "Sunday",
            "isOrNot": "Is"
        }, {"day": "Tuesday", "isOrNot": "Not"}];
        $scope.courseNumberModel = {"courseNumber": 2410};
    };

    $scope.selectTestModelsTwo = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.weekendUModel = {"weekendU": "Yes"};

    };

    $scope.selectTestModelsThree = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.isCoreModel = {"is-core": "Yes"};
        $scope.formatModel = [{"format": "Online", "isOrNot": "Is"}];
    };

    $scope.selectTestModelsOneClassResult = function() {
        clearAllModels();
        $scope.termModel = {"term": 2000, "year": 2016, "semester": "Fall"};
        $scope.formatModel = [{"format": "Online", "isOrNot": "Is"}];
        $scope.sessionModel = [{"session": 6, "sessionTitle": "Session 6"}];
        $scope.subjectModel = [{"subject": "SOC", "subjectFullName": "Sociology"}];

    };

    var clearAllModels = function() {
        $scope.allJSONAndScopeNames.forEach(function(e) {
            $scope.deleteModel(e.modelName);
        });
    };

    $scope.getSyllabus = function(syllabus) {
        if(syllabus == 'Unavailable') {
            return "Syllabus Not Available.";
        }
        else {
            return "Syllabus";
        }
    };

    $scope.getClassDaysMessage = function(classDaysForAClass) {
        var allClassDays = $scope.classDays.dayAbbreviations;

        return allClassDays
                .filter(function(aClassDay) {
                    return classDaysForAClass[aClassDay.classDay];
                })
                .map(function(aClassDay) {
                    return aClassDay.classDayAbbreviation;
                })
                .join("") || "No Class Days";
    };

    $scope.getCoreCategoriesMessage = function(coreCategories) {
        return coreCategories
                .filter(function(aCoreCategory) {
                    return aCoreCategory.length != 0;
                })
                .map(function(aCoreCategory) {
                    return $scope.coreCategories.availableOptions[aCoreCategory - 1].categoryName;
                })
                .join(", ") || "Not a core class.";
    };

    $scope.displayDetailedOfferedClass = function(aClass) {
        $scope.displayDetailed = true;
        $scope.detailedClass = aClass;
        $scope.detailedOfferedClassMessage = aClass.classTitle;
    };

    $scope.exitDisplayDetailedOfferedClass = function() {
        $scope.displayDetailed = false;
        $scope.detailedClass = undefined;
    };

    $scope.getSeatInformationMessage = function(status, seatInformation) {
        return status + " (" + seatInformation.seatsTaken + "/" +
            seatInformation.seatsTotal + ")";
    };

    $scope.getMeetingTimesMessage = function(startTime, endTime) {
        if(startTime == "" && endTime == "") {
            return "Class does not meet in person.";
        }
        else {
            var newStartTime = convertClassTimeToAMPM(startTime);
            var newEndTime = convertClassTimeToAMPM(endTime);
            return newStartTime + " - " + newEndTime;
        }
    };

    var convertClassTimeToAMPM = function(time) {
        var hours = parseInt(time.substr(0, 2));
        var minutes = time.substr(3, 2);
        var isPM = false;

        if(hours - 12 >= 0) {
            isPM = true;
        }

        var baseTime = hours + ":" + minutes;

        return isPM ? baseTime + "P.M." : baseTime + "A.M.";
    };

    $scope.getMeetingDatesMessage = function(startDate, endDate) {
        return startDate.replace("-", "/") + " to " + endDate.replace("-", "/");
    };

}]);
<|endoftext|>


<commit_before>import Component from '@ember/component';
import { inject as service } from '@ember/service'
import { reads } from '@ember/object/computed';
import { get, computed } from '@ember/object';

const headers = [
  {
    translationKey: 'generic.state',
    name:           'alertState',
    searchField:    'alertState',
    sort:           ['alertState', 'name'],
    width:          '120'
  },
  {
    translationKey: 'generic.name',
    name:           'name',
    sort:           ['name', 'id'],
    searchField:    ['name', 'description', 'displayName'],
  },
  {
    translationKey: 'alertPage.index.table.target',
    name:           'target',
    searchField:    ['nodeName', 'resourceKind', 'displayTargetType'],
    sort:           ['nodeName', 'resourceKind', 'displayTargetType', 'name'],
  },
  {
    translationKey: 'alertPage.index.table.condition',
    name:           'displayCondition',
    sort:           ['displayCondition', 'id'],
    searchField:    ['displayCondition', 'name'],
  },
  {
    translationKey: 'alertPage.index.table.recipients',
    name:           'recipients',
    searchField:    ['recipient', 'firstRecipient'],
    sort:           ['displayRecipient', 'name'],
  },
];

export default Component.extend({
  scope:             service(),
  // input
  model:             null,
  sortBy:            'name',
  headers,
  showNode:          true,
  showStats:         false,
  showInstanceState: true,
  paging:            true,
  bulkActions:       true,
  search:            true,
  searchText:        null,

  clusterId: reads('scope.currentCluster.id'),
  projectId: reads('scope.currentProject.id'),
  pageScope: reads('scope.currentPageScope'),

  groupsWithoutAlerts: computed('groups.@each.{clusterId,projectId}', 'filteredAlerts', function() {
    const groups = get(this, 'groups') || [];
    const alerts = get(this, 'filteredAlerts') || [];

    return groups.filter((group) => !alerts.findBy('groupId', get(group, 'id'))).map((group) => {
      return { group: get(group, 'id') }
    });
  }),

  filteredNotifiers: computed('clusterId', 'notifiers.@each.{clusterId}', function() {
    const clusterId = get(this, 'clusterId');

    return get(this, 'notifiers').filterBy('clusterId', clusterId);
  }),

  filteredAlerts: computed('alerts.@each.{clusterId,projectId}', 'clusterId', 'projectId', 'pageScope', function() {
    const clusterId = get(this, 'clusterId');
    const projectId = get(this, 'projectId');
    const ps = get(this, 'pageScope');

    if (ps === 'cluster') {
      return get(this, 'alerts').filterBy('clusterId', clusterId);
    } else {
      return get(this, 'alerts').filterBy('projectId', projectId);
    }
  })
});
<commit_msg>Fix groups without alert issue<commit_after>import Component from '@ember/component';
import { inject as service } from '@ember/service'
import { reads } from '@ember/object/computed';
import { get, computed } from '@ember/object';

const headers = [
  {
    translationKey: 'generic.state',
    name:           'alertState',
    searchField:    'alertState',
    sort:           ['alertState', 'name'],
    width:          '120'
  },
  {
    translationKey: 'generic.name',
    name:           'name',
    sort:           ['name', 'id'],
    searchField:    ['name', 'description', 'displayName'],
  },
  {
    translationKey: 'alertPage.index.table.target',
    name:           'target',
    searchField:    ['nodeName', 'resourceKind', 'displayTargetType'],
    sort:           ['nodeName', 'resourceKind', 'displayTargetType', 'name'],
  },
  {
    translationKey: 'alertPage.index.table.condition',
    name:           'displayCondition',
    sort:           ['displayCondition', 'id'],
    searchField:    ['displayCondition', 'name'],
  },
  {
    translationKey: 'alertPage.index.table.recipients',
    name:           'recipients',
    searchField:    ['recipient', 'firstRecipient'],
    sort:           ['displayRecipient', 'name'],
  },
];

export default Component.extend({
  scope:             service(),
  // input
  model:             null,
  sortBy:            'name',
  headers,
  showNode:          true,
  showStats:         false,
  showInstanceState: true,
  paging:            true,
  bulkActions:       true,
  search:            true,
  searchText:        null,

  clusterId: reads('scope.currentCluster.id'),
  projectId: reads('scope.currentProject.id'),
  pageScope: reads('scope.currentPageScope'),

  groupsWithoutAlerts: computed('clusterId', 'projectId', 'groups.@each.{clusterId,projectId}', 'filteredAlerts', 'pageScope', function() {
    const ps = get(this, 'pageScope');
    const clusterId = get(this, 'clusterId');
    const projectId = get(this, 'projectId');
    let groups = get(this, 'groups') || [];
    const alerts = get(this, 'filteredAlerts') || [];

    if ( ps === 'cluster' ) {
      groups = groups.filterBy('clusterId', clusterId);
    } else {
      groups = groups.filterBy('projectId', projectId);
    }

    return groups.filter((group) => !alerts.findBy('groupId', get(group, 'id'))).map((group) => {
      return { group: get(group, 'id') }
    });
  }),

  filteredNotifiers: computed('clusterId', 'notifiers.@each.{clusterId}', function() {
    const clusterId = get(this, 'clusterId');

    return get(this, 'notifiers').filterBy('clusterId', clusterId);
  }),

  filteredAlerts: computed('alerts.@each.{clusterId,projectId}', 'clusterId', 'projectId', 'pageScope', function() {
    const clusterId = get(this, 'clusterId');
    const projectId = get(this, 'projectId');
    const ps = get(this, 'pageScope');

    if (ps === 'cluster') {
      return get(this, 'alerts').filterBy('clusterId', clusterId);
    } else {
      return get(this, 'alerts').filterBy('projectId', projectId);
    }
  })
});
<|endoftext|>


<commit_before>/*eslint-env browser*/
import React from "react";
import * as globals from "../../util/globals";
import {dataFont, infoPanelStyles} from "../../globalStyles";
import { prettyString } from "./tipSelectedPanel";

const InfoPanel = ({tree, hovered, responsive, viewer}) => {

  /* this is a function - we can bail early */
  if (!(tree && hovered)) {
    return null
  }

  /**
   * This maps tree coordinates to coordinates on the svg.
   * @param  {float} x
   * @param  {float}} y
   * @param  {reactSVGpanZoom} V viewer state
   * @return {[x,y]} point on plane
   */
  const treePosToViewer = (x,y, V) =>{
    const dx = (x*V.a + V.e); //this is the distance form the upper left corner
    const dy = (y*V.d + V.f); //at the current zoome level
    return {x: dx, y: dy};
  }

  /* I have no idea how these should acually be calculated, but apparently
  they are relative to the HTML body */
  const viewerState = viewer.getValue();
  const xOffset = 10;
  const yOffset = 10;

  const width = 200;
  const height = 200;
  const pos = treePosToViewer(hovered.d.xTip, hovered.d.yTip, viewerState);
  const left = pos.x + ((pos.x<viewerState.viewerWidth *0.7)? xOffset : (-width - 5*xOffset));
  const top =  pos.y + ((pos.y<viewerState.viewerHeight*0.7)? 5*yOffset : (-height-yOffset));

  const styles = {
    container: {
      position: "absolute",
      width,
      left,
      top,
      padding: "10px",
      borderRadius: 10,
      zIndex: 1000,
      pointerEvents: "none",
      fontFamily: infoPanelStyles.panel.fontFamily,
      fontSize: 14,
      lineHeight: 1,
      fontWeight: infoPanelStyles.panel.fontWeight,
      color: infoPanelStyles.panel.color,
      backgroundColor: infoPanelStyles.panel.backgroundColor,
      wordWrap: "break-word"
    }
  };

  /* getX methods return styled JSX */
  const getMutations = (d) => {
    let mutations = "";
    if ((typeof d.muts !== "undefined") && (globals.mutType === "nuc") && (d.muts.length)) {
      const numMutsToShow = 5;
      let tmp_muts = d.muts;
      const nmuts = tmp_muts.length;
      tmp_muts = tmp_muts.slice(0, Math.min(numMutsToShow, nmuts));
      mutations += tmp_muts.join(", ");
      if (nmuts > numMutsToShow) {
        mutations += " + " + (nmuts - numMutsToShow) + " more";
      }
    }
    return (
      mutations !== "" ?
      Mutations at {mutations} :
      No mutations
    );
  };
  const getFrequencies = (d) => {
    if (d.frequency !== undefined) {
      const disp = "Frequency: " + (100 * d.frequency).toFixed(1) + "%";
      return (
{disp}
);
    }
    return null;
  }

  const makeInfoPanel = () => {
    if (hovered.type === ".tip") {
      const tip = hovered.d;
      return (

              {tip.n.attr.strain}

              {prettyString(tip.n.attr.country)}, {prettyString(tip.n.attr.date)}

              Click on tip to display more info

      );
    } else { /* BRANCH */
      const branch = hovered.d;
      return (

              {`Branch with ${branch.n.fullTipCount} children`}

              {getFrequencies(branch.n)}
              {getMutations(branch.n)}

              Click on branch to zoom into this clade

      );
    }
  };

  return (
    makeInfoPanel()
  );
};

export default InfoPanel;
<commit_msg>uses left/right, top/bottom instead of width and height<commit_after>/*eslint-env browser*/
import React from "react";
import * as globals from "../../util/globals";
import {dataFont, infoPanelStyles} from "../../globalStyles";
import { prettyString } from "./tipSelectedPanel";

const InfoPanel = ({tree, hovered, responsive, viewer}) => {

  /* this is a function - we can bail early */
  if (!(tree && hovered)) {
    return null
  }

  /**
   * This maps tree coordinates to coordinates on the svg.
   * @param  {float} x
   * @param  {float}} y
   * @param  {reactSVGpanZoom} V viewer state
   * @return {[x,y]} point on plane
   */
  const treePosToViewer = (x,y, V) =>{
    const dx = (x*V.a + V.e); //this is the distance form the upper left corner
    const dy = (y*V.d + V.f); //at the current zoome level
    return {x: dx, y: dy};
  }

  /* I have no idea how these should acually be calculated, but apparently
  they are relative to the HTML body */
  const viewerState = viewer.getValue();
  const xOffset = 10;
  const yOffset = 10;

  const width = 200;
  const height = 200;
  const pos = treePosToViewer(hovered.d.xTip, hovered.d.yTip, viewerState);
  const left = pos.x + ((pos.x<viewerState.viewerWidth *0.7)? xOffset : (-width - 3*xOffset));
  const top =  pos.y + ((pos.y<viewerState.viewerHeight*0.7)? 3*yOffset : (-height-yOffset));

  const styles = {
    container: {
      position: "absolute",
      width,
      padding: "10px",
      borderRadius: 10,
      zIndex: 1000,
      pointerEvents: "none",
      fontFamily: infoPanelStyles.panel.fontFamily,
      fontSize: 14,
      lineHeight: 1,
      fontWeight: infoPanelStyles.panel.fontWeight,
      color: infoPanelStyles.panel.color,
      backgroundColor: infoPanelStyles.panel.backgroundColor,
      wordWrap: "break-word",
    }
  };
  if (pos.x<viewerState.viewerWidth*0.7){
    styles.container.left = pos.x + xOffset;
  }else{
    styles.container.right = viewerState.viewerWidth-pos.x + xOffset;
  }
  if (pos.y<viewerState.viewerHeight*0.7){
    styles.container.top = pos.y + 4*yOffset;
  }else{
    styles.container.bottom = viewerState.viewerHeight-pos.y+yOffset;
  }

  /* getX methods return styled JSX */
  const getMutations = (d) => {
    let mutations = "";
    if ((typeof d.muts !== "undefined") && (globals.mutType === "nuc") && (d.muts.length)) {
      const numMutsToShow = 5;
      let tmp_muts = d.muts;
      const nmuts = tmp_muts.length;
      tmp_muts = tmp_muts.slice(0, Math.min(numMutsToShow, nmuts));
      mutations += tmp_muts.join(", ");
      if (nmuts > numMutsToShow) {
        mutations += " + " + (nmuts - numMutsToShow) + " more";
      }
    }
    return (
      mutations !== "" ?
      Mutations at {mutations} :
      No mutations
    );
  };
  const getFrequencies = (d) => {
    if (d.frequency !== undefined) {
      const disp = "Frequency: " + (100 * d.frequency).toFixed(1) + "%";
      return (
{disp}
);
    }
    return null;
  }

  const makeInfoPanel = () => {
    if (hovered.type === ".tip") {
      const tip = hovered.d;
      return (

              {tip.n.attr.strain}

              {prettyString(tip.n.attr.country)}, {prettyString(tip.n.attr.date)}

              Click on tip to display more info

      );
    } else { /* BRANCH */
      const branch = hovered.d;
      return (

              {`Branch with ${branch.n.fullTipCount} children`}

              {getFrequencies(branch.n)}
              {getMutations(branch.n)}

              Click on branch to zoom into this clade

      );
    }
  };

  return (
    makeInfoPanel()
  );
};

export default InfoPanel;
<|endoftext|>


<commit_before>'use strict';

var queries = angular.module('cliffhanger.query_wizard', ['ngRoute']);

queries.controller('QueryWizardCtrl', function ($scope, $uibModalInstance, $log, datasets, queryService) {
    $scope.query = {}; //container for query
    $scope.step = 1; //which step in the modal is on
    $scope.maxSteps = 4; //number of steps in modal
    $scope.datasets = datasets;
    $scope.tags = []; //TODO load based upon datasets

    $scope.addJoinColumn = false; //whether to include joined column in select
    $scope.selectedDatasets = []; //datasets selected to be joined
    $scope.selectedTags = []; //selected join tags
    $scope.selectedColumns = []; //select columns for query

    //method responsible for handling changes due to checkboxes
    //d -> item selected/deselected
    //selections -> array to add/remove item
    $scope.change = function (d, selections) {
        if (d.selected) {
            selections.push(d);
        } else {
            for (var i = 0; i < selections.length; i++) {
                $log.debug(selections[i]);
                if (selections[i].name == d.name) {
                    selections.splice(i, 1);
                }
            }
        }
        $log.debug(selections);
    }

    //load the joinable tags only in the selected datasets
    $scope.loadTags = function () {
        for (var i = 0; i < $scope.selectedDatasets[0].tags.length; i++) {
            var tagA = $scope.selectedDatasets[0].tags[i];
            for (var j = 0; j < $scope.selectedDatasets[1].tags.length; j++) {
                var tagB = $scope.selectedDatasets[1].tags[j];
                //if tags match, aren't EMPTY and are not a duplicate
                if (tagA.name == tagB.name && tagA.name != '<EMPTY>' && $scope.tags.indexOf(tagA) == -1) {
                    $scope.tags.push(tagA);
                    break;
                }
            }
        }
        $log.debug($scope.tags);
    }

    //advance the modal to the next step
    $scope.next = function () {
        $scope.step++;
        if ($scope.step == 2) {
            $scope.loadTags();
        } else if ($scope.step == $scope.maxSteps) $scope.buildQuery();
    };

    //go back a step in the modal
    $scope.previous = function () {
        $scope.step--;
        if ($scope.step < $scope.maxSteps) $scope.progressType = null;
    };

    //dismiss the modal
    $scope.cancel = function () {
        $uibModalInstance.dismiss('cancel');
    };

    //complete the modal
    $scope.submit = function () {};

    $scope.addToQuery = function () {
        $scope.clearStatement();
        if ($scope.statement.where != "" && $scope.statement.limit != "") {
            $scope.statement.text = "\n" + "WHERE " + $scope.statement.where + "\n" + "LIMIT " + $scope.statement.limit;
        } else if ($scope.statement.where != "" && $scope.statement.limit == "") {
            $scope.statement.text = "WHERE " + $scope.statement.where;
        } else if ($scope.statement.where == "" && $scope.statement.limit != "") {
            $scope.statement.text = "LIMIT " + $scope.statement.limit;
        } else if ($scope.statement.where != "" && $scope.statement.limit != "") {
            $scope.statement.text = "";
        }
        $log.debug($scope.statement);
    };

    $scope.buildQuery = function () {
        var queryInput = {
            datasets: $scope.selectedDatasets,
            joinTag: $scope.selectedTags,
            addJoinColumn: $scope.addJoinColumn,
            columns: $scope.selectedColumns
        }

        queryService.buildQuery(queryInput)
            .then(function (response) {
                if (data.status == 'Success') {
                    $scope.query = response.data;
                    $scope.progressType = 'success';
                } else {
                    $scope.progressType = 'danger'
                    $scope.buildQueryError = true;
                    $log.error(response.data);

                }
            }, function (data) {
                $scope.progressType = 'danger'
                $scope.buildQueryError = true;
                $log.error('Failed to connect to server');
            })
    }

});<commit_msg>nothing<commit_after>'use strict';

var queries = angular.module('cliffhanger.query_wizard', ['ngRoute']);

queries.controller('QueryWizardCtrl', function ($scope, $uibModalInstance, $log, datasets, queryService) {
    $scope.query = {}; //container for query
    $scope.step = 1; //which step in the modal is on
    $scope.maxSteps = 4; //number of steps in modal
    $scope.datasets = datasets;
    $scope.tags = []; //TODO load based upon datasets

    $scope.addJoinColumn = false; //whether to include joined column in select
    $scope.selectedDatasets = []; //datasets selected to be joined
    $scope.selectedTags = []; //selected join tags
    $scope.selectedColumns = []; //select columns for query

    //method responsible for handling changes due to checkboxes
    //d -> item selected/deselected
    //selections -> array to add/remove item
    $scope.change = function (d, selections) {
        if (d.selected) {
            selections.push(d);
        } else {
            for (var i = 0; i < selections.length; i++) {
                $log.debug(selections[i]);
                if (selections[i].name == d.name) {
                    selections.splice(i, 1);
                }
            }
        }
        $log.debug(selections);
    }

    //load the joinable tags only in the selected datasets
    $scope.loadTags = function () {
        for (var i = 0; i < $scope.selectedDatasets[0].tags.length; i++) {
            var tagA = $scope.selectedDatasets[0].tags[i];
            for (var j = 0; j < $scope.selectedDatasets[1].tags.length; j++) {
                var tagB = $scope.selectedDatasets[1].tags[j];
                //if tags match, aren't EMPTY and are not a duplicate
                if (tagA.name == tagB.name && tagA.name != '<EMPTY>' && $scope.tags.indexOf(tagA) == -1) {
                    $scope.tags.push(tagA);
                    break;
                }
            }
        }
        $log.debug($scope.tags);
    }

    //advance the modal to the next step
    $scope.next = function () {
        $scope.step++;
        if ($scope.step == 2) {
            $scope.loadTags();
        } else if ($scope.step == $scope.maxSteps) $scope.buildQuery();
    };

    //go back a step in the modal
    $scope.previous = function () {
        $scope.step--;
        if ($scope.step < $scope.maxSteps) $scope.progressType = null;
    };

    //dismiss the modal
    $scope.cancel = function () {
        $uibModalInstance.dismiss('cancel');
    };

    //complete the modal
    $scope.submit = function () {};

    $scope.addToQuery = function () {
        $scope.query = $scope.query.replace(";", "");
        if ($scope.statement.where != "" && $scope.statement.limit != "") {
            $scope.statement.text = "\nWHERE " + $scope.statement.where + "\n" + "LIMIT " + $scope.statement.limit + ";";
        } else if ($scope.statement.where != "" && $scope.statement.limit == "") {
            $scope.statement.text = "\nWHERE " + $scope.statement.where + ";";
        } else if ($scope.statement.where == "" && $scope.statement.limit != "") {
            $scope.statement.text = "\nLIMIT " + $scope.statement.limit + ";";
        } else if ($scope.statement.where != "" && $scope.statement.limit != "") {
            $scope.statement.text = ";";
        }
        $log.debug($scope.statement);
    };

    $scope.buildQuery = function () {
        var queryInput = {
            datasets: $scope.selectedDatasets,
            joinTag: $scope.selectedTags,
            addJoinColumn: $scope.addJoinColumn,
            columns: $scope.selectedColumns
        }

        queryService.buildQuery(queryInput)
            .then(function (response) {
                if (response.status == 'Success') {
                    $scope.query = response.data;
                    $scope.progressType = 'success';
                } else {
                    $scope.progressType = 'danger'
                    $scope.buildQueryError = true;
                    $log.error(response.data);

                }
            }, function (data) {
                $scope.progressType = 'danger'
                $scope.buildQueryError = true;
                $log.error('Failed to connect to server');
            })
    }

});<|endoftext|>