1/ Text and Typography
Fit Text to Frame by Adjusting Font Size
Deciphering Special Characters in Text Entities
How FontChangeLocking May Impact GREP Change Method

2/ Graphics and Geometry
Does My Container Extend Beyond Its Inner Image Frame?
Reframing Pages: a Dangerous Sport

3/ Miscellaneous
Basic Prototype for Testing Array Equality
Repairing toSource() Method for Enumerations
Making a Group Responsive Using an IdleTask

1/ Text and Typography

Fit Text to Frame by Adjusting Font Size

We already discussed in this series some text-frame-fitting scripts (e.g. “Fit Text Frame to Content – Supporting Multicolumn”, or “Apply Fit-Frame-to-Content to All Overset Items”), the most flexible being “Fit Horizontal Text Scale to Frame” as it sketches a general dichotomic algorithm for dealing with various cases.

As an illustration the code template can be reused for fitting text to frame by adjusting font size rather than width or horizontal scale. Only two changes must be done to update the original code: (a) Replacing horizontalScale by pointSize as input parameter (and adjusting min-max values accordingly), (b) removing condition on lines.length in the wrong() function if not needed (this depends on whether you have to handle single-line or multiline frames.)

The final snippet then looks like:

// Fit text in the selected frames by adjusting font size.
// ---
    var t, s, v, w, m = [],    
        wrong = function(){ return +t.overflows };  
    while( t=a.pop() )    
        if( 'TextFrame'!=t.__class__ ) continue;    
        // Init    
        // ---    
        v = (s=t.parentStory)[k];    
        m[0] = MIN;    
        m[1] = MAX;    
        // Try extremum and quit if status doesn't change
        // ---    
        s[k] = m[1-(w=wrong())];    
        if( w==wrong() ){ s[k]=v; continue; }    
        // Binary search    
        // ---    
        while( m[1]-m[0] > PRECISION )
          { m[w=wrong()] = s[k] = (m[0]+m[1])/2; }
        w && (s[k] = m[0]);    
})(app.properties.selection||[], 'pointSize', 6, 36, .1);

Fit Text to Frame by Ajusting Font Size.

Note. — As observed by oleh.melnyk this is not a line-by-line script, the process targets each frame as a whole multiline box and uniformly adjusts the font size. However, a very similar technique can be used to achieve other tasks at various Text levels. In itself, the dichotomic pattern doesn't change, and will remain highly efficient.

• Original discussion: forums.adobe.com/message/8847812#8847812

• See also: “On Dichotomic Divide-and-Conquer Algorithms”

Deciphering Special Characters in Text Entities

Quick reminder on those recurring questions that relate to SpecialCharacters in InDesign text flows.

1. How do I check whether myCharacter.contents is a SpecialCharacters entity?

• In CS4, use the test ( 'number' == typeof myCharacter.contents ).

• In CS5 and later, use ( 'object' == typeof myCharacter.contents ) instead.

Indeed, in any other case typeof will return 'string' (assuming myCharacter is a valid Character instance.) From there, we can even offer a universal test that works in any InDesign version:

// Is myCaracter a 'Special Character'?
// ---
alert( 'string' != typeof myCharacter.contents );

2. How can I safely convert a single Character (or any Text) into a clean JS string?

Whatever the status of a Character object (special vs. regular), the following expression always returns a pure JavaScript string:

// Character (or Text) to-String conversion
// ---
var str = myCharacter.texts[0].contents;

This trick prevents the contents property from returning a SpecialCharacters enum even if myCharacter is a special character.

Important note. — The texts[0] selector operates as desired if it is immediately followed by .contents. The whole thing consists in invoking the contents property from a specifier that ends with ...texts[0]. So if texts[0] is used upstream in the path you'll need it again at the end. For example, the expression

myChars = story.texts[0].characters.everyItem().getElements()

returns an array of Character instances that may still embed special entities (despite texts[0].) And then myChars[i].contents is still unsafe, while myChars[i].texts[0].contents does the job again (that is, the Enumerator issue vanishes.)

