/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // function MenuManager(commandManager, menuSpecs, contextFunction, commandStr) { var menuManager = this; this.commandManager = commandManager; this.menuSpecs = menuSpecs; this.contextFunction = contextFunction; this.commandStr = commandStr; this.repeatId = 0; this.cxStore = new Object(); this.onPopupShowing = function mmgr_onshow(event) { return menuManager.showPopup(event); }; this.onPopupHiding = function mmgr_onhide(event) { return menuManager.hidePopup(event); }; this.onMenuCommand = function mmgr_oncmd(event) { return menuManager.menuCommand(event); }; /* The code using us may override these with functions which will be called * after all our internal processing is done. Both are called with the * arguments 'event' (DOM), 'cx' (JS), 'popup' (DOM). */ this.onCallbackPopupShowing = null; this.onCallbackPopupHiding = null; } MenuManager.prototype.appendMenuItems = function mmgr_append(menuId, items) { for (var i = 0; i < items.length; ++i) this.menuSpecs[menuId].items.push(items[i]); } MenuManager.prototype.createContextMenus = function mmgr_initcxs (document) { for (var id in this.menuSpecs) { if (id.indexOf("context:") == 0) this.createContextMenu(document, id); } } MenuManager.prototype.createContextMenu = function mmgr_initcx (document, id) { if (!document.getElementById(id)) { if (!ASSERT(id in this.menuSpecs, "unknown context menu " + id)) return; var dp = document.getElementById("dynamic-popups"); var popup = this.appendPopupMenu (dp, null, id, id); var items = this.menuSpecs[id].items; this.createMenuItems (popup, null, items); if (!("uiElements" in this.menuSpecs[id])) this.menuSpecs[id].uiElements = [popup]; else if (!arrayContains(this.menuSpecs[id].uiElements, popup)) this.menuSpecs[id].uiElements.push(popup); } } MenuManager.prototype.createMenus = function mmgr_createtb(document, menuid) { var menu = document.getElementById(menuid); for (var id in this.menuSpecs) { var domID; if ("domID" in this.menuSpecs[id]) domID = this.menuSpecs[id].domID; else domID = id; if (id.indexOf(menuid + ":") == 0) this.createMenu(menu, null, id, domID); } } MenuManager.prototype.createMainToolbar = function mmgr_createtb(document, id) { var toolbar = document.getElementById(id); var spec = this.menuSpecs[id]; for (var i in spec.items) { this.appendToolbarItem (toolbar, null, spec.items[i]); } toolbar.className = "toolbar-primary chromeclass-toolbar"; } MenuManager.prototype.updateMenus = function mmgr_updatemenus(document, menus) { // Cope with one string (update just the one menu)... if (isinstance(menus, String)) { menus = [menus]; } // Or nothing/nonsense (update everything). else if ((typeof menus != "object") || !isinstance(menus, Array)) { menus = []; for (var k in this.menuSpecs) { if ((/^(mainmenu|context)/).test(k)) menus.push(k); } } var menuBar = document.getElementById("mainmenu"); // Loop through this array and update everything we need to. for (var i = 0; i < menus.length; i++) { var id = menus[i]; if (!(id in this.menuSpecs)) continue; var menu = this.menuSpecs[id]; var domID; if ("domID" in this.menuSpecs[id]) domID = this.menuSpecs[id].domID; else domID = id; // Context menus need to be deleted in order to be regenerated... if ((/^context/).test(id)) { var cxMenuNode; if ((cxMenuNode = document.getElementById(id))) cxMenuNode.parentNode.removeChild(cxMenuNode); this.createContextMenu(document, id); } else if ((/^mainmenu/).test(id) && !("uiElements" in this.menuSpecs[id])) { this.createMenu(menuBar, null, id, domID); continue; } else if ((/^(mainmenu|popup)/).test(id) && ("uiElements" in this.menuSpecs[id])) { for (var j = 0; j < menu.uiElements.length; j++) { var node = menu.uiElements[j]; domID = node.parentNode.id; // Clear the menu node. while (node.lastChild) node.removeChild(node.lastChild); this.createMenu(node.parentNode.parentNode, node.parentNode.nextSibling, id, domID); } } } } /** * Internal use only. * * Registers event handlers on a given menu. */ MenuManager.prototype.hookPopup = function mmgr_hookpop (node) { node.addEventListener ("popupshowing", this.onPopupShowing, false); node.addEventListener ("popuphiding", this.onPopupHiding, false); } /** * Internal use only. * * |showPopup| is called from the "onpopupshowing" event of menus managed * by the CommandManager. If a command is disabled, represents a command * that cannot be "satisfied" by the current command context |cx|, or has an * "enabledif" attribute that eval()s to false, then the menuitem is disabled. * In addition "checkedif" and "visibleif" attributes are eval()d and * acted upon accordingly. */ MenuManager.prototype.showPopup = function mmgr_showpop (event) { /* returns true if the command context has the properties required to * execute the command associated with |menuitem|. */ function satisfied() { if (menuitem.hasAttribute("isSeparator") || !menuitem.hasAttribute("commandname")) { return true; } if (menuitem.hasAttribute("repeatfor")) return false; if (!("menuManager" in cx)) { dd ("no menuManager in cx"); return false; } var name = menuitem.getAttribute("commandname"); var commandManager = cx.menuManager.commandManager; var commands = commandManager.commands; if (!ASSERT (name in commands, "menu contains unknown command '" + name + "'")) { return false; } var rv = commandManager.isCommandSatisfied(cx, commands[name]); delete cx.parseError; return rv; }; /* Convenience function for "enabledif", etc, attributes. */ function has (prop) { return (prop in cx); }; /* evals the attribute named |attr| on the node |node|. */ function evalIfAttribute (node, attr) { var ex; var expr = node.getAttribute(attr); if (!expr) return true; expr = expr.replace (/\Wand\W/gi, " && "); expr = expr.replace (/\Wor\W/gi, " || "); try { return eval("(" + expr + ")"); } catch (ex) { dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" + attr + "': '" + expr + "'\n" + ex); } return true; }; /* evals the attribute named |attr| on the node |node|. */ function evalAttribute(node, attr) { var ex; var expr = node.getAttribute(attr); if (!expr) return null; try { return eval(expr); } catch (ex) { dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" + attr + "': '" + expr + "'\n" + ex); } return null; }; var cx; var popup = event.originalTarget; var menuName = popup.getAttribute("menuName"); /* If the host provided a |contextFunction|, use it now. Remember the * return result as this.cx for use if something from this menu is actually * dispatched. */ if (typeof this.contextFunction == "function") { cx = this.cx = this.contextFunction(menuName, event); } else { cx = this.cx = { menuManager: this, originalEvent: event }; } // Keep the context around by menu name. Removed in hidePopup. this.cxStore[menuName] = cx; var menuitem = popup.firstChild; do { if (!menuitem.hasAttribute("repeatfor")) continue; // Remove auto-generated items (located prior to real item). while (menuitem.previousSibling && menuitem.previousSibling.hasAttribute("repeatgenerated")) { menuitem.parentNode.removeChild(menuitem.previousSibling); } if (!("repeatList" in cx)) cx.repeatList = new Object(); /* Get the array of new items to add by evaluating "repeatfor" with * "cx" in scope. Usually will return an already-calculated Array * either from "cx" or somewhere in the object model. */ var ary = evalAttribute(menuitem, "repeatfor"); if ((typeof ary != "object") || !isinstance(ary, Array)) ary = []; /* The item itself should only be shown if there's no items in the * array - this base item is always disabled. */ if (ary.length > 0) menuitem.setAttribute("hidden", "true"); else menuitem.removeAttribute("hidden"); // Save the array in the context object. cx.repeatList[menuitem.getAttribute("repeatid")] = ary; /* Get the maximum number of items we're allowed to show from |ary| by * evaluating "repeatlimit" with "cx" in scope. This could be a fixed * limit or dynamically calculated (e.g. from prefs). */ var limit = evalAttribute(menuitem, "repeatlimit"); // Make sure we've got a number at all... if (typeof limit != "number") limit = ary.length; // ...and make sure it's no higher than |ary.length|. limit = Math.min(ary.length, limit); var cmd = menuitem.getAttribute("commandname"); var props = { repeatgenerated: true, repeatindex: -1, repeatid: menuitem.getAttribute("repeatid"), repeatmap: menuitem.getAttribute("repeatmap") }; /* Clone non-repeat attributes. All attributes except those starting * with 'repeat', and those matching 'hidden' or 'disabled' are saved * to |props|, which is then supplied to |appendMenuItem| later. */ for (var i = 0; i < menuitem.attributes.length; i++) { var name = menuitem.attributes[i].nodeName; if (!name.match(/^(repeat|(hidden|disabled)$)/)) props[name] = menuitem.getAttribute(name); } var lastGroup = ""; for (i = 0; i < limit; i++) { /* Check for groupings. For each item we add, if "repeatgroup" gives * a different value, we insert a separator. */ if (menuitem.getAttribute("repeatgroup")) { cx.index = i; ary = cx.repeatList[menuitem.getAttribute("repeatid")]; var item = ary[i]; /* Apply any updates to "cx" for this item by evaluating * "repeatmap" with "cx" and "item" in scope. This may just * copy some attributes from "item" to "cx" or it may do more. */ evalAttribute(menuitem, "repeatmap"); /* Get the item's group by evaluating "repeatgroup" with "cx" * and "item" in scope. Usually will return an appropriate * property from "item". */ var group = evalAttribute(menuitem, "repeatgroup"); if ((i > 0) && (lastGroup != group)) this.appendMenuSeparator(popup, menuitem, props); lastGroup = group; } props.repeatindex = i; this.appendMenuItem(popup, menuitem, cmd, props); } } while ((menuitem = menuitem.nextSibling)); menuitem = popup.firstChild; do { if (menuitem.hasAttribute("repeatgenerated") && menuitem.hasAttribute("repeatmap")) { cx.index = menuitem.getAttribute("repeatindex"); ary = cx.repeatList[menuitem.getAttribute("repeatid")]; var item = ary[cx.index]; /* Apply any updates to "cx" for this item by evaluating * "repeatmap" with "cx" and "item" in scope. This may just * copy some attributes from "item" to "cx" or it may do more. */ evalAttribute(menuitem, "repeatmap"); } /* should it be visible? */ if (menuitem.hasAttribute("visibleif")) { if (evalIfAttribute(menuitem, "visibleif")) menuitem.removeAttribute ("hidden"); else { menuitem.setAttribute ("hidden", "true"); continue; } } /* it's visible, maybe it has a dynamic label? */ if (menuitem.hasAttribute("format")) { var label = replaceVars(menuitem.getAttribute("format"), cx); if (label.indexOf("\$") != -1) label = menuitem.getAttribute("backupLabel"); menuitem.setAttribute("label", label); } /* ok, it's visible, maybe it should be disabled? */ if (satisfied()) { if (menuitem.hasAttribute("enabledif")) { if (evalIfAttribute(menuitem, "enabledif")) menuitem.removeAttribute ("disabled"); else menuitem.setAttribute ("disabled", "true"); } else menuitem.removeAttribute ("disabled"); } else { menuitem.setAttribute ("disabled", "true"); } /* should it have a check? */ if (menuitem.hasAttribute("checkedif")) { if (evalIfAttribute(menuitem, "checkedif")) menuitem.setAttribute ("checked", "true"); else menuitem.removeAttribute ("checked"); } } while ((menuitem = menuitem.nextSibling)); if (typeof this.onCallbackPopupShowing == "function") this.onCallbackPopupShowing(event, cx, popup); return true; } /** * Internal use only. * * |hidePopup| is called from the "onpopuphiding" event of menus * managed by the CommandManager. Clean up this.cxStore, but * not this.cx because that messes up nested menus. */ MenuManager.prototype.hidePopup = function mmgr_hidepop(event) { var popup = event.originalTarget; var menuName = popup.getAttribute("menuName"); if (typeof this.onCallbackPopupHiding == "function") this.onCallbackPopupHiding(event, this.cxStore[menuName], popup); delete this.cxStore[menuName]; return true; } MenuManager.prototype.menuCommand = function mmgr_menucmd(event) { /* evals the attribute named |attr| on the node |node|. */ function evalAttribute(node, attr) { var ex; var expr = node.getAttribute(attr); if (!expr) return null; try { return eval(expr); } catch (ex) { dd ("caught exception evaling '" + node.getAttribute("id") + "'.'" + attr + "': '" + expr + "'\n" + ex); } return null; }; var menuitem = event.originalTarget; var cx = this.cx; /* We need to re-run the repeat-map if the user has selected a special * repeat-generated menu item, so that the context object is correct. */ if (menuitem.hasAttribute("repeatgenerated") && menuitem.hasAttribute("repeatmap")) { cx.index = menuitem.getAttribute("repeatindex"); var ary = cx.repeatList[menuitem.getAttribute("repeatid")]; var item = ary[cx.index]; /* Apply any updates to "cx" for this item by evaluating * "repeatmap" with "cx" and "item" in scope. This may just * copy some attributes from "item" to "cx" or it may do more. */ evalAttribute(menuitem, "repeatmap"); } eval(this.commandStr); }; /** * Appends a sub-menu to an existing menu. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param domId ID of the sub-menu to add. * @param label Text to use for this sub-menu. * @param accesskey Accesskey to use for the sub-menu. * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendSubMenu = function mmgr_addsmenu(parentNode, beforeNode, menuName, domId, label, accesskey, attribs) { var document = parentNode.ownerDocument; /* sometimes the menu is already there, for overlay purposes. */ var menu = document.getElementById(domId); if (!menu) { menu = document.createElement ("menu"); menu.setAttribute ("id", domId); } var menupopup = menu.firstChild; if (!menupopup) { menupopup = document.createElement ("menupopup"); menupopup.setAttribute ("id", domId + "-popup"); menu.appendChild(menupopup); menupopup = menu.firstChild; } menupopup.setAttribute ("menuName", menuName); menu.setAttribute("accesskey", accesskey); label = label.replace("&", ""); menu.setAttribute ("label", label); menu.setAttribute ("isSeparator", true); // Only attach the menu if it's not there already. This can't be in the // if (!menu) block because the updateMenus code clears toplevel menus, // orphaning the submenus, to (parts of?) which we keep handles in the // uiElements array. See the updateMenus code. if (!menu.parentNode) parentNode.insertBefore(menu, beforeNode); if (typeof attribs == "object") { for (var p in attribs) menu.setAttribute (p, attribs[p]); } this.hookPopup (menupopup); return menupopup; } /** * Appends a popup to an existing popupset. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param id ID of the popup to add. * @param label Text to use for this popup. Popup menus don't normally have * labels, but we set a "label" attribute anyway, in case * the host wants it for some reason. Any "&" characters will * be stripped. * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendPopupMenu = function mmgr_addpmenu (parentNode, beforeNode, menuName, id, label, attribs) { var document = parentNode.ownerDocument; var popup = document.createElement ("menupopup"); popup.setAttribute ("id", id); if (label) popup.setAttribute ("label", label.replace("&", "")); if (typeof attribs == "object") { for (var p in attribs) popup.setAttribute (p, attribs[p]); } popup.setAttribute ("menuName", menuName); parentNode.insertBefore(popup, beforeNode); this.hookPopup (popup); return popup; } /** * Appends a menuitem to an existing menu or popup. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param command A reference to the CommandRecord this menu item will represent. * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendMenuItem = function mmgr_addmenu (parentNode, beforeNode, commandName, attribs) { var menuManager = this; var document = parentNode.ownerDocument; if (commandName == "-") return this.appendMenuSeparator(parentNode, beforeNode, attribs); var parentId = parentNode.getAttribute("id"); if (!ASSERT(commandName in this.commandManager.commands, "unknown command " + commandName + " targeted for " + parentId)) { return null; } var command = this.commandManager.commands[commandName]; var menuitem = document.createElement ("menuitem"); menuitem.setAttribute ("id", parentId + ":" + commandName); menuitem.setAttribute ("commandname", command.name); // Add keys if this isn't a context menu: if (parentId.indexOf("context") != 0) menuitem.setAttribute("key", "key:" + command.name); menuitem.setAttribute("accesskey", command.accesskey); var label = command.label.replace("&", ""); menuitem.setAttribute ("label", label); if (command.format) { menuitem.setAttribute("format", command.format); menuitem.setAttribute("backupLabel", label); } if ((typeof attribs == "object") && attribs) { for (var p in attribs) menuitem.setAttribute (p, attribs[p]); if ("repeatfor" in attribs) menuitem.setAttribute("repeatid", this.repeatId++); } command.uiElements.push(menuitem); parentNode.insertBefore (menuitem, beforeNode); /* It seems, bob only knows why, that this must be done AFTER the node is * added to the document. */ menuitem.addEventListener("command", this.onMenuCommand, false); return menuitem; } /** * Appends a menuseparator to an existing menu or popup. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendMenuSeparator = function mmgr_addsep (parentNode, beforeNode, attribs) { var document = parentNode.ownerDocument; var menuitem = document.createElement ("menuseparator"); menuitem.setAttribute ("isSeparator", true); if (typeof attribs == "object") { for (var p in attribs) menuitem.setAttribute (p, attribs[p]); } parentNode.insertBefore (menuitem, beforeNode); return menuitem; } /** * Appends a toolbaritem to an existing box element. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param command A reference to the CommandRecord this toolbaritem will * represent. * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendToolbarItem = function mmgr_addtb (parentNode, beforeNode, commandName, attribs) { if (commandName == "-") return this.appendToolbarSeparator(parentNode, beforeNode, attribs); var parentId = parentNode.getAttribute("id"); if (!ASSERT(commandName in this.commandManager.commands, "unknown command " + commandName + " targeted for " + parentId)) { return null; } var command = this.commandManager.commands[commandName]; var document = parentNode.ownerDocument; var tbitem = document.createElement ("toolbarbutton"); var id = parentNode.getAttribute("id") + ":" + commandName; tbitem.setAttribute ("id", id); tbitem.setAttribute ("class", "toolbarbutton-1"); if (command.tip) tbitem.setAttribute ("tooltiptext", command.tip); tbitem.setAttribute ("label", command.label.replace("&", "")); tbitem.setAttribute ("oncommand", "dispatch('" + commandName + "');"); if (typeof attribs == "object") { for (var p in attribs) tbitem.setAttribute (p, attribs[p]); } command.uiElements.push(tbitem); parentNode.insertBefore (tbitem, beforeNode); return tbitem; } /** * Appends a toolbarseparator to an existing box. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param attribs Object containing CSS attributes to set on the element. */ MenuManager.prototype.appendToolbarSeparator = function mmgr_addmenu (parentNode, beforeNode, attribs) { var document = parentNode.ownerDocument; var tbitem = document.createElement ("toolbarseparator"); tbitem.setAttribute ("isSeparator", true); if (typeof attribs == "object") { for (var p in attribs) tbitem.setAttribute (p, attribs[p]); } parentNode.appendChild (tbitem); return tbitem; } /** * Creates menu DOM nodes from a menu specification. * @param parentNode DOM Node to insert into * @param beforeNode DOM Node already contained by parentNode, to insert before * @param menuSpec array of menu items */ MenuManager.prototype.createMenu = function mmgr_newmenu (parentNode, beforeNode, menuName, domId, attribs) { if (typeof domId == "undefined") domId = menuName; if (!ASSERT(menuName in this.menuSpecs, "unknown menu name " + menuName)) return null; var menuSpec = this.menuSpecs[menuName]; if (!("accesskey" in menuSpec)) menuSpec.accesskey = getAccessKey(menuSpec.label); var subMenu = this.appendSubMenu(parentNode, beforeNode, menuName, domId, menuSpec.label, menuSpec.accesskey, attribs); // Keep track where we're adding popup nodes derived from some menuSpec if (!("uiElements" in this.menuSpecs[menuName])) this.menuSpecs[menuName].uiElements = [subMenu]; else if (!arrayContains(this.menuSpecs[menuName].uiElements, subMenu)) this.menuSpecs[menuName].uiElements.push(subMenu); this.createMenuItems (subMenu, null, menuSpec.items); return subMenu; } MenuManager.prototype.createMenuItems = function mmgr_newitems (parentNode, beforeNode, menuItems) { function itemAttribs() { return (1 in menuItems[i]) ? menuItems[i][1] : null; }; var parentId = parentNode.getAttribute("id"); for (var i in menuItems) { var itemName = menuItems[i][0]; if (itemName[0] == ">") { itemName = itemName.substr(1); if (!ASSERT(itemName in this.menuSpecs, "unknown submenu " + itemName + " referenced in " + parentId)) { continue; } this.createMenu (parentNode, beforeNode, itemName, parentId + ":" + itemName, itemAttribs()); } else if (itemName in this.commandManager.commands) { this.appendMenuItem (parentNode, beforeNode, itemName, itemAttribs()); } else if (itemName == "-") { this.appendMenuSeparator (parentNode, beforeNode, itemAttribs()); } else { dd ("unknown command " + itemName + " referenced in " + parentId); } } }