• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
0

Get parent story of a table, row, column or cell

Participant ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

In the script I'm working on, I need to get the parent story of the selection. This will typically be a Text object which is super easy with `mySelection.parentStory`. But in many cases it could be an object that doesn't have a parentStory method, like a table, row, column or cell. If it's a table, I worked out that I can get the parent story with `mySelection.storyOffset.parentStory`, but this doesn't work for rows, columns or cells (which have no storyOffset method). I could try navigating up the DOM tree with various checks and balances, and maybe end up with something like `mySelection.parent.parent.storyOffset.parentStory`, but that seems like a crazy way to do something that I'd have thought would be simple.

Is there a simpler way?

TOPICS
Scripting

Views

3.7K

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Participant , Apr 04, 2019 Apr 04, 2019

Laubender: So I think the best I can come up with is this variation of your idea:

var mySelection = app.selection[0];

var myStory = mySelection.hasOwnProperty("storyOffset") ? mySelection.storyOffset.parentStory : mySelection.insertionPoints[0].parentStory;

It seems to work for every possible selection.

Votes

Translate

Translate
Enthusiast ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

Not that it's simple, but one way to get there is to climb up the parent tree checking for the constructor name until you find "Story".

In most cases I've encountered it seems the only time a text selection lacks the property parentStory is when the parent constructor is "Cell". So in some scripts I have code such as below, to signal when I'm inside a table.

// (assuming 'selection' is var of selected text)

var parent = selection.parent;

if (parent.constructor.name != "Cell") {

    parent = selection.parentStory;

}

