text: /**
   * @license
   * Copyright 2020 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  /**
   * @fileoverview Flyout that supports always-open continuous scrolling.
   */
  
  import * as Blockly from 'blockly/core';
  import {ContinuousToolbox} from './ContinuousToolbox';
  import {ContinuousFlyoutMetrics} from './ContinuousMetricsFlyout';
  
  /**
   * Class for continuous flyout.
   */
  export class ContinuousFlyout extends Blockly.VerticalFlyout {
    /** @override */
    constructor(workspaceOptions) {
      super(workspaceOptions);
  
      /**
       * List of scroll positions for each category.
       * @type {!Array<{name: string, position: !Object}>}
       */
      this.scrollPositions = [];
  
      /**
       * Target scroll position, used to smoothly scroll to a given category
       * location when selected.
       * @type {?number}
       */
      this.scrollTarget = null;
  
      /**
       * The percentage of the distance to the scrollTarget that should be
       * scrolled at a time. Lower values will produce a smoother, slower scroll.
       * @type {number}
       */
      this.scrollAnimationFraction = 0.3;
  
      /**
       * Whether to recycle blocks when refreshing the flyout. When false, do not
       * allow anything to be recycled. The default is to recycle.
       * @type {boolean}
       * @private
       */
      this.recyclingEnabled_ = true;
  
      this.workspace_.setMetricsManager(
          new ContinuousFlyoutMetrics(this.workspace_, this));
  
      this.autoClose = false;
    }
  
    /**
     * Gets parent toolbox.
     * Since we registered the ContinuousToolbox, we know that's its type.
     * @return {!ContinuousToolbox} Toolbox that owns this flyout.
     * @private
     */
    getParentToolbox_() {
      const toolbox = this.targetWorkspace.getToolbox();
      return /** @type {!ContinuousToolbox} */ (toolbox);
    }
  
    /**
     * Records scroll position for each category in the toolbox.
     * The scroll position is determined by the coordinates of each category's
     * label after the entire flyout has been rendered.
     * @package
     */
    recordScrollPositions() {
      this.scrollPositions = [];
      const categoryLabels = this.buttons_.filter((button) => button.isLabel() &&
          this.getParentToolbox_().getCategoryByName(button.getButtonText()));
      for (const button of categoryLabels) {
        if (button.isLabel()) {
          this.scrollPositions.push({
            name: button.getButtonText(),
            position: button.getPosition(),
          });
        }
      }
    }
  
    /**
     * Returns the scroll position for the given category name.
     * @param {string} name Category name.
     * @return {?Object} Scroll position for given category, or null if not found.
     * @package
     */
    getCategoryScrollPosition(name) {
      for (const scrollInfo of this.scrollPositions) {
        if (scrollInfo.name === name) {
          return scrollInfo.position;
        }
      }
      console.warn(`Scroll position not recorded for category ${name}`);
      return null;
    }
  
    /**
     * Selects an item in the toolbox based on the scroll position of the flyout.
     * @param {number} position Current scroll position of the workspace.
     * @private
     */
    selectCategoryByScrollPosition_(position) {
      // If we are currently auto-scrolling, due to selecting a category by
      // clicking on it, do not update the category selection.
      if (this.scrollTarget) {
        return;
      }
      const scaledPosition = Math.round(position / this.workspace_.scale);
      // Traverse the array of scroll positions in reverse, so we can select the
      // furthest category that the scroll position is beyond.
      for (let i = this.scrollPositions.length - 1; i >= 0; i--) {
        const category = this.scrollPositions[i];
        if (scaledPosition >= category.position.y) {
          this.getParentToolbox_().selectCategoryByName(category.name);
          return;
        }
      }
    }
  
    /**
     * Scrolls flyout to given position.
     * @param {number} position The x coordinate to scroll to.
     */
    scrollTo(position) {
      // Set the scroll target to either the scaled position or the lowest
      // possible scroll point, whichever is smaller.
      const metrics = this.workspace_.getMetrics();
      this.scrollTarget = Math.min(position * this.workspace_.scale,
          metrics.scrollHeight - metrics.viewHeight);
  
      this.stepScrollAnimation_();
    }
  
    /**
     * Step the scrolling animation by scrolling a fraction of the way to
     * a scroll target, and request the next frame if necessary.
     * @private
     */
    stepScrollAnimation_() {
      if (!this.scrollTarget) {
        return;
      }
  
      const currentScrollPos = -this.workspace_.scrollY;
      const diff = this.scrollTarget - currentScrollPos;
      if (Math.abs(diff) < 1) {
        this.workspace_.scrollbar.setY(this.scrollTarget);
        this.scrollTarget = null;
        return;
      }
      this.workspace_.scrollbar.setY(
          currentScrollPos + diff * this.scrollAnimationFraction);
  
      requestAnimationFrame(this.stepScrollAnimation_.bind(this));
    }
  
    /**
     * Add additional padding to the bottom of the flyout if needed,
     * in order to make it possible to scroll to the top of the last category.
     * @param {!Blockly.MetricsManager.ContainerRegion} contentMetrics Content
     *    metrics for the flyout.
     * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics View metrics
     *    for the flyout.
     * @return {number} Additional bottom padding.
     */
    calculateBottomPadding(contentMetrics, viewMetrics) {
      if (this.scrollPositions.length > 0) {
        const lastCategory =
            this.scrollPositions[this.scrollPositions.length - 1];
        const lastPosition = lastCategory.position.y * this.workspace_.scale;
        const lastCategoryHeight = contentMetrics.height - lastPosition;
        if (lastCategoryHeight < viewMetrics.height) {
          return viewMetrics.height - lastCategoryHeight;
        }
      }
      return 0;
    }
  
    /** @override */
    setMetrics_(xyRatio) {
      super.setMetrics_(xyRatio);
      if (this.scrollPositions) {
        this.selectCategoryByScrollPosition_(-this.workspace_.scrollY);
      }
    }
  
    /**
     * Overrides the position function solely to change the x coord in RTL mode.
     * The base function allows the workspace to go "under" the flyout, so
     * to calculate the left edge of the flyout in RTL you would just subtract
     * the flyout width from the total viewWidth to get x. However, in our
     * flyout, the workspace already starts at the left edge of the flyout, so
     * we don't need to subtract the flyout width again.
     * Ideally there would be a smaller method for us to override instead,
     * but for now we copy/paste this method and make our fixes.
     * @override
     */
    position() {
      if (!this.isVisible()) {
        return;
      }
      const targetWorkspaceMetrics = this.targetWorkspace.getMetrics();
      if (!targetWorkspaceMetrics) {
        // Hidden components will return null.
        return;
      }
      // Record the height for Blockly.Flyout.getMetrics_
      this.height_ = targetWorkspaceMetrics.viewHeight;
  
      const edgeWidth = this.width_ - this.CORNER_RADIUS;
      const edgeHeight =
          targetWorkspaceMetrics.viewHeight - 2 * this.CORNER_RADIUS;
      this.setBackgroundPath_(edgeWidth, edgeHeight);
  
      // Y is always 0 since this is a vertical flyout.
      const y = 0;
      let x = 0;
      // If this flyout is the toolbox flyout.
      if (this.targetWorkspace.toolboxPosition == this.toolboxPosition_) {
        // If there is a category toolbox.
        if (targetWorkspaceMetrics.toolboxWidth) {
          if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
            x = targetWorkspaceMetrics.toolboxWidth;
          } else {
            // TODO(https://github.com/google/blockly/issues/4396): Use a better
            // API to adjust this value.
            // This is the only line that changed from the original.
            x = targetWorkspaceMetrics.viewWidth;
          }
        } else {
          if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
            x = 0;
          } else {
            x = targetWorkspaceMetrics.viewWidth;
          }
        }
      } else {
        if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
          x = 0;
        } else {
          // Because the anchor point of the flyout is on the left, but we want
          // to align the right edge of the flyout with the right edge of the
          // blocklyDiv, we calculate the full width of the div minus the width
          // of the flyout.
          x = targetWorkspaceMetrics.viewWidth +
              targetWorkspaceMetrics.absoluteLeft - this.width_;
        }
      }
      this.positionAt_(this.width_, this.height_, x, y);
    }
  
    /**
     * @override
     */
    show(flyoutDef) {
      super.show(flyoutDef);
      this.recordScrollPositions();
      this.workspace_.resizeContents();
    }
  
    /**
     * Determine if this block can be recycled in the flyout.  Blocks that have no
     * variables and are not dynamic shadows can be recycled.
     * @param {!Blockly.BlockSvg} block The block to attempt to recycle.
     * @return {boolean} True if the block can be recycled.
     * @protected
     */
    blockIsRecyclable_(block) {
      if (!this.recyclingEnabled_) {
        return false;
      }
  
      // If the block needs to parse mutations, never recycle.
      if (block.mutationToDom && block.domToMutation) {
        return false;
      }
  
      for (const input of block.inputList) {
        for (const field of input.fieldRow) {
          // No variables.
          if (field instanceof Blockly.FieldVariable) {
            return false;
          }
          if (field instanceof Blockly.FieldDropdown) {
            if (field.isOptionListDynamic()) {
              return false;
            }
          }
        }
        // Check children.
        if (input.connection) {
          const targetBlock = /** @type {Blockly.BlockSvg} */
              (input.connection.targetBlock());
          if (targetBlock && !this.blockIsRecyclable_(targetBlock)) {
            return false;
          }
        }
      }
      return true;
    }
  
    /**
     * Sets the function used to determine whether a block is recyclable.
     * @param {function(!Blockly.BlockSvg):boolean} func The function used to
     *     determine if a block is recyclable.
     * @public
     */
    setBlockIsRecyclable(func) {
      this.blockIsRecyclable_ = func;
    }
  
    /**
     * Set whether the flyout can recycle blocks.
     * @param {boolean} isEnabled True to allow blocks to be recycled, false
     *     otherwise.
     * @public
     */
    setRecyclingEnabled(isEnabled) {
      this.recyclingEnabled_ = isEnabled;
    }
  }
  