1/ Manipulating Text and Containers
How to Properly Duplicate a Table
Group PageItems per Script Label
How to Compare Two Characters (and What Does that Mean)?
Apply Fit-Frame-to-Content to All Overset Items
Override Master Text Frames without Breaking the Thread
Design a Custom Method to Transfer Table Cells

2/ Graphics, Geometry and Coordinate Spaces
Syncing InDesign and Illustrator Layers
What's the Shape?
Moving an Object Considering various Reference Points
Sizing an Object
Put a Gap between Pages

3/ On the JavaScript/ExtendScript Side
Quick Note about XML.prototype
Identify All Subparts of an Array
A Trick to Avoid if() and switch()
Why You Shouldn't Use Numerals as Object Keys
Not Calling a Function as a Constructor is Sometimes Risky!

4/ Miscellaneous and Advanced Topics
Implementing "whose" in ExtendScript JS
Propagating Custom Events in ScriptUI
Note about InDesign DOM Events
Scanning All State Containers in a Whole Document


1/ Manipulating Text and Containers

How to Properly Duplicate a Table

Considering how tables are managed within the DOM, Table duplication actually amounts to just duplicating a Character. Not a big deal! The following routine exposes a cool trick which from this point on I will call the Specifier-Rewriting Trick :) What it allows is to bypass unnecessary DOM access by just rewriting the literal specifier of something to makes it point out to something else. In the below code we use it to convert an InsertionPoint into a Character:

function dupTable(/*Cell|Table*/obj, /*bool=false*/DUP_BEFORE)
{
    var LO = +LocationOptions[DUP_BEFORE ? 'before' : 'after'],
        c;
 
    // Make sure that obj is
    // a Cell, Table, Row, or Column
    // ---
    if( !obj.hasOwnProperty('rows') ) return null;
 
    // Seek the table
    // ---
    while( !(obj instanceof Table)){obj=obj.parent;}
 
    // Get the character that holds the table
    // using the 'Specifier-Rewriting Trick'
    // ---
    c = resolve(
        obj.            // Table
        storyOffset.    // InsertionPoint
        toSpecifier().  // contains "insertion-point" once
        replace('insertion-point','character')
        );
 
    // Duplicate the character
    // ---
    c = c.duplicate(LO,c);
 
    // Return the dup table
    // ---
    return c.tables[0];
}
 
// ---
// Assumed that a table or some cell is selected
// ---
var newTable = dupTable(app.selection[0]);
app.select(newTable);
 

• Original discussion: http://forums.adobe.com/message/5066429#5066429

Group PageItems per Script Label

InDesign users have a convenient way to tag page items throughout a document. They just need to open the Script Label panel and to affect some arbitrary or conventional string to the selected component(s). ExtendScript then allows to read this script label and to process some task accordingly. In this example we had to group items sharing some determined label prefix:

// Group labelled items spread by spread
// =====================================
 
var LABEL_PREFIX = "kkk22_33_";
 
var doc = app.documents.length && app.activeDocument,
    spreads = doc && doc.spreads,
    s = spreads ? spreads.length : 0,
    items, a=[], t, i, z=0;
 
while( s-- )
    {
    items = spreads[s].pageItems.everyItem().getElements();
    i = items.length;
    z = 0;
    while( i-- )
        {
        if( 0 != (t=items[i]).label.indexOf(LABEL_PREFIX) ) continue;
        a[z++] = t;
        }
    z && spreads[s].groups.add(a);
    a.length = 0;
    }
 

• Original discussion: http://forums.adobe.com/message/4295476#4295476

How to Compare Two Characters (and What Does that Mean)?

Given a Character instance c1 and a Character instance c2, ExtendScript will never assert c1===c2 unless both c1 and c2 resolve to the same location in the document, i.e. at the same index in the same story or container. This can be easily demonstrated by comparing a character against a pure duplicate:

var sto = app.activeDocument.stories[0],
    c1 = sto.characters[0],
    c2 = c1.duplicate(LocationOptions.AT_END, sto);
 
alert( c1===c2 ); // => false, simply because c1 and c2
                  //    don't share the same location
 

In other words, the only way to reach c1===c2 is to have c1.toSpecifier()==c2.toSpecifier() too. (By the way, note that Character objects are in no way equivalent to strings. Comparing objects and comparing strings are very different things.)

To get an efficient comparison routine between objects we now must determine which properties are relevant, and which are 'negligible'. For example, we could decide to disregard the following character properties: index, parent, parentStory, parentTextFrames... But there are other location properties, such as baseline or horizontalOffset, that we may want to skip.