Additional note. — In ExtendScript the expression "blabla" + myEnumerator returns a string based on myEnumerator.valueOf() (i.e. the underlying Number) instead of using myEnumerator.toString() (i.e. the underlying String.) This is a generic bug of the Enumerator object, which hasn't been implemented with respect to ECMA's specification regarding type conversion. Normally, given an Object myObj and a literal string "blabla", the string concatenation "blabla" + myObj should implicitly invoke myObj.toString() in order to achieve the type conversion before concatenation. Unfortunately, the Enumerator type does not follow this rule. I suspect that the + operator hasn't been fully and properly implemented. Using the unary syntax (+myEnumeror) we get, as expected, a number—as +myObj is a shorcut of myObj.valueOf(). Now, regarding the expression anything + myObj, the JavaScript interpreter must select either an addition if anything is a Number (and then myObj.valueOf() is the right guy), or a concatenation if anything is a String, and then myObj.toString() should be invoked… which is not the case!

In short, ExtendScript wrongly interprets myString + myEnumerator as myString + myEnumerator.valueOf(), which leads to the string concatenation myString + (myEnumerator.valueOf()).toString() instead of myString + myEnumerator.toString().

The following and surprising results illustrate that point:

var specialChar = SpecialCharacters.BULLET_CHARACTER;
alert( specialChar.__class__ );  // => Enumerator
alert( specialChar );            // => BULLET_CHARACTER
alert( '' + specialChar );       // => 1396862068

Added on 01/17/2017. — In case the bug above becomes a critical issue in your project, you can fix it using operator overloading, as follows:

// Globally allows to use the syntax myString + myEnum.
// ---
NothingEnum.NOTHING.constructor.prototype['+'] = function(x)
return this['string' == typeof x ? 'toString' : 'valueOf']() + x;

• Original discussion: forums.adobe.com/message/8900483#8900483

How FontChangeLocking May Impact GREP Change Method

In a recent project I had to temporarily use a special character (U+E100) as a marker for applying some specific paragraph style to the active document:

The current font doesn't provide a glyph for U+E100 (Unicode's Private Use Area.)

The fact that this temporary character has no corresponding glyph in the current font didn't sound like a serious problem, since my script was then using changeGrep() to both remove this marker and change the paragraph style at the corresponding location. It looked like this:

var myDoc = app.properties.activeDocument,  
    myBoldStyle = myDoc.paragraphStyles.itemByName('MyBoldStyle');  
