How to Augment and Process Nested Groups (CS4-CS6)
October 22, 2012 | Tips | en
Dealing with groups is a major headache for InDesign scripters. On one hand, the Group
object has no injection method to offer, which makes difficult to append existing page items into a given group. On the other hand, although the Groups
API exposes a promising add()
method, nobody has ever managed to use this feature from any PageItem.groups
area, so we seem doomed to perpetually rebuild groups from scratch.
These limits are even more frustrating in that the InDesign user himself can very easily augment groups (or change hierarchical relationships) by simply dragging nodes in the Layer panel! As far as I know, the Scripting DOM offers no such facilities. When we need to create and/or manage nested groups, we must reconstruct the entire structure. This method involves producing each new group from the Spread
level.
Suppose you have the following path to a target group:
SPREAD => GP1 => GP2 => GP3
Say you want to add a new object in the group GP3
. Here is the way to go:
1. Ungroup GP1.
2. Ungroup GP2.
3. Ungroup GP3.
4. Create a new group, GP33, based on GP3's old items and the new object.
5. Create a new group, GP22, based on GP2's old items and GP33.
6. Create a new group, GP11, based on GP1's old items and GP22.
Also, during this tedious process you will have to:
— backup temporarily ungrouped items,
— make sure that the respective transformations applied to both simple items and parent groups are preserved (or restored) in the proper order,
— deal with possible z-order issues!
My Own Aggregator
Being constantly confronted with that boring topic, I decided to create a generic helper that saves me a lot of time in processing nested groups. I suspect this routine might help some of my colleagues, so here it is in its current state of development:
var aggregate = function( /*Group|PageItem*/target, /*PageItem|arr*/newItems, /*bool=false*/overTarget ) //-------------------------------------- // CS4-CS6 Group Aggregator // (1) If <target> is a group, add <newItems> within that group // (2) If <target> is a simple item, group it with <newItems> // Return the resulting group // --- // <newItems> can be: // (a) a singular specifier -- e.g. mySpread.textFrames[0] // (b) a plural specifier -- e.g. mySpread.ovals.everyItem() // (c) an array of specifiers -- e.g. [myOval, myRectangle] // --- // If !!<overTarget>, <newItems> are z-ordered OVER the target // Default is false => newItems are z-ordered UNDER the target // --- // This routine attempts to preserve: // # The hierarchical level of <target> // in the case it is nested in existing group(s), // # The z-order of the involved elements, // # Respective item transformations. // --- // Limitations: // # <newItems> must all be 'free' i.e. direct children of the Spread // # Anchored/inline items are not supported { var CS_SPREAD = CoordinateSpaces.SPREAD_COORDINATES, CS_ORIGIN = [[0,0],CS_SPREAD]; var stack = [], z, p, t, o, i, a, g, r = null; // Feed the stack (from target to top container) // --- for ( p=t=target, z=0 ; p instanceof Group ? t=p.pageItems.everyItem() : !z ; p=p.parent ) stack[z++] = { items: t.getElements(), id: p.id, mx: p.transformValuesOf(CS_SPREAD)[0], }; // Check that the top container is OK // --- if( !((p instanceof Spread)||(p instanceof MasterSpread)||(p instanceof Page)) ) { (stack.length=0)||(stack=null); throw "Unable to aggregate page items"; } // Normalize newItems --> array of resolved specs // --- ('getElements' in newItems) && (newItems=newItems.getElements()); // Ungroup stack elems (except the target) // --- a = []; while( z-- ) { o = stack[z]; i = a.length; while( i-- ){ if( a[i].id==o.id ){ a.splice(i,1); break; } } t = p.pageItems.itemByID(o.id).getElements()[0]; z && t.ungroup(); delete o.id; a = o.items; } // Manage the z-order of newItems relative to t (==target) // --- if( 1 < newItems.length ) { g = p.groups.add(newItems); g[overTarget?'bringToFront':'sendToBack'](t); g.ungroup(); g = null; } else newItems[0][overTarget?'bringToFront':'sendToBack'](t); // If needed, ungroup the target // --- (t instanceof Group) && t.ungroup(); // Append newItems to the base array // --- o.items = o.items.concat(newItems); // Recreate the whole hierarchy // --- for( z=0, t=null ; z < stack.length ; ++z ) { o=stack[z]; t && o.items[o.items.length]=t; t = p.groups.add(o.items); t.transform(CS_SPREAD, CS_ORIGIN, o.mx); t.pageItems.everyItem().transform(CS_SPREAD, CS_ORIGIN, o.mx.invertMatrix()); t = t.getElements()[0]; (o.items.length=0)||(o.items=null)||(o.mx=null); r || (r=t); } (stack.length=0)||(stack=null); t = o = p = a = null; return r; }; // ===================================== // Sample code // ===================================== var spd = app.activeDocument.spreads[0], target = spd.groups[0].groups[0], // assumed a nested group exists newItems = spd.ovals.everyItem(); // assumed some ovals exist var g = aggregate(target, newItems, true); app.select(g);
Any feedback highly appreciated.
Comments
Great one Marc!
I think this is one of the biggest limitations/differences between UI and scripting. One possible issue I can see when you ungroup a group with transparency settings/blending modes applied to it. Group.ungroup() even doesn't give a warning when you do it.
> One possible issue I can see when you
> ungroup a group with transparency
> settings/blending modes applied to it.
Good point! It sounds like my snippet still needs some improvements ;-)
Thank you for your support.
@+
Marc
Hi, Marc!
I'm sure that you are totally aware of the following two solution scenarios, but I want to point out that there are alternatives if one want to maintain transparency settings/blending modes applied to the group:
Scenario 1:
Changing hierarchical relationships of page items inside a group
Solution:
1. Make the spread of the group the activeSpread
2. Select the page item inside the group we want to change the hierarchie of
and finally:
3. Invoke the appropriate scriptMenuAction
like, for example:
app.scriptMenuActions.itemByID(11285).invoke();
for "Send backwards". We could do this several times, if we want…
Of course afterwards we have to de-select the pageItem with app.select(null) and restore the old active spread.
Scenario 2:
Appending other existing objects to a group
Solution:
1. Add a new text frame to the group
2. Move the object you want to append to the first inserton point of the new text frame.
(You can fine-tune the result with several anchored object settings…)
Of course, the two solutions have their downsides: fiddle with the activePage or activeSpread, select, de-select or adding extra text frames as helper objects.
Uwe
Hi Uwe,
Thanks for your suggestions.
About scenario #1, isn't it possible to use e.g. myPageItem.sendBackward() rather than playing w/ menu actions? (I confess I wasn't aware about that.)
About scenario #2, well, this sounds like a hack to me ;-) But I'm not sure final users will be OK to have silent anchored object(s) in the background...
BTW, another limit of my snippet is that the Text Wrap settings of the original group are lost too!
@+
Marc
Hi, Marc!
Of course, you are right: sendBackward() is the way to go.
You are also right on "Text Wrap".
And another thing, which is not so obvious:
If we consider Adobe DPS (esp. its overlay features) or 3rd party PlugIns, where some or all the features are injected using the "insertLabel()"-method and readout by using "extractLabel()", those features also will be destroyed…
Uwe