Now, the most obvious solution to process comparison of Character or any DOM objects is to extract all data from the properties property and then to compare each key individually. Here is a possible implementation of this idea:

var compareCharacters = function F(/*Character*/c1, /*Character*/c2)
// -------------------------------------
// Return TRUE if some difference is found
// [check compareCharacters.lastDiff for details]
// otherwise return FALSE
{
    // Here are some properties that we won't regard
    // as expressing the 'identity' of a Character.
    // (This list is probably not exhaustive...)
    // ---
    var FF = F.filters || (F.filters = {
        index: null,
        // ---
        contents: null,
        // ---
        parent: null,
        parentStory: null,
        parentTextFrames: null,
        // ---
        baseline: null,
        horizontalOffset: null,
        endBaseline: null,
        endHorizontalOffset: null,
        // ---
        bulletChar: null,
        numberingRestartPolicies: null,
        });
 
    // Comparison routine
    // ---
    var o1 = c1.properties,
        o2 = c2.properties,
        t1, t2, k,
        r = '';
 
    for( k in o1 )
        {
        if( !o1.hasOwnProperty(k) ) continue;
        if( k in FF ) continue; // filtered prop
        if( !o2.hasOwnProperty(k) ){ r=k; break; } // missing prop
 
        t1 = o1[k];
        t2 = o2[k];
 
        if( t1 && ('object'==typeof t1) && 'toSource' in t1 )
            {
            t1 = t1.toSource();
            t2 = t2.toSource();
            }
 
        if( t1 !== t2 ){ r=k; break; } // not the same value
        }
 
    // Return
    // ---
    F.lastDiff = r;
    return !!r;
}
 
 
// =====================================
// Sample code
// =====================================
 
var doc = app.activeDocument,
    c1 = doc.textFrames[0].characters[0],
    c2 = doc.textFrames[1].characters[1];
 
if( compareCharacters(c1,c2) )
    {
    alert( "The characters differ in the following property:\r\r" +
        compareCharacters.lastDiff );
    }
else
    {
    alert( "The characters are (seen as) equivalent." );
    }
 

• Original discussion: http://forums.adobe.com/message/4724959#4724959

Apply Fit-Frame-to-Content to All Overset Items

Just a short snippet to take a breath:

var FO = FitOptions.FRAME_TO_CONTENT,
    tfs = ([]).concat.apply(
        [],
        app.activeDocument.stories.everyItem().textContainers
        ),
    t, i = tfs.length;
 
while( i-- ) (t=tfs[i]).overflows && ( t.locked || t.fit(FO) );
 

• Original discussion: http://forums.adobe.com/message/4241646#4241646

Override Master Text Frames without Breaking the Thread

Manan Joshi wrote: "I have a master page with two threaded textframes. I apply this master page on a document page and want to change the text in these boxes without effecting the boxes on the master. So I try to override these textframes however overriding breaks the thread between the textframes. So what I need is the capability to change the content of the masterpage items without breaking the thread between the boxes."

An approach is to temporarily group the threaded frames, then apply that master group to the destination page, then ungroup. This way the thread is preserved.

Suggested implementation:

function threadOverride(/*MasterSpread*/spd, /*TextFrame*/tf, /*Page*/pg)
{
    var t, g;
 
    try {
        // Get the array of threaded text frames
        // ---
        t = tf.parentStory.textContainers;
 
        // Create a temporary master group --if possible!
        // ---
        g = spd.groups.add(t);
        t.length = 0;
 
        // Apply the master group to pg --if possible!
        // ---
        t = g.override(pg);
        pg = true;
 
        // Ungroup the master group
        // ---
        g.ungroup();
 
        // Store the new frames into an array
        // ---
        t = (g=t).pageItems.everyItem().getElements();
 
        // Ungroup the new frames
        // ---
        g.ungroup();
 
        g = null;
        return t;
        }
    catch(e)
        {
        if( !g )
            {
            throw "Unable to create the group from the threaded frames.";
            }
        if( true!==pg )
            {
            g.ungroup(); // restore the initial state
            throw "Unable to send the master objects to the destination page.";
            }
        throw e;
        }
}
 
 
// Sample code
// ---
var doc = app.activeDocument,
    sourceSpread = doc.masterSpreads[0],
    destPage = doc.pages[1],
    anyThreadedFrame = sourceSpread.textFrames[2];
 
var newFrames = threadOverride(sourceSpread, anyThreadedFrame, destPage);
if( newFrames )
    {
    try{app.select(newFrames);}
    catch(_){}
    }
 

