All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.ui.SoftContextMenu.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import * as Host from '../host/host.js';

import * as ARIAUtils from './ARIAUtils.js';
import {AnchorBehavior, GlassPane, MarginBehavior, PointerEventsBehavior, SizeBehavior,} from './GlassPane.js';  // eslint-disable-line no-unused-vars
import {Icon} from './Icon.js';
import {ElementFocusRestorer} from './UIUtils.js';

/**
 * @unrestricted
 */
export class SoftContextMenu {
  /**
   * @param {!Array.} items
   * @param {function(string)} itemSelectedCallback
   * @param {!SoftContextMenu=} parentMenu
   */
  constructor(items, itemSelectedCallback, parentMenu) {
    this._items = items;
    this._itemSelectedCallback = itemSelectedCallback;
    this._parentMenu = parentMenu;
    /** @type {?Element} */
    this._highlightedMenuItemElement = null;
  }

  /**
   * @param {!Document} document
   * @param {!AnchorBox} anchorBox
   */
  show(document, anchorBox) {
    if (!this._items.length) {
      return;
    }

    this._document = document;

    this._glassPane = new GlassPane();
    this._glassPane.setPointerEventsBehavior(
        this._parentMenu ? PointerEventsBehavior.PierceGlassPane : PointerEventsBehavior.BlockedByGlassPane);
    this._glassPane.registerRequiredCSS('ui/softContextMenu.css');
    this._glassPane.setContentAnchorBox(anchorBox);
    this._glassPane.setSizeBehavior(SizeBehavior.MeasureContent);
    this._glassPane.setMarginBehavior(MarginBehavior.NoMargin);
    this._glassPane.setAnchorBehavior(this._parentMenu ? AnchorBehavior.PreferRight : AnchorBehavior.PreferBottom);

    this._contextMenuElement = this._glassPane.contentElement.createChild('div', 'soft-context-menu');
    this._contextMenuElement.tabIndex = -1;
    ARIAUtils.markAsMenu(this._contextMenuElement);
    this._contextMenuElement.addEventListener('mouseup', e => e.consume(), false);
    this._contextMenuElement.addEventListener('keydown', this._menuKeyDown.bind(this), false);

    for (let i = 0; i < this._items.length; ++i) {
      this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
    }

    this._glassPane.show(document);
    this._focusRestorer = new ElementFocusRestorer(this._contextMenuElement);

    if (!this._parentMenu) {
      this._hideOnUserGesture = event => {
        // If a user clicks on any submenu, prevent the menu system from closing.
        let subMenu = this._subMenu;
        while (subMenu) {
          if (subMenu._contextMenuElement === event.path[0]) {
            return;
          }
          subMenu = subMenu._subMenu;
        }

        this.discard();
        event.consume(true);
      };
      this._document.body.addEventListener('mousedown', this._hideOnUserGesture, false);
      this._document.defaultView.addEventListener('resize', this._hideOnUserGesture, false);
    }
  }

  discard() {
    if (this._subMenu) {
      this._subMenu.discard();
    }
    if (this._focusRestorer) {
      this._focusRestorer.restore();
    }
    if (this._glassPane) {
      this._glassPane.hide();
      delete this._glassPane;
      if (this._hideOnUserGesture) {
        this._document.body.removeEventListener('mousedown', this._hideOnUserGesture, false);
        this._document.defaultView.removeEventListener('resize', this._hideOnUserGesture, false);
        delete this._hideOnUserGesture;
      }
    }
    if (this._parentMenu) {
      delete this._parentMenu._subMenu;
    }
  }

