To make a script available from an InDesign menu, it is necessary to understand how the Menu object communicates with event listeners through the MenuAction object.

Menu and MenuAction models

In short, a Menu / Submenu object is a simple container designed like a “composite pattern”. Each menu element (child) belongs to one of three possible categories: MenuItem, MenuSeparator, or Submenu. MenuItem and MenuSeparator correspond to the primitive objects, while Submenu behaves recursively as another menu. Therefore Menu and Submenu objects are basically the same thing and expose the same interface, except that Menu represents the root class, parent of which is the Application.

Given a (sub)menu, you can access primitive and complex objects uniformly through the .menuElements property (MenuElements collection), which is the union of .menuItems (MenuItems), .menuSeparators (MenuSeparators), and .submenus (Submenus) collections.

The Menu / Submenu object provides event listeners (.eventListeners) intended to handle menu display events ("beforeDisplay"), but as a general rule you won't use those listeners to trigger your process. The key point is that menu actions are ‘decoupled’ from the menu object. The InDesign JS DOM provides a specific MenuAction object, and a ScriptMenuAction twin object, to encapsulate menu actions apart from the menu model. Thus, you can attach a menu action to one or more MenuItem object, but you could also invoke a menu action irrespective of any menu component.

Let's see how to navigate within the menu and action models:

Navigate within the Menu/Submenu and (Script)MenuAction models.

A MenuItem (the menu element that you can ‘link’ to an action) mainly contains status and titling properties, plus a reference to a MenuAction (.associatedMenuAction) which actually deals with events. As explained in the InDesign CS4 Scripting Guide, chapter 8: “The properties of the menuAction define what happens when the menu item is chosen. In addition to the menuActions defined by the user interface, InDesign scripters can create their own, scriptMenuActions, which associate a script with a menu selection.”