Subsidiary question: is it possible to emulate a Cmd + Shift + Click through scripting? I don't think so, and as a general rule I wouldn't recommend mimicking-UI strategies as they often lead to performance costs and tricky contextual issues. Anyway, it is possible to perform some operations through the corresponding MenuAction, which leads to this alternative approach:

function threadOverrideAlt(/*MasterSpread*/spd, /*TextFrame*/tf, /*Page*/pg)
{
    // Active the dest page
    // ---
    app.activeWindow.activePage = pg;
 
    // Get the array of threaded text frames
    // ---
    var a = tf.parentStory.textContainers,
        i = a.length;
 
    // Make all master items disallow override
    // ---
    spd.pageItems.everyItem().allowOverrides = false;
 
    // ...except our targeted frames
    // ---
    while( i-- ) a[i].allowOverrides = true;
 
    // Invoke the menu action
    // ---
    app.menuActions.itemByName
        ('$ID/Override All Master Page Items').invoke();
 
    // Restore allowOverrides (to all master items)
    // ---
    spd.pageItems.everyItem().allowOverrides = true;
}
 
 
// Sample code
// ---
var doc = app.activeDocument,
    sourceSpread = doc.masterSpreads[0],
    destPage = doc.pages[2],
    anyThreadedFrame = sourceSpread.textFrames[2];
 
threadOverrideAlt(sourceSpread, anyThreadedFrame, destPage);
 

• Original discussion: http://forums.adobe.com/message/4679553#4679553

Design a Custom Method to Transfer Table Cells

It sounds like a good idea to use the everyItem() structure to speed up the process of moving contents "from one cell of a table to another cell of the same table" in a constant way. For example, you may think that such code has a chance to work:

var every = myStory.tables.everyItem();
 
// Would be so cool:
every.cells[-1].texts[0].move(
    LocationOptions.AT_BEGINNING,
    every.cells[0]
    );
 

but it does not. The reason is that you cannot use a collective specifier —xxx.everyItem()— as the target of a move(). More generally, collective specifiers must be involved in N-to-1 commands, such as xxx.everyItem().fillColor = /*singular Color*/ or xxx.everyItem().duplicate(…, /*singular Destination*/). You can't implement N-to-N commands through that approach.

Also, there is no way to specify a bijective relationship between two collective specifiers. When we write something like:

xxx.everyItem().selectorA.move(…, xxx.everyItem().selectorB);

we wrongly assume that there could be a kind of implicit N-to-N connection between the receivers of xxx.everyItem().selectorA and the receivers of xxx.everyItem().selectorB, while they simply are, well, two collective specifiers.

Whatever the source specifier, mySourceSpecifier.move(…, collectiveSpecifier) can't work because a unique, single destination is required.

However, it is still possible to get a method compliant with a collective specifier, as illustrated in this example:

Table.prototype.myCustomMoveCells = function()
//--------------------------------------
{
    var LO_BEG = LocationOptions.AT_BEGINNING;
 
    var a = this.getElements(),
        i = a.length,
        t;
 
    while( i-- )
        {
        t = a[i].columns;
        t[-1].cells[0].texts[0].move(
            LO_BEG,
            t[0].cells[0].texts[0].insertionPoints[0]
            );
        }
 
    a.length = 0;
    a = t = null;
};
 
// Sample code
// ---
var story = app.activeDocument.stories[0];
story.tables.everyItem().myCustomMoveCells();
 

• Original discussion: http://forums.adobe.com/message/4656692#4656692

• See also: On EveryItem() – Part 1 | Part 2

2/ Graphics, Geometry and Coordinate Spaces

Syncing InDesign and Illustrator Layers

Given a series of Illustrator files placed on different InDesign layers, our goal here is to "set the visibility of the layers contained in the Illustrator files basing on the name of the Indesign layers to which they belong."

The underlying algorithm leads to play with the GraphicLayer.currentVisibility property, but InDesign scripters have discovered a critical issue in that field. When you change the currentVisibility of a graphic layer, its specifier instantly becomes invalid! As a matter of fact, it seems that the Graphic container (e.g. myPDF) gets a new id so the whole chain is disabled: myPDF, myPDF.graphicLayerOptions, etc.

This is a very special case where we need to use unresolved specifiers in order to recreate a valid access to the graphic object each time the state of a graphic layer is changed within. In other words, I recommend we do not use resolved arrays such as myRectangle.allGraphics. Instead, we re-access the element by its index from a collection (myRectangle.graphics). Using indices rather than ids seems to work, provided that a resolved specifier is based on ids.

