Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="
https://cdn.jsdelivr.net/npm/long-press-event@2.4.6/dist/long-press-event.min.js
" type="module"></script>

<style type="text/css">
  * {
    -webkit-user-select: none; 
   /* -webkit-touch-callout: none;*/
  }
</style>
</head>

<body>
    <div class="container">
      <h1>Demo</h1>

      <button id="toggle" style="margin-bottom:2rem">disable/enable menu</button>

      <div style="width: 400px; height: 400px; padding: 2em; background: #ccc; overflow-y: scroll" id="block">
        <p>Click in here for context menu</p>
        <p style="display: block; margin-bottom: 400px">Scroll scroll scroll</p>
        <p>Scroll scroll scroll</p>
      </div>

      <div
        style="width: 400px; height: 400px; padding: 2em; background: #ddd; overflow-y: scroll; position: fixed; top: 50px; right: 50px"
        id="fixed"
      >
        <p>Click in here for context menu</p>
        <p style="display: block; margin-bottom: 400px">Scroll scroll scroll</p>
        <p>Scroll scroll scroll</p>
      </div>

      <div style="width: 200px; height: 200px; margin: 2em; padding: 2em; background: #ccc" id="target1">I'm target 1</div>
      <div style="width: 200px; height: 200px; margin: 2em; padding: 2em; background: #ccc" id="target2">I'm target 2</div>

      <p style="display: block; margin-bottom: 100vh">Scroll the page</p>

      <p>This is the end of the page</p>
    </div>
              
            
!

CSS

              
                
              
            
!

JS

              
                let globalListenerSet = false;
let baseOptions = {
  contextMenuClass: "pure-context-menu",
  dropdownClass: "dropdown-menu",
  dividerClass: "dropdown-divider",
  itemClass: "dropdown-item",
  zIndex: "9999",
  preventCloseOnClick: false,
  show: (event) => true,
};

/**
 * Easily manage context menus
 * Works out of the box with bootstrap css
 */
class PureContextMenu {
  _el;
  _items;
  _options;
  _currentEvent;

  /**
   * @param {HTMLElement} el
   * @param {object} items
   * @param {object} opts
   */
  constructor(el, items, opts) {
    this._items = items;
    this._el = el;

    this._options = Object.assign(baseOptions, opts);

    // bind the menu on context menu
    el.addEventListener("contextmenu", this);

    // add also long press support, this helps with ios browsers
    // include https://cdn.jsdelivr.net/npm/long-press-event@2.4/dist/long-press-event.min.js in your pages
    el.addEventListener("long-press", this);

    // close if the user clicks outside of the menu
    if (!globalListenerSet) {
      document.addEventListener("click", this);
      globalListenerSet = true;
    }
  }

  /**
   * @link https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4#handling-events
   * @param {Event} event
   */
  handleEvent(event) {
    console.log(event);
    const type = event.type === "long-press" ? "contextmenu" : event.type;
    this[`on${type}`](event);
  }

  /**
   * @param {object} opts
   */
  static updateDefaultOptions(opts) {
    baseOptions = Object.assign(baseOptions, opts);
  }

  /**
   * @returns {object}
   */
  static getDefaultOptions() {
    return baseOptions;
  }

  /**
   * Create the menu
   * @returns {HTMLElement}
   */
  _buildContextMenu = () => {
    const contextMenu = document.createElement("ul");
    contextMenu.style.minWidth = "120px";
    contextMenu.style.maxWidth = "240px";
    contextMenu.style.display = "block";
    contextMenu.classList.add(this._options.contextMenuClass);
    contextMenu.classList.add(this._options.dropdownClass);

    for (const item of this._items) {
      const child = document.createElement("li");
      if (item === "-") {
        const divider = document.createElement("hr");
        divider.classList.add(this._options.dividerClass);
        child.appendChild(divider);
      } else {
        const link = document.createElement("a");
        link.innerText = item.label;
        link.style.cursor = "pointer";
        link.style.whiteSpace = "normal";
        link.classList.add(this._options.itemClass);
        child.appendChild(link);
      }

      contextMenu.appendChild(child);
    }
    return contextMenu;
  };

  /**
   * Normalize the context menu position so that it won't get out of bounds
   * @param {number} mouseX
   * @param {number} mouseY
   * @param {HTMLElement} contextMenu
   */
  _normalizePosition = (mouseX, mouseY, contextMenu) => {
    const scope = this._el;
    const contextStyles = window.getComputedStyle(contextMenu);
    // clientWidth exclude borders and we add 1px for good measure
    const offset = parseInt(contextStyles.borderWidth) + 1;

    // compute what is the mouse position relative to the container element (scope)
    const bounds = scope.getBoundingClientRect();

    let scopeX = mouseX;
    let scopeY = mouseY;

    if (!["BODY", "HTML"].includes(scope.tagName)) {
      scopeX -= bounds.left;
      scopeY -= bounds.top;
    }

    const menuWidth = parseInt(contextStyles.width);

    // check if the element will go out of bounds
    const outOfBoundsOnX = scopeX + menuWidth > scope.clientWidth;
    const outOfBoundsOnY = scopeY + contextMenu.clientHeight > scope.clientHeight;

    let normalizedX = mouseX;
    let normalizedY = mouseY;

    // normalize on X
    if (outOfBoundsOnX) {
      normalizedX = scope.clientWidth - menuWidth - offset;
      if (!["BODY", "HTML"].includes(scope.tagName)) {
        normalizedX += bounds.left;
      }
    }

    // normalize on Y
    if (outOfBoundsOnY) {
      normalizedY = scope.clientHeight - contextMenu.clientHeight - offset;
      if (!["BODY", "HTML"].includes(scope.tagName)) {
        normalizedY += bounds.top;
      }
    }

    return { normalizedX, normalizedY };
  };

