1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
3 * ***** BEGIN LICENSE BLOCK *****
4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 * The contents of this file are subject to the Mozilla Public License Version
7 * 1.1 (the "License"); you may not use this file except in compliance with
8 * the License. You may obtain a copy of the License at
9 * http://www.mozilla.org/MPL/
11 * Software distributed under the License is distributed on an "AS IS" basis,
12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 * for the specific language governing rights and limitations under the
16 * The Original Code is The JavaScript Debugger.
18 * The Initial Developer of the Original Code is
19 * Netscape Communications Corporation.
20 * Portions created by the Initial Developer are Copyright (C) 1998
21 * the Initial Developer. All Rights Reserved.
24 * Robert Ginda, <rginda@netscape.com>, original author
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
38 * ***** END LICENSE BLOCK ***** */
40 function MenuManager (commandManager, menuSpecs, contextFunction, commandStr)
42 var menuManager = this;
44 this.commandManager = commandManager;
45 this.menuSpecs = menuSpecs;
46 this.contextFunction = contextFunction;
47 this.commandStr = commandStr;
51 function mmgr_onshow (event) { return menuManager.showPopup (event); };
53 function mmgr_onhide (event) { return menuManager.hidePopup (event); };
55 function mmgr_oncmd (event) { return menuManager.menuCommand (event); };
58 MenuManager.prototype.appendMenuItems =
59 function mmgr_append(menuId, items)
61 for (var i = 0; i < items.length; ++i)
62 this.menuSpecs[menuId].items.push(items[i]);
65 MenuManager.prototype.createContextMenus =
66 function mmgr_initcxs (document)
68 for (var id in this.menuSpecs)
70 if (id.indexOf("context:") == 0)
71 this.createContextMenu(document, id);
75 MenuManager.prototype.createContextMenu =
76 function mmgr_initcx (document, id)
78 if (!document.getElementById(id))
80 if (!ASSERT(id in this.menuSpecs, "unknown context menu " + id))
83 var dp = document.getElementById("dynamic-popups");
84 var popup = this.appendPopupMenu (dp, null, id, id);
85 var items = this.menuSpecs[id].items;
86 this.createMenuItems (popup, null, items);
88 if (!("uiElements" in this.menuSpecs[id]))
89 this.menuSpecs[id].uiElements = [popup];
90 else if (!arrayContains(this.menuSpecs[id].uiElements, popup))
91 this.menuSpecs[id].uiElements.push(popup);
96 MenuManager.prototype.createMenus =
97 function mmgr_createtb(document, menuid)
99 var menu = document.getElementById(menuid);
100 for (id in this.menuSpecs)
103 if ("domID" in this.menuSpecs[id])
104 domID = this.menuSpecs[id].domID;
108 if (id.indexOf(menuid + ":") == 0)
109 this.createMenu(menu, null, id, domID);
113 MenuManager.prototype.createMainToolbar =
114 function mmgr_createtb(document, id)
116 var toolbar = document.getElementById(id);
117 var spec = this.menuSpecs[id];
118 for (var i in spec.items)
120 this.appendToolbarItem (toolbar, null, spec.items[i]);
123 toolbar.className = "toolbar-primary chromeclass-toolbar";
126 MenuManager.prototype.updateMenus =
127 function mmgr_updatemenus(document, menus)
129 // Cope with one string (update just the one menu)...
130 if (isinstance(menus, String))
134 // Or nothing/nonsense (update everything).
135 else if ((typeof menus != "object") || !isinstance(menus, Array))
138 for (var k in this.menuSpecs)
140 if ((/^(mainmenu|context)/).test(k))
145 var menuBar = document.getElementById("mainmenu");
147 // Loop through this array and update everything we need to.
148 for (var i = 0; i < menus.length; i++)
151 if (!(id in this.menuSpecs))
153 var menu = this.menuSpecs[id];
155 if ("domID" in this.menuSpecs[id])
156 domID = this.menuSpecs[id].domID;
160 // Context menus need to be deleted in order to be regenerated...
161 if ((/^context/).test(id))
164 if ((cxMenuNode = document.getElementById(id)))
165 cxMenuNode.parentNode.removeChild(cxMenuNode);
166 this.createContextMenu(document, id);
168 else if ((/^mainmenu/).test(id) &&
169 !("uiElements" in this.menuSpecs[id]))
171 this.createMenu(menuBar, null, id, domID);
174 else if ((/^(mainmenu|popup)/).test(id) &&
175 ("uiElements" in this.menuSpecs[id]))
177 for (var j = 0; j < menu.uiElements.length; j++)
179 var node = menu.uiElements[j];
180 domID = node.parentNode.id;
181 // Clear the menu node.
182 while (node.lastChild)
183 node.removeChild(node.lastChild);
185 this.createMenu(node.parentNode.parentNode,
186 node.parentNode.nextSibling,
199 * Registers event handlers on a given menu.
201 MenuManager.prototype.hookPopup =
202 function mmgr_hookpop (node)
204 node.addEventListener ("popupshowing", this.onPopupShowing, false);
205 node.addEventListener ("popuphiding", this.onPopupHiding, false);
211 * |showPopup| is called from the "onpopupshowing" event of menus managed
212 * by the CommandManager. If a command is disabled, represents a command
213 * that cannot be "satisfied" by the current command context |cx|, or has an
214 * "enabledif" attribute that eval()s to false, then the menuitem is disabled.
215 * In addition "checkedif" and "visibleif" attributes are eval()d and
216 * acted upon accordingly.
218 MenuManager.prototype.showPopup =
219 function mmgr_showpop (event)
221 /* returns true if the command context has the properties required to
222 * execute the command associated with |menuitem|.
226 if (menuitem.hasAttribute("isSeparator") ||
227 !menuitem.hasAttribute("commandname"))
232 if (menuitem.hasAttribute("repeatfor"))
235 if (!("menuManager" in cx))
237 dd ("no menuManager in cx");
241 var name = menuitem.getAttribute("commandname");
242 var commandManager = cx.menuManager.commandManager;
243 var commands = commandManager.commands;
245 if (!ASSERT (name in commands,
246 "menu contains unknown command '" + name + "'"))
251 var rv = commandManager.isCommandSatisfied(cx, commands[name]);
252 delete cx.parseError;
256 /* Convenience function for "enabledif", etc, attributes. */
262 /* evals the attribute named |attr| on the node |node|. */
263 function evalIfAttribute (node, attr)
266 var expr = node.getAttribute(attr);
270 expr = expr.replace (/\Wand\W/gi, " && ");
271 expr = expr.replace (/\Wor\W/gi, " || ");
275 return eval("(" + expr + ")");
279 dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
280 attr + "': '" + expr + "'\n" + ex);
285 /* evals the attribute named |attr| on the node |node|. */
286 function evalAttribute(node, attr)
289 var expr = node.getAttribute(attr);
299 dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
300 attr + "': '" + expr + "'\n" + ex);
306 var popup = event.originalTarget;
308 /* If the host provided a |contextFunction|, use it now. Remember the
309 * return result as this.cx for use if something from this menu is actually
310 * dispatched. this.cx is deleted in |hidePopup|. */
311 if (typeof this.contextFunction == "function")
313 cx = this.cx = this.contextFunction (popup.getAttribute("menuName"),
318 cx = this.cx = { menuManager: this, originalEvent: event };
321 var menuitem = popup.firstChild;
324 if (!menuitem.hasAttribute("repeatfor"))
327 // Remove auto-generated items (located prior to real item).
328 while (menuitem.previousSibling &&
329 menuitem.previousSibling.hasAttribute("repeatgenerated"))
331 menuitem.parentNode.removeChild(menuitem.previousSibling);
334 if (!("repeatList" in cx))
335 cx.repeatList = new Object();
337 // Get the array of new items to add.
338 var ary = evalAttribute(menuitem, "repeatfor");
340 if ((typeof ary != "object") || !isinstance(ary, Array))
343 /* The item itself should only be shown if there's no items in the
344 * array - this base item is always disabled.
347 menuitem.setAttribute("hidden", "true");
349 menuitem.removeAttribute("hidden");
351 // Save the array in the context object.
352 cx.repeatList[menuitem.getAttribute("repeatid")] = ary;
354 // Get the max. number of items we're allowed to show from |ary|.
355 var limit = evalAttribute(menuitem, "repeatlimit");
356 // Make sure we've got a number at all...
357 if (typeof limit != "number")
359 // ...and make sure it's no higher than |ary.length|.
360 limit = Math.min(ary.length, limit);
362 var cmd = menuitem.getAttribute("commandname");
363 var props = { repeatgenerated: true, repeatindex: -1,
364 repeatid: menuitem.getAttribute("repeatid"),
365 repeatmap: menuitem.getAttribute("repeatmap") };
367 /* Clone non-repeat attributes. All attributes except those starting
368 * with 'repeat', and those matching 'hidden' or 'disabled' are saved
369 * to |props|, which is then supplied to |appendMenuItem| later.
371 for (var i = 0; i < menuitem.attributes.length; i++)
373 var name = menuitem.attributes[i].nodeName;
374 if (!name.match(/^(repeat|(hidden|disabled)$)/))
375 props[name] = menuitem.getAttribute(name);
378 for (i = 0; i < limit; i++)
380 props.repeatindex = i;
381 this.appendMenuItem(popup, menuitem, cmd, props);
383 } while ((menuitem = menuitem.nextSibling));
385 menuitem = popup.firstChild;
388 if (menuitem.hasAttribute("repeatgenerated") &&
389 menuitem.hasAttribute("repeatmap"))
391 cx.index = menuitem.getAttribute("repeatindex");
392 ary = cx.repeatList[menuitem.getAttribute("repeatid")];
393 var item = ary[cx.index];
394 evalAttribute(menuitem, "repeatmap");
397 /* should it be visible? */
398 if (menuitem.hasAttribute("visibleif"))
400 if (evalIfAttribute(menuitem, "visibleif"))
401 menuitem.removeAttribute ("hidden");
404 menuitem.setAttribute ("hidden", "true");
409 /* it's visible, maybe it has a dynamic label? */
410 if (menuitem.hasAttribute("format"))
412 var label = replaceVars(menuitem.getAttribute("format"), cx);
413 if (label.indexOf("\$") != -1)
414 label = menuitem.getAttribute("backupLabel");
415 menuitem.setAttribute("label", label);
418 /* ok, it's visible, maybe it should be disabled? */
421 if (menuitem.hasAttribute("enabledif"))
423 if (evalIfAttribute(menuitem, "enabledif"))
424 menuitem.removeAttribute ("disabled");
426 menuitem.setAttribute ("disabled", "true");
429 menuitem.removeAttribute ("disabled");
433 menuitem.setAttribute ("disabled", "true");
436 /* should it have a check? */
437 if (menuitem.hasAttribute("checkedif"))
439 if (evalIfAttribute(menuitem, "checkedif"))
440 menuitem.setAttribute ("checked", "true");
442 menuitem.removeAttribute ("checked");
444 } while ((menuitem = menuitem.nextSibling));
452 * |hidePopup| is called from the "onpopuphiding" event of menus
453 * managed by the CommandManager. Nothing to do here anymore.
454 * We used to just clean up this.cx, but that's a problem for nested
457 MenuManager.prototype.hidePopup =
458 function mmgr_hidepop (id)
463 MenuManager.prototype.menuCommand =
464 function mmgr_menucmd(event)
466 /* evals the attribute named |attr| on the node |node|. */
467 function evalAttribute(node, attr)
470 var expr = node.getAttribute(attr);
480 dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
481 attr + "': '" + expr + "'\n" + ex);
486 var menuitem = event.originalTarget;
488 /* We need to re-run the repeat-map if the user has selected a special
489 * repeat-generated menu item, so that the context object is correct.
491 if (menuitem.hasAttribute("repeatgenerated") &&
492 menuitem.hasAttribute("repeatmap"))
494 cx.index = menuitem.getAttribute("repeatindex");
495 var ary = cx.repeatList[menuitem.getAttribute("repeatid")];
496 var item = ary[cx.index];
497 evalAttribute(menuitem, "repeatmap");
500 eval(this.commandStr);
505 * Appends a sub-menu to an existing menu.
506 * @param parentNode DOM Node to insert into
507 * @param beforeNode DOM Node already contained by parentNode, to insert before
508 * @param id ID of the sub-menu to add.
509 * @param label Text to use for this sub-menu. The & character can be
510 * used to indicate the accesskey.
511 * @param attribs Object containing CSS attributes to set on the element.
513 MenuManager.prototype.appendSubMenu =
514 function mmgr_addsmenu (parentNode, beforeNode, menuName, domId, label, attribs)
516 var document = parentNode.ownerDocument;
518 /* sometimes the menu is already there, for overlay purposes. */
519 var menu = document.getElementById(domId);
523 menu = document.createElement ("menu");
524 menu.setAttribute ("id", domId);
527 var menupopup = menu.firstChild;
531 menupopup = document.createElement ("menupopup");
532 menupopup.setAttribute ("id", domId + "-popup");
533 menu.appendChild(menupopup);
534 menupopup = menu.firstChild;
537 menupopup.setAttribute ("menuName", menuName);
539 menu.setAttribute ("accesskey", getAccessKey(label));
540 label = label.replace("&", "");
541 menu.setAttribute ("label", label);
542 menu.setAttribute ("isSeparator", true);
544 // Only attach the menu if it's not there already. This can't be in the
545 // if (!menu) block because the updateMenus code clears toplevel menus,
546 // orphaning the submenus, to (parts of?) which we keep handles in the
547 // uiElements array. See the updateMenus code.
548 if (!menu.parentNode)
549 parentNode.insertBefore(menu, beforeNode);
551 if (typeof attribs == "object")
553 for (var p in attribs)
554 menu.setAttribute (p, attribs[p]);
557 this.hookPopup (menupopup);
563 * Appends a popup to an existing popupset.
564 * @param parentNode DOM Node to insert into
565 * @param beforeNode DOM Node already contained by parentNode, to insert before
566 * @param id ID of the popup to add.
567 * @param label Text to use for this popup. Popup menus don't normally have
568 * labels, but we set a "label" attribute anyway, in case
569 * the host wants it for some reason. Any "&" characters will
571 * @param attribs Object containing CSS attributes to set on the element.
573 MenuManager.prototype.appendPopupMenu =
574 function mmgr_addpmenu (parentNode, beforeNode, menuName, id, label, attribs)
576 var document = parentNode.ownerDocument;
577 var popup = document.createElement ("popup");
578 popup.setAttribute ("id", id);
580 popup.setAttribute ("label", label.replace("&", ""));
581 if (typeof attribs == "object")
583 for (var p in attribs)
584 popup.setAttribute (p, attribs[p]);
587 popup.setAttribute ("menuName", menuName);
589 parentNode.insertBefore(popup, beforeNode);
590 this.hookPopup (popup);
596 * Appends a menuitem to an existing menu or popup.
597 * @param parentNode DOM Node to insert into
598 * @param beforeNode DOM Node already contained by parentNode, to insert before
599 * @param command A reference to the CommandRecord this menu item will represent.
600 * @param attribs Object containing CSS attributes to set on the element.
602 MenuManager.prototype.appendMenuItem =
603 function mmgr_addmenu (parentNode, beforeNode, commandName, attribs)
605 var menuManager = this;
607 var document = parentNode.ownerDocument;
608 if (commandName == "-")
609 return this.appendMenuSeparator(parentNode, beforeNode, attribs);
611 var parentId = parentNode.getAttribute("id");
613 if (!ASSERT(commandName in this.commandManager.commands,
614 "unknown command " + commandName + " targeted for " +
620 var command = this.commandManager.commands[commandName];
621 var menuitem = document.createElement ("menuitem");
622 menuitem.setAttribute ("id", parentId + ":" + commandName);
623 menuitem.setAttribute ("commandname", command.name);
624 menuitem.setAttribute ("key", "key:" + command.name);
625 menuitem.setAttribute ("accesskey", getAccessKey(command.label));
626 var label = command.label.replace("&", "");
627 menuitem.setAttribute ("label", label);
630 menuitem.setAttribute("format", command.format);
631 menuitem.setAttribute("backupLabel", label);
634 if ((typeof attribs == "object") && attribs)
636 for (var p in attribs)
637 menuitem.setAttribute (p, attribs[p]);
638 if ("repeatfor" in attribs)
639 menuitem.setAttribute("repeatid", this.repeatId++);
642 command.uiElements.push(menuitem);
643 parentNode.insertBefore (menuitem, beforeNode);
644 /* It seems, bob only knows why, that this must be done AFTER the node is
645 * added to the document.
647 menuitem.addEventListener("command", this.onMenuCommand, false);
653 * Appends a menuseparator to an existing menu or popup.
654 * @param parentNode DOM Node to insert into
655 * @param beforeNode DOM Node already contained by parentNode, to insert before
656 * @param attribs Object containing CSS attributes to set on the element.
658 MenuManager.prototype.appendMenuSeparator =
659 function mmgr_addsep (parentNode, beforeNode, attribs)
661 var document = parentNode.ownerDocument;
662 var menuitem = document.createElement ("menuseparator");
663 menuitem.setAttribute ("isSeparator", true);
664 if (typeof attribs == "object")
666 for (var p in attribs)
667 menuitem.setAttribute (p, attribs[p]);
669 parentNode.insertBefore (menuitem, beforeNode);
675 * Appends a toolbaritem to an existing box element.
676 * @param parentNode DOM Node to insert into
677 * @param beforeNode DOM Node already contained by parentNode, to insert before
678 * @param command A reference to the CommandRecord this toolbaritem will
680 * @param attribs Object containing CSS attributes to set on the element.
682 MenuManager.prototype.appendToolbarItem =
683 function mmgr_addtb (parentNode, beforeNode, commandName, attribs)
685 if (commandName == "-")
686 return this.appendToolbarSeparator(parentNode, beforeNode, attribs);
688 var parentId = parentNode.getAttribute("id");
690 if (!ASSERT(commandName in this.commandManager.commands,
691 "unknown command " + commandName + " targeted for " +
697 var command = this.commandManager.commands[commandName];
698 var document = parentNode.ownerDocument;
699 var tbitem = document.createElement ("toolbarbutton");
701 var id = parentNode.getAttribute("id") + ":" + commandName;
702 tbitem.setAttribute ("id", id);
703 tbitem.setAttribute ("class", "toolbarbutton-1");
705 tbitem.setAttribute ("tooltiptext", command.tip);
706 tbitem.setAttribute ("label", command.label.replace("&", ""));
707 tbitem.setAttribute ("oncommand",
708 "dispatch('" + commandName + "');");
709 if (typeof attribs == "object")
711 for (var p in attribs)
712 tbitem.setAttribute (p, attribs[p]);
715 command.uiElements.push(tbitem);
716 parentNode.insertBefore (tbitem, beforeNode);
722 * Appends a toolbarseparator to an existing box.
723 * @param parentNode DOM Node to insert into
724 * @param beforeNode DOM Node already contained by parentNode, to insert before
725 * @param attribs Object containing CSS attributes to set on the element.
727 MenuManager.prototype.appendToolbarSeparator =
728 function mmgr_addmenu (parentNode, beforeNode, attribs)
730 var document = parentNode.ownerDocument;
731 var tbitem = document.createElement ("toolbarseparator");
732 tbitem.setAttribute ("isSeparator", true);
733 if (typeof attribs == "object")
735 for (var p in attribs)
736 tbitem.setAttribute (p, attribs[p]);
738 parentNode.appendChild (tbitem);
744 * Creates menu DOM nodes from a menu specification.
745 * @param parentNode DOM Node to insert into
746 * @param beforeNode DOM Node already contained by parentNode, to insert before
747 * @param menuSpec array of menu items
749 MenuManager.prototype.createMenu =
750 function mmgr_newmenu (parentNode, beforeNode, menuName, domId, attribs)
752 if (typeof domId == "undefined")
755 if (!ASSERT(menuName in this.menuSpecs, "unknown menu name " + menuName))
758 var menuSpec = this.menuSpecs[menuName];
760 var subMenu = this.appendSubMenu (parentNode, beforeNode, menuName, domId,
761 menuSpec.label, attribs);
763 // Keep track where we're adding popup nodes derived from some menuSpec
764 if (!("uiElements" in this.menuSpecs[menuName]))
765 this.menuSpecs[menuName].uiElements = [subMenu];
766 else if (!arrayContains(this.menuSpecs[menuName].uiElements, subMenu))
767 this.menuSpecs[menuName].uiElements.push(subMenu);
769 this.createMenuItems (subMenu, null, menuSpec.items);
773 MenuManager.prototype.createMenuItems =
774 function mmgr_newitems (parentNode, beforeNode, menuItems)
776 function itemAttribs()
778 return (1 in menuItems[i]) ? menuItems[i][1] : null;
781 var parentId = parentNode.getAttribute("id");
783 for (var i in menuItems)
785 var itemName = menuItems[i][0];
786 if (itemName[0] == ">")
788 itemName = itemName.substr(1);
789 if (!ASSERT(itemName in this.menuSpecs,
790 "unknown submenu " + itemName + " referenced in " +
795 this.createMenu (parentNode, beforeNode, itemName,
796 parentId + ":" + itemName, itemAttribs());
798 else if (itemName in this.commandManager.commands)
800 this.appendMenuItem (parentNode, beforeNode, itemName,
803 else if (itemName == "-")
805 this.appendMenuSeparator (parentNode, beforeNode, itemAttribs());
809 dd ("unknown command " + itemName + " referenced in " + parentId);