The following code should be refined if you have nested graphics and/or nested graphic layers, but in basic situations this is a good starting point:

// NOTE: turning enabledRedraw off is highly recommended!
 
// =====================================
// YOUR SETTINGS
// =====================================
var ID_TO_AI_LAYERS = {
        "ES": ["ES", "quote", "disegno"],
        "EN": ["EN", "quote", "design"],
        "FR": ["FR"],
        /* etc.*/
        },
    TARGET_COLLECTION = 'rectangles';
 
// =====================================
// SYNCHRONIZE ID<->AI LAYERS
// =====================================
const EXTRA_CHAR = '\uE080';
 
var items = app.activeDocument[TARGET_COLLECTION],
    i = items.length,
    r, gx, j,
    s, okNames, a,
    t, j, k, v;
 
while( i-- )
    {
    // Name of the ID layer the current rectangle (r) belongs to
    // ---
    s = (r=items[i]).itemLayer.name;
 
    // Make sure that this layer is supported
    // ---
    if(! ID_TO_AI_LAYERS.hasOwnProperty(s) ) continue;
 
    // Store the allowed AI layer names in a String (okNames)
    // ---
    okNames = EXTRA_CHAR + ID_TO_AI_LAYERS[s].join(EXTRA_CHAR) + EXTRA_CHAR;
 
    // Consider only the direct graphic children of the current rectangle
    // (We need unresolved specifiers here => gx remains a collection.)
    // ---
    gx = r.graphics;
    j = gx.length;
    while( j-- )
        {
        // Temporarily resolve the underlying graphic object (--> PDF)...
        // ---
        t = gx[j].getElements()[0];
 
        // ...in order to check whether t has graphic layers
        // ---
        if( !('graphicLayerOptions' in t) ) continue;
 
        // Consider the names of the contained graphic layers
        // ---
        a = t.graphicLayerOptions.graphicLayers.everyItem().name;
        k = a.length;
        while( k-- )
            {
            s = a[k];
 
            // Depending on a graphic layer name, do we want to show or hide it?
            // ---
            v = 0 <= okNames.indexOf(EXTRA_CHAR + s + EXTRA_CHAR);
 
            // Now, the problem is to point out to the graphic layer although
            // the parent graphic specifier may not be valid anymore!
            // But gx[j] should work, as we re-access the element per index
            // (from the Graphics coll.)
            // ---
            t = gx[j].graphicLayerOptions.graphicLayers.itemByName(s);
 
            // Hit the DOM *only* if a change is possible AND required
            // ---
            ( ! t.isValid ) ||
            ( t.currentVisibility === v ) ||
            ( t.currentVisibility = v );
            }
        }
    }
 

• Original discussion: http://forums.adobe.com/message/4920745#4920745

What's the Shape?

I love this challenge: "I have three textframes with different shapes. One is a rectangular frame, other is oval and last one is triangular. How to determine the respective shape of each frame?"

An option is to temporarily convert a copy of some textframe to the three regular shapes (rectangle, oval, triangle), and then to compare entire paths. As follows:

var CSO = ['OVAL','RECTANGLE','TRIANGLE'];
 
var tf = app.selection[0], // assuming a frame is selected
    ep = tf.paths[0].entirePath.toSource(),
    i = CSO.length,
    k, t;
 
while( i-- )
    {
    k = 'CONVERT_TO_' + CSO[i];
    with( tf.duplicate() )
        {
        convertShape(ConvertShapeOptions[k]);
        t = paths[0].entirePath.toSource();
        remove();
        if( t == ep ) break;
        }
    }
 
alert( 0 <= i ?
    ("The textframe's shape is: " + CSO[i]):
    ("Unable to detect the shape")
    );
 

• Original discussion: http://forums.adobe.com/message/4692442#4692442

Moving an Object Considering various Reference Points

In PageItem.move(to, by) the to parameter is always topLeftAnchor relative, whatever the current transformReferencePoint. So if you need to position a PageItem by specifying the location of its center anchor (or any other one), you need a custom moveTo routine which supports such option:

