Greasy Fork is available in English.

MyContextMenu

原生js右键弹出菜单

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/464425/1178359/MyContextMenu.js

// ==UserScript==
// @name         MyContextMenu
// @description   http://https://wish123.cnblogs.com/?MyContextMenu
// @version      1.0
// @description  原生js右键弹出菜单
// @author       Wilson
// @license      MIT


// modify by https://github.com/electerious/basicContext/
(function() {
  if(document.querySelector("#myContextMenuStyle")) {
    return;
  }
  let style = `
<style id="myModalStyle">
/* base css */
.basicContext,
.basicContext * {
  box-sizing: border-box;
}
.basicContextContainer {
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 1000;
  -webkit-tap-highlight-color: transparent;
}
.basicContext {
  position: absolute;
  opacity: 0;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.basicContext__item {
  cursor: pointer;
}
.basicContext__item--separator {
  float: left;
  width: 100%;
  height: 1px;
  cursor: default;
}
.basicContext__item--disabled {
  cursor: default;
}
.basicContext__data {
  min-width: 140px;
  padding-right: 20px;
  text-align: left;
  white-space: nowrap;
}
.basicContext__icon {
  display: inline-block;
}
.basicContext--scrollable {
  height: 100%;
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
}
.basicContext--scrollable .basicContext__data {
  min-width: 160px;
}

/* default theme css */
.basicContext {
  padding: 6px;
  background-color: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}
.basicContext__item {
  margin-bottom: 2px;
}
.basicContext__item--separator {
  margin: 4px 0;
  background-color: rgba(0, 0, 0, 0.1);
}
.basicContext__item--disabled {
  opacity: 0.5;
}
.basicContext__item:last-child {
  margin-bottom: 0;
}
.basicContext__data {
  padding: 6px 8px;
  color: #333;
  border-radius: 2px;
}
.basicContext__item:not(.basicContext__item--disabled):hover
  .basicContext__data {
  color: #fff;
  background-color: #4393e6;
}
.basicContext__item:not(.basicContext__item--disabled):active
  .basicContext__data {
  background-color: #1d79d9;
}
.basicContext__icon {
  margin-right: 10px;
  width: 12px;
  text-align: center;
}

/* [自定义] 自定义css样式 */
.basicContextContainer{
    width: auto;
    height: auto;
}
.basicContext {
    padding: 0px;
    min-width: 150px;
    height: auto;
    background-color: rgba(241, 240, 240, 0.96);
}
.basicContext table{
    padding: 4px 0;
    width: 100%;
}
.basicContext__item {
    margin-bottom: 0px;
}
.basicContext__data {
    padding: 2px 12px;
    border-radius:0px;
    font-size: 13.8px;
    color: #3a383a;
}
.basicContext__item--separator {
    margin: 2px 0;
}
.basicContext__item:not(.basicContext__item--disabled):hover .basicContext__data {
    background-color: #4d92f8;
}
</style>
  `
  document.body.insertAdjacentHTML("beforeend", style);
})();

"use strict";
!(function (basicContext, callback) {
  "undefined" != typeof module && module.exports
    ? (module.exports = callback())
    : "function" == typeof define && define.amd
    ? define(callback)
    : (window[basicContext] = callback());
})("basicContext", function () {

let overflow = null

const ITEM      = 'item',
      SEPARATOR = 'separator'

const dom = function(elem = '') {

	return document.querySelector('.basicContext ' + elem)

}

const valid = function(item = {}) {

	let emptyItem = (Object.keys(item).length===0 ? true : false)

	if (emptyItem===true)     item.type    = SEPARATOR
	if (item.type==null)      item.type    = ITEM
	if (item.class==null)     item.class   = ''
	if (item.visible!==false) item.visible = true
	if (item.icon==null)      item.icon    = null
	if (item.title==null)     item.title   = 'Undefined'

	// Add disabled class when item disabled
	if (item.disabled!==true) item.disabled = false
	if (item.disabled===true) item.class += ' basicContext__item--disabled'

	// Item requires a function when
	// it's not a separator and not disabled
	if (item.fn==null && item.type!==SEPARATOR && item.disabled===false) {

		console.warn(`Missing fn for item '${ item.title }'`)
		return false

	}

	return true

}

const buildItem = function(item, num) {

	let html = '',
	    span = ''

	// Parse and validate item
	if (valid(item)===false) return ''

	// Skip when invisible
	if (item.visible===false) return ''

	// Give item a unique number
	item.num = num

	// Generate span/icon-element
	if (item.icon!==null) span = `<span class='basicContext__icon ${ item.icon }'></span>`

	// [自定义]
	item.extAttr = item.extAttr || ""

	// Generate item
	if (item.type===ITEM) {

		html = `
		       <tr class='basicContext__item ${ item.class }'>
		           <td class='basicContext__data' data-num='${ item.num }' ${item.extAttr}>${ span }${ item.title }</td>
		       </tr>
		       `

	} else if (item.type===SEPARATOR) {

		html = `
		       <tr class='basicContext__item basicContext__item--separator'></tr>
		       `

	}

	return html

}

const build = function(items) {

	let html = ''

	html += `
	        <div class='basicContextContainer'>
	            <div class='basicContext'>
	                <table cellspacing="0">
	                    <tbody>
	        `

	items.forEach((item, i) => html += buildItem(item, i))

	html += `
	                    </tbody>
	                </table>
	            </div>
	        </div>
	        `

	return html

}

const getNormalizedEvent = function(e = {}) {

	let pos = {
		x : e.clientX,
		y : e.clientY
	}

	if (e.type==='touchend' && (pos.x==null || pos.y==null)) {

		// We need to capture clientX and clientY from original event
		// when the event 'touchend' does not return the touch position

		let touches = e.changedTouches

		if (touches!=null&&touches.length>0) {
			pos.x = touches[0].clientX
			pos.y = touches[0].clientY
		}

	}

	// Position unknown
	if (pos.x==null || pos.x < 0) pos.x = 0
	if (pos.y==null || pos.y < 0) pos.y = 0

	return pos

}

const getPosition = function(e, context) {

	// Get the click position
	let normalizedEvent = getNormalizedEvent(e)

	// Set the initial position
	let x = normalizedEvent.x,
	    y = normalizedEvent.y

	// Get size of browser
	let browserSize = {
		width  : window.innerWidth,
		height : window.innerHeight
	}

	// Get size of context
	let contextSize = {
		width  : context.offsetWidth,
		height : context.offsetHeight
	}

	// Fix position based on context and browser size
	if ((x + contextSize.width) > browserSize.width)   x = x - ((x + contextSize.width) - browserSize.width)
	if ((y + contextSize.height) > browserSize.height) y = y - ((y + contextSize.height) - browserSize.height)

	// Make context scrollable and start at the top of the browser
	// when context is higher than the browser
	if (contextSize.height > browserSize.height) {
		y = 0
		context.classList.add('basicContext--scrollable')
	}

	// Calculate the relative position of the mouse to the context
	let rx = normalizedEvent.x - x,
	    ry = normalizedEvent.y - y

	return { x, y, rx, ry }

}

const bind = function(item = {}) {

	if (item.fn==null)        return false
	if (item.visible===false) return false
	if (item.disabled===true) return false

	dom(`td[data-num='${ item.num }']`).onclick       = item.fn
	dom(`td[data-num='${ item.num }']`).oncontextmenu = item.fn

	return true

}

const show = function(items, e, fnClose, fnCallback) {
    //[自定义] delete old menu
    let basicContextContainer = document.querySelector('.basicContextContainer');
    if(basicContextContainer){
        basicContextContainer.remove();
    }

	// Build context
	let html = build(items)

	// Add context to the body
	document.body.insertAdjacentHTML('beforeend', html)

	// Save current overflow and block scrolling of site
	if (overflow==null) {
		overflow = document.body.style.overflow
		document.body.style.overflow = 'hidden'
	}

	// Cache the context
	let context = dom()

	// Calculate position
	let position = getPosition(e, context)

	// Set position
	context.style.left            = `${ position.x }px`
	context.style.top             = `${ position.y }px`
	context.style.transformOrigin = `${ position.rx }px ${ position.ry }px`
	context.style.opacity         = 1

	// Close fn fallback
	if (fnClose==null) fnClose = close

	// Bind click on background
	context.parentElement.onclick       = fnClose
	context.parentElement.oncontextmenu = fnClose

	// Bind click on items
	items.forEach(bind)

	// Do not trigger default event or further propagation
	if (typeof e.preventDefault === 'function')  e.preventDefault()
	if (typeof e.stopPropagation === 'function') e.stopPropagation()

	// Call callback when a function
	if (typeof fnCallback === 'function') fnCallback()

	return true

}

const visible = function() {

	let elem = dom()

	if (elem==null || elem.length===0) return false
	else                               return true

}

const close = function() {

	if (visible()===false) return false

	let container = document.querySelector('.basicContextContainer')

	container.parentElement.removeChild(container)

	// Reset overflow to its original value
	if (overflow!=null) {
		document.body.style.overflow = overflow
		overflow = null
	}

	return true

}

return {
	ITEM,
	SEPARATOR,
	show,
	visible,
	close
}

});

//[自定义] 解决basicContextContainer出现系统菜单的bug
document.addEventListener('click', function(e){
    if(basicContext) basicContext.close();
});
document.addEventListener('contextmenu', function(e){
    if(['basicContextContainer','basicContext'].indexOf(e.target.className)!==-1) {
        e.preventDefault();
        return false;
    }
});

//使用示例
// const clicked = function(e) {
//     console.log(e.target.innerHTML);
// }
// document.querySelector('.my-context-menu-btn').addEventListener('contextmenu', function(e){
//     const items = [
//         { title: '新标签打开链接', extAttr: "data-name='new-blank'", fn: clicked },
//         { },
//         { title: '复制链接地址', extAttr: "data-name='copy-link'", fn: clicked },
//         { title: '复制选中的文本', extAttr: "data-name='copy-text'", fn: clicked, disabled: true },
//         { title: '复制响应数据', extAttr: "data-name='copy-response'", fn: clicked},
//         { },
//         { title: '复制为cURL格式', extAttr: "data-name='copy-curl'", fn: clicked},
//         { title: '复制为fetch格式', extAttr: "data-name='copy-fetch'", fn: clicked},
//         { title: '复制为await格式', extAttr: "data-name='copy-await'", fn: clicked},
//         { title: '复制为xhr格式', extAttr: "data-name='copy-xhr'", fn: clicked},
//         { title: '复制为分享链接', extAttr: "data-name='copy-share'", fn: clicked},
//         { },
//         { title: '删除该请求', extAttr: "data-name='del-request'", fn: clicked},
//         { title: '删除所有请求', extAttr: "data-name='del-all-request'", fn: clicked }
//     ]
//     basicContext.show(items, e);
// });