GM_context

A html5 contextmenu library

当前为 2017-09-08 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name GM_context
  3. // @version 0.1.0
  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. // @namespace https://greasyfork.org/users/813
  12. // ==/UserScript==
  13.  
  14. /* exported GM_context */
  15.  
  16. const GM_context = (function() {
  17. const EDITABLE_INPUT = [
  18. "text", "number", "email", "number", "search", "tel", "url"
  19. ];
  20. const menus = new Map;
  21. const inc = function() {
  22. let i = 1;
  23. return () => i++;
  24. }();
  25. let contextEvent;
  26. let contextSelection;
  27. let menuContainer;
  28. document.addEventListener("contextmenu", e => {
  29. contextEvent = e;
  30. contextSelection = document.getSelection() + "";
  31. const context = getContext(e);
  32. const matchedMenus = [...menus.values()]
  33. .filter(m => !m.context || m.context.some(c => context.has(c)));
  34. if (!matchedMenus.length) return;
  35. const {el: container, destroy: destroyContainer} = createContainer(e);
  36. const removeMenus = [];
  37. for (const menu of matchedMenus) {
  38. if (!menu.el) {
  39. buildMenu(menu);
  40. }
  41. if (!menu.static) {
  42. updateMenu(menu);
  43. }
  44. removeMenus.push(appendMenu(container, menu));
  45. }
  46. setTimeout(() => {
  47. for (const removeMenu of removeMenus) {
  48. removeMenu();
  49. }
  50. destroyContainer();
  51. });
  52. });
  53. function updateMenu(menu) {
  54. // update label
  55. updateItems(menu.items);
  56. }
  57. function checkStatic(menu) {
  58. return checkItems(menu.items);
  59. function checkItems(items) {
  60. for (const item of items) {
  61. if (item.label && item.label.includes("%s")) {
  62. return false;
  63. }
  64. if (item.items && checkItems(item.items)) {
  65. return false;
  66. }
  67. }
  68. return true;
  69. }
  70. }
  71. function updateItems(items) {
  72. for (const item of items) {
  73. if (item.label && item.el) {
  74. item.el.label = buildLabel(item.label);
  75. }
  76. if (item.items) {
  77. updateItems(item.items);
  78. }
  79. }
  80. }
  81. function createContainer(e) {
  82. let el = e.target;
  83. while (!el.contextMenu) {
  84. if (el == document.documentElement) {
  85. if (!menuContainer) {
  86. menuContainer = document.createElement("menu");
  87. menuContainer.type = "context";
  88. menuContainer.id = "gm-context-menu";
  89. document.body.appendChild(menuContainer);
  90. }
  91. el.setAttribute("contextmenu", menuContainer.id);
  92. break;
  93. }
  94. el = el.parentNode;
  95. }
  96. return {
  97. el: el.contextMenu,
  98. destroy() {
  99. if (el.contextMenu == menuContainer) {
  100. el.removeAttribute("contextmenu");
  101. }
  102. }
  103. };
  104. }
  105. function getContext(e) {
  106. const el = e.target;
  107. const context = new Set;
  108. if (el.nodeName == "IMG") {
  109. context.add("image");
  110. }
  111. if (el.closest("a")) {
  112. context.add("link");
  113. }
  114. if (el.isContentEditable ||
  115. el.nodeName == "INPUT" && EDITABLE_INPUT.includes(el.type) ||
  116. el.nodeName == "TEXTAREA"
  117. ) {
  118. context.add("editable");
  119. }
  120. if (!document.getSelection().isCollapsed) {
  121. context.add("selection");
  122. }
  123. if (!context.size) {
  124. context.add("page");
  125. }
  126. return context;
  127. }
  128. function buildMenu(menu) {
  129. menu.el = buildItems(menu.items);
  130. menu.startEl = menu.el.firstChild;
  131. menu.endEl = menu.el.lastChild;
  132. menu.static = checkStatic(menu);
  133. }
  134. function buildLabel(s) {
  135. return s.replace(/%s/g, contextSelection);
  136. }
  137. function buildItems(items) {
  138. const root = document.createDocumentFragment();
  139. for (const item of items) {
  140. let el;
  141. if (item.type == "submenu") {
  142. el = document.createElement("menu");
  143. Object.assign(el, item, {items: null});
  144. el.appendChild(buildItems(item.items));
  145. } else if (item.type == "separator") {
  146. el = document.createElement("hr");
  147. } else if (item.type == "checkbox") {
  148. el = document.createElement("menuitem");
  149. Object.assign(el, item);
  150. if (item.onclick) {
  151. el.onclick = () => {
  152. item.onclick.call(el, contextEvent, el.checked);
  153. };
  154. }
  155. } else if (item.type == "radiogroup") {
  156. item.id = `gm-context-radio-${inc()}`;
  157. el = document.createDocumentFragment();
  158. for (const i of item.items) {
  159. const iEl = document.createElement("menuitem");
  160. iEl.type = "radio";
  161. iEl.radiogroup = item.id;
  162. Object.assign(iEl, i);
  163. if (item.onchange) {
  164. iEl.onclick = () => {
  165. item.onchange.call(iEl, contextEvent, i.value);
  166. };
  167. }
  168. i.el = iEl;
  169. el.appendChild(iEl);
  170. }
  171. } else {
  172. el = document.createElement("menuitem");
  173. Object.assign(el, item);
  174. if (item.onclick) {
  175. el.onclick = () => {
  176. item.onclick.call(el, contextEvent);
  177. };
  178. }
  179. }
  180. if (item.type != "radiogroup") {
  181. item.el = el;
  182. }
  183. root.appendChild(el);
  184. }
  185. return root;
  186. }
  187. function appendMenu(container, menu) {
  188. container.appendChild(menu.el);
  189. return () => {
  190. const range = document.createRange();
  191. range.setStartBefore(menu.startEl);
  192. range.setEndAfter(menu.endEl);
  193. menu.el = range.extractContents();
  194. };
  195. }
  196. function add(menu) {
  197. menu.id = inc();
  198. menus.set(menu.id, menu);
  199. }
  200. function remove(id) {
  201. menus.delete(id);
  202. }
  203. return {add, remove};
  204. })();