function moveToConsideringAnchor(obj, xyDest, anchorPt)
// -------------------------------------
// obj      :  any singular PageItem specifier
// xyDest   :  [x,y] location expressed in the current ruler space
// anchorPt :  the considered AnchorPoint (default is topLeftAnchor)
{
    if( !(obj && 'transform' in obj) ) return;
 
    var CS_PASTEBOARD = CoordinateSpaces.pasteboardCoordinates;
 
    anchorPt || (anchorPt=AnchorPoint.topLeftAnchor);
 
    var xy0 = obj.resolve(anchorPt,CS_PASTEBOARD)[0],
        xy1 = obj.resolve([xyDest, anchorPt],CS_PASTEBOARD,true)[0],
        dx = xy1[0]-xy0[0],
        dy = xy1[1]-xy0[1];
 
    obj.transform(CS_PASTEBOARD,[0,0],[1,0,0,1,dx,dy]);
}
 
// ---
// SAMPLE CODE
// ---
 
 
// Your settings:
var CONSIDERED_ANCHOR = AnchorPoint.BOTTOM_RIGHT_ANCHOR,
    XY_DESTINATION = [159,61.3]; // in ruler space
 
var sel = (1==app.selection.length) && app.selection[0];
 
if( sel instanceof Rectangle )
    {
    moveToConsideringAnchor(sel, XY_DESTINATION, CONSIDERED_ANCHOR);
    }
 

• Original discussion: http://forums.adobe.com/message/5021396#5021396

Sizing an Object

PanDulka wrote: "Is there a simple method to set a width/height of an object in absolute values without messing with .geometricBounds?"

In InDesign CS4 and better, the resize() method is the way to go:

function setWidthHeight(/*PageItem*/o, /*str*/w, /*str*/h, /*bool=false*/useVisibleBounds)
{
    if( !(o && 'resize' in o) ) return;
 
    var CS_INNER = CoordinateSpaces.INNER_COORDINATES,
        BB = BoundingBoxLimits[
            (useVisibleBounds?'OUTER_STROKE':'GEOMETRIC_PATH')
            + '_BOUNDS'];
 
    var wPt = UnitValue(w).as('pt'),
        hPt = UnitValue(h).as('pt');
 
    if( 0 >= wPt || 0 >= hPt ) return;
 
    o.resize(
        [CS_INNER,BB],
        AnchorPoint.CENTER_ANCHOR,
        ResizeMethods.REPLACING_CURRENT_DIMENSIONS_WITH,
        [wPt,hPt,CS_INNER]
        );
}
 
// Sample code
// ---
setWidthHeight(app.selection[0], "5cm", "50pt");
 

But in CS3, we have to invoke the transform() method (in a very special way):

function setWidthHeightCS3(/*PageItem*/o, /*str*/w, /*str*/h, /*bool=false*/useVisibleBounds)
{
    if( !(o && 'transform' in o) ) return;
 
    var CS_INNER = CoordinateSpaces.INNER_COORDINATES,
        BB = BoundingBoxLimits[
            (useVisibleBounds?'OUTER_STROKE':'GEOMETRIC_PATH')
            + '_BOUNDS'];
 
    var wPt = UnitValue(w).as('pt'),
        hPt = UnitValue(h).as('pt');
 
    if( 0 >= wPt || 0 >= hPt ) return;
 
    var wsBkp = app.transformPreferences.whenScaling,
        a = o.resolve([[1,1], BB], CS_INNER)[0].
            concat(o.resolve([[0,0], BB], CS_INNER)[0]),
        sx = wPt / (a[2]-a[0]),
        sy = hPt / (a[3]-a[1]);
 
    app.transformPreferences.whenScaling = WhenScalingOptions.APPLY_TO_CONTENT;
 
    o.transform(
        CS_INNER,
        AnchorPoint.CENTER_ANCHOR,
        [sx,0,0,sy,0,0],
        MatrixContent.SCALE_VALUES
        );
 
    app.transformPreferences.whenScaling = wsBkp;
}
 
// Sample code
// ---
setWidthHeightCS3(app.selection[0], "5cm", "100pt");
 

• Original discussion: http://forums.adobe.com/message/4842820#4842820

Put a Gap between Pages

There are various solutions to deal with the problem of making room between pages in InDesign CS5 and later—including the popular Separate Pages Script from InTools.

I just suggest here an educative approach, based on the fact that CS5 and later fully supports page transformations, which makes it easy to apply fine-tuned translations:

// =====================================
// NOTE: This WILL NOT work on facing-page docs
// =====================================
 
var X_GAP = 50, // in points
    CS_INNER = +CoordinateSpaces.INNER_COORDINATES;
 
// Example with the 1st spread
// ---
var spd = app.activeDocument.spreads[0],
    pages = spd.pages.everyItem().getElements(),
    n = pages.length,
    i;
 
for( i=1 ; i < n ; ++i )
    pages[i].transform(
        CS_INNER,            // consider the inner bounding box
        [0,0],               // arbitrary location (translation disregards origin)
        [1,0,0,1,i*X_GAP,0]  // x-translation by i*X_GAP
        );
 

• Original discussion: http://forums.adobe.com/message/4994728#4994728

3/ On the JavaScript/ExtendScript Side

Quick Note about XML.prototype

Any attempt to enrich XML.prototype in ExtendScript will fail. Why so?

The strict E4X specification (ECMA-357) made XML.prototype [[read-only]] (which indeed was a bad idea!)

In order to allow prototype inheritance, a special syntax has been proposed later, based on a reserved function namespace, as follows:

XML.prototype.function::test = function( ){ /*my method*/ };
 

but ExtendScript, unlike SpiderMonkey, doesn't seem to support this syntax so far.

• Original discussion: http://forums.adobe.com/message/5099477#5099477

Identify All Subparts of an Array

Given an array A, how to produce a new array that contains all subsets formed of A's elements? For example:
[X, Y, Z] => [ [X], [Y], [Z], [X,Y], [X,Z], [Y,Z], [X,Y,Z] ]

This is the purpose of the arrayParts() function:

function arrayParts(/*arr*/a, r,i,j,t,s,p)
// -------------------------------------
// Note: a is supposed to contain unique items
// [if necessary, apply a makeUnique routine first]
{
    if(! (i = Math.pow(2,a.length)-1) ){ return null; }
    (r=[]).toString = function(){return this.join('\r');};
 
    while(i--)
        {
        r[i] = t = [];
        s = (1+i).toString(2);
        p = (j=s.length) - 1;
        while( j-- ) '1'==s[j] && t[t.length]=a[p-j];
        }
 
    // Reorder by length [if needed!]
    // ---
    r.sort( function(x,y){return x.length-y.length;} );
 
    return  r;
}
 
// Sample code
// ---
var arrTest = ['Blue','Red','Yellow','Green','Black'];
alert( arrayParts(arrTest) );
 

• Original discussion: http://forums.adobe.com/message/4500105#4500105

A Trick to Avoid if() and switch()

Given a set of exclusive conditions cond1, cond2, cond3..., how to set an expression to value1 if cond1 is true, to value2 if cond2 is true, to value3 if cond3 is true, etc., otherwise to a default value.

In other words, we want the expression to be set according to this pseudocode:

myExpression = (function()
    {
    if( cond1 ) return value1;
    if( cond2 ) return value2;
    if( cond3 ) return value3;
    // etc.
    return defValue;
    })();
 

In JavaScript, a very compact way to get such expression is:

myExpression = [defValue, value1, value2, value3]
               [1*(cond1)||2*(cond2)||3*(cond3)];
 

• Original discussion: http://forums.adobe.com/message/4682849#4682849

Why You Shouldn't Use Numerals as Object Keys

There is a critical issue in ExtendScript. Object keys that look like numerals (as they only contains digits) are seen as actual numeric values in some contexts. This is evidenced by the following code:

var obj = {"0123":"foo"},
    clone = eval(obj.toSource());
 
alert( clone.toSource() ); // => ({83:"foo"})  !!
 

The key "0123" unexpectedly becomes 83 because, actually, the interpreter saw an octal number: 0123 (=83).

In regular JS:

alert( {"0123": "foobar"}.toSource() );
 

would display ({"0123":"foobar"}) but in ExtendScript you get ({0123:"foobar"}). In other words, the quotes are lost so the key improperly evaluates to 0123, i.e. 83!

Note that you have similar issue with a '0x' prefix:

alert( eval({'0x123' : "foobar"}.toSource()).toSource() );
// => ({291:"foobar"})
 

Conclusion: Never use numerals as associative keys in ExtendScript. When you really need to manage such things you can use a prefix, like '_', that prevents the interpreter from parsing the key as a number.

Note. — Using JSON.stringify() is an option too, but this forces you to include an external library.

• Original discussion: http://forums.adobe.com/message/4826651#4826651

Not Calling a Function as a Constructor is Sometimes Risky!

Let's study the following code:

function someFunction()
{
    this.value = Math.random();
    return this;
}
 
var test1 = someFunction();
alert( test1.value ); // some random number
 
var test2 = someFunction(); // new call
 
alert( test1.value ); // changed!!
 
// In fact, we have:
alert( test1===test2 ); // true
 

