MyContextMenu

原生js右键弹出菜单

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/464425/1178359/MyContextMenu.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
// });