// Init.  
app.findGrepPreferences = app.changeGrepPreferences = null;
// Find the marker U+E100
//    + the next char (captured in $1)
// ---
app.findGrepPreferences.findWhat = "\uE100(.)";  
// Preset changeGrep so that it both removes the  
// marker and change the paragraph style.
// ---
app.changeGrepPreferences.properties = {  
    changeTo: '$1',  
    appliedParagraphStyle: myBoldStyle,  
// Go!  

Note that findWhat looks for two characters: the marker ("\uE100") and the next character captured into $1 thanks to the parentheses in the "\uE100(.)" regular expression. Then changeGrepPreferences is preset so that the whole capture is changed into $1 (which removes the marker) while assigning the desired paragraph style (myBoldStyle).

However, using exactly the same document and script, I got two different results in two different environments:

Expected result (in the first environment.)

Unexpected result (in another environment.)

The unexpected behavior (platform #2) shows that, although the paragraph style is applied everywhere else, the very first character (i.e., the $1 variable) retains its old attributes. In other words, myBoldStyle does not seem to override the formatting, while changeText/changeGrep commands having an appliedParagraphStyle property should definitely reset the style.

The surprising fact is, anyway, that results differ depending on the environment. In such circumstance the most likely hypothesis is that an unidentified preference parameter disrupts the homogeneity of the process.

Thanks to Trevor and Uwe Laubender—and after some trial and error—we finally found the invisible cause of that weird issue: app.fontLockingPreferences. Here we have a boolean fontChangeLocking property defined as follows, “If true, turns on missing glyph protection during font change.” And, as you can now guess, this option impacts how changeGREP() deals with missing glyphs. Keep this in mind when you tweak odd characters and missing glyphs through find/change…

• Original discussion: forums.adobe.com/message/8503226#8503226

3/ Graphics and Geometry

Does My Container Extend Beyond Its Inner Image Frame?

How to check whether a frame does not extend beyond the image it contains? Simple question. Consider the figure below. There is no doubt that the left container is OK (it doesn’t jut out from the image area) while the right container is appreciably larger (its magenta background shines out.)

Isn't it easy to check whether a frame extends over its graphics content?

So, at first sight, the question is all about comparing the PARENT (=container) bounds against the CHILD (=image) bounds, making sure that the parent area is entirely contained within the child area. So the OK condition could be expressed as follows: PARENT ⊆ CHILD.

Using visibleBounds (or geometricBounds) coordinates might seem the way to go. Unfortunately this approach fails as soon as rotations or complicate transformations come into play, because xxxBounds properties return rectangular coordinates relative to the ruler space. So, in the figure below, the image bounds seen from the rulers (gray rectangle) wrongly lead to assert PARENT ⊆ CHILD while the magenta warning signal is visible again.

The naive approach does not work in a simple rotation case.

So we likely need to take the actual (inner) bounding boxes into account, and their underlying transformation spaces. The question could first be addressed as follows. To ensure that the container does not extend beyond the image area, it is sufficient to check that the parent inner box is contained within the image inner box. In short we just refined the OK condition that way:

And that specific condition could be instantly tested using (u,v) coordinates of the respective boxes, without any transformation or Pathfinder operations.

Alas and alack, a closer look at the problem reveals that our intuitive equation is only a sufficient condition. The below counterexample proves it is not necessary:

A counterexample of our intuitive condition.

Here we have an OK case (no magenta signal) while the parent inner box is not contained within the child inner box. How is it possible? The parent box does not match the detailed geometry of the polygon it encloses. The polygon itself (the shape) is entirely contained within the child box, while the enclosing parallelogram (parent box) spills over.

So, the definitive OK condition is PARENT-SHAPE ⊆ CHILD-INNER-BOX rather than PARENT-INNER-BOX ⊆ CHILD-INNER-BOX (which is only a sufficient condition since PARENT-SHAPE ⊆ PARENT-INNER-BOX.)

What would be great for solving the problem would be to handle the “in-child” box of the parent, that is, the visible bounding box of the parent framed in the perspective of the child coordinate space. Something like this (purple frame):

Bad news: InDesign's DOM doesn't provide access to the “in-child” box of a parent object.

Unfortunately, the DOM does not provide (simple) access to that imaginary “in-child” box, as the concept of CoordinateSpaces.CHILD_COORDINATES is not implemented! One can access the in-parent box of the child, but not the in-child box of the parent.

Note. — You may think that checking the state of the in-parent box of the child relative to the parent inner box could offer an indirect way of solving the original problem. But this is wrong again. The figure below clearly shows that the in-parent box of the child can entirely contain the parent box while the polygonal shape extends beyond the image area:

Reasoning from the parent perspective does not rescue us either!

At this stage of the discussion, our seemingly simple problem became so convoluted that one would be tempted to turn to more frontal methods, based on PathPoint coordinates, Bézier curve interpolations, and so on.

But… Wait a minute! There is a workaround for testing the parent shape bounding box in the perspective of the image space. Indeed, we have two crucial properties at hand:

1. The original question (“extending beyond the image area?”) is invariant by any transformation. That is, if we transform the container in some way this doesn't change the problem.

2. The whole image area by nature coincides with its inner box, which is always a parallelogram (in the perspective of the pasteboard.)

Therefore we might find a transformation T that, being applied to the parent, will change the image box into a rectangle in the pasteboard space. And then, we could consider the “in-board” box of the parent, that is, the rectangle that encloses the parent in the perspective of the pasteboard space. Thus, we would have two comparable rectangles PARENT-REC and CHILD-REC so that the problem reduces to checking whether PARENT-REC ⊆ CHILD-REC.

The figure below summarizes my trick:

A workaround: Immersing the whole problem into the Pasteboard space.

Note. — In my code I use the pasteboard space (CoordinateSpaces.PASTEBOARD_COORDINATES) because it obviously supersedes any involved coordinate space, but the spread space would have done as well.

As usual, the implementation requires a “machine epsilon” (see the EPSILON constant) in order to prevent floating-point approximation errors which frequently occur in InDesign coordinate system. Here is the suggested code:

const CS_BOARD = +CoordinateSpaces.pasteboardCoordinates,
      BB_VISIBLE = +BoundingBoxLimits.outerStrokeBounds,
      BB_PATH = +BoundingBoxLimits.geometricPathBounds;
const EPSILON = 1e-6;  
var image = app.selection[0], // target image (selection) 
    parent = image.parent;  
// Temporarily transform the container so its image box
// be a rectangle in the perspective of the pasteboard.
// ---  
var origin = image.resolve([[.5,.5],BB_PATH],CS_BOARD)[0],
    mx = image.transformValuesOf(CS_BOARD)[0],  
    T = app.transformationMatrices.add(1,1,
// Retrieve image corners in board coord space.  
// ---  
var iTL = image.resolve([[0,0],BB_PATH],CS_BOARD)[0],  
    iBR = image.resolve([[1,1],BB_PATH],CS_BOARD)[0];  
// Retrieve parent *inboard box* corners in board coord space.
// ---  
var pTL = parent.resolve([[0,0],BB_VISIBLE,CS_BOARD],CS_BOARD)[0],
    pBR = parent.resolve([[1,1],BB_VISIBLE,CS_BOARD],CS_BOARD)[0];
// Revert the transformation.  
// ---  
// Check whether the rectangle <pTL,pBR> is included in <iTL,iBR>
// ---  
var r = pTL[0] >= iTL[0]-EPSILON && pTL[1] >= iTL[1]-EPSILON
     && pBR[0] <= iBR[0]+EPSILON && pBR[1] <= iBR[1]+EPSILON;
alert( r ? 'OK' : 'KO' ); 

Here are some tested examples:


Note. — As pointed out by Uwe Laubender in the original discussion you may still encounter clinical cases based on important gaps between outer strokes and inner strokes in sharp-edged polygons using a significant stroke weight. In such context, a possible option is to increase EPSILON by the stroke weight to mitigate the constraint. Not tested though.

• Original discussion: forums.adobe.com/message/9158846#9158846

• See also: “Coordinate Spaces & Transformations in InDesign”

Reframing Pages: a Dangerous Sport

Brett G. wrote: “I’m having trouble. I am changing the page size to crop then exporting to jpeg. Then I change the page size back; works great but the master page becomes disconnected. I can’t figure out how to apply it again.”

Fact is that changing pages size through reframe() is really tricky, especially in a facing-page layout. Indeed, since those facing pages are not supposed to undergo any ‘move’ along the X-axis, each time a reframe occurs that involves changing X-coordinates, unexpected things may happen behind the scene. The page geometry is modified, its translation attributes are readjusted in its transformation matrix, the relationship with the master spread may be somehow altered, and bugs or issues with the masterPageTransform property may occur (weird shifts that seem unrepairable), not to mention side-effects related to layout rules, and so on.

Hence, it’s hard to design a script that works in all environments, all versions, and all configurations. There is always an obscure preference that someone has activated, or disabled, which interacts with the whole system and ruins our effort.

Anyway, there is one law that we learned from experience: DO NEVER TRUST page.bounds property. (See Coordinate Spaces & Transformations in InDesign for advanced details.) If you attempt to reframe pages, assuming that no better solution has been found, try to work with properly resolved coordinates—myPage.resolve(...)—and stick to the tools that coordinate spaces and transformation provide. Here is an attempt to fix Brett's problem on this basis.

// =====================================  
// Values required IN POINTS (negative numbers allowed => increase the area.)
// ---  
const CROP_SIDE   = 12.225/*pt*/,  
      CROP_TOP    = 12.225/*pt*/,  
      CROP_BOTTOM = 100/*pt*/;  
// Export options, etc.  
// ---  
const DOC_NAME_MAX_SIZE = 12,  
      OUT_PATH = "/your/output/path/";
const JPG_EXPORT_PREFS = {  
    jpegQuality:      +JPEGOptionsQuality.high,  
    exportResolution: 170,  
    jpegExportRange:  +ExportRangeOrAllPages.exportRange,  
(function exportPageAreas()  
// =====================================  
// Should support the tricky 'facing-page' case, including when  
// pages have different sizes, and whatever the rulerOrigin mode.  
    // Boring constants.  
    // ---  
    const CS_INNER = +CoordinateSpaces.INNER_COORDINATES,  
          CS_SPREAD = +CoordinateSpaces.SPREAD_COORDINATES,  
          TL = +AnchorPoint.TOP_LEFT_ANCHOR,  
          BR = +AnchorPoint.BOTTOM_RIGHT_ANCHOR;  
    const LEFT_HAND = +PageSideOptions.LEFT_HAND;  
    // Context validation.  
    // ---  
    var doc = app.properties.activeDocument;  
    if( !doc ){ alert( "No document!" ); return; }  
    // Export prefs.  
    // ---  
    const EXP_JPG = +ExportFormat.JPG,  
          JEP = app.jpegExportPreferences,  
          filePrefix = OUT_PATH +
    JEP.properties = JPG_EXPORT_PREFS;  
    // Last variables.  
    // ---  
    var pages = doc.pages.everyItem().getElements(),  
        pg, pgName,  
        dxLeft, dxRight,  
        xyTL, xyBR;  
    // Loop.  
    // ---  
    while( pg=pages.pop() )  
        // Page corners coords in the parent space.  
        // (Note: Do Never Trust page.bounds!)  
        // ---  
        xyTL = pg.resolve(TL, CS_SPREAD)[0];  
        xyBR = pg.resolve(BR, CS_SPREAD)[0];  
        // Opposite corners => reframe.  
        // ---  
        dxLeft  = LEFT_HAND == +pg.side ? CROP_SIDE : 0;  
        dxRight = dxLeft ? 0 : CROP_SIDE;  
        pg.reframe( CS_SPREAD,  
            [ xyTL[0]+dxLeft,  xyTL[1]+CROP_TOP ],  
            [ xyBR[0]-dxRight, xyBR[1]-CROP_BOTTOM ]  
        // Export.  
        // ---  
        JEP.pageString = pgName = pg.name;  
          new File(filePrefix+('00'+pgName).substr(-3)+'.jpg')
        // Restore page. Here we can't safely re-use [xyTL,xyBR] as it is,
        // because spread space origin *may* have moved during reframe.
        // Therefore we need to formally reverse the calculation.
        // ---  
        xyTL = pg.resolve(TL, CS_SPREAD)[0];  
        xyBR = pg.resolve(BR, CS_SPREAD)[0];  
        pg.reframe( CS_SPREAD,  
            [ xyTL[0]-dxLeft,  xyTL[1]-CROP_TOP ],  
            [ xyBR[0]+dxRight, xyBR[1]+CROP_BOTTOM ]  
    alert( 'Page areas have been successfully exported.' );  

• Original discussion: forums.adobe.com/message/8887166#8887166

3/ Miscellaneous

Basic Prototype for Testing Array Equality

You can't compare arrays using code like [1,2,3,4]==[1,2,3,4] (result is false.)

The reason is, when two objects are involved in a soft equality (==) comparison, JavaScript tests them for strict equality (===). And, in case of Object instances, which includes Array instances, strict equality just means testing operands by reference. Therefore distinct instances in terms of distinct new calls will always fail in both == and === tests. Here you must keep in mind that litteral objects {...} or arrays [...] still result from an implicit invocation of the new operator.

Thus, the operator == is not only useless for comparing arrays, it also is redundant with ===. Good news is, in ExtendScript, you can override == in order to compare the elements of distinct instances :-) The trick is to redefine Array.prototype['=='].

To my knowledge the shortest way to do so is the following code:

// Make myArray1==myArray2 relevant
// ---
Array.prototype['=='] = function(a)
   return this.toSource() == a.toSource()

Just include that block at the beginning of your scripts and a==b will magically make sense with Array operands:

Array.prototype['=='] = function(a)
   return this.toSource() == a.toSource()
var a = [1,5,9,12,16,18,22,34];
var b = [1,5,9,12,96,98,22,34];
var c = [1,5,9,12,16,18,22,34];
alert( a==b ); // false
alert( a==c ); // true
// Works with heterogeneous items as well.
// ---
var d = ["aaa","bbb",999,true];
var e = ["aaa","bbb",999,false];
var f = ["aaa","bbb",999,true];
alert( d==e ); // false
alert( d==f ); // true

• Original discussion (incidental note): forums.adobe.com/message/8991900#8991900

Repairing toSource() Method for Enumerations

A recurring problem with ExtendScript enumerators is, they don't provide human-readable data once unevaluated through the native toSource() method. Take for example DiacriticPositionOptions.OPENTYPE_POSITION.toSource(). This will return either the string "({})" (CS5 and later), or at best "(new Number(1685090164))" (previous InDesign versions.) Either way that's not very interesting! We usually invoke .toSource() on DOM object properties in the hope of fully revealing their inner state. At best we can coerce a punctual enumerator into a Number using XXX.valueOf(), which always works but doesn't speak for itself at all. And even with objectified enumerators (CS5+) that supports .toString() we only retrieve the formal key of the Enumerator, e.g. "OPENTYPE_POSITION", but the parent Enumeration (DiacriticPositionOptions) is missing so we cannot easily reconstruct a clean syntax that would evaluate to the enum value.

Fortunately the great Dirk Becker from Ixta.com published a brilliant workaround a few years ago. Its enumToSource.jsx script simply “repairs toSource() for enumerations” in CS5 and later, based on scanning the $.dictionary structure and overriding Enumerator's prototype.

Note.Enumerator, as a constructor, is something of a hidden object in ExtendScript. In fact, those strange things are called live objects and we need to wake them up before any explicit access. That's why Dirk uses the syntax NothingEnum.NOTHING.constructor to handle the Enumerator function (and then its prototype.)

As Dirk's code relies on parsing the $.dictionary database, it always properly reflects the current DOM in use. It fixes Enumerator's prototype in a way that makes toSource() both effective and verbose, including when you invoke it from any DOM object properties that may contain enumerators. Also, since the code only instantiates NothingEnum in the global scope, one can consider it very 'non-polluting' when included in a larger project or framework.

On my side, I wrote a (slightly) different implementation which just (slightly) increases the verbosity of the result. Numbers are now formatted in 0xHEXA form and the Adobe 4-char tag is shown too. Here it is, improved from Dirk's comments:

// Repair toSource() for enumerations [alternative version]  
// for InDesign CS5 and later -- v.2.0  
// -------------------------------------  
// Based on Dirk Becker's 2014 original code at  
// http://ixta.com/scripts/utilities/enumToSource.html  
// -------------------------------------  
// Output either the format  
//     <Enumeration>.<Enumerator> /* <Hexa> [<Tag>] */  
//     if <Enumeration> is the unique parent for that value,  
//     e.g: AnchorPosition.INLINE_POSITION /* 414F5069 [AOPi] */  
// or  
//     0x<Hexa> /* <Enumerator> [<Tag>] */  
//     if Enumerator's value belongs to multiple parents,  
//     e.g: 0x74787466 /* TEXT_FRAME [txtf] */  
// -------------------------------------  
// TIP: You can access the whole 'database' (cache)  
//      browsing the following object  
//      NothingEnum.NOTHING.__proto__.toSource.Q  
// -------------------------------------  
(function(/*obj&*/Q,  n,i,a,s,x,t,k,v)  
    const DIC = $.dictionary;  
    const CHR = String.fromCharCode;  
    const FMT = $.global.localize;  
    const REGULAR_PTN = "%1.%2 /*\xA0%3\xA0[%4]\xA0*/";  
    const SPECIAL_PTN = "0x%3 /*\xA0%2\xA0[%4]\xA0*/";  
    for( a=DIC.getClasses(), n=a.length, i=-1 ; ++i < n ; )  
        x = DIC.getClass(s=a[i]).toXML();  
        if( 'true' != x.@enumeration ) continue;  
        x = x.elements.property;  
        for each( t in x )  
            k = String(t.@name);  
            v = Number(t..value);  
            Q[k] = FMT(  
                // %1 :: Enumeration class name  
                // %2 :: Enumerator key  
                // %3 :: Hexa representation  
                // %4 :: Adobe 4-char tag  
})( (NothingEnum.NOTHING.constructor.prototype.toSource=
function F(){ return F.Q[this] || "({})" }).Q={} );

• Original discussion (scroll down): forums.adobe.com/message/4152441#4152441

• See also: ixta.com/scripts/utilities/enumToSource.html

Making a Group Responsive Using an IdleTask

Ypsillon wrote: “Every day I’m working with grouped elements which consist of one rectangle and several text trames—usually three. I need to change dimension of the rectangle every time but not to ungroup the objects. So I’m clicking twice on the rectangle, move it and resize. In this case all the text frames in group remain sometimes far ... far from the rectangle but there is a need to bring them together again. I’m wondering if there is a possibility to write a script which can concentrate the elements of group by moving text frames to the bounds (or origin) of the rectangle.”

This request sounded like a quite relevant opportunity for implementing an IdleTask. During Idle events we can make InDesign check whether the currently selected Rectangle, if any, has been resized by the user. Then we can check whether this object belongs to a group where other elements need to be adjusted accordingly. Here is the magic (based on Ypsillon's conditions):

#targetengine magnetGroupRectangle  
function follow()    
    const T=0, L=1, B=2, R=3;  
    if( !app.properties.activeDocument ) return;  
    if( !app.properties.selection ) return;  
    if( 1 != app.selection.length ) return;  
    var a, i, every, b, i, dx, dy,  
        r = app.selection[0],  
        g = r.parent;  
    // Adjust the following to your own conditions.
    // ---
    if( !( r instanceof Rectangle ) ) return;
    if( !( g instanceof Group ) ) return;
    if( 3 != g.pageItems.count() ) return;
    a = (every=g.textFrames.everyItem()).getElements();  
    if( 2 != a.length ) return;  
    b = every.visibleBounds;  
    i = +(b[0][T] > b[1][T]);  
    r = r.visibleBounds;  
    // Right magnet  
    dx = r[R]-b[i][L];  
    // Bottom magnet  
    i = 1 - i;  
    dy = r[B]-b[i][T];  
    a[0].visibleBounds = b[0];  
    a[1].visibleBounds = b[1];  
    var t = tasks.itemByName(name);    
    if( t.isValid )  
    tasks.add({name:name, sleep:rate})  
       .addEventListener(IdleEvent.ON_IDLE, callback, false);    

Note. — What makes the script session-persistent is the #targetengine directive. What makes the script responsive is the IdleTask instance (and the underlying event listener.)

YouTube demo:

• Original discussion: forums.adobe.com/message/8515468#8515468

• See also: ISFR #9 — “Using an IdleTask to Create Responsive PageItems”

GitHub page: github.com/indiscripts

Twitter stream: twitter.com/indiscripts

YouTube: youtube.com/user/IndiscriptsTV