On ‘everyItem( )’ – Part 1
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.
DISCLAIMER (September 22, 2023). — I was very unpleasantly surprised to find that almost all of the links mentioned in my original article were now obsolete, including resources as fundamental as those originally published by Apple about AppleScript. I therefore decided — since no one seems to care about saving old sources of information — to host on this server the mentioned material, i.e. the PDFs to which I refer for educational purposes. This undoubtedly constitutes a “copyright violation” in the eyes of a lawyer and I am fully aware of it, but I then respectfully request the rights holders to contact me and provide a RELIABLE link to refer my readers to. Thank you for your attention.
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), page 6.
Note. — See also the “Object Specifier” entry in 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( fakeDoc.constructor.name ); // "Document" alert( fakeDoc.toSpecifier() ); // "/document[999]" 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 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; 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] app.documents[0].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 ==
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.
Comments
Hi Marc,
Nice and instructive topic.
A few discussions though:
1. As I told you, I was used to call labeled items with such command pageItems.item(label). And it worked fine until CS5. You told me what you thought of that approach but found useful to recall.
2. I have been starting thinking about some way to extend the everyItem() class by adding some filter. I like the applescript commands such as "whose".
It would be nice to get, let's say for textframes a command like :
app.activeDocument.everyItem(whose contents == "some text");
But I couldn't dif that idea that far.
Do you think it's possible to keep the everyItem() advantages and yet extend it ?
Loic
Hi Loic,
> I was used to call labeled items with
> such command pageItems.item(label)...
Yes, this worked in CS4 but wasn't it confusing? As you know, pageItems.item(string) is a shortcut for pageItems.itemByName(string). Since many objects can have both a 'name' and a 'label' property, allowing to select a label through itemByName() is pretty misleading. A much better solution would be that the corresponding collections provide an itemByLabel() method.
> I have been starting thinking about some
> way to extend the everyItem() class by adding
> some filter [...] Do you think it's possible
> to keep the everyItem() advantages and yet
> extend it?
I don't think you can actually override the collection's inner behavior to return filtered specifiers. (I mean: ‘true’ filtered specifiers like with AppleScript's ‘whose’.) But you can create functions or methods that return the required object(s) as resolved specifiers, or as JS Arrays comprising resolved specifiers.
Harbs demonstrated this approach by prototyping a custom itemByLabel() method callable on PageItems:
http://bit.ly/b6uY9q
[The second part of my article will provide practical applications and workarounds on this subject.]
> Yes, this worked in CS4 but wasn't it confusing?
Yeah I didn't see that part of the problem. You have convinced me.
Thx for all.
Loic
Thank you so much for this clear explanation. I've wondered what toSpecifier() was for years, and have avoided everyItem() because I didn't understand exactly how it worked. I think I've been suffering from a fuzzy understanding of the distinction between, as you call them, JavaScript objects and InDesign objects. Feel like shedding some light on getElements() as well?
@ absqua
Glad to bring you some hints on this complex subject. Details on getElements() in Part 2 (coming soon.)
Thanks,
Marc
great and clear summary on this often underestimated topic.
in the beginning of my scripting career i had some strange experiences with everyItem() - based on my misunderstanding - and stopped using it. i came back last year using it more and more. the only good thing about for-loops for accesing child objects is better code readability.
i happy to hear there will be a second part. for me "getElements()" was a key for understanding the topic entirely.
thanks for the work!
gregor
I never quite understood these collection objects. I knew how to use them, but I couldn't figure out how they were implemented. The everyItem seemed like a ForEach(delegate) construct I knew from programming in C#, so the first time I thought I needed to pass it a function as an argument.
I have always thought it poorly designed, negative indexes, anyItem, what were they smoking? The weird interface is probably one reason not many people actually understand these objects.
Thanks for explaining!
Wow, Marc (and Dave, in the link), what great posts! I have been scripting InDesign for a little while now and have always shied away from certain topics because they were confusing and, to me at least, not well documented. I thought I just didn't understand Javascript very well. If only I had known that it was because Adobe was trying to shoehorn three languages together in one embedded environment and trying to make them behave the same.
I am taking a class at the moment in using Javascript for the web (actually it's mostly theory and a crash course in functional programming, so it's applicable in many situations), and the teacher was curious about the use of Javascript with InDesign, which he'd never heard of.
I decided to put together some examples of scripts I'd written, and in doing so, I was forced finally to confront my lack of understanding of why, when you search for something that's not there like myDoc.paragraphStyles.item( "idontexist" ), you don't get undefined but instead you get an object that maddeningly actually exists but is completely broken for all practical purposes and whose brokenness can only be detected by the mysterious isValid method.
Now I understand everything. It's amazing how having to explain something to someone else can really motivate one to learn new things.
@hp @Michiel @richardh
Thank you all for your really positive comments :) I am pleased that this article opens new horizons to scripters.
@+
Marc