PageMenu.jsm 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. this.EXPORTED_SYMBOLS = ["PageMenu"];
  5. this.PageMenu = function PageMenu() {
  6. }
  7. PageMenu.prototype = {
  8. PAGEMENU_ATTR: "pagemenu",
  9. GENERATEDITEMID_ATTR: "generateditemid",
  10. _popup: null,
  11. _builder: null,
  12. // Given a target node, get the context menu for it or its ancestor.
  13. getContextMenu: function(aTarget) {
  14. let target = aTarget;
  15. while (target) {
  16. let contextMenu = target.contextMenu;
  17. if (contextMenu) {
  18. return contextMenu;
  19. }
  20. target = target.parentNode;
  21. }
  22. return null;
  23. },
  24. // Given a target node, generate a JSON object for any context menu
  25. // associated with it, or null if there is no context menu.
  26. maybeBuild: function(aTarget) {
  27. let pageMenu = this.getContextMenu(aTarget);
  28. if (!pageMenu) {
  29. return null;
  30. }
  31. pageMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
  32. pageMenu.sendShowEvent();
  33. // the show event is not cancelable, so no need to check a result here
  34. this._builder = pageMenu.createBuilder();
  35. if (!this._builder) {
  36. return null;
  37. }
  38. pageMenu.build(this._builder);
  39. // This serializes then parses again, however this could be avoided in
  40. // the single-process case with further improvement.
  41. let menuString = this._builder.toJSONString();
  42. if (!menuString) {
  43. return null;
  44. }
  45. return JSON.parse(menuString);
  46. },
  47. // Given a JSON menu object and popup, add the context menu to the popup.
  48. buildAndAttachMenuWithObject: function(aMenu, aBrowser, aPopup) {
  49. if (!aMenu) {
  50. return false;
  51. }
  52. let insertionPoint = this.getInsertionPoint(aPopup);
  53. if (!insertionPoint) {
  54. return false;
  55. }
  56. let fragment = aPopup.ownerDocument.createDocumentFragment();
  57. this.buildXULMenu(aMenu, fragment);
  58. let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR);
  59. if (pos == "start") {
  60. insertionPoint.insertBefore(fragment,
  61. insertionPoint.firstChild);
  62. } else if (pos.startsWith("#")) {
  63. insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos));
  64. } else {
  65. insertionPoint.appendChild(fragment);
  66. }
  67. this._popup = aPopup;
  68. this._popup.addEventListener("command", this);
  69. this._popup.addEventListener("popuphidden", this);
  70. return true;
  71. },
  72. // Construct the XUL menu structure for a given JSON object.
  73. buildXULMenu: function(aNode, aElementForAppending) {
  74. let document = aElementForAppending.ownerDocument;
  75. let children = aNode.children;
  76. for (let child of children) {
  77. let menuitem;
  78. switch (child.type) {
  79. case "menuitem":
  80. if (!child.id) {
  81. continue; // Ignore children without ids
  82. }
  83. menuitem = document.createElement("menuitem");
  84. if (child.checkbox) {
  85. menuitem.setAttribute("type", "checkbox");
  86. if (child.checked) {
  87. menuitem.setAttribute("checked", "true");
  88. }
  89. }
  90. if (child.label) {
  91. menuitem.setAttribute("label", child.label);
  92. }
  93. if (child.icon) {
  94. menuitem.setAttribute("image", child.icon);
  95. menuitem.className = "menuitem-iconic";
  96. }
  97. if (child.disabled) {
  98. menuitem.setAttribute("disabled", true);
  99. }
  100. break;
  101. case "separator":
  102. menuitem = document.createElement("menuseparator");
  103. break;
  104. case "menu":
  105. menuitem = document.createElement("menu");
  106. if (child.label) {
  107. menuitem.setAttribute("label", child.label);
  108. }
  109. let menupopup = document.createElement("menupopup");
  110. menuitem.appendChild(menupopup);
  111. this.buildXULMenu(child, menupopup);
  112. break;
  113. }
  114. menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0);
  115. aElementForAppending.appendChild(menuitem);
  116. }
  117. },
  118. // Called when the generated menuitem is executed.
  119. handleEvent: function(event) {
  120. let type = event.type;
  121. let target = event.target;
  122. if (type == "command" && target.hasAttribute(this.GENERATEDITEMID_ATTR)) {
  123. // If a builder is assigned, call click on it directly. Otherwise, this is
  124. // likely a menu with data from another process, so send a message to the
  125. // browser to execute the menuitem.
  126. if (this._builder) {
  127. this._builder.click(target.getAttribute(this.GENERATEDITEMID_ATTR));
  128. }
  129. } else if (type == "popuphidden" && this._popup == target) {
  130. this.removeGeneratedContent(this._popup);
  131. this._popup.removeEventListener("popuphidden", this);
  132. this._popup.removeEventListener("command", this);
  133. this._popup = null;
  134. this._builder = null;
  135. }
  136. },
  137. // Get the first child of the given element with the given tag name.
  138. getImmediateChild: function(element, tag) {
  139. let child = element.firstChild;
  140. while (child) {
  141. if (child.localName == tag) {
  142. return child;
  143. }
  144. child = child.nextSibling;
  145. }
  146. return null;
  147. },
  148. // Return the location where the generated items should be inserted into the
  149. // given popup. They should be inserted as the next sibling of the returned
  150. // element.
  151. getInsertionPoint: function(aPopup) {
  152. if (aPopup.hasAttribute(this.PAGEMENU_ATTR))
  153. return aPopup;
  154. let element = aPopup.firstChild;
  155. while (element) {
  156. if (element.localName == "menu") {
  157. let popup = this.getImmediateChild(element, "menupopup");
  158. if (popup) {
  159. let result = this.getInsertionPoint(popup);
  160. if (result) {
  161. return result;
  162. }
  163. }
  164. }
  165. element = element.nextSibling;
  166. }
  167. return null;
  168. },
  169. // Returns true if custom menu items were present.
  170. maybeBuildAndAttachMenu: function(aTarget, aPopup) {
  171. let menuObject = this.maybeBuild(aTarget);
  172. if (!menuObject) {
  173. return false;
  174. }
  175. return this.buildAndAttachMenuWithObject(menuObject, null, aPopup);
  176. },
  177. // Remove the generated content from the given popup.
  178. removeGeneratedContent: function(aPopup) {
  179. let ungenerated = [];
  180. ungenerated.push(aPopup);
  181. let count;
  182. while (0 != (count = ungenerated.length)) {
  183. let last = count - 1;
  184. let element = ungenerated[last];
  185. ungenerated.splice(last, 1);
  186. let i = element.childNodes.length;
  187. while (i-- > 0) {
  188. let child = element.childNodes[i];
  189. if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) {
  190. ungenerated.push(child);
  191. continue;
  192. }
  193. element.removeChild(child);
  194. }
  195. }
  196. }
  197. }