Both the MenuAction and ScriptMenuAction objects belong to Application and have exactly the same structure. However, the MenuAction instances are persistent and not removable. They reflect InDesign built-in functionalities, such as creating a new document, pasting, or grouping the selected objects. On the other hand the ScriptMenuAction instances are created by scripts and live only during a session (remember that you need to use the #targetengine directive to get your code session-persistent).

You can do many sly things with the InDesign existing menu actions. For example, you can invoke most of the UI dialogs from your own script:

// displays the Document Setup dialog
var maDocSetup = app.menuActions.item("$ID/Document Setup...");
maDocSetup.invoke();
 

You can also —at your own risks!— extend a primitive action behaviour by “listening” the beforeInvoke and/or afterInvoke event:

#targetengine "ExtendPaste"
 
var maPaste = app.menuActions.item("$ID/Paste");
maPaste.addEventListener(
    "afterInvoke",
    function(){alert("You pasted something");}
    );
 

The code above installs a custom function which will be called after every Paste action initiated by the user (from the Edit menu, or the Ctrl/Cmd V shortcut).

The Localization Problem

A chink in the armor of the InDesign JavaScript DOM is that menus, submenus, menuItems, and menuActions are all referred to by localized name. It means that something like app.menuActions.item("Paste") in UK/US installations must be translated into app.menuActions.item("Coller") in French installation, etc.

If you write scripts for an international audience, you probably want to work around this boring constraint. The solution is to use the $ID/ prefix at the beginning of “locale-independent key strings”, for example: "$ID/Paste". The point is that InDesign stores internal key strings in translation tables you can access to via the scripting DOM. The syntax "$ID/<internal_key_string>" is one way to get a key string translated into the locale InDesign interface, on condition of passing through the InDesign JavaScript object model. If you need to handle a key string from the core JS context (string affectation, alert(), etc.) then you must use the Application.translateKeyString() method:

// This WILL work:
app.menuActions.item("$ID/Paste").invoke();
 
// This WON'T display the translated string:
alert( "$ID/Paste" );
 
// This WILL display the translated string:
alert( app.translateKeyString("$ID/Paste") );
 

Conversely, when you work from a non-English InDesign platform, you often need to find the locale-independent form of a localized string, such as "Coller" (FR), or "Einfügen" (DE). Then you use the Application.findKeyStrings() method, which returns an array of “locale-independent string(s) from the internal string localization database that correspond to the specified string (in the current locale).”

// Extract the "$ID/Paste" key string
// from the French corresponding token:
alert( app.findKeyStrings("Coller").join('\r') );
 

Menu and Script Menu Action API

To create a new action, we use the app.scriptMenuActions.add() method. At this point we pass only a title argument, and an optional properties object. The ScriptMenuAction instance lives in the session scope. It can now be linked to one or several event listener(s) through the addEventListener() method. An event listener is a callback mechanism which allows to trigger a specific function (an “event handler”) when a specific event occurs in the target object. A ScriptMenuAction target supports the following events:

ScriptMenuAction Event Description
beforeDisplay Occurs before an internal request for the enabled/checked status of the target.
beforeInvoke Occurs before the target action is invoked.
onInvoke Occurs when the target action is invoked.
afterInvoke Occurs after the onInvoke event.

Now let's see how to add an event listener observing the onInvoke event:

#targetengine 'SampleScriptMenuAction'
 
var smaTitle = "Sample Script Menu Action";
 
// Create a new Script Menu Action
var sma = app.scriptMenuActions.add(smaTitle);
 
// Add an Event Listener
sma.addEventListener(
    /*event type*/   'onInvoke',
    /*event handler*/ function(){alert('Hello World!');}
    );
 
// Internal call
sma.invoke();
 

The internal call —sma.invoke()— emulates an onInvoke event within the script menu action. The event is ‘captured’ by the event listener. Then the event listener calls the event handler, which displays the message.

At first sight it's the most stupid way to get a “Hello World!” in JavaScript! But the advantage of using the action event model is not in calling a routine from your script, it is to connect all this stuff with a menu item. To do that, we use the menuItems.add() method from a Menu / Submenu object:

#targetengine 'Sample Script Menu Action'
 
var smaTitle = "Sample Script Menu Action";
 
// Create the Script Menu Action (SMA)
var sma = app.scriptMenuActions.add(smaTitle);
 
// Add an Event Listener
sma.addEventListener(
    /*event type*/   'onInvoke',
    /*event handler*/ function(){alert('Hello World!');}
    );
 
// Create a new menu item in the Help submenu
var mnu = app.menus.item("$ID/Main").submenus.item("$ID/&Help");
mnu.menuItems.add(sma);
 

Here is the result:

Sample Script Menu Action connected to a Menu Item.

Note that our “Sample Script Menu Action” is session-persistent thanks to the #targetengine directive. But it is also possible to use a script File rather than a direct code as the event handler, in which case you automatically obtain session persistence even from a script scope. Why? Because a new MenuItem is always registered at the session level, so if the .associatedMenuAction event handlers reside in script files the connection will remain.

Another side effect to keep in mind is that the script above installs a new clone of the menu item each time it is called! Moreover, if you create your own submenu to manage several menu items the Submenu object is “application persistent,” that is InDesign will restore it the next session from your user profile. That's why menu scripters need to provide for a clean installation management.

Creation interface of the Menu/Submenu and (Script)MenuAction models.

Sample Code: Adding a “Close All” Feature in the File Menu

The basic approach I suggest in menu management is to separate the main process (i.e. the event handlers) from the menu installation. A good thing is to encapsulate the installation within an autoexecutable function returning true when the job is done. We affect the return value to a variable which tests itself preventing the user from calling once again the installer.

/**************************************************/
/*   FileCloseAll.js                              */
/*                                                */
/*   Add a "Close All" feature in the File menu   */
/*                                                */
/**************************************************/
 
#targetengine "FileCloseAll"
 
// THE MAIN PROCESS
// -----------------------------------------------
var fcaTitle = "Close All";
 
var fcaHandlers = {
    'beforeDisplay' : function(ev)
        {
        ev.target.enabled = (app.documents.length>1);
        },
 
    'onInvoke' : function()
        {
        var doc;
        for( var i = app.documents.length-1 ; i>=0 ; i-- )
            {
            doc = app.documents[i];
            doc.close();
            }
        }
    };
 
 
// THE MENU INSTALLER
// -----------------------------------------------
var fcaMenuInstaller = fcaMenuInstaller||
(function(mnuTitle,mnuHandlers)
{
// 1. Create the script menu action
var mnuAction = app.scriptMenuActions.add(mnuTitle);
 
// 2. Attach the event listener
var ev;
for( ev in mnuHandlers )
    {
    mnuAction.eventListeners.add(ev,mnuHandlers[ev]);
    }
 
// 3. Create the menu item
var fileMenu = app.menus.item("$ID/Main").submenus.item("$ID/&File");
var refItem = fileMenu.menuItems.item("$ID/&Close");
 
fileMenu.menuItems.add(mnuAction,LocationOptions.after,refItem);
 
return true;
})(fcaTitle, fcaHandlers);
 

Of course, a good place for that script is the "Startup Scripts" folder if you want to keep at hand a “Close All” feature in every InDesign session.