Skip to content

Instantly share code, notes, and snippets.

@lifeart
Created January 27, 2021 20:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lifeart/6a968c00770b14127a3b5939677cdd29 to your computer and use it in GitHub Desktop.
Save lifeart/6a968c00770b14127a3b5939677cdd29 to your computer and use it in GitHub Desktop.
right-menu-modifier
import { modifier } from 'ember-modifier';
// https://raw.githubusercontent.com/UnrealSecurity/context-js/main/context/context.js
function hasProp(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
const CONTEXT_CLASSES = {
ITEM: 'item',
CONTEXT: 'right-menu',
SEPARATOR: 'separator',
DISABLED: 'disabled',
ENABLED: 'enabled',
LABEL: 'label',
HAS_SUBITEMS: 'has-subitems',
HOTKEY: 'hotkey',
};
/* Author: @UnrealSec */
class ContextMenu {
constructor(container, items) {
this.container = container;
this.dom = null;
this.shown = false;
this.root = true;
this.parent = null;
this.submenus = [];
this.items = items;
this._onclick = (e) => {
if (
this.dom &&
e.target != this.dom &&
e.target.parentElement != this.dom &&
!e.target.classList.contains(CONTEXT_CLASSES.ITEM) &&
!e.target.parentElement.classList.contains(CONTEXT_CLASSES.ITEM)
) {
this.hideAll();
}
};
this._oncontextmenu = (e) => {
e.preventDefault();
if (
e.target != this.dom &&
e.target.parentElement != this.dom &&
!e.target.classList.contains(CONTEXT_CLASSES.ITEM) &&
!e.target.parentElement.classList.contains(CONTEXT_CLASSES.ITEM)
) {
this.hideAll();
this.show(e.clientX, e.clientY);
}
};
this._oncontextmenu_keydown = (e) => {
if (e.keyCode != 93) return;
e.preventDefault();
this.hideAll();
this.show(e.clientX, e.clientY);
};
this._onblur = () => {
this.hideAll();
};
}
getMenuDom() {
const menu = document.createElement('div');
menu.classList.add(CONTEXT_CLASSES.CONTEXT);
for (const item of this.items) {
menu.appendChild(this.itemToDomEl(item));
}
return menu;
}
itemToDomEl(data) {
const item = document.createElement('div');
if (data === null) {
item.classList = CONTEXT_CLASSES.SEPARATOR;
return item;
}
if (
hasProp(data, 'color') &&
/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(data.color.toString())
) {
item.style.cssText = `color: ${data.color}`;
}
item.classList.add(CONTEXT_CLASSES.ITEM);
const label = document.createElement('span');
label.classList = CONTEXT_CLASSES.LABEL;
label.innerText = hasProp(data, 'text') ? data['text'].toString() : '';
item.appendChild(label);
if (hasProp(data, 'disabled') && data['disabled']) {
item.classList.add(CONTEXT_CLASSES.DISABLED);
} else {
item.classList.add(CONTEXT_CLASSES.ENABLED);
}
const hotkey = document.createElement('span');
hotkey.classList = CONTEXT_CLASSES.HOTKEY;
hotkey.innerText = hasProp(data, 'hotkey') ? data['hotkey'].toString() : '';
item.appendChild(hotkey);
if (
hasProp(data, 'subitems') &&
Array.isArray(data['subitems']) &&
data['subitems'].length > 0
) {
const menu = new ContextMenu(this.container, data['subitems']);
menu.root = false;
menu.parent = this;
const openSubItems = () => {
if (hasProp(data, 'disabled') && data['disabled'] == true) return;
this.hideSubMenus();
const x = this.dom.offsetLeft + this.dom.clientWidth + item.offsetLeft;
const y = this.dom.offsetTop + item.offsetTop;
if (!menu.shown) {
menu.show(x, y);
} else {
menu.hide();
}
};
this.submenus.push(menu);
item.classList.add(CONTEXT_CLASSES.HAS_SUBITEMS);
item.addEventListener('click', openSubItems);
item.addEventListener('mousemove', openSubItems);
} else if (
hasProp(data, 'submenu') &&
data['submenu'] instanceof ContextMenu
) {
const menu = data['submenu'];
menu.root = false;
menu.parent = this;
const openSubItems = () => {
if (hasProp(data, 'disabled') && data['disabled'] == true) return;
this.hideSubMenus();
const x = this.dom.offsetLeft + this.dom.clientWidth + item.offsetLeft;
const y = this.dom.offsetTop + item.offsetTop;
if (!menu.shown) {
menu.show(x, y);
} else {
menu.hide();
}
};
this.submenus.push(menu);
item.classList.add(CONTEXT_CLASSES.HAS_SUBITEMS);
item.addEventListener('click', openSubItems);
item.addEventListener('mousemove', openSubItems);
} else {
item.addEventListener('click', () => {
this.hideSubMenus();
if (item.classList.contains(CONTEXT_CLASSES.DISABLED)) return;
if (hasProp(data, 'onclick') && typeof data['onclick'] === 'function') {
const event = {
handled: false,
item: item,
label: label,
hotkey: hotkey,
items: this.items,
data: data,
};
data['onclick'](event);
if (!event.handled) {
this.hide();
}
} else {
this.hide();
}
});
item.addEventListener('mousemove', () => {
this.hideSubMenus();
});
}
return item;
}
hideAll() {
if (this.root && !this.parent) {
if (this.shown) {
this.hideSubMenus();
this.shown = false;
this.container.removeChild(this.dom);
if (this.parent && this.parent.shown) {
this.parent.hide();
}
}
return;
}
this.parent.hide();
}
hide() {
if (this.dom && this.shown) {
this.shown = false;
this.hideSubMenus();
this.container.removeChild(this.dom);
if (this.parent && this.parent.shown) {
this.parent.hide();
}
}
}
hideSubMenus() {
for (const menu of this.submenus) {
if (menu.shown) {
menu.shown = false;
menu.container.removeChild(menu.dom);
}
menu.hideSubMenus();
}
}
show(x, y) {
this.dom = this.getMenuDom();
this.dom.style.left = `${x}px`;
this.dom.style.top = `${y}px`;
this.shown = true;
this.container.appendChild(this.dom);
}
install() {
this.container.addEventListener('contextmenu', this._oncontextmenu);
this.container.addEventListener('keydown', this._oncontextmenu_keydown);
this.container.addEventListener('click', this._onclick);
window.addEventListener('blur', this._onblur);
}
uninstall() {
this.dom = null;
this.container.removeEventListener('contextmenu', this._oncontextmenu);
this.container.removeEventListener('keydown', this._oncontextmenu_keydown);
this.container.removeEventListener('click', this._onclick);
window.removeEventListener('blur', this._onblur);
}
}
export default modifier(function rightMenu(element, [items]) {
if (!items) {
items = [
{ text: 'Back', disabled: true },
{ text: 'Copy message to "modeling"' },
{ text: 'Copy message to "setup"' },
{ text: 'Copy message to "texturing"' },
{ text: 'Copy message to "shading"' },
];
}
let item = null;
let onEnter = () => {
item = new ContextMenu(element, items);
element.removeEventListener('mouseenter', onEnter);
item.install();
};
element.addEventListener('mouseenter', onEnter);
return () => {
item?.uninstall();
};
});
.right-menu {
display: inline-block;
position: fixed;
top: 0px;
left: 0px;
min-width: 270px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #000;
background: #f5f5f5;
font-size: 9pt;
border: 1px solid #333333;
box-shadow: 4px 4px 3px -1px rgba(0, 0, 0, 0.5);
padding: 3px 0px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.item {
padding: 4px 19px;
cursor: default;
color: inherit;
}
.item:hover {
background: #e3e3e3 !important;
}
.item:hover .hotkey {
color: #000 !important;
}
.disabled {
color: #878B90 !important;
}
.disabled:hover {
background: inherit !important;
}
.disabled:hover .hotkey {
color: #878B90 !important;
}
.separator {
margin: 4px 0px;
height: 0;
padding: 0;
border-top: 1px solid #b3b3b3;
}
.hotkey {
color: #878B90;
float: right;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment