June 30, 2010 | Tips | en
Every collection in the scope of the InDesign Scripting DOM provides a method called
everyItem. It remained undocumented until InDesign CS3, but scripting experts such as Dave Saunders had already pointed out its forcefulness and already knew how to exploit it. Here are some details about this esoteric syntax.
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
TextFrame objects are collected in
Color objects are collected in
Paragraph objects are collected in
Paragraphs, etc. Even the most marginal objects —say
TOCStyleEntry— are addressable through dedicated collections (
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|
||Returns the number of elements in this collection. A length property is available too.|
||Creates a new object and returns it. Not available in all collections.|
|[ ] operator||obj
||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].
||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)!|
||Returns an element by its id property. (Not available in collections whose elements have no id.)|
||Returns this[indexOrName] if you pass a
||Returns the ‘middle’ element from this collection, i.e. this[Math.floor(N/2)], where N==this.length.|
||Returns a randomly-selected element from the collection.|
||Returns the element with the index previous to the specified element.|
||Returns the element whose index follows the specified element in this collection.|
||Documented as returning an
||Documented as returning an
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.
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;
is OK, because
myDoc is nothing but a
var fakeDoc = app.documents; alert( fakeDoc.constructor.name ); // "Document" alert( fakeDoc.toSpecifier() ); // "/document" alert( fakeDoc.isValid ); // false (of course!) // Finally we send a REAL query to InDesign alert( fakeDoc.name ); // Error: Object is invalid
The above code shows that
fakeDoc is fully identified as a
Document object. This is not an
"Document"), we also can call the
toSpecifier() method on it, or query its
isValid property (which returns
false). However, querying any specific
Given an element —like
toSpecifier() method reveals the specifier's internal path coerced to
// Some tests on object specifiers: var elems = app.documents.stories, e; e = elems.firstItem(); e.toSpecifier() == '/document/story[@location=first]'; e = elems; e.toSpecifier() == '/document/story'; e = elems.lastItem(); e.toSpecifier() == '/document/story[@location=last]'; e = elems[-1]; e.toSpecifier() == '/document/story[-1]'; e = elems.itemByName('foo'); // CS5 e.toSpecifier() == '/document/story[@name="foo"]'; e = elems.itemByID(123); e.toSpecifier() == '/document/story[@id=123]'; e = elems.item(5); e.toSpecifier() == '/document/story'; e = elems.item('foo'); e.toSpecifier() == '/document/story[@name="foo"]'; e = elems.itemByRange(1,3); e.toSpecifier() == '(/document/story to /document/story)'; e = elems.everyItem(); e.toSpecifier() == '/document/story'; e = elems.everyItem().words[-1]; e.toSpecifier() == '/document/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
Concretely, this means that an entity like
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] app.documents.pageItems.everyItem().move(undefined,[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
=== 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.