  _createMenuItem(item) {
    if (item.type === 'separator') {
      return this._createSeparator();
    }

    if (item.type === 'subMenu') {
      return this._createSubMenu(item);
    }

    const menuItemElement = createElementWithClass('div', 'soft-context-menu-item');
    menuItemElement.tabIndex = -1;
    ARIAUtils.markAsMenuItem(menuItemElement);
    const checkMarkElement = Icon.create('smallicon-checkmark', 'checkmark');
    menuItemElement.appendChild(checkMarkElement);
    if (!item.checked) {
      checkMarkElement.style.opacity = '0';
    }

    if (item.element) {
      const wrapper = menuItemElement.createChild('div', 'soft-context-menu-custom-item');
      wrapper.appendChild(item.element);
      menuItemElement._customElement = item.element;
      return menuItemElement;
    }

    if (!item.enabled) {
      menuItemElement.classList.add('soft-context-menu-disabled');
    }
    menuItemElement.createTextChild(item.label);
    menuItemElement.createChild('span', 'soft-context-menu-shortcut').textContent = item.shortcut;

    menuItemElement.addEventListener('mousedown', this._menuItemMouseDown.bind(this), false);
    menuItemElement.addEventListener('mouseup', this._menuItemMouseUp.bind(this), false);

    // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    menuItemElement.addEventListener('mouseover', this._menuItemMouseOver.bind(this), false);
    menuItemElement.addEventListener('mouseleave', this._menuItemMouseLeave.bind(this), false);

    menuItemElement._actionId = item.id;

    let accessibleName = item.label;

    if (item.type === 'checkbox') {
      const checkedState = item.checked ? ls`checked` : ls`unchecked`;
      if (item.shortcut) {
        accessibleName = ls`${item.label}, ${item.shortcut}, ${checkedState}`;
      } else {
        accessibleName = ls`${item.label}, ${checkedState}`;
      }
    } else if (item.shortcut) {
      accessibleName = ls`${item.label}, ${item.shortcut}`;
    }
    ARIAUtils.setAccessibleName(menuItemElement, accessibleName);

    return menuItemElement;
  }

  _createSubMenu(item) {
    const menuItemElement = createElementWithClass('div', 'soft-context-menu-item');
    menuItemElement._subItems = item.subItems;
    menuItemElement.tabIndex = -1;
    ARIAUtils.markAsMenuItemSubMenu(menuItemElement);
    // TODO: Once we have AOM, remove the explicit aria-label and use document.queryByAccessibleName
    ARIAUtils.setAccessibleName(menuItemElement, item.label);

    // Occupy the same space on the left in all items.
    const checkMarkElement = Icon.create('smallicon-checkmark', 'soft-context-menu-item-checkmark');
    checkMarkElement.classList.add('checkmark');
    menuItemElement.appendChild(checkMarkElement);
    checkMarkElement.style.opacity = '0';

    menuItemElement.createTextChild(item.label);

    if (Host.Platform.isMac() && !self.UI.themeSupport.hasTheme()) {
      const subMenuArrowElement = menuItemElement.createChild('span', 'soft-context-menu-item-submenu-arrow');
      subMenuArrowElement.textContent = '\u25B6';  // BLACK RIGHT-POINTING TRIANGLE
    } else {
      const subMenuArrowElement = Icon.create('smallicon-triangle-right', 'soft-context-menu-item-submenu-arrow');
      menuItemElement.appendChild(subMenuArrowElement);
    }

    menuItemElement.addEventListener('mousedown', this._menuItemMouseDown.bind(this), false);
    menuItemElement.addEventListener('mouseup', this._menuItemMouseUp.bind(this), false);

    // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
    menuItemElement.addEventListener('mouseover', this._menuItemMouseOver.bind(this), false);
    menuItemElement.addEventListener('mouseleave', this._menuItemMouseLeave.bind(this), false);

    return menuItemElement;
  }

  _createSeparator() {
    const separatorElement = createElementWithClass('div', 'soft-context-menu-separator');
    separatorElement._isSeparator = true;
    separatorElement.createChild('div', 'separator-line');
    return separatorElement;
  }

  _menuItemMouseDown(event) {
    // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
    event.consume(true);
  }

  _menuItemMouseUp(event) {
    this._triggerAction(event.target, event);
    event.consume();
  }

  /**
   * @return {!SoftContextMenu}
   */
  _root() {
    let root = this;
    while (root._parentMenu) {
      root = root._parentMenu;
    }
    return root;
  }

  _triggerAction(menuItemElement, event) {
    if (!menuItemElement._subItems) {
      this._root().discard();
      event.consume(true);
      if (typeof menuItemElement._actionId !== 'undefined') {
        this._itemSelectedCallback(menuItemElement._actionId);
        delete menuItemElement._actionId;
      }
      return;
    }

    this._showSubMenu(menuItemElement);
    event.consume();
  }

