index.js (4170B)
1 import './contextmenu.css'; 2 3 /* 4 items = [ 5 {text: 'Hello', disabled: true}, 6 '-', 7 {text: 'World', callback: fn}, 8 {text: 'section', separator: true}, 9 ] 10 */ 11 12 function isDescendant(parent, child) { 13 if (!parent) { 14 return false; 15 } 16 while (child) { 17 if (child === parent) { 18 return true; 19 } 20 child = child.parentNode; 21 } 22 return false; 23 } 24 25 class Contextmenu { 26 constructor(items) { 27 this.items = items; 28 } 29 30 show(e) { 31 if (this._container) { 32 return; 33 } 34 if (e.originalEvent) { 35 e = e.originalEvent; 36 } 37 if (e.preventDefault && !e.defaultPrevented) { 38 e.preventDefault(); 39 } else { 40 e.returnValue = false; 41 } 42 43 const {clientX: x, clientY: y} = e; 44 45 const container = this._container = document.createElement('div'); 46 document.body.appendChild(container); 47 container.className = 'contextmenu'; 48 container.style.zIndex = 10000; 49 50 window.addEventListener('keydown', this.onKeyDown, true); 51 window.addEventListener('mousedown', this.onMouseDown, true); 52 window.addEventListener('touchstart', this.onMouseDown, true); 53 54 for (let item of this.createItems()) { 55 container.appendChild(item); 56 } 57 this.setPosition(x, y); 58 } 59 60 hide() { 61 if (!this._container) { 62 return; 63 } 64 document.body.removeChild(this._container); 65 this._container = null; 66 67 window.removeEventListener('keydown', this.onKeyDown, true); 68 window.removeEventListener('mousedown', this.onMouseDown, true); 69 window.removeEventListener('touchstart', this.onMouseDown, true); 70 } 71 72 onKeyDown = (e) => { 73 if (e.keyCode === 27) { 74 this.hide(); 75 } 76 }; 77 78 onMouseDown = (e) => { 79 if (!isDescendant(this._container, e.target)) { 80 this.hide(); 81 } 82 }; 83 84 setPosition(x, y) { 85 const window_width = window.innerWidth, 86 window_height = window.innerHeight, 87 menu_width = this._container.offsetWidth, 88 menu_height = this._container.offsetHeight; 89 if (x + menu_width >= window_width) { 90 x -= menu_width; 91 if (x < 0) { 92 x = 0; 93 } 94 } 95 if (y + menu_height >= window_height) { 96 y -= menu_height; 97 if (y < 0) { 98 y = 0; 99 } 100 } 101 this._container.style.left = `${x}px`; 102 this._container.style.top = `${y}px`; 103 } 104 105 *createItems() { 106 let items = this.items; 107 if (typeof items === 'function') { 108 items = items(); 109 } 110 for (let itemOptions of items) { 111 if (typeof itemOptions === 'function') { 112 itemOptions = itemOptions(); 113 } 114 if (itemOptions === '-' || itemOptions.separator) { 115 yield this.createSeparator(itemOptions); 116 } else { 117 yield this.createItem(itemOptions); 118 } 119 } 120 } 121 122 createItem(itemOptions) { 123 const el = document.createElement('a'); 124 let className = 'item'; 125 if (itemOptions.disabled) { 126 className += ' disabled'; 127 } 128 if (itemOptions.header) { 129 className += ' header'; 130 } 131 el.innerHTML = itemOptions.text; 132 133 const callback = itemOptions.callback; 134 if (callback && !itemOptions.disabled) { 135 className += ' action'; 136 el.addEventListener('click', this.onItemClick.bind(this, callback)); 137 } 138 el.className = className; 139 return el; 140 } 141 142 onItemClick(callback, e) { 143 e.stopPropagation(); 144 e.preventDefault(); 145 this.hide(); 146 callback(e); 147 } 148 149 createSeparator(itemOptions) { 150 const el = document.createElement('div'); 151 el.className = 'separator'; 152 if (itemOptions.text) { 153 el.innerHTML = `<span>${itemOptions.text}</span>`; 154 } 155 return el; 156 } 157 } 158 159 export default Contextmenu;