miércoles, 19 de noviembre de 2025

Menú con submenús

 Quiero hacer un área dentro de una página web donde al clickear tanto con el botón derecho como con el izquierdo aparezca un menú de items personalizado.

Perfecto, Gabriel 🙌. Lo que quieres es crear un menú contextual personalizado que aparezca al hacer clic (izquierdo o derecho) dentro de un área específica de tu página. Te muestro cómo hacerlo paso a paso con HTML, CSS y JavaScript.

📂 Ejemplo básico

HTML

html
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Menú personalizado</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div id="area">
    Haz clic aquí (izquierdo o derecho)
  </div>

  <!-- Menú oculto -->
  <ul id="menu">
    <li>Opción 1</li>
    <li>Opción 2</li>
    <li>Opción 3</li>
  </ul>

  <script src="script.js"></script>
</body>
</html>

CSS (styles.css)

css
#area {
  width: 300px;
  height: 200px;
  border: 2px dashed #007bff;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 50px auto;
  cursor: pointer;
}

#menu {
  position: absolute;
  list-style: none;
  margin: 0;
  padding: 5px;
  background: white;
  border: 1px solid #ccc;
  display: none; /* oculto por defecto */
}

#menu li {
  padding: 8px 12px;
  cursor: pointer;
}

#menu li:hover {
  background: #007bff;
  color: white;
}

JavaScript (script.js)

js
const area = document.getElementById("area");
const menu = document.getElementById("menu");

// Mostrar menú en clic izquierdo o derecho
area.addEventListener("click", mostrarMenu);
area.addEventListener("contextmenu", mostrarMenu);

// Ocultar menú al hacer clic fuera
document.addEventListener("click", (e) => {
  if (!menu.contains(e.target) && e.target !== area) {
    menu.style.display = "none";
  }
});

function mostrarMenu(e) {
  e.preventDefault(); // evita menú por defecto en clic derecho
  menu.style.display = "block";
  menu.style.left = e.pageX + "px";
  menu.style.top = e.pageY + "px";
}

🔹 ¿Qué hace este código?

  • contextmenu captura el clic derecho y evita el menú del navegador.

  • click captura el clic izquierdo.

  • Ambos llaman a mostrarMenu, que posiciona el menú en las coordenadas del clic.

  • El menú se oculta si haces clic fuera de él.

✨ Posibles mejoras

  • Cambiar el contenido del menú según el contexto (ej. diferentes opciones si clicas en distintos elementos).

  • Animaciones para mostrar/ocultar el menú.

  • Integrar con frameworks como React o Vue para hacerlo más dinámico.

¿Quieres que te prepare una versión mejorada con animaciones y diferentes opciones según el botón (izquierdo vs derecho) para que veas cómo se puede sofisticar este menú?

Quiero que el menú funcione cuando se clickea sobre un SVG, dependiendo de qué elemento se clickea, pero que también funcione si el click es en un área vacía del SVG.

Perfecto, Gabriel 🙌. Lo que planteas es un caso muy interesante: un menú contextual dinámico sobre un SVG, que muestre opciones distintas según el elemento clickeado, pero que también tenga un menú por defecto si se hace clic en un área vacía del SVG.

📂 Ejemplo completo

HTML

html
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Menú contextual en SVG</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <svg id="miSVG" width="400" height="300" style="border:1px solid #ccc;">
    <!-- Elementos dentro del SVG -->
    <circle id="circulo" cx="100" cy="150" r="40" fill="skyblue" />
    <rect id="rectangulo" x="200" y="100" width="80" height="80" fill="tomato" />
  </svg>

  <!-- Menú contextual -->
  <ul id="menu">
    <!-- Items se llenan dinámicamente -->
  </ul>

  <script src="script.js"></script>
</body>
</html>

CSS (styles.css)

css
#menu {
  position: absolute;
  list-style: none;
  margin: 0;
  padding: 5px;
  background: white;
  border: 1px solid #ccc;
  display: none;
  min-width: 120px;
}

#menu li {
  padding: 8px 12px;
  cursor: pointer;
}

#menu li:hover {
  background: #007bff;
  color: white;
}

JavaScript (script.js)

js
const svg = document.getElementById("miSVG");
const menu = document.getElementById("menu");

// Definimos menús distintos según el target
const menus = {
  circulo: ["Editar círculo", "Eliminar círculo"],
  rectangulo: ["Editar rectángulo", "Eliminar rectángulo"],
  default: ["Agregar elemento", "Propiedades generales"]
};

// Mostrar menú contextual
svg.addEventListener("contextmenu", (e) => {
  e.preventDefault();

  // Determinar sobre qué se hizo clic
  const targetId = e.target.id;
  const opciones = menus[targetId] || menus.default;

  // Limpiar menú anterior
  menu.innerHTML = "";

  // Crear items dinámicamente
  opciones.forEach(opcion => {
    const li = document.createElement("li");
    li.textContent = opcion;
    li.addEventListener("click", () => {
      alert(`Acción: ${opcion}`);
      menu.style.display = "none";
    });
    menu.appendChild(li);
  });

  // Posicionar menú en coordenadas del clic
  menu.style.display = "block";
  menu.style.left = e.pageX + "px";
  menu.style.top = e.pageY + "px";
});

// Ocultar menú al hacer clic fuera
document.addEventListener("click", (e) => {
  if (!menu.contains(e.target)) {
    menu.style.display = "none";
  }
});

🔹 ¿Qué hace este código?

  • Captura el evento contextmenu (clic derecho) sobre el SVG.

  • Identifica si el clic fue sobre un elemento (circle, rect) o sobre el área vacía del SVG.

  • Según el id del elemento clickeado, carga un menú distinto.

  • Si el clic es en un área vacía, muestra el menú por defecto.

  • Los items del menú se crean dinámicamente y pueden ejecutar acciones distintas.

✨ Posibles mejoras

  • Hacer que también funcione con clic izquierdo (click) además del derecho.

  • Añadir animaciones para mostrar/ocultar el menú.

  • Cambiar el contenido del menú según atributos del elemento (ej. color, tamaño).

  • Integrar con React/Vue para manejar estados más complejos.

¿Quieres que te prepare una versión extendida donde el menú cambie también con clic izquierdo y derecho (menús distintos según el botón)?

Lo quiero para usar con React y Tailwind.

Perfecto, Gabriel 🙌. Te muestro cómo implementar un menú contextual dinámico sobre un SVG en React con TailwindCSS, que cambia según el elemento clickeado (círculo, rectángulo, o área vacía).

📂 Ejemplo con React + Tailwind

App.jsx

jsx
import { useState } from "react";

