Naive approach of a Collection

Most of the available objects in the DOM have a collection counterpart. As soon as you have a Document object, you have a Documents collection. TextFrame objects are collected in TextFrames, Color objects are collected in Colors, Paragraph objects are collected in Paragraphs, etc. Even the most marginal objects —say MeasurementEditbox or TOCStyleEntry— are addressable through dedicated collections (MeasurementEditboxes, TOCStyleEntries).

Regarded as a class, a collection is nothing but a generic interface giving an easy and ordered access to homogeneous InDesign objects that live together. That's probably why scripting newcomers often confuse collections —which are scripting DOM entities— and the traditional JavaScript arrays.

Note. — Until 2005 (CS2), the Adobe's Scripting Reference exhibited a pseudo-class named Objects and defined as “an independent collection of objects.” (Adobe InDesign CS2 Scripting Reference, p. 901, 2005.) It reflected the generic interface of what we call a collection —while the SDK documentation uses the concept of “plural form” to indicate that a scripting DOM object can be deployed within a collection.

All collections share the same arsenal of methods. Some of them offer an extra add method allowing to create and insert a new element within the collection.

Generic interface of any collection:

Method Name Generic Prototype Naive Description
count int count( ) Returns the number of elements in this collection. A length property is available too.
add obj add(...) Creates a new object and returns it. Not available in all collections.
[ ] operator obj [](int index) Returns an element by its zero-based index within this collection. Negative indexes are allowed and count backwards from the end.
Use the syntax: this[index].
itemByName obj itemByName(str name) Returns the first element whose name property is name. Not available in collections whose elements have no name. In ID CS4, elements without name property could be accessed to by their label property through itemByName(label)!
itemByID obj itemByID(int id) Returns an element by its id property. (Not available in collections whose elements have no id.)
item obj item(var indexOrName) Returns this[indexOrName] if you pass a Number, else returns this.itemByName(indexOrName).
firstItem obj firstItem() Returns this[0].
lastItem obj lastItem() Returns this[-1].
middleItem obj middleItem() Returns the ‘middle’ element from this collection, i.e. this[Math.floor(N/2)], where N==this.length.
anyItem obj anyItem() Returns a randomly-selected element from the collection.
previousItem obj previousItem(obj ref) Returns the element with the index previous to the specified element.
nextItem obj nextItem(obj ref) Returns the element whose index follows the specified element in this collection.
itemByRange ? itemByRange(var from, var to) Documented as returning an Array of elements within the specified range. The from and to arguments can be passed as Number (index), String (name), or Object.
everyItem ? everyItem() Documented as returning an Array of the elements within the collection. Sounds to be equivalent to this.itemByRange(0,-1).

At first glance the interface of a collection seems easy to understand, but this is highly misleading. The big secret is that none of the above methods actually returns any element. . . except to understand an ‘element’ as an object specifier.

Object Specifiers

Dave Saunders clarifies the concept of Object Specifier —from a JavaScript point of view— in this must-read article published in 2007. We should supplement it by stating that originally an object specifier is an AppleScript key concept. You will find important information on this subject in this AppleScript digest from William R. Cook (PDF), pages 10-19.

Note. — See also the “Object Specifier” automated entry on Cpedia, and the AppleScript Language Guide (PDF).

What we need to visualize is that our Good Old JavaScript DOM mimics an original AppleScript mechanism by wrapping specifiers within objects. Think an Object Specifier as a path in the DOM roadmap. Any method of any collection —e.g. Documents.itemByName(...), or Colors.everyItem()— returns such a path, or a portion of the full path. As long as we do not send a command to this symbolic descriptor, the underlying InDesign object(s) are not really involved, so the specifier doesn't need to be resolved. This explains why a statement like:
   myDoc = app.documents.item("NonExistingDoc");
does not cause any error. Even:
   myDoc = app.documents[999];
is OK, because myDoc is nothing but a Document specifier.

var fakeDoc = app.documents[999];
alert( ); // "Document"
alert( fakeDoc.toSpecifier() ); // "/document[999]"
alert( fakeDoc.isValid ); // false (of course!)
// Finally we send a REAL query to InDesign
alert( ); // Error: Object is invalid