The key point is that someFunction will always update a single instance (the context referred to by this) and return a reference to that single instance. The reason is that we do not call the function as a constructor. So, if we plan to re-use someFunction in order to address several objects, we definitely need to use new someFunction() instead:

function someFunction()
{
    this.value = Math.random();
    return this;
}
 
var test1 = new someFunction();
alert( test1.value ); // some random number
 
var test2 = new someFunction(); // new instance
 
alert( test1.value ); // unchanged
 
alert( test1===test2 ); // false
 

This illustrates the crucial difference between myFunc() and new myFunc().

Now, if someFunction.prototype is not intended to be customized (i.e. we don't need dedicated methods for those objects), then there is no reason to use this scheme. Indeed we could return as well an object instance built on the fly, as shown below:

function someFunction()
    {
        return {
            fun: 'funny',
            t: true
            };
    }
 
var test = someFunction();
 
alert(test.fun + '  |  ' + test.t); // =>  funny  |  true
 

which is the exact counterpart of the Array's approach:

function someFunction()
    {
        return [
            'funny',
            true
            ];
    }
 
var test = someFunction();
 
alert(test[0] + '  |  ' + test[1]); // =>  funny  |  true
 

• Original discussion: http://forums.adobe.com/message/5087446#5087446

4/ Miscellaneous and Advanced Topics

Implementing "whose" in ExtendScript JS

Jongware wrote: "AppleScript has a 'whose' function, which allows you to quickly select specific items out of an array — much to the envy of Javascripters! Would this function be a good idea?"

My two pennies:

Array.prototype.whose = function F(/*obj|fct*/condition)
//--------------------------------------
// Note: support nested objs (see Example 1 below)
{
    // Default test function (cached)
    // ---
    F.OBJ_TEST || (F.OBJ_TEST = function T(t,o)
        {
        var r = 1,
            k;
 
        for( k in o )
            {
            if( !o.hasOwnProperty(k) ) continue;
            r = ( 'object' == typeof o[k] ) ?
                T(t[k],o[k]) :
                +(t[k] == o[k]);
            if( !r ) break;
            }
        return r;
        });
 
    // Vars
    // ---
    var fCond = 'function'==typeof condition || (condition instanceof Function),
        oCond = fCond ? undefined : condition,
        n = this.length,
        z = 0,
        i;
 
    // Do we use the default function?
    // ---
    fCond = fCond ? condition : F.OBJ_TEST;
 
    // Processing...
    // ---
    for( i=0 ; i < n ; ++i )
        fCond(this[i],oCond) && (this[z++] = this[i]);
 
    this.length = z;
    fCond = oCond = null;
 
    // Will allow compound calls :-)
    // ---
    return this;
};
 
 
 
// ---
// Example 1 (based on obj)
// ---
a = app.activeDocument.textFrames.everyItem().getElements().
    whose({ contents:"foobar", fillColor:{name:"None"} });
app.select(a);
 
 
// ---
// Example 2 (based on a custom function)
// ---
a = app.activeDocument.textFrames.everyItem().getElements().
    whose(function(tf){ return tf.texts[0].characters.length > 10; });
app.select(a);
 

• Original discussion: http://forums.adobe.com/message/5109826#5109826

Propagating Custom Events in ScriptUI

The syntax new UIEvent(…) works fine in InDesign (CS4/CS5/CS6) provided that you let the 4th argument (view) undefined.

Here is an example of how you can propagate a custom UI Event in a basic dialog:

// Propagation of a custom UI event (which bubbles)
// Tested in ID CS4/CS5/CS6
 
const EV_TYPE = 'MyCustomEventType';
 
var u,
    w = new Window('dialog'),
    p = w.add('panel'),
    b = p.add('button',u,"Test"),
    // ---
    evHandler = function(ev)
    {
        alert( this + " is listening: " + ev.data );
    },
    myDispatcher = function(data)
    {
        var ev = new UIEvent(EV_TYPE, true, true);
        ev.data = data;
        this.dispatchEvent(ev);
    };
 
w.addEventListener(EV_TYPE, evHandler);
p.addEventListener(EV_TYPE, evHandler);
 
b.onClick = function(){ myDispatcher.call(this, "Some data"); }
 
w.show();
 

• Original discussion: http://forums.adobe.com/message/5286695#5286695

Note about InDesign DOM Events

It's a misconception to think events as owned or triggered by some particular DOM objects. In fact, objects can listen to events (through the addEventListener methods), and usually any object can listen to any event depending on the capturing method and the event phase. During its propagation, an event (instance) provides properties such as target or currentTarget that give details about the context, but there is no actual connection between the event and some object specifier. The event is triggered by the subsystem.

In CS5 and later we have six fields of events, represented by the following 'classes': Event (base class), DocumentEvent, MutationEvent, ImportExportEvent, PrintEvent, IdleEvent. These are just prototyped APIs for all the event types that InDesign accepts to make available within the DOM.

Event Type Class
AFTER_ACTIVATE Event
AFTER_ATTRIBUTE_CHANGED MutationEvent
AFTER_CLOSE DocumentEvent
AFTER_CLOSE Event
AFTER_CONTEXT_CHANGED Event
AFTER_DELETE Event
AFTER_EMBED Event
AFTER_EXPORT ImportExportEvent
AFTER_IMPORT ImportExportEvent
AFTER_INVOKE Event
AFTER_LINKS_CHANGED Event
AFTER_MOVE Event
AFTER_NEW DocumentEvent
AFTER_NEW Event
AFTER_OPEN DocumentEvent
AFTER_OPEN Event
AFTER_PLACE Event
AFTER_PRINT PrintEvent
AFTER_QUIT Event
AFTER_REVERT DocumentEvent
AFTER_SAVE DocumentEvent
AFTER_SAVE_AS DocumentEvent
AFTER_SAVE_A_COPY DocumentEvent
AFTER_SELECTION_ATTRIBUTE_CHANGED Event
AFTER_SELECTION_CHANGED Event
AFTER_UNEMBED Event
AFTER_UPDATE Event
BEFORE_CLOSE DocumentEvent
BEFORE_CLOSE Event
BEFORE_DEACTIVATE Event
BEFORE_DELETE Event
BEFORE_DISPLAY Event
BEFORE_EMBED Event
BEFORE_EXPORT ImportExportEvent
BEFORE_IMPORT ImportExportEvent
BEFORE_INVOKE Event
BEFORE_MOVE Event
BEFORE_NEW DocumentEvent
BEFORE_OPEN DocumentEvent
BEFORE_PLACE Event
BEFORE_PRINT PrintEvent
BEFORE_QUIT Event
BEFORE_REVERT DocumentEvent
BEFORE_SAVE DocumentEvent
BEFORE_SAVE_AS DocumentEvent
BEFORE_SAVE_A_COPY DocumentEvent
BEFORE_UNEMBED Event
BEFORE_UPDATE Event
FAILED_EXPORT ImportExportEvent
ON_IDLE IdleEvent
ON_INVOKE Event

• Original discussion: http://forums.adobe.com/message/4998494#4998494

Scanning All State Containers in a Whole Document

My colleague ~Trevor~ asked me to think over an algorithm that could efficiently identify all MultiStateObjects and Buttons within a document. It's an interesting question because InDesign does not offer an obvious access to all hidden and/or nested States, so we need to deal with both recursivity issues and time consuming DOM exploration.

Although we cannot pretend that my implementation definitely addresses all pathological cases, it seems to provide satisfactory results as far as we tested:

var registerStateContainer = function(/*FormField*/ff, /*str*/k, /*{k=>{...}}&*/o)
// -------------------------------------
// ff must be a 'state container' (i.e. either a Button, or a MSO)
{
    if( o.hasOwnProperty(k) ) return;
    o[k] = {
        type:    ff.__class__,
        name:    ff.name,
        states: ff.states.length,
        // etc.
        };
};
 
var scanStateContainers = function F(/*PageItem[]&*/a, /*{_id=>{...}}&*/o)
// -------------------------------------
{
    // Caching IDs already explored
    // ---
    F.cache || (F.cache={});
 
    // Vars
    // ---
    var subs = [],
        k, t, SC;
 
    while( t=a.pop() )
        {
        k = '_' + t.id;
 
        if( F.cache.hasOwnProperty(k) ) continue;
 
        (SC = ('states' in t)) && registerStateContainer(t, k, o);
 
        subs = subs.concat( SC ?
            t.states.everyItem().pageItems.everyItem().getElements() :
            t.allPageItems );
 
        F.cache[k] = null;
        }
 
    subs.length && F(subs,o);
    subs = t = null;
};
 
// Main
// ---
var doc = app.activeDocument,
    results = {};
 
scanStateContainers(doc.allPageItems, results);
 
alert( results.toSource() );
 
// ---
// Then you can traverse results using for..in
// Also, if you need to access any state container
// (including nested), itemByID should always work:
// doc.formFields.itemByID(parseInt(key.substr(1),10))
// ---
 

• Original discussion: http://forums.adobe.com/message/5231904#5231904