export default function App() {
  const [menu, setMenu] = useState([]);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const [visible, setVisible] = useState(false);

  const menus = {
    circulo: ["Editar círculo", "Eliminar círculo"],
    rectangulo: ["Editar rectángulo", "Eliminar rectángulo"],
    default: ["Agregar elemento", "Propiedades generales"],
  };

  const handleContextMenu = (e) => {
    e.preventDefault();

    // Detectar el target dentro del SVG
    const targetId = e.target.id;
    const opciones = menus[targetId] || menus.default;

    setMenu(opciones);
    setPos({ x: e.pageX, y: e.pageY });
    setVisible(true);
  };

  const handleClickOutside = () => {
    setVisible(false);
  };

  return (
    <div className="w-full h-screen bg-gray-100" onClick={handleClickOutside}>
      <svg
        id="miSVG"
        width="400"
        height="300"
        className="border border-gray-400 mx-auto mt-10"
        onContextMenu={handleContextMenu}
      >
        <circle id="circulo" cx="100" cy="150" r="40" fill="skyblue" />
        <rect id="rectangulo" x="200" y="100" width="80" height="80" fill="tomato" />
      </svg>

      {visible && (
        <ul
          className="absolute bg-white border border-gray-300 rounded shadow-md min-w-[150px] z-50"
          style={{ top: pos.y, left: pos.x }}
        >
          {menu.map((item, idx) => (
            <li
              key={idx}
              className="px-4 py-2 hover:bg-blue-500 hover:text-white cursor-pointer"
              onClick={() => {
                alert(`Acción: ${item}`);
                setVisible(false);
              }}
            >
              {item}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

🔹 ¿Qué hace este código?

  • Usa React state para manejar:

    • menu: las opciones actuales.

    • pos: coordenadas del clic.

    • visible: si el menú está abierto o no.

  • Captura el evento onContextMenu en el SVG.

  • Según el id del elemento clickeado (circulo, rectangulo o vacío), carga un menú distinto.

  • Renderiza el menú en la posición del clic usando estilos de TailwindCSS.

  • Se oculta al hacer clic fuera.

✨ Mejoras posibles

  • Diferenciar entre clic izquierdo y derecho (onClick vs onContextMenu).

  • Animaciones con Tailwind (transition, transform, scale).

  • Menús más complejos con subitems o íconos.

  • Integración con lógica real (ej. editar/eliminar elementos del SVG).

Así tienes un menú contextual totalmente dinámico y estilizado con Tailwind, que funciona tanto sobre elementos del SVG como en áreas vacías.

¿Quieres que te prepare también una versión con menús distintos para clic izquierdo y clic derecho sobre el SVG?

Además quiero que el menú tenga submenús, y que el proyecto esté estructurado para Vite.

Perfecto, Gabriel 🙌. Vamos a armar un mini proyecto con Vite + React + TailwindCSS que tenga un menú contextual con submenús sobre un SVG. Así tendrás la estructura clara y el código listo para expandir.

📂 Estructura del proyecto con Vite

Código
mi-proyecto/
├── index.html
├── package.json
├── vite.config.js
├── tailwind.config.js
├── postcss.config.js
├── /src
│   ├── main.jsx
│   ├── App.jsx
│   ├── /components
│   │   ├── ContextMenu.jsx
│   │   └── SubMenu.jsx
│   ├── /styles
│   │   └── index.css
│   └── /assets
│       └── logo.svg

🔹 Configuración inicial

  1. Crear proyecto con Vite:

    bash
    npm create vite@latest mi-proyecto -- --template react
    cd mi-proyecto
    
  2. Instalar TailwindCSS:

    bash
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
    
  3. Configurar tailwind.config.js:

    js
    export default {
      content: ["./index.html", "./src/**/*.{js,jsx}"],
      theme: {
        extend: {},
      },
      plugins: [],
    };
    
  4. Importar Tailwind en src/styles/index.css:

    css
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

🔹 Componente de Menú con Submenús

ContextMenu.jsx

jsx
import { useState } from "react";
import SubMenu from "./SubMenu";

export default function ContextMenu({ items, pos, visible, onClose }) {
  if (!visible) return null;

  return (
    <ul
      className="absolute bg-white border border-gray-300 rounded shadow-md min-w-[150px] z-50"
      style={{ top: pos.y, left: pos.x }}
    >
      {items.map((item, idx) => (
        <li
          key={idx}
          className="relative px-4 py-2 hover:bg-blue-500 hover:text-white cursor-pointer group"
          onClick={() => {
            if (!item.submenu) {
              alert(`Acción: ${item.label}`);
              onClose();
            }
          }}
        >
          {item.label}
          {item.submenu && (
            <SubMenu items={item.submenu} />
          )}
        </li>
      ))}
    </ul>
  );
}

SubMenu.jsx

jsx
export default function SubMenu({ items }) {
  return (
    <ul className="absolute left-full top-0 bg-white border border-gray-300 rounded shadow-md min-w-[150px] hidden group-hover:block">
      {items.map((subItem, idx) => (
        <li
          key={idx}
          className="px-4 py-2 hover:bg-green-500 hover:text-white cursor-pointer"
          onClick={() => alert(`Acción: ${subItem.label}`)}
        >
          {subItem.label}
        </li>
      ))}
    </ul>
  );
}

App.jsx

jsx
import { useState } from "react";
import ContextMenu from "./components/ContextMenu";

export default function App() {
  const [menuItems, setMenuItems] = useState([]);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const [visible, setVisible] = useState(false);

  const menus = {
    circulo: [
      { label: "Editar círculo" },
      { label: "Más opciones", submenu: [
        { label: "Cambiar color" },
        { label: "Cambiar tamaño" }
      ]}
    ],
    rectangulo: [
      { label: "Editar rectángulo" },
      { label: "Más opciones", submenu: [
        { label: "Rotar" },
        { label: "Duplicar" }
      ]}
    ],
    default: [
      { label: "Agregar elemento" },
      { label: "Propiedades generales" }
    ]
  };

  const handleContextMenu = (e) => {
    e.preventDefault();
    const targetId = e.target.id;
    const opciones = menus[targetId] || menus.default;
    setMenuItems(opciones);
    setPos({ x: e.pageX, y: e.pageY });
    setVisible(true);
  };

  const handleClickOutside = () => setVisible(false);

  return (
    <div className="w-full h-screen bg-gray-100" onClick={handleClickOutside}>
      <svg
        id="miSVG"
        width="400"
        height="300"
        className="border border-gray-400 mx-auto mt-10"
        onContextMenu={handleContextMenu}
      >
        <circle id="circulo" cx="100" cy="150" r="40" fill="skyblue" />
        <rect id="rectangulo" x="200" y="100" width="80" height="80" fill="tomato" />
      </svg>

      <ContextMenu
        items={menuItems}
        pos={pos}
        visible={visible}
        onClose={handleClickOutside}
      />
    </div>
  );
}

✨ Resultado

  • Al hacer clic derecho sobre el círculo o el rectángulo, aparece un menú con opciones específicas.

  • Algunos ítems tienen submenús que se despliegan al pasar el mouse.

  • Si haces clic en un área vacía del SVG, aparece el menú por defecto.

  • Todo estilizado con TailwindCSS y estructurado para un proyecto con Vite.

¿Quieres que te prepare también una versión con menús distintos para clic izquierdo y derecho (ej. menú de edición vs menú de acciones rápidas)?

No hay comentarios: