GM_context

A html5 contextmenu library

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/33034/705360/GM_context.js

  1. // ==UserScript==
  2. // @name GM_context
  3. // @version 0.2.1
  4. // @description A html5 contextmenu library
  5. // @supportURL https://github.com/eight04/GM_context/issues
  6. // @license MIT
  7. // @author eight04 <eight04@gmail.com> (https://github.com/eight04)
  8. // @homepageURL https://github.com/eight04/GM_context
  9. // @compatible firefox >=8
  10. // @grant none
  11. // @include *
  12. // ==/UserScript==
  13. var GM_context = (function (exports) {
  14. 'use strict';
  15.  
  16. const EDITABLE_INPUT = {text: true, number: true, email: true, search: true, tel: true, url: true};
  17. const PROP_EXCLUDE = {parent: true, items: true, onclick: true, onchange: true};
  18.  
  19. let menus;
  20. let contextEvent;
  21. let contextSelection;
  22. let menuContainer;
  23. let isInit;
  24. let increaseNumber = 1;
  25.  
  26. function objectAssign(target, ref, exclude = {}) {
  27. for (const key in ref) {
  28. if (!exclude[key]) {
  29. target[key] = ref[key];
  30. }
  31. }
  32. return target;
  33. }
  34.  
  35. function init() {
  36. isInit = true;
  37. menus = new Set;
  38. document.addEventListener("contextmenu", e => {
  39. contextEvent = e;
  40. contextSelection = document.getSelection() + "";
  41. const context = getContext(e);
  42. const matchedMenus = [...menus]
  43. .filter(m =>
  44. (!m.context || m.context.some(c => context.has(c))) &&
  45. (!m.oncontext || m.oncontext(e) !== false)
  46. );
  47. if (!matchedMenus.length) return;
  48. const {el: container, destroy: destroyContainer} = createContainer(e);
  49. const removeMenus = [];
  50. for (const menu of matchedMenus) {
  51. if (!menu.isBuilt) {
  52. buildMenu(menu);
  53. }
  54. if (!menu.static) {
  55. updateLabel(menu.items);
  56. }
  57. removeMenus.push(appendMenu(container, menu));
  58. }
  59. setTimeout(() => {
  60. for (const removeMenu of removeMenus) {
  61. removeMenu();
  62. }
  63. destroyContainer();
  64. });
  65. });
  66. }
  67.  
  68. function inc() {
  69. return increaseNumber++;
  70. }
  71.  
  72. // check if there are dynamic label
  73. function checkStatic(menu) {
  74. return checkItems(menu.items);
  75. function checkItems(items) {
  76. for (const item of items) {
  77. if (item.label && item.label.includes("%s")) {
  78. return false;
  79. }
  80. if (item.items && checkItems(item.items)) {
  81. return false;
  82. }
  83. }
  84. return true;
  85. }
  86. }
  87.  
  88. function updateLabel(items) {
  89. for (const item of items) {
  90. if (item.label && item.el) {
  91. item.el.label = buildLabel(item.label);
  92. }
  93. if (item.items) {
  94. updateLabel(item.items);
  95. }
  96. }
  97. }
  98.  
  99. function createContainer(e) {
  100. let el = e.target;
  101. while (!el.contextMenu) {
  102. if (el == document.documentElement) {
  103. if (!menuContainer) {
  104. menuContainer = document.createElement("menu");
  105. menuContainer.type = "context";
  106. menuContainer.id = "gm-context-menu";
  107. document.body.appendChild(menuContainer);
  108. }
  109. el.setAttribute("contextmenu", menuContainer.id);
  110. break;
  111. }
  112. el = el.parentNode;
  113. }
  114. return {
  115. el: el.contextMenu,
  116. destroy() {
  117. if (el.contextMenu == menuContainer) {
  118. el.removeAttribute("contextmenu");
  119. }
  120. }
  121. };
  122. }
  123.  
  124. function getContext(e) {
  125. const el = e.target;
  126. const context = new Set;
  127. if (el.nodeName == "IMG") {
  128. context.add("image");
  129. }
  130. if (el.closest("a")) {
  131. context.add("link");
  132. }
  133. if (el.isContentEditable ||
  134. el.nodeName == "INPUT" && EDITABLE_INPUT[el.type] ||
  135. el.nodeName == "TEXTAREA"
  136. ) {
  137. context.add("editable");
  138. }
  139. if (!document.getSelection().isCollapsed) {
  140. context.add("selection");
  141. }
  142. if (!context.size) {
  143. context.add("page");
  144. }
  145. return context;
  146. }
  147.  
  148. function buildMenu(menu) {
  149. const el = buildItems(null, menu.items);
  150. menu.startEl = document.createComment(`<menu ${menu.id}>`);
  151. el.insertBefore(menu.startEl, el.childNodes[0]);
  152. menu.endEl = document.createComment("</menu>");
  153. el.appendChild(menu.endEl);
  154. if (menu.static == null) {
  155. menu.static = checkStatic(menu);
  156. }
  157. menu.frag = el;
  158. menu.isBuilt = true;
  159. }
  160.  
  161. function buildLabel(s) {
  162. return s.replace(/%s/g, contextSelection);
  163. }
  164.  
  165. // build item's element
  166. function buildItem(parent, item) {
  167. let el;
  168. item.parent = parent;
  169. if (item.type == "submenu") {
  170. el = document.createElement("menu");
  171. objectAssign(el, item, PROP_EXCLUDE);
  172. el.appendChild(buildItems(item, item.items));
  173. } else if (item.type == "separator") {
  174. el = document.createElement("hr");
  175. } else if (item.type == "checkbox") {
  176. el = document.createElement("menuitem");
  177. objectAssign(el, item, PROP_EXCLUDE);
  178. } else if (item.type == "radiogroup") {
  179. el = document.createDocumentFragment();
  180. item.id = `gm-context-radio-${inc()}`;
  181. item.startEl = document.createComment(`<radiogroup ${item.id}>`);
  182. el.appendChild(item.startEl);
  183. el.appendChild(buildItems(item, item.items));
  184. item.endEl = document.createComment("</radiogroup>");
  185. el.appendChild(item.endEl);
  186. } else if (parent && parent.type == "radiogroup") {
  187. el = document.createElement("menuitem");
  188. item.type = "radio";
  189. item.radiogroup = parent.id;
  190. objectAssign(el, item, PROP_EXCLUDE);
  191. } else {
  192. el = document.createElement("menuitem");
  193. objectAssign(el, item, PROP_EXCLUDE);
  194. }
  195. if (item.type !== "radiogroup") {
  196. item.el = el;
  197. buildHandler(item);
  198. }
  199. item.isBuilt = true;
  200. return el;
  201. }
  202.  
  203. function buildHandler(item) {
  204. if (item.type === "radiogroup") {
  205. if (item.onchange) {
  206. item.items.forEach(buildHandler);
  207. }
  208. } else if (item.type === "radio") {
  209. if (!item.el.onclick && (item.parent.onchange || item.onclick)) {
  210. item.el.onclick = () => {
  211. if (item.onclick) {
  212. item.onclick.call(item.el, contextEvent);
  213. }
  214. if (item.parent.onchange) {
  215. item.parent.onchange.call(item.el, contextEvent, item.value);
  216. }
  217. };
  218. }
  219. } else if (item.type === "checkbox") {
  220. if (!item.el.onclick && item.onclick) {
  221. item.el.onclick = () => {
  222. if (item.onclick) {
  223. item.onclick.call(item.el, contextEvent, item.el.checked);
  224. }
  225. };
  226. }
  227. } else {
  228. if (!item.el.onclick && item.onclick) {
  229. item.el.onclick = () => {
  230. if (item.onclick) {
  231. item.onclick.call(item.el, contextEvent);
  232. }
  233. };
  234. }
  235. }
  236. }
  237.  
  238. // build items' element
  239. function buildItems(parent, items) {
  240. const root = document.createDocumentFragment();
  241. for (const item of items) {
  242. root.appendChild(buildItem(parent, item));
  243. }
  244. return root;
  245. }
  246.  
  247. // attach menu to DOM
  248. function appendMenu(container, menu) {
  249. container.appendChild(menu.frag);
  250. return () => {
  251. const range = document.createRange();
  252. range.setStartBefore(menu.startEl);
  253. range.setEndAfter(menu.endEl);
  254. menu.frag = range.extractContents();
  255. };
  256. }
  257.  
  258. // add a menu
  259. function add(menu) {
  260. if (!isInit) {
  261. init();
  262. }
  263. menu.id = inc();
  264. menus.add(menu);
  265. }
  266.  
  267. // remove a menu
  268. function remove(menu) {
  269. menus.delete(menu);
  270. }
  271.  
  272. // update item's properties. If @changes includes an `items` key, it would replace item's children.
  273. function update(item, changes) {
  274. if (changes.type) {
  275. throw new Error("item type is not changable");
  276. }
  277. if (changes.items) {
  278. if (item.isBuilt) {
  279. item.items.forEach(removeElement);
  280. }
  281. item.items.length = 0;
  282. changes.items.forEach(i => addItem(item, i));
  283. delete changes.items;
  284. }
  285. Object.assign(item, changes);
  286. if (item.el) {
  287. buildHandler(item);
  288. objectAssign(item.el, changes, PROP_EXCLUDE);
  289. }
  290. }
  291.  
  292. // add an item to parent
  293. function addItem(parent, item, pos = parent.items.length) {
  294. if (parent.isBuilt) {
  295. const el = buildItem(parent, item);
  296. if (parent.el) {
  297. parent.el.insertBefore(el, parent.el.childNodes[pos]);
  298. } else {
  299. // search from end, so it would be faster to insert multiple item to end
  300. let ref = parent.endEl,
  301. i = pos < 0 ? -pos : parent.items.length - pos;
  302. while (i-- && ref) {
  303. ref = ref.previousSibling;
  304. }
  305. parent.startEl.parentNode.insertBefore(el, ref);
  306. }
  307. }
  308. parent.items.splice(pos, 0, item);
  309. }
  310.  
  311. // remove an item from parent
  312. function removeItem(parent, item) {
  313. const pos = parent.items.indexOf(item);
  314. parent.items.splice(pos, 1);
  315. if (item.isBuilt) {
  316. removeElement(item);
  317. }
  318. }
  319.  
  320. // remove item's element
  321. function removeElement(item) {
  322. if (item.el) {
  323. item.el.remove();
  324. } else {
  325. while (item.startEl.nextSibling != item.endEl) {
  326. item.startEl.nextSibling.remove();
  327. }
  328. item.startEl.remove();
  329. item.endEl.remove();
  330. }
  331. }
  332.  
  333. exports.add = add;
  334. exports.addItem = addItem;
  335. exports.buildMenu = buildMenu;
  336. exports.remove = remove;
  337. exports.removeItem = removeItem;
  338. exports.update = update;
  339.  
  340. return exports;
  341.  
  342. }({}));

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址