Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/devtools/client/shared/components/menu/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 13 kB image not shown  

Quelle  MenuButton.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */


/* eslint-env browser */
"use strict";

// A button that toggles a doorhanger menu.

const flags = require("resource://devtools/shared/flags.js");
const {
  createRef,
  PureComponent,
} = require("resource://devtools/client/shared/vendor/react.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const { button } = dom;

const isMacOS = Services.appinfo.OS === "Darwin";

loader.lazyRequireGetter(
  this,
  "HTMLTooltip",
  "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
  true
);

loader.lazyRequireGetter(
  this,
  "focusableSelector",
  "resource://devtools/client/shared/focus.js",
  true
);

loader.lazyRequireGetter(
  this,
  "createPortal",
  "resource://devtools/client/shared/vendor/react-dom.js",
  true
);

// Return a copy of |obj| minus |fields|.
const omit = (obj, fields) => {
  const objCopy = { ...obj };
  for (const field of fields) {
    delete objCopy[field];
  }
  return objCopy;
};

class MenuButton extends PureComponent {
  static get propTypes() {
    return {
      // The toolbox document that will be used for rendering the menu popup.
      toolboxDoc: PropTypes.object.isRequired,

      // A text content for the button.
      label: PropTypes.string,

      // Optional, either:
      // - false or missing if no icon should be displayed
      // - true if an icon should be displayed and is set via CSS
      // - a string set to the URL of the icon to associate with the MenuButton
      //   e.g. chrome://devtools/skin/image/foo.svg
      icon: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),

      // An optional ID to assign to the menu's container tooltip object.
      menuId: PropTypes.string,

      // The preferred side of the anchor element to display the menu.
      // Defaults to "bottom".
      menuPosition: PropTypes.string.isRequired,

      // The offset of the menu from the anchor element.
      // Defaults to -5.
      menuOffset: PropTypes.number.isRequired,

      // The menu content.
      children: PropTypes.any,

      // Callback function to be invoked when the button is clicked.
      onClick: PropTypes.func,

      // Callback function to be invoked when the child panel is closed.
      onCloseButton: PropTypes.func,
    };
  }

  static get defaultProps() {
    return {
      menuPosition: "bottom",
      menuOffset: -5,
    };
  }

  constructor(props) {
    super(props);

    this.showMenu = this.showMenu.bind(this);
    this.hideMenu = this.hideMenu.bind(this);
    this.toggleMenu = this.toggleMenu.bind(this);
    this.onHidden = this.onHidden.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onTouchStart = this.onTouchStart.bind(this);

    this.buttonRef = createRef();

    this.state = {
      expanded: false,
      // In tests, initialize the menu immediately.
      isMenuInitialized: flags.testing || false,
      win: props.toolboxDoc.defaultView.top,
    };
    this.ignoreNextClick = false;

    this.initializeTooltip();
  }

  componentDidMount() {
    if (!this.state.isMenuInitialized) {
      // Initialize the menu when the button is focused or moused over.
      for (const event of ["focus""mousemove"]) {
        this.buttonRef.current.addEventListener(
          event,
          () => {
            if (!this.state.isMenuInitialized) {
              this.setState({ isMenuInitialized: true });
            }
          },
          { once: true }
        );
      }
    }
  }

  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
  UNSAFE_componentWillReceiveProps(nextProps) {
    // If the window changes, we need to regenerate the HTMLTooltip or else the
    // XUL wrapper element will appear above (in terms of z-index) the old
    // window, and not the new.
    const win = nextProps.toolboxDoc.defaultView.top;
    if (
      nextProps.toolboxDoc !== this.props.toolboxDoc ||
      this.state.win !== win ||
      nextProps.menuId !== this.props.menuId
    ) {
      this.setState({ win });
      this.resetTooltip();
      this.initializeTooltip();
    }
  }

  componentDidUpdate() {
    // The MenuButton creates the child panel when initializing the MenuButton.
    // If the children function is called during the rendering process,
    // this child list size might change. So we need to adjust content size here.
    if (typeof this.props.children === "function") {
      this.resizeContent();
    }
  }

  componentWillUnmount() {
    this.resetTooltip();
  }

  initializeTooltip() {
    const tooltipProps = {
      type: "doorhanger",
      useXulWrapper: true,
      isMenuTooltip: true,
    };

    if (this.props.menuId) {
      tooltipProps.id = this.props.menuId;
    }

    this.tooltip = new HTMLTooltip(this.props.toolboxDoc, tooltipProps);
    this.tooltip.on("hidden"this.onHidden);
  }

  async resetTooltip() {
    if (!this.tooltip) {
      return;
    }

    // Mark the menu as closed since the onHidden callback may not be called in
    // this case.
    this.setState({ expanded: false });
    this.tooltip.off("hidden"this.onHidden);
    this.tooltip.destroy();
    this.tooltip = null;
  }

  async showMenu(anchor) {
    this.setState({
      expanded: true,
    });

    if (!this.tooltip) {
      return;
    }

    await this.tooltip.show(anchor, {
      position: this.props.menuPosition,
      y: this.props.menuOffset,
    });
  }

  async hideMenu() {
    this.setState({
      expanded: false,
    });

    if (!this.tooltip) {
      return;
    }

    await this.tooltip.hide();
  }

  async toggleMenu(anchor) {
    return this.state.expanded ? this.hideMenu() : this.showMenu(anchor);
  }

  // Used by the call site to indicate that the menu content has changed so
  // its container should be updated.
  resizeContent() {
    if (!this.state.expanded || !this.tooltip || !this.buttonRef.current) {
      return;
    }

    this.tooltip.show(this.buttonRef.current, {
      position: this.props.menuPosition,
      y: this.props.menuOffset,
    });
  }

  // When we are closing the menu we will get a 'hidden' event before we get
  // a 'click' event. We want to re-enable the pointer-events: auto setting we
  // use on the button while the menu is visible, but we don't want to do it
  // until after the subsequent click event since otherwise we will end up
  // re-opening the menu.
  //
  // For mouse events, we achieve this by using setTimeout(..., 0) to schedule
  // a separate task to run after the click event, but in the case of touch
  // events the event order differs and the setTimeout callback will run before
  // the click event.
  //
  // In order to prevent that we detect touch events and set a flag to ignore
  // the next click event. However, we need to differentiate between touch drag
  // events and long press events (which don't generate a 'click') and "taps"
  // (which do). We do that by looking for a 'touchmove' event and clearing the
  // flag if we get one.
  onTouchStart(evt) {
    const touchend = () => {
      const anchorRect = this.buttonRef.current.getClientRects()[0];
      const { clientX, clientY } = evt.changedTouches[0];
      // We need to check that the click is inside the bounds since when the
      // menu is being closed the button will currently have
      // pointer-events: none (and if we don't check the bounds we will end up
      // ignoring unrelated clicks).
      if (
        anchorRect.x <= clientX &&
        clientX <= anchorRect.x + anchorRect.width &&
        anchorRect.y <= clientY &&
        clientY <= anchorRect.y + anchorRect.height
      ) {
        this.ignoreNextClick = true;
      }
    };

    const touchmove = () => {
      this.state.win.removeEventListener("touchend", touchend);
    };

    this.state.win.addEventListener("touchend", touchend, { once: true });
    this.state.win.addEventListener("touchmove", touchmove, { once: true });
  }

  onHidden() {
    this.setState({ expanded: false });
    // While the menu is open, if we click _anywhere_ outside the menu, it will
    // automatically close. This is performed by the XUL wrapper before we get
    // any chance to see any event. To avoid immediately re-opening the menu
    // when we process the subsequent click event on this button, we set
    // 'pointer-events: none' on the button while the menu is open.
    //
    // After the menu is closed we need to remove the pointer-events style (so
    // the button works again) but we don't want to do it immediately since the
    // "popuphidden" event which triggers this callback might be dispatched
    // before the "click" event that we want to ignore.  As a result, we queue
    // up a task using setTimeout() to run after the "click" event.
    this.state.win.setTimeout(() => {
      if (this.buttonRef.current) {
        this.buttonRef.current.style.pointerEvents = "auto";
      }
      this.state.win.removeEventListener("touchstart"this.onTouchStart, true);
    }, 0);

    this.state.win.addEventListener("touchstart"this.onTouchStart, true);

    if (this.props.onCloseButton) {
      this.props.onCloseButton();
    }
  }

  async onClick(e) {
    if (this.ignoreNextClick) {
      this.ignoreNextClick = false;
      return;
    }

    if (e.target === this.buttonRef.current) {
      // On Mac, even after clicking the button it doesn't get focus.
      // Force focus to the button so that our keydown handlers get called.
      this.buttonRef.current.focus();

      if (this.props.onClick) {
        this.props.onClick(e);
      }

      if (!e.defaultPrevented) {
        const wasKeyboardEvent = e.screenX === 0 && e.screenY === 0;
        // If the popup menu will be shown, disable this button in order to
        // prevent reopening the popup menu. See extended comment in onHidden().
        // above.
        //
        // Also, we should _not_ set 'pointer-events: none' if
        // ui.popup.disable_autohide pref is in effect since, in that case,
        // there's no redundant hiding behavior and we actually want clicking
        // the button to close the menu.
        if (
          !this.state.expanded &&
          !Services.prefs.getBoolPref("ui.popup.disable_autohide"false)
        ) {
          this.buttonRef.current.style.pointerEvents = "none";
        }
        await this.toggleMenu(e.target);
        // If the menu was activated by keyboard, focus the first item.
        if (wasKeyboardEvent && this.tooltip) {
          this.tooltip.focus();
        }

        // MenuButton creates the children dynamically when clicking the button,
        // so execute the goggle menu after updating the children panel.
        if (typeof this.props.children === "function") {
          this.forceUpdate();
        }
      }
      // If we clicked one of the menu items, then, by default, we should
      // auto-collapse the menu.
      //
      // We check for the defaultPrevented state, however, so that menu items can
      // turn this behavior off (e.g. a menu item with an embedded button).
    } else if (
      this.state.expanded &&
      !e.defaultPrevented &&
      e.target.matches(focusableSelector)
    ) {
      this.hideMenu();
    }
  }

  onKeyDown(e) {
    if (!this.state.expanded) {
      return;
    }

    const isButtonFocussed =
      this.props.toolboxDoc &&
      this.props.toolboxDoc.activeElement === this.buttonRef.current;

    switch (e.key) {
      case "Escape":
        this.hideMenu();
        e.preventDefault();
        break;

      case "Tab":
      case "ArrowDown":
        if (isButtonFocussed && this.tooltip) {
          if (this.tooltip.focus()) {
            e.preventDefault();
          }
        }
        break;

      case "ArrowUp":
        if (isButtonFocussed && this.tooltip) {
          if (this.tooltip.focusEnd()) {
            e.preventDefault();
          }
        }
        break;
      case "t":
        if ((isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey)) {
          // Close the menu if the user opens a new tab while it is still open.
          //
          // Bug 1499271: Once toolbox has been converted to XUL we should watch
          // for the 'visibilitychange' event instead of explicitly looking for
          // Ctrl+T.
          this.hideMenu();
        }
        break;
    }
  }

  render() {
    const buttonProps = {
      // Pass through any props set on the button, except the ones we handle
      // here.
      ...omit(this.props, Object.keys(MenuButton.propTypes)),
      onClick: this.onClick,
      "aria-expanded"this.state.expanded,
      "aria-haspopup""menu",
      ref: this.buttonRef,
    };

    if (this.state.expanded) {
      buttonProps.onKeyDown = this.onKeyDown;
    }

    if (this.props.menuId) {
      buttonProps["aria-controls"] = this.props.menuId;
    }

    if (this.props.icon) {
      const iconClass = "menu-button--iconic";
      buttonProps.className = buttonProps.className
        ? `${buttonProps.className} ${iconClass}`
        : iconClass;
      // `icon` may be a boolean and the icon URL will be set in CSS.
      if (typeof this.props.icon == "string") {
        buttonProps.style = {
          "--menuitem-icon-image""url(" + this.props.icon + ")",
        };
      }
    }

    if (this.state.isMenuInitialized) {
      const menu = createPortal(
        typeof this.props.children === "function"
          ? this.props.children()
          : this.props.children,
        this.tooltip.panel
      );

      return button(buttonProps, this.props.label, menu);
    }

    return button(buttonProps, this.props.label);
  }
}

module.exports = MenuButton;

92%


¤ Dauer der Verarbeitung: 0.18 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung ist noch experimentell.