  _showSubMenu(menuItemElement) {
    if (menuItemElement._subMenuTimer) {
      clearTimeout(menuItemElement._subMenuTimer);
      delete menuItemElement._subMenuTimer;
    }
    if (this._subMenu) {
      return;
    }

    this._subMenu = new SoftContextMenu(menuItemElement._subItems, this._itemSelectedCallback, this);
    const anchorBox = menuItemElement.boxInWindow();
    // Adjust for padding.
    anchorBox.y -= 5;
    anchorBox.x += 3;
    anchorBox.width -= 6;
    anchorBox.height += 10;
    this._subMenu.show(this._document, anchorBox);
  }

  _menuItemMouseOver(event) {
    this._highlightMenuItem(event.target, true);
  }

  _menuItemMouseLeave(event) {
    if (!this._subMenu || !event.relatedTarget) {
      this._highlightMenuItem(null, true);
      return;
    }

    const relatedTarget = event.relatedTarget;
    if (relatedTarget === this._contextMenuElement) {
      this._highlightMenuItem(null, true);
    }
  }

  /**
   * @param {?Element} menuItemElement
   * @param {boolean} scheduleSubMenu
   */
  _highlightMenuItem(menuItemElement, scheduleSubMenu) {
    if (this._highlightedMenuItemElement === menuItemElement) {
      return;
    }

    if (this._subMenu) {
      this._subMenu.discard();
    }
    if (this._highlightedMenuItemElement) {
      this._highlightedMenuItemElement.classList.remove('force-white-icons');
      this._highlightedMenuItemElement.classList.remove('soft-context-menu-item-mouse-over');
      if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
        clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
        delete this._highlightedMenuItemElement._subMenuTimer;
      }
    }
    this._highlightedMenuItemElement = menuItemElement;
    if (this._highlightedMenuItemElement) {
      if (self.UI.themeSupport.hasTheme() || Host.Platform.isMac()) {
        this._highlightedMenuItemElement.classList.add('force-white-icons');
      }
      this._highlightedMenuItemElement.classList.add('soft-context-menu-item-mouse-over');
      if (this._highlightedMenuItemElement._customElement) {
        this._highlightedMenuItemElement._customElement.focus();
      } else {
        this._highlightedMenuItemElement.focus();
      }
      if (scheduleSubMenu && this._highlightedMenuItemElement._subItems &&
          !this._highlightedMenuItemElement._subMenuTimer) {
        this._highlightedMenuItemElement._subMenuTimer =
            setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement), 150);
      }
    }
  }

  _highlightPrevious() {
    let menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling :
                                                             this._contextMenuElement.lastChild;
    while (menuItemElement &&
           (menuItemElement._isSeparator || menuItemElement.classList.contains('soft-context-menu-disabled'))) {
      menuItemElement = menuItemElement.previousSibling;
    }
    if (menuItemElement) {
      this._highlightMenuItem(menuItemElement, false);
    }
  }

  _highlightNext() {
    let menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling :
                                                             this._contextMenuElement.firstChild;
    while (menuItemElement &&
           (menuItemElement._isSeparator || menuItemElement.classList.contains('soft-context-menu-disabled'))) {
      menuItemElement = menuItemElement.nextSibling;
    }
    if (menuItemElement) {
      this._highlightMenuItem(menuItemElement, false);
    }
  }

  _menuKeyDown(event) {
    switch (event.key) {
      case 'ArrowUp':
        this._highlightPrevious();
        break;
      case 'ArrowDown':
        this._highlightNext();
        break;
      case 'ArrowLeft':
        if (this._parentMenu) {
          this._highlightMenuItem(null, false);
          this.discard();
        }
        break;
      case 'ArrowRight':
        if (!this._highlightedMenuItemElement) {
          break;
        }
        if (this._highlightedMenuItemElement._subItems) {
          this._showSubMenu(this._highlightedMenuItemElement);
          this._subMenu._highlightNext();
        }
        break;
      case 'Escape':
        this.discard();
        break;
      case 'Enter':
        if (!isEnterKey(event)) {
          return;
        }
      // Fall through
      case ' ':  // Space
        if (!this._highlightedMenuItemElement || this._highlightedMenuItemElement._customElement) {
          return;
        }
        this._triggerAction(this._highlightedMenuItemElement, event);
        if (this._highlightedMenuItemElement._subItems) {
          this._subMenu._highlightNext();
        }
        break;
    }
    event.consume(true);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy