Import from 1.9a8 tarball
[mozilla-extra.git] / extensions / irc / js / lib / menu-manager.js
blob2a87a2d67518cd215112fbfea009f5e049d938f4
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2  *
3  * ***** BEGIN LICENSE BLOCK *****
4  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5  *
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/
10  *
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
14  * License.
15  *
16  * The Original Code is The JavaScript Debugger.
17  *
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.
22  *
23  * Contributor(s):
24  *   Robert Ginda, <rginda@netscape.com>, original author
25  *
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.
37  *
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;
48     this.repeatId = 0;
50     this.onPopupShowing =
51         function mmgr_onshow (event) { return menuManager.showPopup (event); };
52     this.onPopupHiding =
53         function mmgr_onhide (event) { return menuManager.hidePopup (event); };
54     this.onMenuCommand =
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)
69     {
70         if (id.indexOf("context:") == 0)
71             this.createContextMenu(document, id);
72     }
75 MenuManager.prototype.createContextMenu =
76 function mmgr_initcx (document, id)
78     if (!document.getElementById(id))
79     {
80         if (!ASSERT(id in this.menuSpecs, "unknown context menu " + id))
81             return;
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);
92     }
96 MenuManager.prototype.createMenus =
97 function mmgr_createtb(document, menuid)
99     var menu = document.getElementById(menuid);
100     for (id in this.menuSpecs)
101     {
102         var domID;
103         if ("domID" in this.menuSpecs[id])
104             domID = this.menuSpecs[id].domID;
105         else
106             domID = id;
108         if (id.indexOf(menuid + ":") == 0)
109             this.createMenu(menu, null, id, domID);
110     }
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)
119     {
120         this.appendToolbarItem (toolbar, null, spec.items[i]);
121     }
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))
131     {
132         menus = [menus];
133     }
134     // Or nothing/nonsense (update everything).
135     else if ((typeof menus != "object") || !isinstance(menus, Array))
136     {
137         menus = [];
138         for (var k in this.menuSpecs)
139         {
140             if ((/^(mainmenu|context)/).test(k))
141                 menus.push(k);
142         }
143     }
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++)
149     {
150         var id = menus[i];
151         if (!(id in this.menuSpecs))
152             continue;
153         var menu = this.menuSpecs[id];
154         var domID;
155         if ("domID" in this.menuSpecs[id])
156             domID = this.menuSpecs[id].domID;
157         else
158             domID = id;
160         // Context menus need to be deleted in order to be regenerated...
161         if ((/^context/).test(id))
162         {
163             var cxMenuNode;
164             if ((cxMenuNode = document.getElementById(id)))
165                 cxMenuNode.parentNode.removeChild(cxMenuNode);
166             this.createContextMenu(document, id);
167         }
168         else if ((/^mainmenu/).test(id) &&
169                  !("uiElements" in this.menuSpecs[id]))
170         {
171             this.createMenu(menuBar, null, id, domID);
172             continue;
173         }
174         else if ((/^(mainmenu|popup)/).test(id) &&
175                  ("uiElements" in this.menuSpecs[id]))
176         {
177             for (var j = 0; j < menu.uiElements.length; j++)
178             {
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,
187                                 id, domID);
188             }
189         }
190         
191         
192     }
197  * Internal use only.
199  * Registers event handlers on a given menu.
200  */
201 MenuManager.prototype.hookPopup =
202 function mmgr_hookpop (node)
204     node.addEventListener ("popupshowing", this.onPopupShowing, false);
205     node.addEventListener ("popuphiding",  this.onPopupHiding, false);
209  * Internal use only.
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.
217  */
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|.
223      */
224     function satisfied()
225     {
226         if (menuitem.hasAttribute("isSeparator") ||
227             !menuitem.hasAttribute("commandname"))
228         {
229             return true;
230         }
232         if (menuitem.hasAttribute("repeatfor"))
233             return false;
235         if (!("menuManager" in cx))
236         {
237             dd ("no menuManager in cx");
238             return false;
239         }
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 + "'"))
247         {
248             return false;
249         }
251         var rv = commandManager.isCommandSatisfied(cx, commands[name]);
252         delete cx.parseError;
253         return rv;
254     };
256     /* Convenience function for "enabledif", etc, attributes. */
257     function has (prop)
258     {
259         return (prop in cx);
260     };
262     /* evals the attribute named |attr| on the node |node|. */
263     function evalIfAttribute (node, attr)
264     {
265         var ex;
266         var expr = node.getAttribute(attr);
267         if (!expr)
268             return true;
270         expr = expr.replace (/\Wand\W/gi, " && ");
271         expr = expr.replace (/\Wor\W/gi, " || ");
273         try
274         {
275             return eval("(" + expr + ")");
276         }
277         catch (ex)
278         {
279             dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
280                 attr + "': '" + expr + "'\n" + ex);
281         }
282         return true;
283     };
285     /* evals the attribute named |attr| on the node |node|. */
286     function evalAttribute(node, attr)
287     {
288         var ex;
289         var expr = node.getAttribute(attr);
290         if (!expr)
291             return null;
293         try
294         {
295             return eval(expr);
296         }
297         catch (ex)
298         {
299             dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
300                 attr + "': '" + expr + "'\n" + ex);
301         }
302         return null;
303     };
305     var cx;
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")
312     {
313         cx = this.cx = this.contextFunction (popup.getAttribute("menuName"),
314                                              event);
315     }
316     else
317     {
318         cx = this.cx = { menuManager: this, originalEvent: event };
319     }
321     var menuitem = popup.firstChild;
322     do
323     {
324         if (!menuitem.hasAttribute("repeatfor"))
325             continue;
327         // Remove auto-generated items (located prior to real item).
328         while (menuitem.previousSibling &&
329                menuitem.previousSibling.hasAttribute("repeatgenerated"))
330         {
331             menuitem.parentNode.removeChild(menuitem.previousSibling);
332         }
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))
341             ary = [];
343         /* The item itself should only be shown if there's no items in the
344          * array - this base item is always disabled.
345          */
346         if (ary.length > 0)
347             menuitem.setAttribute("hidden", "true");
348         else
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")
358             limit = ary.length;
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.
370          */
371         for (var i = 0; i < menuitem.attributes.length; i++)
372         {
373             var name = menuitem.attributes[i].nodeName;
374             if (!name.match(/^(repeat|(hidden|disabled)$)/))
375                 props[name] = menuitem.getAttribute(name);
376         }
378         for (i = 0; i < limit; i++)
379         {
380             props.repeatindex = i;
381             this.appendMenuItem(popup, menuitem, cmd, props);
382         }
383     } while ((menuitem = menuitem.nextSibling));
385     menuitem = popup.firstChild;
386     do
387     {
388         if (menuitem.hasAttribute("repeatgenerated") &&
389             menuitem.hasAttribute("repeatmap"))
390         {
391             cx.index = menuitem.getAttribute("repeatindex");
392             ary = cx.repeatList[menuitem.getAttribute("repeatid")];
393             var item = ary[cx.index];
394             evalAttribute(menuitem, "repeatmap");
395         }
397         /* should it be visible? */
398         if (menuitem.hasAttribute("visibleif"))
399         {
400             if (evalIfAttribute(menuitem, "visibleif"))
401                 menuitem.removeAttribute ("hidden");
402             else
403             {
404                 menuitem.setAttribute ("hidden", "true");
405                 continue;
406             }
407         }
409         /* it's visible, maybe it has a dynamic label? */
410         if (menuitem.hasAttribute("format"))
411         {
412             var label = replaceVars(menuitem.getAttribute("format"), cx);
413             if (label.indexOf("\$") != -1)
414                 label = menuitem.getAttribute("backupLabel");
415             menuitem.setAttribute("label", label);
416         }
418         /* ok, it's visible, maybe it should be disabled? */
419         if (satisfied())
420         {
421             if (menuitem.hasAttribute("enabledif"))
422             {
423                 if (evalIfAttribute(menuitem, "enabledif"))
424                     menuitem.removeAttribute ("disabled");
425                 else
426                     menuitem.setAttribute ("disabled", "true");
427             }
428             else
429                 menuitem.removeAttribute ("disabled");
430         }
431         else
432         {
433             menuitem.setAttribute ("disabled", "true");
434         }
436         /* should it have a check? */
437         if (menuitem.hasAttribute("checkedif"))
438         {
439             if (evalIfAttribute(menuitem, "checkedif"))
440                 menuitem.setAttribute ("checked", "true");
441             else
442                 menuitem.removeAttribute ("checked");
443         }
444     } while ((menuitem = menuitem.nextSibling));
446     return true;
450  * Internal use only.
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
455  * menus.
456  */
457 MenuManager.prototype.hidePopup =
458 function mmgr_hidepop (id)
460     return true;
463 MenuManager.prototype.menuCommand =
464 function mmgr_menucmd(event)
466     /* evals the attribute named |attr| on the node |node|. */
467     function evalAttribute(node, attr)
468     {
469         var ex;
470         var expr = node.getAttribute(attr);
471         if (!expr)
472             return null;
474         try
475         {
476             return eval(expr);
477         }
478         catch (ex)
479         {
480             dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" +
481                 attr + "': '" + expr + "'\n" + ex);
482         }
483         return null;
484     };
486     var menuitem = event.originalTarget;
487     var cx = this.cx;
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.
490      */
491     if (menuitem.hasAttribute("repeatgenerated") &&
492         menuitem.hasAttribute("repeatmap"))
493     {
494         cx.index = menuitem.getAttribute("repeatindex");
495         var ary = cx.repeatList[menuitem.getAttribute("repeatid")];
496         var item = ary[cx.index];
497         evalAttribute(menuitem, "repeatmap");
498     }
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.
512  */
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);
521     if (!menu)
522     {
523         menu = document.createElement ("menu");
524         menu.setAttribute ("id", domId);
525     }
527     var menupopup = menu.firstChild;
529     if (!menupopup)
530     {
531         menupopup = document.createElement ("menupopup");
532         menupopup.setAttribute ("id", domId + "-popup");
533         menu.appendChild(menupopup);
534         menupopup = menu.firstChild;
535     }
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")
552     {
553         for (var p in attribs)
554             menu.setAttribute (p, attribs[p]);
555     }
557     this.hookPopup (menupopup);
559     return 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
570  *                be stripped.
571  * @param attribs Object containing CSS attributes to set on the element.
572  */
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);
579     if (label)
580         popup.setAttribute ("label", label.replace("&", ""));
581     if (typeof attribs == "object")
582     {
583         for (var p in attribs)
584             popup.setAttribute (p, attribs[p]);
585     }
587     popup.setAttribute ("menuName", menuName);
589     parentNode.insertBefore(popup, beforeNode);
590     this.hookPopup (popup);
592     return 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.
601  */
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 " +
615                 parentId))
616     {
617         return null;
618     }
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);
628     if (command.format)
629     {
630         menuitem.setAttribute("format", command.format);
631         menuitem.setAttribute("backupLabel", label);
632     }
634     if ((typeof attribs == "object") && attribs)
635     {
636         for (var p in attribs)
637             menuitem.setAttribute (p, attribs[p]);
638         if ("repeatfor" in attribs)
639             menuitem.setAttribute("repeatid", this.repeatId++);
640     }
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.
646      */
647     menuitem.addEventListener("command", this.onMenuCommand, false);
649     return menuitem;
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.
657  */
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")
665     {
666         for (var p in attribs)
667             menuitem.setAttribute (p, attribs[p]);
668     }
669     parentNode.insertBefore (menuitem, beforeNode);
671     return menuitem;
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
679  *                represent.
680  * @param attribs Object containing CSS attributes to set on the element.
681  */
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 " +
692                 parentId))
693     {
694         return null;
695     }
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");
704     if (command.tip)
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")
710     {
711         for (var p in attribs)
712             tbitem.setAttribute (p, attribs[p]);
713     }
715     command.uiElements.push(tbitem);
716     parentNode.insertBefore (tbitem, beforeNode);
718     return tbitem;
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.
726  */
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")
734     {
735         for (var p in attribs)
736             tbitem.setAttribute (p, attribs[p]);
737     }
738     parentNode.appendChild (tbitem);
740     return 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
748  */
749 MenuManager.prototype.createMenu =
750 function mmgr_newmenu (parentNode, beforeNode, menuName, domId, attribs)
752     if (typeof domId == "undefined")
753         domId = menuName;
755     if (!ASSERT(menuName in this.menuSpecs, "unknown menu name " + menuName))
756         return null;
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);
770     return subMenu;
773 MenuManager.prototype.createMenuItems =
774 function mmgr_newitems (parentNode, beforeNode, menuItems)
776     function itemAttribs()
777     {
778         return (1 in menuItems[i]) ? menuItems[i][1] : null;
779     };
781     var parentId = parentNode.getAttribute("id");
783     for (var i in menuItems)
784     {
785         var itemName = menuItems[i][0];
786         if (itemName[0] == ">")
787         {
788             itemName = itemName.substr(1);
789             if (!ASSERT(itemName in this.menuSpecs,
790                         "unknown submenu " + itemName + " referenced in " +
791                         parentId))
792             {
793                 continue;
794             }
795             this.createMenu (parentNode, beforeNode, itemName,
796                              parentId + ":" + itemName, itemAttribs());
797         }
798         else if (itemName in this.commandManager.commands)
799         {
800             this.appendMenuItem (parentNode, beforeNode, itemName,
801                                  itemAttribs());
802         }
803         else if (itemName == "-")
804         {
805             this.appendMenuSeparator (parentNode, beforeNode, itemAttribs());
806         }
807         else
808         {
809             dd ("unknown command " + itemName + " referenced in " + parentId);
810         }
811     }