  _removeExistingContextMenu = () => {
    document.querySelector(`.${this._options.contextMenuClass}`)?.remove();
  };

  _bindCallbacks = (contextMenu) => {
    this._items.forEach((menuItem, index) => {
      if (menuItem === "-") {
        return;
      }

      const htmlEl = contextMenu.children[index];

     
     htmlEl.ontouchstart = htmlEl.onclick = () => {
        menuItem.callback(this._currentEvent);

        // do not close the menu if set
        const preventCloseOnClick = menuItem.preventCloseOnClick ?? this._options.preventCloseOnClick ?? false;
        if (!preventCloseOnClick) {
          this._removeExistingContextMenu();
        }
      };
    });
  };

  /**
   * @param {MouseEvent} event
   */
  oncontextmenu = (event) => {
    if (!this._options.show(event)) {
      return;
    }
    event.preventDefault();
    event.stopPropagation();

    // Store event for callbakcs
    this._currentEvent = event;

    // the current context menu should disappear when a new one is displayed
    this._removeExistingContextMenu();

    // build and show on ui
    const contextMenu = this._buildContextMenu();
    document.querySelector("body").append(contextMenu);

    // set the position already so that width can be computed
    contextMenu.style.position = "fixed";
    contextMenu.style.zIndex = this._options.zIndex;


    // adjust the position according to mouse position
    const mouseX = event.detail.clientX ?? event.clientX;
    const mouseY = event.detail.clientY ?? event.clientY;
    const { normalizedX, normalizedY } = this._normalizePosition(mouseX, mouseY, contextMenu);
    contextMenu.style.top = `${normalizedY}px`;
    contextMenu.style.left = `${normalizedX}px`;

    // disable context menu for it
    contextMenu.oncontextmenu = (e) => e.preventDefault();

    // bind the callbacks on each option
    this._bindCallbacks(contextMenu);
  };

  /**
   * Used to determine if the user has clicked outside of the context menu and if so to close it
   * @param {MouseEvent} event
   */
  onclick = (event) => {
    const clickedTarget = event.target;
    if (clickedTarget.closest(`.${this._options.contextMenuClass}`)) {
      return;
    }
    this._removeExistingContextMenu();
  };

  /**
   * Remove all the event listeners that were registered for this feature
   */
  off() {
    this._removeExistingContextMenu();
    document.removeEventListener("click", this);
    globalListenerSet = false;
    this._el.removeEventListener("contextmenu", this);
    this._el.removeEventListener("long-press", this);
  }
}

 let menuEnabled = true;

      document.getElementById("toggle").addEventListener("click", (e) => {
        menuEnabled = !menuEnabled;
        console.log("menu state: " + menuEnabled);
      });

      const items = [
        {
          label: "Click here",
          callback: () => {
            alert("You clicked here");
          },
        },
        "-",
        {
          label: "Target click",
          callback: (e) => {
            if (e.target.id) {
              alert("You clicked on target " + e.target.id);
            } else {
              alert("You didn't click on a target");
            }
          },
        },
        {
          label: "This is a long menu item that spans on two line",
          callback: () => {
            alert("You clicked on a long line");
          },
        },
        {
          label: "This will not close",
          preventCloseOnClick: true,
          callback: () => {
            alert("It didn't close");
          },
        },
      ];
      // bind to html if body does not have 100% height
      let bodyMenu = new PureContextMenu(document.querySelector("html"), items, {
        show: (e) => {
          return menuEnabled;
        },
      });
  
    document.addEventListener('long-press', function(e) {
      console.log("long press");
  document.querySelector("html").dispatchEvent(new Event('contextmenu'))
});

      const items2 = [
        {
          label: "Block here",
          callback: () => {
            alert("You clicked here");
          },
        },
        "-",
        {
          label: "Block there",
          callback: () => {
            alert("You clicked there");
          },
        },
      ];
      let blockMenu = new PureContextMenu(document.querySelector("#block"), items2);

      const items3 = [
        {
          label: "Fixed here",
          callback: () => {
            alert("You clicked here");
          },
        },
        "-",
        {
          label: "Fixed there",
          callback: () => {
            alert("You clicked there");
          },
        },
      ];
      let fixedMenu = new PureContextMenu(document.querySelector("#fixed"), items3);
  
              
            
!
999px

Console