The above code shows that fakeDoc is fully identified as a Document object. This is not an undefined JavaScript entity. We can display its constructor name ("Document"), we also can call the toSpecifier() method on it, or query its isValid property (which returns false). However, querying any specific Document property or method on it will cause a runtime “invalid object” error. The conclusion is that the object exists at the JavaScript level but cannot be resolved as a true InDesign's object. Any attempt to send a command to this virtual object will fail.

The key concept is that our scripts never directly handle, create or store any ID's object. As explained in the SDK Documentation: “InDesign does not hand out pointers to objects but rather references that need to get resolved every time they are used.” The scripting objects follow the same rule: from any point of our code, all that we handle are object specifiers. The whole JavaScript DOM is a class hierarchy that hides real InDesign entities behind specifiers, which allow to delegate or delay commands that finally need to get performed on the actual objects.

Note. — In AppleScript a subtle distinction is made between “object specifier” and “object reference”, in that an object reference specifies an Apple Event. For the sake of clarity it would be better to avoid the AppleScript terminology of references in a JavaScript context, because EcmaScript has its own abstract Reference type. (A JS reference is described as a “resolved name binding”. It consists of specific components and is governed by specific algorithms.)

Given an element —like app.documents[0], or app.documents.everyItem()— the toSpecifier() method reveals the specifier's internal path coerced to String:

// Some tests on object specifiers:
var elems = app.documents[0].stories,
e = elems.firstItem();
e.toSpecifier() == '/document[0]/story[@location=first]';
e = elems[0];
e.toSpecifier() == '/document[0]/story[0]';
e = elems.lastItem();
e.toSpecifier() == '/document[0]/story[@location=last]';
e = elems[-1];
e.toSpecifier() == '/document[0]/story[-1]';
e = elems.itemByName('foo'); // CS5
e.toSpecifier() == '/document[0]/story[@name="foo"]';
e = elems.itemByID(123);
e.toSpecifier() == '/document[0]/story[@id=123]';
e = elems.item(5);
e.toSpecifier() == '/document[0]/story[5]';
e = elems.item('foo');
e.toSpecifier() == '/document[0]/story[@name="foo"]';
e = elems.itemByRange(1,3);
e.toSpecifier() == '(/document[0]/story[1] to /document[0]/story[3])';
e = elems.everyItem();
e.toSpecifier() == '/document[0]/story';
e = elems.everyItem().words[-1];
e.toSpecifier() == '/document[0]/story/word[-1]';

What you see above are the paths internally attached to various elements that encapsulate object specifiers. You may have noticed that collections don't provide a toSpecifier() method. This is because a collection only provides an interface to build more complex specifiers. It is not itself a specifier and we cannot send any direct command to a collection. However, nothing prevents an object specifier from including a range of receivers, thanks to itemByRange() and everyItem().

Concretely, this means that an entity like elems.everyItem() is a single scripting DOM object —i.e. an object specifier— that could cause a command to hit several actual UI objects. This is perhaps the most difficult aspect to understand, so let me repeat: JavaScript regards app.documents.everyItem() as a single Document object, even though any property or method being invoked on that entity sends a COMMAND that multiple InDesign actual documents will receive.

About Verb-First Commands

Methods and properties show a surprisingly collective behavior when you call them on a specifier like elems.everyItem(). Try the following code on a document owning a handful of frames:

// Move every page item by [10,10]

Now go to Edit/Undo Move Item. What is amazing? A single move is registered, and the undo performs a all-at-once backward move. Everything works as if InDesign had moved a single entity. And it actually did! That's why elems.everyItem().move(...) is considerably more effective than a script-driven loop performing elems[i].move(...) on each element.

As said in the InDesign SDK, everyItem() allows to “fetch and cache data of a collection object all at once, instead of querying the properties with separate calls.” This mechanism is known as a verb-first command: instead of invoking the specified method on each receiver “a single method performs the action (or verb) on any number of objects.” (Apple's Cocoa Scripting Guide.)

What's the key rule? A command is sent through InDesign each time you set or get a specific property of the specifier and each time you call a specific method on the specifier (including == and === comparisons). Whatever the way you hit the InDesign's object layer, the inner state of the JS object specifier is updated in order to reflect the actual receiver(s) to which it points out.

On ‘everyItem()’ – Part 2