text: /**
   * @license
   * Copyright 2016 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  /**
   * @fileoverview Stores and updates information about state and categories
   * in workspace factory. Each list element is either a separator or a category,
   * and each category stores its name, XML to load that category, color,
   * custom tags, and a unique ID making it possible to change category names and
   * move categories easily. Keeps track of the currently selected list
   * element. Also keeps track of all the user-created shadow blocks and
   * manipulates them as necessary.
   *
   * @author Emma Dauterman (evd2014)
   */
  
  /**
   * Class for a WorkspaceFactoryModel
   * @constructor
   */
  WorkspaceFactoryModel = function() {
    // Ordered list of ListElement objects. Empty if there is a single flyout.
    this.toolboxList = [];
    // ListElement for blocks in a single flyout. Null if a toolbox exists.
    this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
    // Array of block IDs for all user created shadow blocks.
    this.shadowBlocks = [];
    // Reference to currently selected ListElement. Stored in this.toolboxList if
    // there are categories, or in this.flyout if blocks are displayed in a single
    // flyout.
    this.selected = this.flyout;
    // Boolean for if a Variable category has been added.
    this.hasVariableCategory = false;
    // Boolean for if a Procedure category has been added.
    this.hasProcedureCategory = false;
    // XML to be pre-loaded to workspace. Empty on default;
    this.preloadXml = Blockly.utils.xml.createElement('xml');
    // Options object to be configured for Blockly inject call.
    this.options = new Object(null);
    // Block Library block types.
    this.libBlockTypes = [];
    // Imported block types.
    this.importedBlockTypes = [];
    //
  };
  
  /**
   * Given a name, determines if it is the name of a category already present.
   * Used when getting a valid category name from the user.
   * @param {string} name String name to be compared against.
   * @return {boolean} True if string is a used category name, false otherwise.
   */
  WorkspaceFactoryModel.prototype.hasCategoryByName = function(name) {
    for (var i = 0; i < this.toolboxList.length; i++) {
      if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY &&
          this.toolboxList[i].name == name) {
        return true;
      }
    }
    return false;
  };
  
  /**
   * Determines if a category with the 'VARIABLE' tag exists.
   * @return {boolean} True if there exists a category with the Variables tag,
   * false otherwise.
   */
  WorkspaceFactoryModel.prototype.hasVariables = function() {
    return this.hasVariableCategory;
  };
  
  /**
   * Determines if a category with the 'PROCEDURE' tag exists.
   * @return {boolean} True if there exists a category with the Procedures tag,
   * false otherwise.
   */
  WorkspaceFactoryModel.prototype.hasProcedures = function() {
    return this.hasProcedureCategory;
  };
  
  /**
   * Determines if the user has any elements in the toolbox. Uses the length of
   * toolboxList.
   * @return {boolean} True if elements exist, false otherwise.
   */
  WorkspaceFactoryModel.prototype.hasElements = function() {
    return this.toolboxList.length > 0;
  };
  
  /**
   * Given a ListElement, adds it to the toolbox list.
   * @param {!ListElement} element The element to be added to the list.
   */
  WorkspaceFactoryModel.prototype.addElementToList = function(element) {
    // Update state if the copied category has a custom tag.
    this.hasVariableCategory = element.custom == 'VARIABLE' ? true :
        this.hasVariableCategory;
    this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true :
        this.hasProcedureCategory;
    // Add element to toolboxList.
    this.toolboxList.push(element);
    // Empty single flyout.
    this.flyout = null;
  };
  
  /**
   * Given an index, deletes a list element and all associated data.
   * @param {number} index The index of the list element to delete.
   */
  WorkspaceFactoryModel.prototype.deleteElementFromList = function(index) {
    // Check if index is out of bounds.
    if (index < 0 || index >= this.toolboxList.length) {
      return; // No entry to delete.
    }
    // Check if need to update flags.
    this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ?
        false : this.hasVariableCategory;
    this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ?
        false : this.hasProcedureCategory;
    // Remove element.
    this.toolboxList.splice(index, 1);
  };
  
  /**
   * Sets selected to be an empty category not in toolbox list if toolbox list
   * is empty. Should be called when removing the last element from toolbox list.
   * If the toolbox list is empty, selected stores the XML for the single flyout
   * of blocks displayed.
   */
  WorkspaceFactoryModel.prototype.createDefaultSelectedIfEmpty = function() {
    if (this.toolboxList.length == 0) {
      this.flyout = new ListElement(ListElement.TYPE_FLYOUT);
      this.selected = this.flyout;
    }
  };
  
  /**
   * Moves a list element to a certain position in toolboxList by removing it
   * and then inserting it at the correct index. Checks that indices are in
   * bounds (throws error if not), but assumes that oldIndex is the correct index
   * for list element.
   * @param {!ListElement} element The element to move in toolboxList.
   * @param {number} newIndex The index to insert the element at.
   * @param {number} oldIndex The index the element is currently at.
   */
  WorkspaceFactoryModel.prototype.moveElementToIndex = function(element, newIndex,
      oldIndex) {
    // Check that indexes are in bounds.
    if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 ||
        oldIndex >= this.toolboxList.length) {
      throw Error('Index out of bounds when moving element in the model.');
    }
    this.deleteElementFromList(oldIndex);
    this.toolboxList.splice(newIndex, 0, element);
  };
  
  /**
   * Returns the ID of the currently selected element. Returns null if there are
   * no categories (if selected == null).
   * @return {string} The ID of the element currently selected.
   */
  WorkspaceFactoryModel.prototype.getSelectedId = function() {
    return this.selected ? this.selected.id : null;
  };
  
  /**
   * Returns the name of the currently selected category. Returns null if there
   * are no categories (if selected == null) or the selected element is not
   * a category (in which case its name is null).
   * @return {string} The name of the category currently selected.
   */
  WorkspaceFactoryModel.prototype.getSelectedName = function() {
    return this.selected ? this.selected.name : null;
  };
  
  /**
   * Returns the currently selected list element object.
   * @return {ListElement} The currently selected ListElement
   */
  WorkspaceFactoryModel.prototype.getSelected = function() {
    return this.selected;
  };
  
  /**
   * Sets list element currently selected by id.
   * @param {string} id ID of list element that should now be selected.
   */
  WorkspaceFactoryModel.prototype.setSelectedById = function(id) {
    this.selected = this.getElementById(id);
  };
  
  /**
   * Given an ID of a list element, returns the index of that list element in
   * toolboxList. Returns -1 if ID is not present.
   * @param {string} id The ID of list element to search for.
   * @return {number} The index of the list element in toolboxList, or -1 if it
   * doesn't exist.
   */
  WorkspaceFactoryModel.prototype.getIndexByElementId = function(id) {
    for (var i = 0; i < this.toolboxList.length; i++) {
      if (this.toolboxList[i].id == id) {
        return i;
      }
    }
    return -1;  // ID not present in toolboxList.
  };
  
  /**
   * Given the ID of a list element, returns that ListElement object.
   * @param {string} id The ID of element to search for.
   * @return {ListElement} Corresponding ListElement object in toolboxList, or
   *     null if that element does not exist.
   */
  WorkspaceFactoryModel.prototype.getElementById = function(id) {
    for (var i = 0; i < this.toolboxList.length; i++) {
      if (this.toolboxList[i].id == id) {
        return this.toolboxList[i];
      }
    }
    return null;  // ID not present in toolboxList.
  };
  
  /**
   * Given the index of a list element in toolboxList, returns that ListElement
   * object.
   * @param {number} index The index of the element to return.
   * @return {ListElement} The corresponding ListElement object in toolboxList.
   */
  WorkspaceFactoryModel.prototype.getElementByIndex = function(index) {
    if (index < 0 || index >= this.toolboxList.length) {
      return null;
    }
    return this.toolboxList[index];
  };
  
  /**
   * Returns the XML to load the selected element.
   * @return {!Element} The XML of the selected element, or null if there is
   * no selected element.
   */
  WorkspaceFactoryModel.prototype.getSelectedXml = function() {
    return this.selected ? this.selected.xml : null;
  };
  
  /**
   * Return ordered list of ListElement objects.
   * @return {!Array.<!ListElement>} ordered list of ListElement objects
   */
  WorkspaceFactoryModel.prototype.getToolboxList = function() {
    return this.toolboxList;
  };
  
  /**
   * Gets the ID of a category given its name.
   * @param {string} name Name of category.
   * @return {number} ID of category
   */
  WorkspaceFactoryModel.prototype.getCategoryIdByName = function(name) {
    for (var i = 0; i < this.toolboxList.length; i++) {
      if (this.toolboxList[i].name == name) {
        return this.toolboxList[i].id;
      }
    }
    return null;  // Name not present in toolboxList.
  };
  
  /**
   * Clears the toolbox list, deleting all ListElements.
   */
  WorkspaceFactoryModel.prototype.clearToolboxList = function() {
    this.toolboxList = [];
    this.hasVariableCategory = false;
    this.hasProcedureCategory = false;
    this.shadowBlocks = [];
    this.selected.xml = Blockly.utils.xml.createElement('xml');
  };
  
  /**
   * Class for a ListElement
   * Adds a shadow block to the list of shadow blocks.
   * @param {string} blockId The unique ID of block to be added.
   */
  WorkspaceFactoryModel.prototype.addShadowBlock = function(blockId) {
    this.shadowBlocks.push(blockId);
  };
  
  /**
   * Removes a shadow block ID from the list of shadow block IDs if that ID is
   * in the list.
   * @param {string} blockId The unique ID of block to be removed.
   */
  WorkspaceFactoryModel.prototype.removeShadowBlock = function(blockId) {
    for (var i = 0; i < this.shadowBlocks.length; i++) {
      if (this.shadowBlocks[i] == blockId) {
        this.shadowBlocks.splice(i, 1);
        return;
      }
    }
  };
  
  /**
   * Determines if a block is a shadow block given a unique block ID.
   * @param {string} blockId The unique ID of the block to examine.
   * @return {boolean} True if the block is a user-generated shadow block, false
   *    otherwise.
   */
  WorkspaceFactoryModel.prototype.isShadowBlock = function(blockId) {
    for (var i = 0; i < this.shadowBlocks.length; i++) {
      if (this.shadowBlocks[i] == blockId) {
        return true;
      }
    }
    return false;
  };
  
  /**
   * Given a set of blocks currently loaded, returns all blocks in the workspace
   * that are user generated shadow blocks.
   * @param {!<Blockly.Block>} blocks Array of blocks currently loaded.
   * @return {!<Blockly.Block>} Array of user-generated shadow blocks currently
   *   loaded.
   */
  WorkspaceFactoryModel.prototype.getShadowBlocksInWorkspace =
      function(workspaceBlocks) {
    var shadowsInWorkspace = [];
    for (var i = 0; i < workspaceBlocks.length; i++) {
      if (this.isShadowBlock(workspaceBlocks[i].id)) {
        shadowsInWorkspace.push(workspaceBlocks[i]);
      }
    }
    return shadowsInWorkspace;
  };
  
  /**
   * Adds a custom tag to a category, updating state variables accordingly.
   * Only accepts 'VARIABLE' and 'PROCEDURE' tags.
   * @param {!ListElement} category The category to add the tag to.
   * @param {string} tag The custom tag to add to the category.
   */
  WorkspaceFactoryModel.prototype.addCustomTag = function(category, tag) {
    // Only update list elements that are categories.
    if (category.type != ListElement.TYPE_CATEGORY) {
      return;
    }
    // Only update the tag to be 'VARIABLE' or 'PROCEDURE'.
    if (tag == 'VARIABLE') {
      this.hasVariableCategory = true;
      category.custom = 'VARIABLE';
    } else if (tag == 'PROCEDURE') {
      this.hasProcedureCategory = true;
      category.custom = 'PROCEDURE';
    }
  };
  
  /**
   * Have basic pre-loaded workspace working
   * Saves XML as XML to be pre-loaded into the workspace.
   * @param {!Element} xml The XML to be saved.
   */
  WorkspaceFactoryModel.prototype.savePreloadXml = function(xml) {
    this.preloadXml = xml
  };
  
  /**
   * Gets the XML to be pre-loaded into the workspace.
   * @return {!Element} The XML for the workspace.
   */
  WorkspaceFactoryModel.prototype.getPreloadXml = function() {
    return this.preloadXml;
  };
  
  /**
   * Sets a new options object for injecting a Blockly workspace.
   * @param {Object} options Options object for injecting a Blockly workspace.
   */
  WorkspaceFactoryModel.prototype.setOptions = function(options) {
    this.options = options;
  };
  
  /**
   * Returns an array of all the block types currently being used in the toolbox
   * and the pre-loaded blocks. No duplicates.
   * TODO(evd2014): Move pushBlockTypesToList to FactoryUtils.
   * @return {!Array.<string>} Array of block types currently being used.
   */
  WorkspaceFactoryModel.prototype.getAllUsedBlockTypes = function() {
    var blockTypeList = [];
  
    // Given XML for the workspace, adds all block types included in the XML
    // to the list, not including duplicates.
    var pushBlockTypesToList = function(xml, list) {
      // Get all block XML nodes.
      var blocks = xml.getElementsByTagName('block');
  
      // Add block types if not already in list.
      for (var i = 0; i < blocks.length; i++) {
        var type = blocks[i].getAttribute('type');
        if (list.indexOf(type) == -1) {
          list.push(type);
        }
      }
    };
  
    if (this.flyout) {
      // If has a single flyout, add block types for the single flyout.
      pushBlockTypesToList(this.getSelectedXml(), blockTypeList);
    } else {
      // If has categories, add block types for each category.
  
      for (var i = 0, category; category = this.toolboxList[i]; i++) {
        if (category.type == ListElement.TYPE_CATEGORY) {
          pushBlockTypesToList(category.xml, blockTypeList);
        }
      }
    }
  
    // Add the block types from any pre-loaded blocks.
    pushBlockTypesToList(this.getPreloadXml(), blockTypeList);
  
    return blockTypeList;
  };
  
  /**
   * Adds new imported block types to the list of current imported block types.
   * @param {!Array.<string>} blockTypes Array of block types imported.
   */
  WorkspaceFactoryModel.prototype.addImportedBlockTypes = function(blockTypes) {
    this.importedBlockTypes = this.importedBlockTypes.concat(blockTypes);
  };
  
  /**
   * Updates block types in block library.
   * @param {!Array.<string>} blockTypes Array of block types in block library.
   */
  WorkspaceFactoryModel.prototype.updateLibBlockTypes = function(blockTypes) {
    this.libBlockTypes = blockTypes;
  };
  
  /**
   * Determines if a block type is defined as a standard block, in the block
   * library, or as an imported block.
   * @param {string} blockType Block type to check.
   * @return {boolean} True if blockType is defined, false otherwise.
   */
  WorkspaceFactoryModel.prototype.isDefinedBlockType = function(blockType) {
    var isStandardBlock = StandardCategories.coreBlockTypes.indexOf(blockType)
        != -1;
    var isLibBlock = this.libBlockTypes.indexOf(blockType) != -1;
    var isImportedBlock = this.importedBlockTypes.indexOf(blockType) != -1;
    return (isStandardBlock || isLibBlock || isImportedBlock);
  };
  
  /**
   * Checks if any of the block types are already defined.
   * @param {!Array.<string>} blockTypes Array of block types.
   * @return {boolean} True if a block type in the array is already defined,
   *    false if none of the blocks are already defined.
   */
  WorkspaceFactoryModel.prototype.hasDefinedBlockTypes = function(blockTypes) {
    for (var i = 0, blockType; blockType = blockTypes[i]; i++) {
      if (this.isDefinedBlockType(blockType)) {
        return true;
      }
    }
    return false;
  };
  
  /**
   * Class for a ListElement.
   * @constructor
   */
  ListElement = function(type, opt_name) {
    this.type = type;
    // XML DOM element to load the element.
    this.xml = Blockly.utils.xml.createElement('xml');
    // Name of category. Can be changed by user. Null if separator.
    this.name = opt_name ? opt_name : null;
    // Unique ID of element. Does not change.
    this.id = Blockly.utils.genUid();
    // Color of category. Default is no color. Null if separator.
    this.color = null;
    // Stores a custom tag, if necessary. Null if no custom tag or separator.
    this.custom = null;
  };
  
  // List element types.
  ListElement.TYPE_CATEGORY = 'category';
  ListElement.TYPE_SEPARATOR = 'separator';
  ListElement.TYPE_FLYOUT = 'flyout';
  
  /**
   * Saves a category by updating its XML (does not save XML for
   * elements that are not categories).
   * @param {!Blockly.workspace} workspace The workspace to save category entry
   * from.
   */
  ListElement.prototype.saveFromWorkspace = function(workspace) {
    // Only save XML for categories and flyouts.
    if (this.type == ListElement.TYPE_FLYOUT ||
        this.type == ListElement.TYPE_CATEGORY) {
      this.xml = Blockly.Xml.workspaceToDom(workspace);
    }
  };
  
  
  /**
   * Changes the name of a category object given a new name. Returns if
   * not a category.
   * @param {string} name New name of category.
   */
  ListElement.prototype.changeName = function (name) {
    // Only update list elements that are categories.
    if (this.type != ListElement.TYPE_CATEGORY) {
      return;
    }
    this.name = name;
  };
  
  /**
   * Sets the color of a category. If tries to set the color of something other
   * than a category, returns.
   * @param {?string} color The color that should be used for that category,
   *     or null if none.
   */
  ListElement.prototype.changeColor = function (color) {
    if (this.type != ListElement.TYPE_CATEGORY) {
      return;
    }
    this.color = color;
  };
  
  /**
   * Makes a copy of the original element and returns it. Everything about the
   * copy is identical except for its ID.
   * @return {!ListElement} The copy of the ListElement.
   */
  ListElement.prototype.copy = function() {
    copy = new ListElement(this.type);
    // Generate a unique ID for the element.
    copy.id = Blockly.utils.genUid();
    // Copy all attributes except ID.
    copy.name = this.name;
    copy.xml = this.xml;
    copy.color = this.color;
    copy.custom = this.custom;
    // Return copy.
    return copy;
  };
  