And if constructor does == cell, then you have to climb up the parent tree (or not, depending on what you're trying to get done).

Maybe that will help. Also perhaps helpful, below is a function I wrote to test if a selection is text or not. Here you can see the list of constructor names you might encounter. Perhaps not an answer to your question but hopefully guide you in the right direction.

function selectionIsText(selection) {

    switch (selection.constructor.name) {

        case "Character":

        case "Word":

        case "TextStyleRange":

        case "Line":

        case "Paragraph":

        case "TextColumn":

        case "Text":

        case "Cell":

        case "Column":

        case "Row":

        case "Table":

            return true;

    }

    return false;

}

William Campbell

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

Thanks for these ideas William.

williamc3112933  wrote

Not that it's simple, but one way to get there is to climb up the parent tree checking for the constructor name until you find "Story".

The problem is, you can go all the way up the tree and never see Story at all. If I select a cell and loop through consecutive parents (being careful to avoid an infinite loop, since the Application seems to think that it is its own parent!), I get this:

Cell

Table

TextFrame

Spread

Document

Application

As you can see, TextFrame is the parent of Table, not Story. According to the docs, the parent of a Table could be Story, along with a dozen other possibilities, but in my testing it is not. That's another thing I don't understand about the parent–child relationship—it seems so random. Thankfully, someone at Adobe thought to give us a parentStory property for text. Why not for tables and cells too? Who knows.

So anyway, at this stage I guess I need to loop through, check for Table, then use my old friend `storyOffset.parentStory` on the Table. I just thought there must be a more sensible way, but I'm slowly learning that Adobe's API is anything but sensible at times.

williamc3112933  wrote

Also perhaps helpful, below is a function I wrote to test if a selection is text or not. Here you can see the list of constructor names you might encounter. Perhaps not an answer to your question but hopefully guide you in the right direction.

Ah yes, that looks very much like some code out of Adobe's JavaScript Scripting Guide (under 'Working with text selections') which I actually used myself in an earlier version of my script. I've since changed it to just:

if (app.selection[0].hasOwnProperty("findGrep")) { … }

Since the findGrep() method is what I actually want to call on the text, this is perfect for filtering out invalid sections, and satisfyingly concise. 🙂 (Unlike the code I'm going to have to write to find the parent story in all possible circumstances!)

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

When you get to TextFrame, that has the property parentStory. Yes weird that it doesn't show up in the parent chain above TextFrame (seems it should), but getting parentStory from the frame should be what you're looking for. If it will work for what you're hoping to do I couldn't say, but it's worth a try.

William Campbell

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

I noticed that TextFrame has a parentStory property, but what worries me is that the docs say a Table's parent can be any of the following:

Cell

Character

InsertionPoint

Line

Paragraph

Story

Text

TextColumn

TextFrame

TextStyleRange

Word

XMLElement

XmlStory

What guarantee do I have that it will always be TextFrame? For that reason, I think it will be safer check for Table and get its parentStory. That deals with tables, and hopefully nested tables too. I might have to do the same for footnotes too, since they don't have a parentStory property either.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 02, 2019 Apr 02, 2019

Copy link to clipboard

Copied

I realised that I can, of course, use the hasOwnProperty() method again (same logic as I use for selection validation) to check for the parentStory property. That way, it doesn't matter what kind of object it happens to be. So my code now looks something like this:

var mySelection = app.selection[0]; // Without validation

var myStory;

var mediator = mySelection;

while (!myStory) {

    if (mediator.hasOwnProperty("parentStory")) {

        myStory = mediator.parentStory;

    } else if (mediator.constructor.name == "Story") {

        myStory = mediator;

    } else {

        mediator = mediator.parent;

        if (mediator.constructor.name == "Application") {

            alert("Could not complete request. The parent story could not be found.");

            break;

        }

    }

}

So I've allowed for various possibilities to make the code as bulletproof as possible, but in all my testing so far, it's completely predictable… We go from Cell, to Table, to TextFrame, and the parentStory is found. 🙂

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Apr 03, 2019 Apr 03, 2019

Copy link to clipboard

Copied

Hi Kals,

Here is an alternate method based on DOM specifiers. You can get quickly the root frame—if any—of a target object this way:

const getStory = function(/*?any*/x)

//----------------------------------

{

   x||((x=app.properties.selection)&&(x=x[0]));

   if( x!==Object(x) || !('toSpecifier' in x) ) return false;

    x = x.toSpecifier().match(/\/document\[.+?\/\/text-frame\[@id=\d+\]/);

    if( !x || !(x=resolve(x[0])).isValid ) return false;

    return x.parentStory;

};

// Test.

alert( getStory() );

@+

Marc

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 03, 2019 Apr 03, 2019

Copy link to clipboard

Copied

Marc Autret​, wow. Not at all 'simple' but it does the job. I won't pretend to understand how it works though. 🙂

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 03, 2019 Apr 03, 2019

Copy link to clipboard

Copied

Excellent as always Marc Autret​, i have a few questions. The code snippet you supplied will report the parent story for objects like cell, table etc and not for objects like text, word, character, am i right in understanding this? So we will have to augment this code snippet with another piece for objects that directly support a property like parentStory

Secondly it was very ingenious of you for using the toSpecifier for this algorithm. Can you give some use cases where this approach may be used, something that you might have written, code snippet would not be needed just an idea would do good, i would like to leverage this, just want to teach myself to think along this path as well.

Thanks,

-Manan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

Hi Manan,

1. You're right. Since Text objects already have a parentStory, we should add a line in the code:

const getStory = function(/*?any*/x)

//----------------------------------

{

    x||((x=app.properties.selection)&&(x=x[0]));

    if( x!==Object(x) || !('toSpecifier' in x) ) return false;

    // Text objects already have a parentStory!

    // ---

    if( 'parentStory' in x ) return x.parentStory;

    x = x.toSpecifier().match(/\/document\[.+?\/\/text-frame\[@id=\d+\]/);

    if( !x || !(x=resolve(x[0])).isValid ) return false;

    return x.parentStory;

};

And as you have observed, addressing this simple case is required, because the specifier of basic Text objects does not involve a text-frame component at all. Thanks for pointing that out.

2. My reasoning about toSpecifier() is, it reveals in one hit the internal ‘path’ of any DOM object. This method is extremely fast compared to the multiple DOM commands we usually invoke to handle complex objects. The specifier is returned as a String, so getting the desired information is just a matter of parsing a JS string, which no longer involves the DOM. This improves time performance in loops and functions that browse the hierarchy of complicate objects, sets, or ranges. An obvious use case is, given whatever DOM entity, which document does it belong to? Instead of looking for the parent of parent of (…), just extract the root segment of the specifier and resolve it. Done. The method toSpecifier() —and its counterpart, resolve()—are useful to make your code more agnostic: the path tells everything about a target, you don't have to assume additional facts. I use this approach in various routines of my script IndexMatic, which needs to quickly scan entire documents and classify text contents.

Best,

Marc

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

Many thanks Uwe and Marc. The magic method or simple one-liner I dreamed of clearly doesn't exist… but there's some good stuff here, not just for me but for others who might have the same questions.

Marc Autret​, thanks for sharing your code and for explaining the performance benefits of using the internal path strings. I only recently read the 2010 article On ‘everyItem()’ (which I only just realised was written by you!) which was great at explaining all the smoke and mirrors—although I admit, a fair bit of it went above my head.

My code from comment #5 performs for my purposes without noticeable lag, so I'll stick with what I understand for now… but if performance does become an issue, I can always lift the bonnet and shamelessly drop in your V8 code to replace it. 🙂

On a semi-related question, I tried to help another user navigate InDesign tables on this thread, but I acknowledge the limits of my own knowledge and ended up by saying, 'Perhaps a genuine ExtendScript guru will stumble onto this thread and explain it to us in a way that makes sense.' Just thought I'd mention it in case you have time to drop by and set us right. 🙂

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

I had to remove my last reply, because the suggested line of code is not working.

Don't know why I thought it is, because insertionPoint has no property storyOffset.

But this will work for every selected text:

app.selection[0].insertionPoints[0].parentStory

Regardless if the text is in a table cell or not.

Tested with InDesign CC 2019 on Windows 10 and InDesign CS6 as well.

Regards,
Uwe

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

Laubender​: OH SO CLOSE!!! It works for everything except a selected table, since a Table object does not support the insertionPoints property. This API does seem intent on thwarting us doesn't it.

Obviously we can test for a table and handle this exception with storyOffset or something, but we're so tantalisingly close to a simple one-liner… So I looked for a common property that is shared by Text, Table, Column, Row, Footnote, etc, and found the textFrames property. Indeed, this works to find a text frame, regardless of the selection:

app.selection[0].textFrames[0]

And, as luck would have it, textFrame supports the parentStory property! So this should work…!

app.selection[0].textFrames[0].parentStory

Only… it doesn't. 😞 It says, 'Object is invalid'.

Edit: I think I see why it isn't working. textFrames[0] is looking for child textFrames, not parent textFrames. I guess my first line only appears to work, because it doesn't actually retrieve anything from the DOM (i.e. the specifier isn't resolved as explained by Marc).

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

LATEST

Laubender: So I think the best I can come up with is this variation of your idea:

var mySelection = app.selection[0];

var myStory = mySelection.hasOwnProperty("storyOffset") ? mySelection.storyOffset.parentStory : mySelection.insertionPoints[0].parentStory;

It seems to work for every possible selection.

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 03, 2019 Apr 03, 2019

Copy link to clipboard

Copied

Hi Kals,

if you can identify the cell, you can identify the table.

table.storyOffset.parentStory

gives you the story the table is sitting in.

storyOffset is an insertionPoint in this case.

Regards,
Uwe

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Apr 03, 2019 Apr 03, 2019

Copy link to clipboard

Copied

Laubender​, I understand why `table.storyOffset.parentStory` works. Indeed, I was originally going tackle it this way as discussed. But can I get you to clarify what you mean by 'if you can identify the cell, you can identify the table'? Is there an easier way than climbing the parent tree until you come to a Table? I note that Cell objects don't have a 'parentTable' property (for some reason).

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Apr 04, 2019 Apr 04, 2019

Copy link to clipboard

Copied

Kals  wrote

Laubender , I understand why `table.storyOffset.parentStory` works. Indeed, I was originally going tackle it this way as discussed. But can I get you to clarify what you mean by 'if you can identify the cell, you can identify the table'? Is there an easier way than climbing the parent tree until you come to a Table? I note that Cell objects don't have a 'parentTable' property (for some reason).

Hi Kals ,

maybe I did not fully understand your task.

Case 1

If indeed text in a cell is selected, parent is always a cell and parent.parent is the table.

So text.parent.parent.storyOffset.parentStory is the story the table is in.

Case 2

Ok. If we think of nested tables then text.parent.parent.storyOffset.parentStory points to the story the outer table is in.

Marc's fantastic getStory of reply 11 is definitely the way to go for both cases and others as well if indeed text is selected. Wow!

Note:

However, if the contents frame of a graphic cell is selected, not text, Marc's function will return false.

Also with selected frames that are anchored in a text cell.

Also worth noting:

With InDesign CC 2019 a footnote can be also part of text in a text cell.

If there is a footnote in your selected text one can go straight to the story with:

text.footnotes[0].storyOffset.parentStory

That is working no matter how deeply nested the table cell is.

Just like storyOffset.parentStory of a nested table ( a table that sits in a table cell ).

Regards,
Uwe

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines