MyContextMenu

原生js右键弹出菜单

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/464425/1178359/MyContextMenu.js

  1. // ==UserScript==
  2. // @name MyContextMenu
  3. // @description http://https://wish123.cnblogs.com/?MyContextMenu
  4. // @version 1.0
  5. // @description 原生js右键弹出菜单
  6. // @author Wilson
  7. // @license MIT
  8.  
  9.  
  10. // modify by https://github.com/electerious/basicContext/
  11. (function() {
  12. if(document.querySelector("#myContextMenuStyle")) {
  13. return;
  14. }
  15. let style = `
  16. <style id="myModalStyle">
  17. /* base css */
  18. .basicContext,
  19. .basicContext * {
  20. box-sizing: border-box;
  21. }
  22. .basicContextContainer {
  23. position: fixed;
  24. width: 100%;
  25. height: 100%;
  26. top: 0;
  27. left: 0;
  28. z-index: 1000;
  29. -webkit-tap-highlight-color: transparent;
  30. }
  31. .basicContext {
  32. position: absolute;
  33. opacity: 0;
  34. -moz-user-select: none;
  35. -webkit-user-select: none;
  36. -ms-user-select: none;
  37. user-select: none;
  38. }
  39. .basicContext__item {
  40. cursor: pointer;
  41. }
  42. .basicContext__item--separator {
  43. float: left;
  44. width: 100%;
  45. height: 1px;
  46. cursor: default;
  47. }
  48. .basicContext__item--disabled {
  49. cursor: default;
  50. }
  51. .basicContext__data {
  52. min-width: 140px;
  53. padding-right: 20px;
  54. text-align: left;
  55. white-space: nowrap;
  56. }
  57. .basicContext__icon {
  58. display: inline-block;
  59. }
  60. .basicContext--scrollable {
  61. height: 100%;
  62. -webkit-overflow-scrolling: touch;
  63. overflow-y: auto;
  64. }
  65. .basicContext--scrollable .basicContext__data {
  66. min-width: 160px;
  67. }
  68.  
  69. /* default theme css */
  70. .basicContext {
  71. padding: 6px;
  72. background-color: #fff;
  73. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.2);
  74. border-radius: 3px;
  75. }
  76. .basicContext__item {
  77. margin-bottom: 2px;
  78. }
  79. .basicContext__item--separator {
  80. margin: 4px 0;
  81. background-color: rgba(0, 0, 0, 0.1);
  82. }
  83. .basicContext__item--disabled {
  84. opacity: 0.5;
  85. }
  86. .basicContext__item:last-child {
  87. margin-bottom: 0;
  88. }
  89. .basicContext__data {
  90. padding: 6px 8px;
  91. color: #333;
  92. border-radius: 2px;
  93. }
  94. .basicContext__item:not(.basicContext__item--disabled):hover
  95. .basicContext__data {
  96. color: #fff;
  97. background-color: #4393e6;
  98. }
  99. .basicContext__item:not(.basicContext__item--disabled):active
  100. .basicContext__data {
  101. background-color: #1d79d9;
  102. }
  103. .basicContext__icon {
  104. margin-right: 10px;
  105. width: 12px;
  106. text-align: center;
  107. }
  108.  
  109. /* [自定义] 自定义css样式 */
  110. .basicContextContainer{
  111. width: auto;
  112. height: auto;
  113. }
  114. .basicContext {
  115. padding: 0px;
  116. min-width: 150px;
  117. height: auto;
  118. background-color: rgba(241, 240, 240, 0.96);
  119. }
  120. .basicContext table{
  121. padding: 4px 0;
  122. width: 100%;
  123. }
  124. .basicContext__item {
  125. margin-bottom: 0px;
  126. }
  127. .basicContext__data {
  128. padding: 2px 12px;
  129. border-radius:0px;
  130. font-size: 13.8px;
  131. color: #3a383a;
  132. }
  133. .basicContext__item--separator {
  134. margin: 2px 0;
  135. }
  136. .basicContext__item:not(.basicContext__item--disabled):hover .basicContext__data {
  137. background-color: #4d92f8;
  138. }
  139. </style>
  140. `
  141. document.body.insertAdjacentHTML("beforeend", style);
  142. })();
  143.  
  144. "use strict";
  145. !(function (basicContext, callback) {
  146. "undefined" != typeof module && module.exports
  147. ? (module.exports = callback())
  148. : "function" == typeof define && define.amd
  149. ? define(callback)
  150. : (window[basicContext] = callback());
  151. })("basicContext", function () {
  152.  
  153. let overflow = null
  154.  
  155. const ITEM = 'item',
  156. SEPARATOR = 'separator'
  157.  
  158. const dom = function(elem = '') {
  159.  
  160. return document.querySelector('.basicContext ' + elem)
  161.  
  162. }
  163.  
  164. const valid = function(item = {}) {
  165.  
  166. let emptyItem = (Object.keys(item).length===0 ? true : false)
  167.  
  168. if (emptyItem===true) item.type = SEPARATOR
  169. if (item.type==null) item.type = ITEM
  170. if (item.class==null) item.class = ''
  171. if (item.visible!==false) item.visible = true
  172. if (item.icon==null) item.icon = null
  173. if (item.title==null) item.title = 'Undefined'
  174.  
  175. // Add disabled class when item disabled
  176. if (item.disabled!==true) item.disabled = false
  177. if (item.disabled===true) item.class += ' basicContext__item--disabled'
  178.  
  179. // Item requires a function when
  180. // it's not a separator and not disabled
  181. if (item.fn==null && item.type!==SEPARATOR && item.disabled===false) {
  182.  
  183. console.warn(`Missing fn for item '${ item.title }'`)
  184. return false
  185.  
  186. }
  187.  
  188. return true
  189.  
  190. }
  191.  
  192. const buildItem = function(item, num) {
  193.  
  194. let html = '',
  195. span = ''
  196.  
  197. // Parse and validate item
  198. if (valid(item)===false) return ''
  199.  
  200. // Skip when invisible
  201. if (item.visible===false) return ''
  202.  
  203. // Give item a unique number
  204. item.num = num
  205.  
  206. // Generate span/icon-element
  207. if (item.icon!==null) span = `<span class='basicContext__icon ${ item.icon }'></span>`
  208.  
  209. // [自定义]
  210. item.extAttr = item.extAttr || ""
  211.  
  212. // Generate item
  213. if (item.type===ITEM) {
  214.  
  215. html = `
  216. <tr class='basicContext__item ${ item.class }'>
  217. <td class='basicContext__data' data-num='${ item.num }' ${item.extAttr}>${ span }${ item.title }</td>
  218. </tr>
  219. `
  220.  
  221. } else if (item.type===SEPARATOR) {
  222.  
  223. html = `
  224. <tr class='basicContext__item basicContext__item--separator'></tr>
  225. `
  226.  
  227. }
  228.  
  229. return html
  230.  
  231. }
  232.  
  233. const build = function(items) {
  234.  
  235. let html = ''
  236.  
  237. html += `
  238. <div class='basicContextContainer'>
  239. <div class='basicContext'>
  240. <table cellspacing="0">
  241. <tbody>
  242. `
  243.  
  244. items.forEach((item, i) => html += buildItem(item, i))
  245.  
  246. html += `
  247. </tbody>
  248. </table>
  249. </div>
  250. </div>
  251. `
  252.  
  253. return html
  254.  
  255. }
  256.  
  257. const getNormalizedEvent = function(e = {}) {
  258.  
  259. let pos = {
  260. x : e.clientX,
  261. y : e.clientY
  262. }
  263.  
  264. if (e.type==='touchend' && (pos.x==null || pos.y==null)) {
  265.  
  266. // We need to capture clientX and clientY from original event
  267. // when the event 'touchend' does not return the touch position
  268.  
  269. let touches = e.changedTouches
  270.  
  271. if (touches!=null&&touches.length>0) {
  272. pos.x = touches[0].clientX
  273. pos.y = touches[0].clientY
  274. }
  275.  
  276. }
  277.  
  278. // Position unknown
  279. if (pos.x==null || pos.x < 0) pos.x = 0
  280. if (pos.y==null || pos.y < 0) pos.y = 0
  281.  
  282. return pos
  283.  
  284. }
  285.  
  286. const getPosition = function(e, context) {
  287.  
  288. // Get the click position
  289. let normalizedEvent = getNormalizedEvent(e)
  290.  
  291. // Set the initial position
  292. let x = normalizedEvent.x,
  293. y = normalizedEvent.y
  294.  
  295. // Get size of browser
  296. let browserSize = {
  297. width : window.innerWidth,
  298. height : window.innerHeight
  299. }
  300.  
  301. // Get size of context
  302. let contextSize = {
  303. width : context.offsetWidth,
  304. height : context.offsetHeight
  305. }
  306.  
  307. // Fix position based on context and browser size
  308. if ((x + contextSize.width) > browserSize.width) x = x - ((x + contextSize.width) - browserSize.width)
  309. if ((y + contextSize.height) > browserSize.height) y = y - ((y + contextSize.height) - browserSize.height)
  310.  
  311. // Make context scrollable and start at the top of the browser
  312. // when context is higher than the browser
  313. if (contextSize.height > browserSize.height) {
  314. y = 0
  315. context.classList.add('basicContext--scrollable')
  316. }
  317.  
  318. // Calculate the relative position of the mouse to the context
  319. let rx = normalizedEvent.x - x,
  320. ry = normalizedEvent.y - y
  321.  
  322. return { x, y, rx, ry }
  323.  
  324. }
  325.  
  326. const bind = function(item = {}) {
  327.  
  328. if (item.fn==null) return false
  329. if (item.visible===false) return false
  330. if (item.disabled===true) return false
  331.  
  332. dom(`td[data-num='${ item.num }']`).onclick = item.fn
  333. dom(`td[data-num='${ item.num }']`).oncontextmenu = item.fn
  334.  
  335. return true
  336.  
  337. }
  338.  
  339. const show = function(items, e, fnClose, fnCallback) {
  340. //[自定义] delete old menu
  341. let basicContextContainer = document.querySelector('.basicContextContainer');
  342. if(basicContextContainer){
  343. basicContextContainer.remove();
  344. }
  345.  
  346. // Build context
  347. let html = build(items)
  348.  
  349. // Add context to the body
  350. document.body.insertAdjacentHTML('beforeend', html)
  351.  
  352. // Save current overflow and block scrolling of site
  353. if (overflow==null) {
  354. overflow = document.body.style.overflow
  355. document.body.style.overflow = 'hidden'
  356. }
  357.  
  358. // Cache the context
  359. let context = dom()
  360.  
  361. // Calculate position
  362. let position = getPosition(e, context)
  363.  
  364. // Set position
  365. context.style.left = `${ position.x }px`
  366. context.style.top = `${ position.y }px`
  367. context.style.transformOrigin = `${ position.rx }px ${ position.ry }px`
  368. context.style.opacity = 1
  369.  
  370. // Close fn fallback
  371. if (fnClose==null) fnClose = close
  372.  
  373. // Bind click on background
  374. context.parentElement.onclick = fnClose
  375. context.parentElement.oncontextmenu = fnClose
  376.  
  377. // Bind click on items
  378. items.forEach(bind)
  379.  
  380. // Do not trigger default event or further propagation
  381. if (typeof e.preventDefault === 'function') e.preventDefault()
  382. if (typeof e.stopPropagation === 'function') e.stopPropagation()
  383.  
  384. // Call callback when a function
  385. if (typeof fnCallback === 'function') fnCallback()
  386.  
  387. return true
  388.  
  389. }
  390.  
  391. const visible = function() {
  392.  
  393. let elem = dom()
  394.  
  395. if (elem==null || elem.length===0) return false
  396. else return true
  397.  
  398. }
  399.  
  400. const close = function() {
  401.  
  402. if (visible()===false) return false
  403.  
  404. let container = document.querySelector('.basicContextContainer')
  405.  
  406. container.parentElement.removeChild(container)
  407.  
  408. // Reset overflow to its original value
  409. if (overflow!=null) {
  410. document.body.style.overflow = overflow
  411. overflow = null
  412. }
  413.  
  414. return true
  415.  
  416. }
  417.  
  418. return {
  419. ITEM,
  420. SEPARATOR,
  421. show,
  422. visible,
  423. close
  424. }
  425.  
  426. });
  427.  
  428. //[自定义] 解决basicContextContainer出现系统菜单的bug
  429. document.addEventListener('click', function(e){
  430. if(basicContext) basicContext.close();
  431. });
  432. document.addEventListener('contextmenu', function(e){
  433. if(['basicContextContainer','basicContext'].indexOf(e.target.className)!==-1) {
  434. e.preventDefault();
  435. return false;
  436. }
  437. });
  438.  
  439. //使用示例
  440. // const clicked = function(e) {
  441. // console.log(e.target.innerHTML);
  442. // }
  443. // document.querySelector('.my-context-menu-btn').addEventListener('contextmenu', function(e){
  444. // const items = [
  445. // { title: '新标签打开链接', extAttr: "data-name='new-blank'", fn: clicked },
  446. // { },
  447. // { title: '复制链接地址', extAttr: "data-name='copy-link'", fn: clicked },
  448. // { title: '复制选中的文本', extAttr: "data-name='copy-text'", fn: clicked, disabled: true },
  449. // { title: '复制响应数据', extAttr: "data-name='copy-response'", fn: clicked},
  450. // { },
  451. // { title: '复制为cURL格式', extAttr: "data-name='copy-curl'", fn: clicked},
  452. // { title: '复制为fetch格式', extAttr: "data-name='copy-fetch'", fn: clicked},
  453. // { title: '复制为await格式', extAttr: "data-name='copy-await'", fn: clicked},
  454. // { title: '复制为xhr格式', extAttr: "data-name='copy-xhr'", fn: clicked},
  455. // { title: '复制为分享链接', extAttr: "data-name='copy-share'", fn: clicked},
  456. // { },
  457. // { title: '删除该请求', extAttr: "data-name='del-request'", fn: clicked},
  458. // { title: '删除所有请求', extAttr: "data-name='del-all-request'", fn: clicked }
  459. // ]
  460. // basicContext.show(items, e);
  461. // });