A Pen by Stephan K. on CodePen.
Created
February 21, 2021 08:18
-
-
Save KMurphs/0d3e7a888a851efc7b5b6089aa93c6e4 to your computer and use it in GitHub Desktop.
ContextMenu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<ul class="container"> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
<li><span>Some List Item</span><span class="btn with-menu"><i class="fas fa-ellipsis-v"></i></span></li> | |
</ul> | |
<nav class="context-menu"> | |
<ul> | |
<li>Some Menu Item</li> | |
<li>Some Menu Item</li> | |
<li>Some Menu Item</li> | |
<li>Some Menu Item</li> | |
</ul> | |
</nav> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
"use strict"; | |
const getMousePosition = evt => { | |
const [posX, posY] = (function(evt){ | |
if (evt.pageX || evt.pageY) return [evt.pageX, evt.pageY]; | |
if (evt.clientX || evt.clientY) return [ | |
evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft, | |
evt.clientY + document.body.scrollTop + document.documentElement.scrollTop | |
]; | |
return [0, 0]; | |
})(evt || window.event) | |
return { x: posX, y: posY } | |
} | |
const moveNodeTo = (node, x, y) => { | |
// Increase width and height of node by a small margin | |
// Then compute the maximum coords x and y can be as to prevent overflowing | |
const margin = 4; | |
const maxX = windowWidth - (menu.offsetWidth + margin); | |
const maxY = windowHeight - (menu.offsetHeight + margin); | |
// Obtain window width and height | |
const {innerWidth: windowWidth, innerHeight: windowHeight} = window; | |
// Move the node to [x, y] unless overflow (in which case don't go behond max coords) | |
node.style.left = `${x < maxX ? x : maxX}px`; | |
node.style.top = `${y < maxY ? y : maxY}px`; | |
} | |
const bubbleToClassOwner = (evt, className) => { | |
// From the origin of the event, climb up the chain | |
// until we either find a dom element with class "className" (then return said element) | |
// or we get to the root of all dom elements (the window - in which case return null) | |
let currNode = evt.srcElement || evt.target; | |
while(currNode){ | |
if(currNode.classList.contains(className)) return currNode; | |
currNode = currNode.parentNode; | |
} | |
return null; | |
} | |
const bringUpMenu = (x, y)=>{ | |
// Manufacture a menu "nav.context-menu > ul > li" | |
const menu = document.createElement("nav"); | |
menu.classList.add("context-menu"); | |
menu.appendChild(document.createElement("ul")); | |
document.body.append(menu); | |
// Move menu to some position using appropriate algorithm | |
// to prevent overflows ... | |
moveNodeTo(menu, x, y); | |
// Return menu, and function to cleanup/remove it from DOM | |
return [menu, ()=>document.body.removeChild(menu)]; | |
} | |
function setupCustomContextMenuListener(menuOwnerClass="with-menu", menuChoiceClass="is-menu-item", cb) { | |
/* Closures to limit scopes of menu and cleanup function here */ | |
let cleanupMenu = null; | |
let menu = null; | |
// Setup a global event listener for right clicks, work up whether | |
// an event must just be ignored or needs the menu to come on, or be removed | |
document.addEventListener("contextmenu", function(evt) { | |
// We got a right click, does it eventually bubble up to an element | |
// that is configured for a custom menu? | |
const menuOwner = bubbleToClassOwner(evt, menuOwnerClass); | |
// If we have an ancestor configured with the custom menu, bring up the menu | |
if(menuOwner) { | |
evt.preventDefault(); | |
[menu, cleanupMenu] = bringUpMenu(...getMousePosition(evt)); | |
// The click was outside of any interesting elements, bring down the menu | |
} else { | |
cleanupMenu && cleanupMenu(); | |
} | |
}); | |
// Handle some edge cases | |
// Window is resized while menu is on screen, remove it | |
window.onresize = () => cleanupMenu && cleanupMenu(); | |
// On "esc" remove menu | |
window.onkeyup = (evt) => cleanupMenu && (evt.keyCode === 27) && cleanupMenu(); | |
// Handle click in menu (callback, bring down menu) and out of menu (bring down menu) | |
document.addEventListener("click", evt => { | |
// We got a left click, does it eventually bubble up to an item in the menu? | |
const menuChoice = bubbleToClassOwner(evt, menuChoiceClass); | |
// We left clicked on a menu item, process it, close menu | |
menuChoice && evt.preventDefault(); | |
menuChoice && cb && cb(menuChoice, evt); | |
menuChoice && cleanupMenu && cleanupMenu(); | |
// We left clicked outside of the menu, we should remove menu. | |
// Except, in Firefox where a right click (contextmenu event) | |
// also causes a click event with right click code (mouseButton = 2) | |
// We need to make sure to remove the menu only | |
// when left clicking (mouseButton = 1) outside of the menu | |
// Otherwise, menu will appear and disappear in some instances | |
const button = evt.which || evt.button; | |
!menuChoice && (mouseButton === 1) && cleanupMenu && cleanupMenu(); | |
}) | |
} | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.container{ | |
width: min(500px, 90vw); | |
margin: auto; | |
margin-top: 100px; | |
border: 1px solid #f4f4f4; | |
padding: 1rem; | |
} | |
.container li{ | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
border-bottom: 1px solid #ddd; | |
padding: .25rem .5rem; | |
} | |
.container li:hover{ | |
background-color: rgba(0,0,0,0.02); | |
} | |
/* button */ | |
.btn{ | |
cursor: pointer; | |
display: inline-flex; | |
justify-content: center; | |
align-items: center; | |
width: 1.7rem; | |
height: 1.7rem; | |
border-radius: 50%; | |
} | |
.btn:hover{ | |
background-color: rgba(0,0,0,0.06); | |
} | |
.context-menu{ | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
border: 1px solid #aaa; | |
} | |
.context-menu li{ | |
border-bottom: 1px solid #ddd; | |
padding: .5rem 1rem; | |
} | |
.context-menu li:hover{ | |
background-color: rgba(0,0,0,0.02); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment