Greasy Fork 支持简体中文。

GM_context

A html5 contextmenu library

目前為 2017-09-08 提交的版本,檢視 最新版本

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