0% found this document useful (0 votes)
797 views

Indesign Cs5 Automation Using XML Javascript Compress

Uploaded by

Balageeon
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
797 views

Indesign Cs5 Automation Using XML Javascript Compress

Uploaded by

Balageeon
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 491

InDesign CS5

Automation
Using XML &
JavaScript

Grant Gamble

InDesign CS5 Automation Using XML and JavaScript


Copyright © 2011 by Grant Gamble
TrainingCompany.Com
ISBN 978-1460915387
Notice of Rights
All rights reserved. No part of this publication may be reproduced, stored or transmitted in any
form, for any reason or by any means, without the prior written permission of the copyright
owner and the publisher.
Notice of Liability
Whilst every effort has been made to ensure that this book contains information which is as
complete and accurate as possible, no warranty is implied. Neither the author nor the publisher
shall have liability to any person or entity with respect to any loss or damages caused by the
information contained in this work.
Trademarks
All trademarks are hereby acknowledged as belonging to their respective owners. Where
trademarked names appear in this book, rather than use a trademark symbol, we have used the
names only in an editorial fashion and to the benefit of the trademark owner, with no intention
of infringement of the trademark.
Source Code
The source code for this book is available for download at the following URL:
http://www.trainingcompany.com/downloads/indesigncs5js1.aspx

Contents
CHAPTER 1. Introduction
What are you letting yourself in for?
The Scripts panel

2
The ExtendScript Toolkit
Workspaces
Line numbers
Syntax highlighting
Code Collapse
Auto completion
Organizing your scripts
Favorites
TUTORIAL: Getting started
1. Installing the work files
2. Setting up the ESTK
3. Creating a basic script
About the tutorials in this book
CHAPTER 2. Scripting essentials
Comments
Writing scripts
Variables
Naming variables
Variable data types
Declaring and initializing variables
Expressions and operators
Arithmetical operators
Comparison operators
Logical operators
The InDesign object model
Properties and methods
The Object Model viewer
Essential object syntax
Working with object properties
Property values
Using enumerations
Working with object methods
Using properties records
Creating new elements
Creating a new document, book or library
Adding a page to a document
Creating default and document colours
Creating default and document paragraph styles
Referencing objects
Using a numeric index
Using a named index
Using an ID
Targeting a range of objects
Targeting every item in a collection
Targeting currently active objects
Active document

3
Active window
Counting objects
JavaScript dialog windows
The alert function
The confirm function
The prompt function
The String object
The length property
The indexOf() method
The charAt() method
The toUpperCase() and toLowerCase() methods

The replace() method


The substring() and slice() methods
InDesign text objects and strings
The Array object
Creating arrays
InDesign objects and arrays
Array properties and methods
The JavaScript Object object
CHAPTER 3. Conditional statements, loops and functions
If else statements
Using else if
Switch statements
Using break statements
For loops
Using a for loop to test whether a document is open
Looping in reverse
Break and continue
While loops
Functions
Defining and calling a function
Returning a value from a function
Passing parameters to a function
Local and global variables
Using namespaces with variables
ExtendScript engines and variables
CHAPTER 4. Creating dialogs
Dialog controls
Layout controls
Text controls
Self-validating text controls
Controls which offer the user a choice
Self-validating combobox controls
Button controls
Referring to controls

4
Displaying a dialog
Creating static labels and text boxes
Placing dialog objects inside variables
The canCancel property
The minWidth property
Dropdown controls
The stringList property
The selectedIndex property
Creating radiobutton and checkbox controls
Radiobutton controls
TUTORIAL: Using self-validating controls
1. The main function
2. Building the dialog
2a. The createDialog() function shell
2b. The watermark text editTextbox
2c. Angle comboBox
2d. Fonts dropdown
2e. Validating user input
3. Adding the watermark text to the master pages
3a. Creating the “watermark” layer
3c. Creating the watermark text frame
3d. Formatting the watermark text
CHAPTER 5. ScriptUI Dialogs
The Window object
Creating a window
Container objects and the add() method
Tabbed panels
Panels
Groups
Coding styles for ScriptUI
Placing containers inside containers
Text Controls
StaticText
StaticText creation properties
Using the multiline and scrolling properties
EditText
EditText creation properties
List Controls
Drop down list
Displaying images
List box
Treeview
Buttons
Creating Cancel and OK buttons
Events

5
Button events
CHAPTER 6. Working with Files and Documents
About files and documents
Creating a new document
Opening a document
Letting the user choose a file
Using filter expressions (Windows only)
Using Filter functions (Mac only)
Creating a cross-platform mask
Allowing multiple selections
Testing whether a document is already open
Testing whether an array of documents is already open
Saving a document
Testing whether a document has been saved
Saving the changes to a document
Using the saveDialog method
Closing a document
Reading from a text file
Testing whether a file exists
Writing to a text file
Creating a new file
Working with folders
Creating folders
Reading files in a folder
TUTORIAL: Navigating folders and files recursively
1. Creating the main function
2. The getFolder() function
3. The inputDialog() function
4. The OKButtonClick() callback function
5. The outputDialog() function
6. The recursive addItem() function
7. The archiveItems() function
CHAPTER 7. Document Layout
Document and default Preferences
View Preferences
Document Preferences
Page attributes
Bleed and slug
MarginPreferences
Margin settings
Column settings
Implementing document setup

6
Placing text and images
Placing via the document object
Placing items onto a page or spread
Placing items into a frame
Placing items inside a text object
TUTORIAL: Automating document setup
1. Creating the main function
2. The getFiles() function
3. The createDialog() function
4. The setupEvents() function
5. The buildDocument() function
CHAPTER 8. Working with text
Understanding text objects
Overwriting text objects
Inserting text
Inserting text before an object
Working with Fonts
Font names
TUTORIAL: Creating a font and style selection dialog
1. Creating an array of font names
2. Creating separate arrays for font and style names
3. Creating the dialog
4. onChange callback for the fonts dropdown
5. onClick callback for the Close button
6. Displaying the dialog
Finding and replacing text
Clearing Find/Change preferences
TUTORIAL: Creating a clean-up text script
1. Creating the skeleton of the script
2. The createDialog() function
2a. The scope dropdownlist
2b. The checkboxes for choosing clean-up operations
2c. The Cancel and Clean up buttons
3. The cleanUpText() function
3a. The function skeleton
3b. Checking the scope of the Change/Find operations
3c. Carrying out the selected clean-up operations
3d. Creating the doFindChange() function
Tables
Creating tables
Adding text to table cells
Writing a value into every cell
Writing an array to a row
TUTORIAL: Updating table data

7
1. Variable declaration and function calls
2. The importDates() function
2a. Reading the data file
2b. Converting the data to an array
2c. Formatting the data to resemble the publication
3. Updating the tables in the publication
CHAPTER 9. Working with Images
InDesign image objects
The image and its container
Targeting all graphic within a document
Accessing graphics via the pageItems collection
TUTORIAL: Relinking images
1. Creating the dialog
2. Callback functions
3. The relinkGraphics() function
Working with links
Ascertaining the link type
Identifying poster images
Excluding both media and posters
FilePath versus name
Embedding and unembedding linked graphics
TUTORIAL: Unembedding unlinked images
1. Creating the main script
2. The findEmbedded() function
3. The createDialog() function
4. The exportImage() function
The image object
Independent and anchored graphics
TUTORIAL: Finding stretched images
1. Creating the main() function
2. The findStretched() function
3. The createDialog() function
4. The fixImages() function
CHAPTER 10. Page Items and Layers
TUTORIAL: Navigating all page items in a document
The hierarchical structure of the allPageItems collection
1. The main() function
2. Creating the window
3. Constructing the treeview control
Ascertaining pageItem type
Using constructor.name
Using instanceof
Identifying textFrames, groups and buttons

8
Identifying picture frames
Identifying movies and sound
Identifying type on a path
Identifying regular graphic objects
PageItems and layers
TUTORIAL: Creating a layer manager utility
1. Creating the main() function
2. The loadPageItems() function
3. Creating the dialog box
3a. Creating the window itself
3b. Creating the object type checkboxes
3c. Creating the multi-column listbox
3d. Creating the selectItems dropdown and button
3e. Creating the move layer dropdown and button
3f. Creating the Delete and Close buttons
4. Creating the updateListbox() function
5. Creating callback functions
5a. Page items listbox onDoubleClick
5b. Select by Type button onClick
5c. Move to Layer button onClick
5d. Delete and Close buttons onClick
6. Testing the script
CHAPTER 11. Error handling and debugging
Creating scripts for different InDesign versions
Detecting the InDesign version
Creating conditional code
Running old scripts with InDesign CS5
Detecting the platform
Basic error handling techniques
Using try ... catch
The error object
Throwing your own errors
Eliminating simple errors
Creating scripts for other people
Avoiding references to specific locations
Using app.activeScript
Using Folder.current
Debugging in break mode
Stepping through a script
Examining variables, objects and statements
Using breakpoints
TUTORIAL: Working in break mode
1. Stepping through a script
2. Setting breakpoints
Using alerts for debugging

9
Writing values to the JavaScript console
CHAPTER 12. Interactive Documents
Overview
Setting preferences
View preferences
Document preferences
Transparency preferences
Adding page transitions
Automatic layout adjustment
Shortening a document
Creating buttons
Button states
Behaviors
TUTORIAL: Creating an interactive presentation from images
1. The main() function
2. Retrieving the image files
3. Creating the dialog
4. Document setup
5. Setting up the title page
6. Adding navigation buttons
7. Importing the images
8. Exporting an interactive PDF
CHAPTER 13: XML Essentials
What is XML?
Structure of an XML document
Elements
Attributes
Entity references
CDATA sections
Comments
Processing instructions
XML validation
Well-formedness
Schema validation
DTDs versus XML schemas
Creating XML
Microsoft Access
Microsoft Excel
FileMaker Pro
CHAPTER 14: InDesign XML Essentials
XML elements, tags and styles
Creating tags
Mapping tags to styles
Importing XML

10
TUTORIAL: Basic XML workflow
1. Renaming the Root tag
2. Loading tags from an XML file
3. Creating paragraph styles
4. Mapping tags to styles
5. Importing the XML file
6. Placing the XML content on the page
CHAPTER 15: Working with DTDs
Using an internal DTD
Using an external DTD
Declaring elements
Declaring elements that contain other elements
Specifying the occurrence of child elements
Limiting occurrences to a choice
Declaring elements that contain only data
Declaring elements with mixed content
Declaring empty elements
Declaring attributes
Attribute data types
TUTORIAL: Creating a DTD and using it for validation
1. The XML file
2. Creating the DTD
CHAPTER 16: XSLT Essentials
Linking an XML document to a stylesheet
The structure of an XSLT document
The stylesheet element
The template element
Using XPath expressions
Examples of XPath expressions
Axes
Abbreviations
Absolute location paths
Relative location paths
Targeting attributes
Using predicates
Examples of predicates
Using <xsl:apply-templates>
Using <xsl:copy>
The <xsl:value-of> element
The <xsl:copy-of> element
Using <xsl:element> and <xsl:attribute>

11
TUTORIAL: Working with XSLT using Dreamweaver and InDesign
1. Creating an XSL stylesheet in Dreamweaver
Defining a new site
Creating the XSLT file
Creating the main (outer) template
Creating the inner template
2. Applying the XSLT stylesheet in InDesign
CHAPTER 17: XSLT Processing-Control Elements
<xsl:if>
<xsl:choose>, <xsl:when> and <xsl:otherwise>
The <xsl:for-each> element
The <xsl:sort> element
TUTORIAL: Using XSLT control elements
1. Creating the stylesheet in Dreamweaver
Creating the output document root element
Creating the <xsl:for-each> element
Creating the <xsl:sort> element
Creating the common elements
Adding the <xsl:choose> statement
2. Creating a layout with placeholders
Creating paragraph styles
Creating text and graphic placeholders
3. Tagging placeholders
4. Importing XML into placeholders
CHAPTER 18: Working with InDesign XML Objects
The InDesign XMLElement Object
Renaming an element
The xmlTag object
Creating tags
Loading tags
Mapping tags to styles
Importing XML
Placing XML data in the layout
TUTORIAL: Basic XML import
1. Create the new document
2. Create paragraph styles
3. Map tags to styles
4. Importing the XML
5. Placing the XML in the document
The xmlAttribute object
Looping through attributes
Changing attributes to elements
Using XSLT stylesheets
Specifying which stylesheet to use

12
Supplying values to stylesheet parameters
TUTORIAL: Using parameters to filter XML import
1. Creating the XSL file
2. Writing the main script
3. Verifying the XML file
4. Creating the dialog
5. Importing the XML
6. Setting up the InDesign document
7. Placing XML content on document pages
8. Producing an interactive PDF
CHAPTER 19. The JavaScript XML Object
Creating an XML object
Node types
Accessing nodes
Accessing specific nodes
Accessing nodes by relationship
TUTORIAL: XML browser utility
1. The main() function
2. Creating the XML object
3. Creating the dialog
4. The updateTreeView() function
5. The addXMLToDoc() function
CHAPTER 20. Exporting XML
Documents which are hard to export as XML
TUTORIAL: Exporting XML from a catalogue
1. Creating the main function
2. Defining the root element
3. Looping through the document pages
4. Sorting page items by distance from top of page
5. Looping through all paragraphs in the text frame
Creating the <series> element and its children
Creating the range element and its children
6. Testing the code we have created so far
7. Creating the product <summary> element
8. Creating the product <details> element
9. Creating the product <image> element
10. Exporting the XML
CHAPTER 21. Conclusion
Deploying your solutions
App.doScript
Export as binary

13
TUTORIAL: Creating a modular solution
1. Creating the start file
2. Creating the include files

Further reading
Adobe documentation
Books on InDesign automation
Video tutorials on InDesign automation
Books on JavaScript
Books on XML
Books on XSLT
Online resources
Adobe resources
Other people's websites

CHAPTER 1. Introduction
InDesign has established itself as the industry standard in print and publishing software. It has
a huge range of functions and a large base of users, many of whom have an in-depth knowledge
of the program. Anyone who uses the program regularly—especially those who use it for fairly
challenging tasks—will welcome the ability to automate some of their workflows. However,
there is no easy way of achieving this in InDesign. There is no equivalent of the recordable
actions facility found in Photoshop and Illustrator.
Instead, InDesign allows the user to write executable scripts using a choice of three
programming languages: JavaScript, AppleScript or Visual Basic. The major benefit of using
JavaScript as the programming language is that it is cross-platform: scripts will run equally
well on Windows and Macintosh.
AppleScript will be the obvious language of choice for anyone working in a Mac-only
environment. It offers the benefit of easy integration with scripts written for automating other
programs on the Macintosh platform—for example, extracting information from FileMaker and
then taking it into InDesign.
Visual Basic is the logical choice for anyone interested in automating InDesign on the
Windows platform. Additionally, it is possible to control InDesign using Visual Basic for
Applications (VBA)—a variant of Visual Basic which is used to automate Microsoft Office.
This option would be ideal for someone automating the production of financial reports heavily
reliant on Microsoft Excel spreadsheets.
Rather than create a book which covers all three possible scripting options, we have chosen to
write a different volume covering each one. This makes it easier to discuss topics which are
unique to each solution and of little interest to anyone using one of the other scripting
languages.
This book is about automating InDesign using JavaScript on either the Windows or Macintosh
platform. It assumes that the user knows InDesign CS5 very well. This is essential; since it is
impossible to understand many of the subtleties of the InDesign object model without an

14
intimate knowledge of the elements of InDesign which these scripting objects represent.
What are you letting yourself in for?
In order to automate InDesign, you need a good knowledge of three elements:
• InDesign itself—the program and all its major functions
• A compatible scripting language—in the case of this book, JavaScript
• The InDesign object model—the programming syntax which is used to represent every
nook and cranny of InDesign itself.
As has been said, this book assumes that you already possess the first element—that you know
InDesign pretty well. If you also know JavaScript and use it in another context, such as client-
side web development, this knowledge will stand you in good stead. However, in any case, the
major component in learning to automate InDesign is knowing how to use JavaScript to control
and manipulate the huge and extensive InDesign object model.
The Scripts panel
Everything you need to automate InDesign comes with the program itself. Firstly, inside
InDesign, we have the Scripts panel which can be used to run scripts. To make the Scripts
panel visible, choose Window > Utilities > Scripts. The Scripts panel displays a folder
containing sample scripts created by Adobe to demonstrate scripting techniques in both
JavaScript and VBScript—if you are running InDesign on Windows; and JavaScript and
AppleScript if you are using the Mac version. There is also an empty folder called “User": any
scripts that you place in this folder will be available in the Scripts panel in InDesign.
The Scripts panel provides a useful way of locating these folders on your hard drive: simply
highlight one of them and, from the Scripts panel menu, choose Reveal in Explorer
(Windows) or Reveal in Finder (Mac).

Figure 1-1: Locating the folder in which scripts need to be saved in order for them to appear in the
Scripts panel

To run any script listed in the Scripts panel, you can either double-click on its name or
highlight the name and choose Run Script from the panel menu. If you choose Edit Script
instead, InDesign opens the script and launches the ExtendScript Toolkit application (ESTK)
which is the default editor for InDesign scripts. The ESTK is a separate application to
InDesign and can be launched by going to Start > Programs (Windows) or looking in

15
Applications > Utilities > Adobe Utilities - CS5 (Mac).

Figure 1-2: Highlighting a script and choosing Edit Script from the Scripts panel menu launches the
Extend Script Toolkit application

The ExtendScript Toolkit


ExtendScript is the Adobe implementation of JavaScript which is used to automate the Adobe
Creative Suite. The ESTK is an ExtendScript editing utility which contains a number of useful
features; however, there is nothing to stop you using your favourite code editor instead. The
ESTK development environment now resembles a standard Adobe application with everything
in its usual place; which makes it fairly easy to get used to.
Panels are docked on the right and are hidden or made visible by choosing their names from
the Window menu. The arrows for Collapse to Icons and Expand Dock work as they do in the
Creative Suite programs.
Workspaces
On the right of the menu bar is a drop-down menu containing preset workspace names which,
when chosen, automatically rearrange the floating panels into a given configuration. If you
come up with an arrangement of panels which you find ideal, you can save it by choosing
Create New Workspace from the workspace drop-down menu in the top right of your screen.

Figure 1-3: Saving a workspace configuration in the Adobe ExtendScript Toolkit CS5

Line numbers
The ESTK can display line numbers—an essential feature when working with programming
code. This feature can be activated or deactivated by choosing View > Line Numbers or by
using the keyboard shortcut—Control-Shift-L on Windows; Command-Shift-L on Mac.
Syntax highlighting

16
It also offers the colour-coded highlighting of syntax, whereby the colour of words and
statements confirms their role within the code—for example, JavaScript keywords are shown
in blue and fixed text strings (inside quotation marks) in burgundy.
Code Collapse
Another useful feature is Code Collapse whereby the program automatically displays buttons
for hiding and revealing blocks of code. This feature simplifies navigation in longer scripts:
you can temporarily hide lines as an alternative to constantly having to scroll past them to get
to another part of your script. This feature can be activated and deactivated by choosing View
> Code Collapse.

Figure 1-4: The ESTK's Code Collapse feature facilitates navigation within your scripts by allowing
you to temporarily hide blocks of code.

The program automatically recognizes blocks of code and places a minus-sign icon at the start
of each block. If you click on the minus-sign icon, the entire block is hidden and replaced by a
horizontal line. The minus-sign icon then changes to a plus-sign. To reveal the hidden block
once more, click on the plus-sign icon.
Auto completion
If you have used any of the Microsoft code-editing tools such as the Visual Basic Editor used
to create macros in Microsoft Office, you will be familiar with the concept of intelliSense,
whereby the code editor offers you a list of context-sensitive options as you enter keywords.
By choosing an option from the list, you avoid making syntax errors and you don't spend so
much time looking things up. Adobe Dreamweaver offers a virtually identical feature called
code hinting. The ESTK also has an auto completion feature. However, it falls far short of the
equivalent features found in Dreamweaver and the Microsoft editors.
When you type certain words, lists of suggestions do appear; but they are not very closely
targeted to the code you are entering. However, when you are learning a new programming
language, every little helps; so why not try it and see what you think. This feature is activated
by default: if you wish to deactivate it, choose Edit > Preferences (Windows) or
ExtendScript Toolkit > Preferences (Macintosh). Click on the Help category on the left of
the Preferences dialog then uncheck the option Enable Auto Completion.

17
Figure 1-5: Deactivating the ESTK's rather disappointing Auto Completion feature.

Organizing your scripts


As we saw earlier, in order for scripts to appear in the InDesign Scrips panel, they have to be
saved in a specific location. The ESTK offers much more flexibility in this respect and this
makes it a great way of launching and testing your scripts. The Scripts panel is displayed by
default: if it is not visible, choose Windows > Scripts.
In the top left of the panel is a drop-down menu which allows you to specify the location from
which you want to display scripts. If you choose InDesign CS5, you will see the same items as
you would in the InDesign Scripts panel.
Favorites
The flexibility of this option comes when you choose Favorites; because you can add favorites
from any location. This means that you can save your scripts in any convenient directory and
have them appear in the ESTK's Scripts panel simply by adding them to your list of favorites.
To add a folder to your list of favorites, choose Add Favorite from the menu in the top right of
the Scripts panel. When the Add Favorite dialog box appears, enter a name for the new item—
which does not have to be the same as the name of the folder you are adding. Next, click on the
folder icon, browse for the folder you wish to add and click OK. Repeat this process as many
times as necessary to build a list of items pointing to as many locations as necessary.

18
Figure 1-6: Adding a folder to the Scripts panel favorites

This book aims to provide you with practical examples of using JavaScript to automate
InDesign and will attempt to give you as much practice as possible. Each chapter contains
tutorials headed Try it for yourself! The materials required to complete all of these tutorials
is in the work files folder. If you have not already done so, you should download the folder
from the following URL:
http://www.trainingcompany.com/downloads/indesigncs5js1.aspx
When you unzip the file, you will find that it contains a folder called “indesigncs5js1” and,
inside this folder, you will find the various folders for each chapter of the book. The best place
to save this folder is in your InDesign scripts folder. This will enable you to run scripts either
from InDesign or from the ESTK.

Try it for yourself!


TUTORIAL: Getting started
Let's get started and create our first script.
1. Installing the work files
Download and unzip the file "indesigncs5js1.zip". Here's the URL
again:
http://www.trainingcompany.com/downloads/indesigncs5js1.aspx

In the Finder or Windows Explorer, right-click on the file


(Control-click if you are using a one-button mouse on a Mac) and
choose Copy.
Switch over to InDesign and choose Window > Utilities >
Scripts.
In the Scripts panel, highlight the User folder and choose Reveal

19
in Finder (Mac) or Reveal in Explorer (Windows) from the menu
in the top right of the Scripts panel.

Right-click in the folder which opens on your desktop an choose


Paste.
Return to InDesign and look at your User folder. The
“indesigncs5js1” folder and all its contents should now be
accessible inside it.

2. Setting up the ESTK


20
If you are using Windows, choose Start > All Programs > Adobe
Group > Adobe ExtendScript Toolkit CS5, where Adobe
Group represents the group in which you normally access
InDesign CS5—for example, Adobe Master Collection CS5.
If you are using a Mac, go to the Finder and open the Applications
folder, then Utilities, then Adobe Utilities - CS5, then
ExtendScript Utility; and there you will find the ESTK application.
Drag the ESTK onto your Dock for easy access in the future.

As with most text-editing programs, when you launch the ESTK, you will be presented with a
blank document.
Choose File > Save and then save the file as "01-basic-script.jsx"
in the "chapter01" folder of the “indesigncs5js1” folder.
(On Windows, you may find it easier to go to the window that
opened when you chose Reveal in Explorer; open the
"indesigncsjs1" folder; open the "chapter01" folder and copy the
file path from the address bar;

21
go back to the ESTK; paste the URL into the filename box and then add
"\01-basic-script.jsx".
If you have not already made line numbers visible, choose View >
Line Numbers.
Choose Windows > Scripts.
Choose Add Favorite from the menu in the top right of the panel.
Enter the name "InDesign CS5 JS1" then click the Browse icon—
the folder icon.
Navigate to the “indesigncs5js1” folder; open it and click OK. (On
Windows, just paste in the file path you copied earlier.)
Activate the checkbox marked Recursive Folders. If this is active,
folders within the selected folder will also be displayed in the
Scripts panel. When the option is inactive, the Scripts panel will
only list scripts created in the top level folder, ignoring all sub-
folders.

22
The Scripts panel will now display the sub-folders within the
“indesigncs5js1” folder.

If you have scripts in other locations, you can repeat this procedure to add more favorites to
the Scripts panel. A dropdown menu in the top right of the panel allows you to switch between
favorites.
3. Creating a basic script
Now we are ready to create our first script. I won't go into any details regarding the syntax:
we'll get to that in the next chapter.
Enter the line shown below.

1 var doc = app.documents.add();

Choose File > Save then switch over to InDesign.


In the Scripts panel, open User > indesigncs5js1 > chapter01.
Right click on "01-basic-script.jsx" and choose Run Script from the
menu.

23
InDesign duly creates a new document.
Close the new document without saving the changes then return to
the ESTK.
Now let's add a text frame to the first page of the new document.
Add the following line to your code.
1 var doc = app.documents.add();
2 var frm = doc.pages[0].textFrames.add();
Let's run the code again; but, this time, we'll use the ESTK. First, we need to set InDesign as
the target application.
Choose InDesign CS5 from the target application drop-down
menu in the top left of the document window.

Click on the Run button in the top right of the document window
or choose Debug > Run.

Switch over to InDesign and you will see that a new document has
24
again been created and, in the top left, is a tiny box—our text
frame.

Close the document without saving and return to the ESTK.


Now let's make the text box bigger—in fact, let's make fill the page—and add some text to it.
Add the following lines to your script.
1 var doc = app.documents.add();
2 var frm = doc.pages[0].textFrames.add();
3 var w = doc.documentPreferences.pageWidth;
4 var h = doc.documentPreferences.pageHeight;
5 frm.geometricBounds = [0, 0, h, w];
6 frm.contents = "Here we go!";

Click on the run button once more then switch to InDesign to see
the result.
The text frame now occupies the entire page with the words "Here we go!" in the top left.

Close the file, abandoning the changes, then return to the ESTK.
Finally, let's centre the text horizontally and vertically and make the point size larger.
Add the following three lines of code to the end of your script.
1 var doc = app.documents.add();
2 var frm = doc.pages[0].textFrames.add();
3 var w = doc.documentPreferences.pageWidth;
4 var h = doc.documentPreferences.pageHeight;
5 frm.geometricBounds = [0, 0, h, w];
6 frm.contents = "Here we go!";
7 frm.paragraphs[0].justification = Justification.centerAlign;
8 frm.textFramePreferences.verticalJustification = VerticalJustification.centerAlign;
9 frm.paragraphs[0].pointSize = 72;

25
Save your changes then switch over to InDesign to run the script.

The text should now be 72 point and centred horizontally and vertically.

About the tutorials in this book


Tutorials will play a large part in this book and will, hopefully, help to reinforce the various
topics covered in each chapter. The idea is that you get used to creating InDesign scripts from
scratch and, in doing so, become more and more familiar with the syntax. Although laborious,
actually writing code and getting it to work is a tried and testing method of learning a scripting
language.
The exercise you have just completed was designed purely to show you how to write a script
and test it out, both in the ESTK and from inside InDesign itself; hence we have not discussed
the syntax used. However, in future tutorials, we will be looking line by line at new techniques
as we encounter them.
Scripting can never be learned just by rote: it's more a case of becoming increasingly able to
recognize key patterns and when to use certain techniques to solve a given type of problem.
You will find that, as you become more familiar with keywords, statements and the names of
the InDesign objects being manipulated by the scripts, you will begin to anticipate more and
more bits of code and syntax structures.
The completed versions of the scripts covered in the tutorials can be found in the appropriate
chapter folder and always end with "_completed.jsx". If you find entering the code difficult or
just too tedious, feel free to copy and paste each section being discussed from the completed

26
version into the version your are building from scratch. However, if your keyboard speed is
reasonable, entering the code yourself and learning to anticipate and deduce what should come
next is usually the best approach.
CHAPTER 2. Scripting essentials
InDesign scripts are basically text files saved with the file extension “.jsx". To create a script
using the ExtendScript Toolkit (ESTK), choose File > New then save the file in the desired
location—usually in one of your designated favorites folders. You are then ready to start
writing code.
Comments
The good news is: even if you have never written a line of code in your life, you are already
qualified to write one of the key elements of all scripts. Comments are lines of useful
explanatory text included in a script for the benefit of anyone reading the code—including
yourself. Comments should be a feature of every script that you write: the more complex the
code, the greater the need for explanation and, hence, the greater the need for commentary.
JavaScript recognizes single line and multiline comments. To create a single line comment,
place two forward slashes ("//") at the start of the line. To create a multiline comment, put a
forward slash followed by an asterisk ("/*") before the first line and an asterisk followed by a
forward slash after the last line, thus:
// This is a single line comment

/*
This is
a multiline
comment
*/
Comments can also be used as headings, enabling the eye to pick out the start and end of the
key sections of your scripts. You can use any characters you like to help make these headings
stand out—for example:
// ====== Initialization ======
stands out more than:
// Initialization
You can also place short comments at the end of the lines of code to which they relate—for
example:
var arrFiles = []; // list of all files to be processed

Writing scripts
All of the code that you place in a text file saved with a “.jsx” file extension constitutes a
script. Your code does not need to be placed in any particular containing structure such as a
sub-routine. However, the scripts that you write will almost always be divided into several
different blocks called functions.
Each JavaScript statement that you write should end in a semi-colon. Although this is not a

27
syntactical requirement, it is a very useful convention, enabling you to distinguish between two
lines of code and a long line which has word-wrapped.
Variables
The statements you will use in your code will be many and varied; however, one of the most
logical places to begin our look at scripting is the use of variables—named sections of
memory in which you can store various types of data as well as references to the various
objects within the InDesign hierarchy, such as documents, pages and text frames.
Essentially, a variable is a name which represent a value—typically, some piece of useful
information: wherever you use the name, that particular piece of data is targeted. In one
context, a variable is like an abbreviation or acronym: “Whenever I say docNew, I mean the
new document that I have just created". However, the great thing about variables is that the
values and elements they represent can be constantly altered and manipulated: by your code, by
user action or by the InDesign environment. It all depends on the nature of the values,
information or object assigned to the variable.
Naming variables
Most scripts that you write will contain many variables, so the names that you assign to
variables will have an impact on the clarity of the code you end up with. Naturally, there are
some restrictions on the names that you can use.
• You cannot use as a variable name a word which is part of the JavaScript syntax. This
includes some fairly common words such as “if", “switch", “for” and “final".
• Although there is no such restriction on using InDesign object names as variable names,
it is a bad idea, since it makes you code harder to decipher. (The InDesign sample scripts
use a convention of giving variables which hold objects, the same name as the object with
the prefix “my": e.g. “myTextFrame".)
• Variable names cannot contain any spaces.
• The first character of a variable must be a letter or an underscore: you cannot start a
variable name with a number or a special character.
• JavaScript is case-sensitive; so you should decide on a set of rules regarding how you
will use case in your variable names.
Bearing these restrictions in mind, it is usually best to adopt a system of some sort when
naming your variables.
• One technique widely used by programmers is the use of prefixes which indicate the
nature of the variable—typically, the type of data it is meant to store.
• It is usually best to use a minimum of two words for each variable name and to separate
them either with an underscore or by capitalizing the first letter of each word.
• Be consistent with case: for example, always use lower or initial uppercase letters, and
so forth. This way, you will not have to constantly scroll back to check the case you used
for individual variables.
Variable data types
JavaScript is not a strongly typed language: it does not require that you specify the type of data

28
which each variable is meant to store. However, variables are used to store three main types
of information:
• String—any combination of alphanumeric characters, be it a word, phrase, sentence
paragraph or story.
• Numeric—data which definitely consists of numbers as opposed to strings which
happen to contain numbers. Thus, for example, a person's salary is numeric; their
telephone number is a string.
• Boolean—data which may either be true or false and nothing but.
In addition to these basic data types, variables can also be used to hold references to InDesign
objects. To get an idea of why the use of variables in this context is so useful, let us imagine
that we want to create a new document and carry out a series of operations on it. We can use
this line of code to create the document:
app.documents.add();
However, when we want to manipulate the document we have thus created, how do we refer to
it? We can't guarantee that it will remain the active document, we can't anticipate what default
name InDesign will assign to it... The simple answer is to put a reference to the document
being created inside a variable—for example:
var doc = app.documents.add();
Now, whenever we need to refer to the document we have created, we just use the variable
name doc.
Declaring and initializing variables
Although not demanded by JavaScript, it is always best to declare a variable and, if possible,
assign it a value consistent with the type of data it is meant to store. Variables are declared
with the keyword var—for example:
var txfHeader;
is a statement which declares a variable called txfHeader. The three letter prefix has been
chosen to indicate that this variable will contain a reference to a text frame. It is also possible
to assign a value to a variable as you declare it. We saw an example of doing this a moment
ago:
var doc = app.documents.add();
Here, we are both declaring the variable doc and assigning a new document to it. Let's take
another example:
var intCounter = 0;
Here, we are declaring a variable called intCounter and assigning it the neutral or default
value of zero to indicate that it is a numeric, integer variable. If we want to do the same thing
with a string variable, we could use a statement like the following:
var strTitle = “";
In this example, we declare a variable called strTitle and assign an empty string to it to
indicate the type of data it will be used to store. We could do the same thing with a boolean
variable:
var blnOpen = false;

29
Here, we declare a variable which might be used to record whether or not a given document is
currently open and assign it the initial value of false.
When a variable is being used to store an InDesign object, it tends to be declared and given a
value at the same time. There are a huge number of objects in InDesign and so the possible
statements are too numerous to mention; but we will see a great many examples throughout the
book. The rule to remember is that any InDesign object which you plan to refer back to should
be placed in a variable when you make your first reference to it. Thereafter, when you need to
refer to that object again, you simply use the variable name.
Expressions and operators
Having assigned an initial value to a variable, you will usually need to update or modify that
value as your script progresses. To do this you will need to use expressions and operators. An
expression is the programming equivalent of a grammatical phrase, a fragment of code which
produces a result. Thus to assign a value to a variable, we would use a line of code in the
following format:
variable = expression;
For example:
intTotalWidth = 210 + intLeftBleed + intRightBleed
In the above example, the expression 210 + intLeftBleed + intRightBleed will be evaluated
at runtime and will return a given value.
Most expressions use operators—symbols or keywords which manipulate values; arithmetical
operators being one example.
Arithmetical operators
Binary (requiring two or more arguments)
Assignment = e.g. var int_total = 0;
Addition +
Subtraction -
Multiplication *
Division /
Modulus % e.g. var intPagesOver = intPages % 4
(Puts the remainder of dividing intPages by 4 into the variable intPagesOver)
Unary (requiring a single operator)
Increment ++ e.g. intTotal ++ (increase value of intTotal by 1)
Decrement — e.g. intTotal — (decrease value of intTotal by 1)
Comparison operators
Comparison operators are used to compare two values.
Equal to == e.g. blnMatch = strName == "newsletter1251"
(If strName contains the string “newsletter1251”, blnMatch will be set to true.)
Identical to === e.g. strWidth === 2

30
(Equal to and of the same data type. In the above example, if strWidth is a string value entered
by the user, strWidth === 2 will return false, whereas strWidth == 2 will return true, since
JavaScript will automatically convert the string value to a number.)
Not equal to !=
Not identical to !==
Greater than > Greater than or equal to >=
Less than < Less than or equal to <=
Logical operators
Logical operators are applied to two or more boolean expressions and return either true or
false.
And && e.g. intTotal >= 5 && intTotal <=10
Or || e.g. strSize == “A4” || strSize == “Letter"
Not ! e.g. ! (strSize == “A4” || strSize == “Letter")
The InDesign object model
In order to automate InDesign, you need to use JavaScript to refer to the InDesign elements that
you wish to manipulate. Each element within InDesign is programmatically represented as an
object. As with InDesign itself, objects are arranged in a hierarchical structure, with the
application object at the top of the hierarchy.
Properties and methods
To understand any object in the hierarchy, you need to examine its properties and its methods.
The properties of an object are its attributes or characteristics. However, any objects which
form part of another object are also treated as properties of that object. Thus, in the InDesign
object model, the document, book and library objects are all properties of the application
object.
Methods are the actions which the object can perform or which the user can do to, or with, the
object. Thus, the application object has an open method which can be used to open a
document, book or library. Similarly, the document object has a print method which is used to
print the document.
This objectification is something that we do with everyday objects. Thus, for example, if we
look at cars, we could place the car object at the top of a hierarchy, with engine, chassis and
interior both as subordinate objects and also properties of the car object. The car object would
also have properties such as make and model and the engine would have a size and fuel-type
property as well as a start and a stop method.
The Object Model viewer
The ExtendScript Toolkit provides a handy utility for exploring the InDesign object model. In
the ESTK, choose Help > Object Model Viewer. When the utility launches, choose InDesign
CS5 (7.0) from the Browser drop-down menu in the top right. On the left of your screen, you
will see a list of the classes or object blue-prints which make up the InDesign object model.

31
The items which have a cube icon to the left of the name are all objects: the items with an icon
consisting of a pattern of dots next to them are enumerations—fixed sets of options which are
used as possible values for properties.

To explore the InDesign object hierarchy, click on the name of an object in the Classes pane.
The utility then displays the properties and methods of that object in the Properties and
Methods pane in the bottom left of the viewer. Properties can be identified by a blue icon
bearing the text “= X”, methods are indicated by a red icon containing “{ }”.

Figure 2-1: The Object Model Viewer

Each time you click on a property or method, details regarding that item are displayed on the
right. When you click on another property or method, a new set of details appears above
whatever was previously there. This provides a kind of history feature, enabling you to go
back and review anything you have already looked up since launching the ESTK.

If you close the Object Model Viewer and relaunch it later, the list of items that you have
looked up will still be there, provided you have not quit the ExtendScript Toolkit in the
meantime. You can clear individual items from the details history by clicking on the Close
button in the bottom right of each item. You can also clear the list of details completely by
clicking on the Close All button in the top right of the viewer.

32
Figure 2-2: Removing an item from the details list

Naturally, as soon as you quit the ESTK, your details list will be lost. However, The Object
Model Viewer also offers a Favorites feature which enables you to permanently save any item
you have viewed. Simply click on the Bookmark link in the bottom right of the detail item.

Figure 2-3: Adding an item to the bookmarks in the Object Model Viewer

To view your Bookmarks subsequently, click on the Bookmarks button in the bottom left of the
dialog. This closes the Browser panel and opens the Bookmarks panel which contains a list of
all the items you have bookmarked. Each item in the list is also a link which, when clicked,
adds the item at the top of the details section of the dialog. In the bottom right of the item, there
is a link which enables you to remove it from the Bookmarks panel.
Essential object syntax
In spite of the vast and varied nature of the InDesign object model, there are a number of key
syntax structures which, if learned, make the whole exercise of scripting InDesign seem less
daunting.

33
Working with object properties
Testing the properties of an object can be very useful—for example, we can gauge its
condition or its appropriateness for a given role.
Read-only and read/write properties
All object properties can be tested or read and many can also be set. Properties which can
only be read are referred to as read-only; those which can be both tested and modified are
called read/write. Thus, for example, the length property of the document object is a read-only
property. The statement:
app.documents.length = 5;
will generate an error telling us that “length is read-only".
Preference objects
Simple properties are directly related to an object. For example, every document object has a
name property—which returns the name of the document as well as a fullName property—
which returns the full file path to the document, including its name. In addition to these simple
properties, the InDesign object model often groups propeties together to form a separate
object. These are referred to as preference objects. For example, if you want to set the bleed
width of a document, you need to do so via the documentPreferences object of the document
object. For example:
app.activeDocument.documentPreferences.documentBleedUniformSize = true;
app.activeDocument.documentPreferences.documentBleedTopOffset = “3 mm";
This is very consistent with the InDesign interface, since if you are changing the bleed setting,
the same dialog will give you access to things like the page width and height and the slug.
Therefore, it makes sense to lump these settings together as a group and treat them as one
object.
The use of preference objects containing associated properties which really belong to a parent
object is efficient in a second way—the same set of properties can be applied to more than one
parent object. Thus, the documentPreferences object can be applied to the application object—
where it can be used to change InDesign's default document settings and the document object—
where it allows us to change the settings for a particular document.
Property values
When setting a property, you must supply a value of the appropriate type. The Object Model
Viewer provided by the ESTK will confirm the type of data that is permitted. Here are a few
pointers.
When supplying measurements—such as width and height, you may enter either a number or a
string which includes the measurement type. Thus both of the following statements are valid:
app.activeDocument.documentPreferences.documentBleedTopOffset = 3;
app.activeDocument.documentPreferences.documentBleedTopOffset = “3 mm";
When a number is used, the current measurement preferences will be used: these may have
been set by the user or by your script.
Using enumerations

34
When you consult the Object Model Viewer, you will find some properties requiring a special
value called an enumeration—one element in a preset list of permitted values. Enumerations
are normally written in the following format:
object.propertyName = EnumerationName.VALUE
The word or words following the final dot can be written in two ways: firstly, in all caps, with
multiple words separated by an underscore and, secondly, with the first word in all lower case
and the first letter of each subsequent word capitalized. Thus, to set the vertical justification of
a text frame, we can either use:
myTextFrame.textFramePreferences.verticalJustification = VerticalJustification.CENTER_ALIGN;
or:
myTextFrame.textFramePreferences.verticalJustification = VerticalJustification.centerAlign;
Here are a few more examples where enumerations have to used as the value of a property.
myTextFrame.paragraphs[0].justification = Justification.CENTER_ALIGN;
app.documentPreferences.pageOrientation = PageOrientation.LANDSCAPE
app.documentPreferences.pageOrientation = PageOrientation.PORTRAIT

For the sake of convenience, enumerations are listed as separate items in the Object Model
Viewer and can be bookmarked, as described on page 20.

Figure 2-4: Enumerations are listed as separate items in the Object Model Viewer

Working with object methods


Object methods are used to initiate processes and to generally make things happen. Just as
properties require values, so methods require arguments—values which are placed between
the parentheses following the name of the method, in a fixed order. In fact, some methods
require no arguments at all—for example: app.undo() and app.redo(), the scripting
equivalents of Edit > Undo and Edit > Redo. However, even when no arguments are required
by a method, the parentheses following the method name are still required.
Some arguments are optional and are always listed after those which are obligatory. For
example the open method of the application object has one required argument and two optional
ones. (By tradition, in all reference material, optional arguments are shown in square brackets

35
—as shown below. Naturally, however, square brackets are not required when these
arguments are used in actual scripts.)

SYNTAX app.open (from[showing window, open option])


SUMMARY ... Method of Application Object
Opens the document at the specified location.

From A file path specifying the location of the item to be opened: a document, book or library.
Showing Optional. A boolean value which dictates whether the document is visible when it
window opens.
Open option Optional. Whether the application opens the original item or a copy.

The data types of the values supplied for each argument varies from method to method and
include enumerations. Thus, for example, the third argument of the app.open() method—
OpenOption uses one of three possible enumeration items:
OpenOptions.DEFAULT_VALUE, OpenOptions.OPEN_ORIGINAL and
OpenOptions.OPEN_COPY.
Using properties records
Whenever you create a new object using the add() method of the object collection or container
in question, you have the option of including what's called a properties record as the last item
inside the parentheses. A properties record is a comma separated list of properties and values,
in the following format:
object.add(...{property1:value1, property2:value2...})
For example, if you want to create a new text frame within the active document, you could use
the following statement:
app.activeDocument.textFrames.add();
However, you could also use a properties record to specify the position of the new text frame
on the page, thus:
app.activeDocument.textFrames.add({geometricBounds:[20, 20, 277, 190]});
(The position of a text frame is specified using the geometricBounds property which takes an
array of four values in the format [y1, x1, y2, x2].)
Creating new elements
In many of the scripts that you write, you will need to create new elements: documents, books,
paragraph styles, colours, etc. The good news is that the syntax for doing this follows a
consistent pattern.
The first point I would like to emphasize in this context is the importance of placing each new
element that you create into a variable. This makes it really easy to manipulate the object
thereafter: the variable acts like a kind of tracking device which leads you directly to the

36
object without having to rack your brains over the syntax you need to use—simply using the
variable name will lead you directly to the object.
The general format for creating an object is as follows:

SYNTAX Variable = container.collection.add()


SUMMARY ... Syntax pattern
General syntax for placing a reference to an InDesign object into a variable.

Variable A name that you create which refers to a given object


Container The object which directly contains the object you wish to create within the InDesign hierarchy.
The collection to which the object you wish to create belongs, e.g. documents, textFrames,
Collection
etc.
Add The add() method of the collection.

At this stage, we will not concern ourselves with issues like checking to see whether an object
exists before you create it or what will happen if you attempt to create the object and an error
occurs—that will come later. Let us look at the syntax for creating a few common InDesign
objects.
Creating a new document, book or library
Documents, libraries and books are all contained within the application object itself: this is
borne out by the fact that you can go to the File menu and choose New > Document, New >
Library or New > Book. Documents form part of the documents collection; libraries form
part of the libraries collection; books form part of the books collection. This means that we
will need to use syntax like the following:
var docNew = app.documents.add();
var libNew = app.libraries.add();
var bkNew = app.books.add();
Since documents are one of the most frequently used objects in the InDesign hierarchy, let's
drill down into the document object and look at the syntax for creating more objects inside it.
Given that we have placed our new document inside a variable called docNew, the container
in our generic statement for creating new objects:
variable = container.collection.add()
will now be docNew—which, as we know, refers to an instance of the document object; while
our collection will be the collection of whatever class of object we wish to create.
Adding a page to a document
Thus, if we want to add a page to the end of the document, the container will be our document
and the collection will be the pages collection; so, we can use the same syntax pattern:
var pgNew = docNew.pages.add();
Creating default and document colours

37
The container for a default colour in InDesign is the application object. However, the
container for document colours is a specific document object. Thus to create a default colour,
we might use:
var colNew = app.colors.add();
while to create a document colour, we might say:
var colNew = docNew.colors.add();
Creating default and document paragraph styles
InDesign styles (paragraph, character, cell, table and object) are another example of an object
which can be created either at the application or document level. So, to create a default
paragraph style, we might use:
var stlNew = app.paragraphStyles.add();
and to create a document paragraph style, we might say:
var stlNew = docNew.paragraphStyles.add();

Referencing objects
While the easiest way to create a reference to a new object is to capture it in a variable as it is
being created, ExtendScript does provide a number of other convenient ways of referring to
specific objects within the InDesign object hierarchy. One very useful technique is to refer to
the collection containing the object along with an index, which may be either numeric or
textual.
Using a numeric index
The JavaScript syntax for referencing an object in a collection with a numeric index is:
container.collection[x]
where x represents the position of the item within the collection. However, in keeping with a
well established programming convention, the first item in a collection is always referred to
with the index [0]. Thus, to refer to the first page in a document referenced in the variable
docNew, we can say:
docNew.pages[0]
It is also possible to use negative indexes, which are calculated from the end of the collection.
Thus, rather conveniently, to make a reference to the last page in a document, we can simply
say:
docNew.pages[-1]
Using a named index
Making a numeric reference to a page object is fairly convenient; but it can sometimes be
difficult to anticipate the position of an object within the collection to which it belongs. Since
many objects in the InDesign hierarchy have names, a useful alternative is to reference an
object by its name. The generic syntax for doing so is:
container.collection.itemByName("nameOfObject")
Thus, if we want to target an InDesign document called “annual report.indd” which is currently
open, we could use the syntax:
app.documents.itemByName("annual report.indd")

38
To target a default colour called “Corporate blue” we could say:
app.colors.itemByName("Corporate blue")
ExtendScript also provides an all-purpose syntax which can be used in place of both [] and
itemByName—namely, item. Thus, instead of:
docNew.pages[-1]
you will often encounter:
docNew.pages.item(-1)
and, instead of:
app.colors.itemByName("Corporate blue")
you may find:
app.colors.item("Corporate blue")
Using an ID
Many InDesign objects also have an ID property, which is assigned to them automatically by
the program and which can be used to uniquely identify them. The itemByID () method can be
used to reference an item when you know its ID—for example, if we have previously captured
the ID of a given page item in a variable called txtID, we could target the text frame with a
statement like:
app.activeDocument.pageItems.itemByID (txtID);
Targeting a range of objects
To target a range of objects within a collection, use the itemByRange() method which returns
a subset of objects from within a collection. For example, if we want to work with pages 2 to
15 of the active document, we could use statements like the following:
var pgStart = app.activeDocument.pages[1];
var pgEnd = app.activeDocument.pages[14];
var pgRange = app.activeDocument.pages.itemByRange(pgStart, pgEnd);
(Bear in mind that app.activeDocument.pages[0] refers to the first page of the document.)

SYNTAX myObjectCollection.itemByRange (from, to)


SUMMARY ... Standard method of InDesign object collections
Returns an array of objects extracted from the collection specified by myObjectCollection.

The first object within the collection to be extracted. Can be specified as an object, as an integer or
From
a string.
The last object within the collection to be extracted. Can be specified as an object, as an integer or a
To
string.

Targeting every item in a collection


ExtendScript also provides the extremely useful everyItem() method for targeting every
member within a collection—effectively treating the whole collection as one object.
Additionally, we can simultaneously target the properties of all members within the collection
by referencing the properties of the object returned by the everyItem() method. Thus, if we

39
want to generate a list all of the fonts on the machine running our script, we can say:
var allFonts = app.fonts.everyItem();
var arrFontNames = allFonts.name;
or, more simply:
var arrFontNames = app.fonts.everyItem().name;

Targeting currently active objects


Sometimes in your scripts, you do not want to target a particular object but simply the object
which is currently active or currently highlighted.
Active document
To target the active document, ExtendScript provides the activeDocument property of the
application object:
app.activeDocument
Naturally, this statement always returns a document object—providing at least one document is
open. Since it is difficult to guarantee that a given document will remain active, it is still a
good idea to put a reference to the document into a variable. Thus, if you have a script which
works on whatever document the user is currently working on, the first thing you should do is
to capture that document into a variable. For example:
var currentDoc = app.activeDocument;
Any time you wish to refer to the object again, you can then simply use currentDoc. That way,
if the user activates another document while your script is running, currentDoc will still be
referencing the document that was active when the script began.
Active window
The application object also has an activeWindow property which returns a window object.
app.activeWindow
Whether you target the activeDocument or activeWindow property will depend on what you
need to achieve. For example, the document object does not offer a method of targeting the
currently active page, while activeWidow does—namely, the activePage property.
There are two types of window objects in InDesign: layout (which is the one you will work
with most frequently) and story. The layout window object has three properties which return
active objects: activePage, activeLayer and activeSpread. (Story windows have no such
properties.) We would normally refer to these properties in the context of the active window,
thus:
app.activeWindow.activePage
app.activeWindow.activeLayer
app.activeWindow.activeSpread
To find the number of the current page—i.e. the number which would appear if an automatic
page number is placed on the page, we would simply say:
app.activeWindow.activePage.name
To get the name of the active layer, we would say:
app.activeWindow.activeLayer.name

40
Providing the active page is a master page, we can home in on its name by using the syntax:
app.activeWindow.activeSpread.name
If the name returned by the above statement is a zero length string, then the active page is not a
master page. (If we use app.activeWindow.activePage.name when the active page is a
master page, we simply retrieve the prefix of the master (e.g. “A"), rather than the name.)
Counting objects
Knowing how many elements there are in a collection is a fairly common requirement when
scripting. Often you will need to examine each item in a collection and knowing how many
there are will enable you to construct a loop. (See Chapter 3: Conditional statements, loops
and functions.) InDesign object collections normally have a length property which returns the
number of items in the collection. For example, if the following statement evaluates to false,
we can assume that no documents are currently open in InDesign:
app.documents > 0
To return the number of pages in the active document, we would say:
app.activeDocument.pages.length
Why “length"—well, the length property is also used to find the number of items in a
JavaScript array and, since InDesign object collections closely resemble arrays, giving them a
length property makes perfect sense.
An alternative to the length property is the count() method which produces the same result. For
example:
app.activeDocument.pages.count()

JavaScript dialog windows


Communicating with your users is a fairly standard requirement when you are writing scripts
and displaying dialogs is the usual method of doing so. ExtendScript allows you to build
custom dialog boxes with can include interactive controls which respond to user actions in a
variety of ways. We will look at the creation of such dialogs in chapters 4 and 5. In the
meantime, let's take a look at the three types of dialog window which are built into JavaScript
and which may be all that you need for many forms of interaction with the user.
The alert function
The JavaScrit alert function displays a message which the user reads and then dismisses by
clicking the OK button. Here is an example of its use.
alert("The Pages Required field can only contain numeric values.");

Figure 2-5: The JavaScript alert function can be used to output messages for the users of your scripts

41
SYNTAX alert (message)
SUMMARY ... JavaScript built-in function
Displays a dialog containing the specified message.

Message The text to be displayed in the window.

The confirm function


The confirm function retrieves a boolean value from the user. Like the alert function it displays
a dialog with a message. However, this time, there are two buttons: Yes and No. If the user
clicks Yes, the function returns true; if they click No, it returns false. In order to gauge which
button has been clicked, you need to place the result of the function in a variable which you can
subsequently examine.
var blnContinue = confirm("Are you sure you wish to delete the selected items.");

Figure 2-6: The confirm function allows the user to accept or reject the statement displayed

SYNTAX confirm (message)


SUMMARY JavaScript built-in function
... Displays a dialog containing the specified message with Yes and No buttons. Returns true if user clicks Yes,
false if they click No.

Message The text to be displayed in the window.

The prompt function


The prompt function goes one step further, allowing the user to enter an arbitrary value in a text
box. In addition to the message to be displayed, the function accepts an optional default value
to be initially displayed in the text field when the dialog appears.
var strLocation = prompt("Please enter your location", “London");

Figure 2-7: The prompt function is used to capture input from the user

42
It is generally a good idea to always supply a value, using the empty string ("") if no value is
applicaable. This is because, if no default value is specified, when the dialog appears, the
input text box will contain the text "undefined", which may be a bit confusing to your users.

SYNTAX prompt (message [, default value])


SUMMARY JavaScript built-in function
... Displays a dialog containing the specified message and a text field for the user to make an entry. If the user
clicks OK, returns the value entered in the text field. Returns null if user clicks the Cancel button.

Message The text to be displayed in the window.


Default value Optional. The text to be initially displayed in the input text field.

The String object


Since text is such an important element in InDesign documents, you will find that the methods
of the String object are quite frequently required in your scripts. JavaScript has a built-in
mechanism which makes the properties and methods of the String object available to any string
value or, more typically to any variable containing a string value.
The length property
The length property simply returns the number of characters in a given string. Thus, if we want
to display the character count of a text frame held in a variable called txfTitle, we could use
the following statement:
alert(txtTitle.contents.length);
The statement txtTitle.contents returns a string, which then makes it possible for us to use the
length property of the String object.
The indexOf() method
The indexOf() method is used to test whether a given search string exists within a container
string. If the search string exists, the method returns its position using a zero-based index. If the
search string does not exist within the container string, since zero represents the first character
of the container string, the function returns -1. The indexOf() method is used very frequently in
JavaScript, often in conjunction with other methods. Let's take a simple example.
var strEmail = prompt("Please enter an email address", "");
var intAtSymbol = strEmail.indexOf("@");
alert("Position of @ symbol is ” + intAtSymbol);
Here, we display a prompt inviting the user to enter an email address. We then place the
position of the @ symbol (within the string entered by the user) inside a variable called
intAtSymbol. Finally, we display the position of the @ symbol with an alert statement, which
would become minus one if it has been omitted.
This function has a useful variant—lastIndexOf(), which returns the position of the last
occurrence of the specified string. For example, to calculate the position of the last dot in an

43
email, we could say:
var strEmail = prompt("Please enter an email address", "");
var intDot = strEmail.lastIndexOf(".");
alert("Position of last dot is " + intDot);

containerString.indexOf (search string, [, start at])


SYNTAX JavaScript String object method
SUMMARY Returns the position of the search string within the container string.
... containerString.lastIndexOf (search string, [, start at])
JavaScript String object method
Returns the position of the last occurrence of the search string within the container string.
Search
The string to be located within the container string.
string
If this optional second parameter is supplied, the search starts at this position; otherwise, it starts at
Start at
position zero (i.e. the first character).

The charAt() method


The charAt() method works in the reverse manner to indexOf(): it returns the character at the
position that you specify. It takes a single argument: the position or index of the character—
zero being the first index.
The following lines of code ask the user for a string, it then uses the charAt() method to extract
the first and last characters.
var str= prompt("Please enter a string", “");
var strFirst = str.charAt(0);
var strLast= str.charAt(str.length -1);
alert("First character ” + strFirst + “\rLast character: ” + strLast);
Note the use of the plus sign to link literal text with variables. This is called concatenation and
is often required in scripting. Even if you are new to scripting, you may well have come across
this technique when creating formulas in programs like Excel and FileMaker, where an
ampersand ("&") is used to achieve the same result.
To target the position of the last character, we simply subtract one from the length of the
string, since the first character has an index of zero rather than one. (Using “\r” generates a
carriage return, splitting our alert statement into two lines.)

SYNTAX containerString.charAt(index)
SUMMARY ... JavaScript String object method
Returns the character at the position specified by index.

index An integer between 0 and containerString.length -1, inclusive.

The use of a statement in a position where a value is required is also a very common structure

44
in scripts. Basically, if there is a syntactical requirement for a value of a given data type, you
can use any statement which evaluates to (or returns) the correct type of data.
The toUpperCase() and toLowerCase() methods
These two functions simply convert a string to upper and lower case, respectively. They are
often used to provide a little leeway to users when entering data by making it unnecessary for
them to use a particular case. The following example illustrates their use.
var strCapital= prompt("What is the capital of France?", “");
alert(strCapital == “Paris");
We display a prompt dialog asking the user to enter the capital of France. However, our alert
message will only display true if the user enters “Paris"—if they enter “paris” or “PARIS", the
alert will return false. In situations where case is not important in determining whether two
strings are equal, we can use either toUpperCase() or toLowerCase() to ensure that the
strings being compared have the same case. So, we could modify the above example as
follows:
var strCapital= prompt("What is the capital of France?", “");
alert(strCapital.toUpperCase() == “PARIS");
This time, we convert the string entered by the user to uppercase and then compare it to another
uppercase string, ensuring that case will not prevent two otherwise identical strings from being
seen as equal.

SYNTAX myString.toUpperCase() | .toLowerCase()


SUMMARY JavaScript String object methods
... Convert every character in myString to upper or lower case, respectively. Can be used in conjunction with
other methods—such as charAt(), slice() and subString()—to target only certain characters in myString.

The replace() method


The replace() method replaces one substring with another, within a container string. It takes
two arguments: the search string and the replace string. Thus, for example, if we want to
capitalize the first letter of a string, we might use the replace() method as shown below.
var str= prompt("Please enter a string", “");
var strFirst = str.charAt(0);
str= str.replace(strFirst, strFirst.toUpperCase());
alert(str);
In this example, we use strFirst as the search string and strFirst.toUpperCase() as the
replace string.
When used in this way, replace() only operates on the first occurrence of the search string; any
further occurrences are simply ignored. However, the replace() method has hidden depths; as
well as supplying strings as the two arguments, it is also possible to use regular expressions.
Regular expressions are a rather cryptic syntax which is used in many environments—
including InDesign's own GREP Find/Change utility— for finding strings that match given
patterns. Let's just look at how we can use a regular expression to substitute all occurrences of

45
a string using the replace() method.
var strName= prompt("Please enter a name for the new document", "");
strName = strName.replace(/ /g, "_");
alert("The file name " + strName + " will be used.");
In this example, we ask the user to enter a file name and, because we do not want any spaces in
the name, we then use the replace() method to change all spaces to the underscore character.
The regular expression “/ /g” is an instruction to target the literal string “ ” (space) globally.

SYNTAX containerString.replace(search text, replace text)


SUMMARY ... JavaScript String object method
Replaces first occurrence of search text with replace text.

search text Text to be replaced, specified as a string or regular expression (regex)

replace text Text which takes the place of the search text, specified as a string or regex.

The substring() and slice() methods


The substring() and slice() methods both extract one string from within another. They are
examples of methods which are often used in conjunction with indexOf(). Both methods can
take two parameters: the start position and the end position. The second parameter is optional:
if it is omitted, the end position becomes the last character of the container string.
Let's take a simple example:
var strName = app.activeDocument.name;
var intDot = strName.lastIndexOf(".");
var strExt = strName.substring(intDot);
alert("File extension of current document is " + strExt);
Here, we place the name of the active InDesign document in a variable called strName. Since
we now have a string value inside strName, we can use the indexOf() function to place the
position of the dot into the variable intDot. We then use the substring() function, with intDot
as the single argument, to place the file extension into strExt. Since we have not supplied the
optional last argument, the substring() method returns all characters, starting from the dot, up
to the end of the name.
If we replaced the substring() function with slice(), we would obtain exactly the same result.
However, the two methods are not identical: slice() allows you to use a negative number for
either of the parameters and calculates the position(s) counting backward from the end of the
container string. Let's say that, since we know that the InDesign document in question will have
a four letter file extension, we simply want to extract the last five characters—giving us either
“.indd” or “.indt". We could use the slice() method, as follows:
var strName = app.activeDocument.name;
var strExt = strName.slice(-5);
alert("File extension of current document is " + strExt);
This time, we ignore the dot and retrieve the extension into strExt by using the slice() method

46
with a single parameter of -5. Since no second parameter is supplied, the index of the last
character of the string is used, thus extracting the last five characters of the container string.

SYNTAX containerString.substring | containerString.slice (start at [, end at])


SUMMARY JavaScript String object methods
... Extract a substring from the container string from the start at up to the end at position. (The slice() method
allows the use of negative parameters which are counted in reverse from the end of the container string.)

Start
An integer indicating the position at which to start.
at
End Optional. An integer indicating the position at which to end. If omitted, extraction takes place up to and
at including the last character of the container string.

InDesign text objects and strings


It's very easy, when writing InDesign code, to confuse InDesign text objects with Strings—
after all, characters, words and paragraphs are all pieces of text. The thing to remember is that,
for the purposes of scripting, characters, words and paragraphs are objects which contain
strings: they themselves are not strings. Hopefully, this will become apparent when we discuss
text scripting in chapter 8. Basically, if you want to treat InDesign text as a string, so that you
can access the properties and methods of the String object, you need to drill down inside the
InDesign text object to find the string it contains. Thus, for example, the following statement:
var strNewName = myTextFrame.paragraphs[0].replace("Smith", “Jones");
will generate an error, because the paragraph object does not have a replace() method.
However, if we go inside the paragraph object and target the contents property, we will then
generate an instance of the string object which will have a replace() method.
var strNewName = myTextFrame.paragraphs[0].contents.replace("Smith", "Jones");

The Array object


The JavaScript array object allows you to store several—usually related—values in a single
variable. Thus, instead of having to create lots of variables with similar names and roles—for
example, strDoc1, strDoc2, etc., we are able to create a single array variable and store all of
the related values inside it.
Creating arrays
To declare an array, use a statement like the following:
var arrDocs = [];
To populate an array with values, you can then target the individual slots within the array,
using an index which is written in square brackets after its name. The first element in the array
always has an index of zero.
arrDocs[0] = File("~/desktop/indesign docs/catalog.indd");
arrDocs[1] = File(""~/desktop/indesign docs/newsletter.indd");
arrDocs[2] = File(""~/desktop/indesign docs/leaflet.indd");

47
You can also declare and populate an array in a single statement—for example:
arrBranches = ["London", “Manchester", “Glasgow", “Leeds", “Liverpool"];
The above statement creates an array called arrBranches and places five strings inside it,
saving us having to use six separate statements:
arrBranches = [];
arrBranches [0] = “London";
arrBranches [1] = “Manchester";
arrBranches [2] = “Glasgow";
arrBranches [3] = “Leeds";
arrBranches [4] = “Liverpool";
When we discuss looping statements, in the next chapter, we will encounter another method of
populating arrays: through iteration.
InDesign objects and arrays
InDesign collections have several attributes in common with arrays. As we have seen, the
items in the collection can be referenced using a zero-based index. Both arrays and InDesign
collections have a length property which returns the number of items in the object.
In addition, many InDesign statements return an array of objects. For example, the ExtendScript
File object has an openDialog() method which displays a dialog allowing the user to select
one or more files. If the option for multiple file selection is switched off, the method returns a
single file; if it is switch set to true, it returns an array of file objects. Thus, for example, the
following statement will return an array containing the InDesign files selected by the user.
var arrFiles = File.openDialog("Please select one or more file(s)", “InDesign Files:*.indd", true);

SYNTAX File.openDialog (prompt [, filter, multiselect])


SUMMARY JavaScript File object method
... Displays a dialog allowing the user to choose one or more files via the open file dialog generated by the
operating system.

Prompt The alert text to be displayed at the top of the dialog.


Filter An optional statement which determines the file types to be displayed in the dialog.
Multiselect A boolean value specifying whether the user is able to select more than one file in the dialog.

Some InDesign objects also have properties which use arrays as values. For example, to
specify the position of a text box on the page, the geometricBounds property uses an array of
four values in the order [y1, x1, y2, x2].
myTextBox.geometricBounds = [20, 20, 277, 190];
Similarly, some object method parameters have to be arrays. For example, when creating a
drop down list control in a ScriptUI dialog box (a topic we will be discussing in chapter 5),
you can specify both the size and position of the control, and the items which will appear in the
list, as arrays, when using the add() method which creates the control.

48
var win = new Window('dialog');
var ddl = win.add('dropdownlist', [0, 0, 100, 20], ['A4', ‘A3', ‘Letter', ‘Legal']);
(The first array is the bounds property which sizes and positions the control inside its
container; coordinates are listed in the order [left, top, right, bottom].)
Array properties and methods
The length property
Arrays share the length property with InDesign object collections and the JavaScript String
object: length simply refers to the number of items in the array.
The push and pop methods
The push() method is used to add one or more elements to the end of an array.
var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
arrSizes.push('Ledger'); // arrSizes now contains ['A4', ‘A3', ‘Letter', ‘Legal', ‘Ledger']
The pop() method removes the last item.
var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
arrSizes.pop(); // arrSizes now contains ['A4', ‘A3', ‘Letter']

SYNTAX myArray.push(item 1 [, item 2, ... item n])


SUMMARY ... Method of Array Object
Adds the specified item(s) to the end of an array.

item 1 (etc.) Item(s) to be added.


myArray.pop()
Method of Array Object
Removes the last item from the end of an array.

The splice() method


To add and remove items anywhere within an array, we have the splice() method. In its
simplest form, it inserts an item in the specified position.
var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
arrSizes.splice(1, 2); // arrSizes now contains ['A4', ‘Legal']
Here, the first parameter specifies that we are starting at the second item (index 1), and the
second, that we are removing two items.
The splice() method can also be used to insert items by supplying the optional third paramater.
(Here, the second parameter—the number of items to be deleted—can simply be set to zero.)
var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
arrSizes.splice(2, 0, ‘A2', ‘A1', ‘A0'); // arrSizes now contains ['A4', ‘A3', ‘A2', ‘A1', ‘A0', ‘Letter', ‘Legal']

SYNTAX myArray.splice (index position, how many[, item1, item2, etc.])


SUMMARY ...
Array object method
Removes the number of items specified and (optionally) adds items at the index position.

49
Index position The position within the array where the splicing takes place

How many Number of items to be deleted (can be set to zero)


Item1, etc. Optional element(s) to be inserted at index position

The unshift() method


If you simply need to insert elements at the start of an array, you can use the simpler unshift()
method.
var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
arrSizes.unshift('A6', ‘A5'); // arrSizes now contains ['A6', ‘A5', ‘A4', ‘A3', ‘Letter', ‘Legal']

SYNTAX myArray.unshift (item1, item2, etc.)


SUMMARY ... Method of Array Object
Inserts one or more items at the start of an array causing existing items to move down.

item1, etc. The element(s) to be inserted—any JavaScript element

The split() method


The split() method is actually a method of the JavaScript String object: it creates an array by
parsing a string using the specified delimiter. For example, if we are processing some text in a
variable called strEmails in the format:
"email1@xszwv.com; email2@xszwv.com; email3@xszwv.com; email4@xszwv.com"
since the email addresses are separated by a semi-colon, we could convert the string into an
array of email addresses with a statement like the following:
var arrEmails = strEmails.split(";");

SYNTAX myString.split ([delimiter, limit])


SUMMARY Method of String Object
... Creates an array by splitting a string using the specified delimiter.

The string to be used for the splitting operation. If this optional parameter is omitted, a single item
Delimiter
array containing the original text is produced.
Limit The number of items to be placed into the array. If omitted, all items are returned.

The JavaScript Object object


50
The JavaScript language is built on objects and, when writing scripts, you have access to two
types of object: built-in JavaScript core objects—like Array and String—and objects which
form part of the InDesign object model—such as app, document and layer. In addition,
JavaScript allows you to create your own objects, complete with their own methods and
properties which you can then use in your code.
Let's look at how custom objects can be used, in a similar way to the array object, for storing
data. You begin by creating an empty object, then add properties to it and assign a value to
each one. For example:
var objHeadOffice = {};
objHeadOffice.name = “Glasgow";
objHeadOffice.manager = “Nigel McGregor";
objHeadOffice.phone = “0141 015 659";
It is also possible to declare an object variable and populate it in a single statement:
var objHeadOffice = {name: “Glasgow", manager: “Nigel McGregor", phone: “0141 015 659"};
The values assigned to object properties can be of any data type, including arrays and other
objects. Thus, in the following example, we create an empty object called objBranches, create
three properties—names, manager and phone—and assign values to each property. (Each of
the values being assigned is an array.)
var objBranches = {};
objBranches.names = ["London", “Leeds", “Manchester", “Glasgow", “Liverpool"];
objBranches.managers = ["Bill Blackwell", “Mary Howerd", “Martin Webber", “Nigel McGregor", “Kim Lowe"];
objBranches.phone = ["020 715 6599", “0113 836 599", “0161 256 810", “0141 015 659", “0151 715 642"];
The properties of an object can also be specified using square brackets instead of dot syntax
and placing the property name in quotation marks. Thus, our first object definition could also
have been written:
var objHeadOffice = {};
objHeadOffice["name"] = “Glasgow";
objHeadOffice["manager"] = “Nigel McGregor";
objHeadOffice["phone"] = “0141 015 659";
This offers a good deal of flexibility when referencing object properties: it is also the only
way in which we can use property names which include spaces. For example, you can use a
string stored in a variable as the name of a property using square bracket notation but not using
dot notation. The following artificial example, illustrates this flexibility:
var str1 = "branch"
var str2 = "id";
var objHeadOffice = {};
objHeadOffice[str1 + " " + str2] = "GL";
alert(objHeadOffice["branch id"]);
Here we define the name of a property by concatenating two strings and a space. The resulting
property name ("branch id") can only be used via square bracket notation since it contains a
space. In this example, we have built the name using arbitrarily assigned values: more
typically we would use values inputted by the user or taken from the InDesign environment.

CHAPTER 3. Conditional statements, loops and functions


51
Conditional statements, loops and functions are a standard feature of every programming
language and play a big part in enabling programmers to create flexible, robust and efficient
code. Conditional statements allow your code to cater for different eventualities—if one set of
conditions arise, code block 1 executes, if a second set of conditions exist, code block 2
executes, and so forth. Loops allow you to execute a given block of code over and over, while
or until a particular condition exists or, alternatively, a set number of times. Functions allow
you to make your code modular: blocks of statements are grouped together and given a name
which can then be used to invoke the execution of those statements. Each time the function is
invoked, parameter values can be supplied which modify the way function works and the
values it returns.
If else statements
Even if you have absolutely no programming experience, you will probably have come across
if statements in programs like Excel, Access or FileMaker. In these programs, an if statement
involves a test, a result that will apply if the test is positive and another to be applied if the test
yields a negative result. If statements work in much the same way in JavaScript. The basic
syntax is as follows:
If(test){
// statements to be executed if test is true
}
else{
// statements to be executed if test is false
}
Let's take an example, using some of the syntax that we have already encountered. Let's ask the
user to choose a page orientation then create a new document and apply the specified page
orientation to it. (We will again use the prompt function to display a dialog allowing the user
to enter a value.)
Listing 3-1: If else statement
1 var str_orientation = prompt("Enter page orientation", "Portrait");
2 var doc_new = app.documents.add();
3 if(str_orientation.toUpperCase() == "LANDSCAPE"){
4 doc_new.documentPreferences.pageOrientation = PageOrientation.LANDSCAPE;
5}
6 else{
7 doc_new. documentPreferences.pageOrientation = PageOrientation.PORTRAIT;
8}

I hasten to add that this code is used here purely to illustrate the use of if statements. You
wouldn't normally use a prompt to ask for this type of information: you would use a dialog
with radio buttons or a drop down menu—but we will get to these interface elements later. As
it stands, if the user enters the word “Landscape” (using any case combination), we will create
a landscape page. If they enter any other string or leave the field blank, we will create a
portrait page.
Note the use of the JavaScript String function toUpperCase():

52
strOrientation.toUpperCase() == “LANDSCAPE"
This provides an easy method of ignoring the case of the text entered by the user. It converts
the text they enter to uppercase characters and we then compare the converted string to an
uppercase literal string. Naturally, we could have used the toLowerCase() function to achieve
the same result.
strOrientation.toLowerCase() == “landscape"
If the user deletes the default value, strOrientation will still contain a string—albeit an empty
one. However, if the user clicks the Cancel button, our script generates the error: “Null is not
an object". Clicking Cancel causes the prompt function to return null which leaves us—on line
3—trying to use the toUpperCase() function on a null value instead of a string. This scenario
leads us to one of the main uses of conditional statements: the validation of data.
Before attempting to apply a String function to strOrientation, we need to ensure that it
contains a string. We can verify this by modifying the if statement, as shown below.
1 var strOrientation = prompt("Enter page orientation", “Portrait");
2 var docNew = app.documents.add();
3 if(strOrientation != null && strOrientation.toUpperCase() == “LANDSCAPE")
4{
5 docNew.documentPreferences.pageOrientation = PageOrientation.LANDSCAPE;
6}
7 else
8{
9 docNew. documentPreferences.pageOrientation = PageOrientation.PORTRAIT;
10 }

Using else if
Using if and else in this way allows us to cater for two eventualities: to cater for an indefinite
number of outcomes, we use else if statements, each followed by a new test. Let's take another
simplified, user-unfriendly example: this time we will ask for a page size and test to see
whether it is A4, A5, Letter or Executive. If it is one of these sizes, we will change the page
size accordingly; if it is any other size, we will stay with whatever default size is currently in
place on their system.
Listing 3-2: Using else if statements
1 var strSize = prompt("Enter page size", "A4");
2 if(strSize != null){
3 var docNew = app.documents.add();
4 if(strSize.toUpperCase() == "A4"){
5 docNew.documentPreferences.pageSize = "A4";
6 }
7 else if(strSize.toUpperCase() == "LETTER"){
8 docNew.documentPreferences.pageSize = "Letter";
9 }
10 else if(strSize.toUpperCase() == "A5"){
11 docNew.documentPreferences.pageSize = "A5";
12 }
13 else if(strSize.toUpperCase() == "EXECUTIVE"){
14 docNew.documentPreferences.pageWidth = "7.25 in";

53
15 docNew.documentPreferences.pageHeight = "10.5 in";
16 }
17 else{
18 alert("Size not recognised. Default page size used.");
19 }
20 }
This example also shows us the use of a nested if statement—one if statement residing inside
another. On line 2, we use an if statement to make sure that user has not clicked the Cancel
button. The remaining lines of the script will only run if they have not.
On lines 5, 8 and 11, we set the width and height using the pageSize property of the
documentPreferences object. On lines 14 and 15, since there is no built-in page size called
"Executive", we explicitly set the pageWidth and pageHeight properties.
Switch statements
Switch statements allow you to choose between several different blocks of code based on the
value of a given expression. It saves you repeating very similiar else if statements—as we did
in listing 3-2. With switch, there is just one test expression but this has many possible
outcomes.
Listing 3-3: The switch statement
1 var strSize = prompt("Enter page size", "A4");
2 if(strSize != null){
3 var docNew = app.documents.add();
4 switch(strSize.toUpperCase()){
5 case "A4":
6 docNew.documentPreferences.pageSize = "A4";
7 break;
8 case "LETTER":
9 docNew.documentPreferences.pageSize = "Letter";
10 break;
11 case "A5":
12 docNew.documentPreferences.pageSize = "A5";
13 break;
14 case "EXECUTIVE":
15 docNew.documentPreferences.pageWidth = "7.25 in";
16 docNew.documentPreferences.pageHeight = "10.5 in";
17 break;
18 default:
19 alert("Size not recognised. Default page size used.");
20 break;
21 }
22 }
In listing 3-3, line 4, the test expression converts the page size retrieved from the user to upper
case. The case statements on lines 6, 10, 14 and 18 are then equivalent to if ... else statements:
if(str_size.toUpperCase() == “A4"...
else if(str_size.toUpperCase() == “LETTER"...
else if(str_size.toUpperCase() == “A5"...
else if(str_size.toUpperCase() == “EXECUTIVE"
However, the switch statement is much more efficient in this context and avoids the need to

54
repeat if(strSize.toUpperCase()... so many times.
Using break statements
The break statements found on lines 7, 10, 13 and 17 prevent the execution of any further code
within the switch statement. Although strictly speaking, no break is required on line 20, since
there is no further code in the switch statement anyway, inserting it is useful, since it makes the
code clearer and makes it easier to change the position of items within the switch block.
Use of the break is not obligatory after each case statement: it is possible to have the
statements following a case to “fall through” into the next section.
Listing 3-4: Combining elements in switch statements
1 var strSize = prompt("Enter page size", "A4");
2 if(strSize != null){
3 var docNew = app.documents.add();
4 switch(strSize.toUpperCase()){
5 case "A4":
6 case "A5":
7 docNew.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.millimeters;
8 docNew.viewPreferences.verticalMeasurementUnits = MeasurementUnits.millimeters;
9 break;
10 case "LETTER":
11 case "EXECUTIVE":
12 docNew.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.inches;
13 docNew.viewPreferences.verticalMeasurementUnits = MeasurementUnits.inches;
14 break;
15 default:
16 alert("Size not recognised. Default measurement units used.");
17 break;
18 }
19 }
In lines 5 and 6 of listing 3-4, since no break separates the two case statements, if either of
those cases proves true, line 7 and 8 will execute.
For loops
For loops enable you carry out a series of steps repeatedly, making subtle variations in the task
or tasks being performed inside the loop. One of the most common uses of for statements is
looping through items in arrays—both those you create yourself and the numerous collections
and arrays of objects which are encountered in the InDesign object model. The structure of a
for loop is as follows:
for(counter = startValue; limit test; increment/decrement counter){
statements...
}
The for loop uses a counter variable which has a starting value and a limit defined by a
conditional statement—such as counter < 10. The statements inside the block execute
repeatedly and, each time, the counter value changes in accordance with the
increment/decrement statement. If this statement is such that the counter never reaches the
limits set by the limit test, we get an endless loop.
Here is a simple example of a for loop:

55
for(var i = 0; i <=5; i++){
alert("The value of counter variable i is currently: " + i);
}
Here, the name of our counter variable is i (as in integer)—the name traditionally used by
programmers for this purpose, since it is as short and as clear a name as anyone will ever
come up with. Our limit test says that the loop will continue for as long as i is less than or
equal to 5; while our increment/decrement statement says that, with each loop, the value of i
will increase by 1.
Inside the loop, we have a simple alert statement which concatenates the value of i on to the
end of a literal string. This use of the value of i to create variations inside the loop is very
typical and is one of the key benefits of using for loops.
Using a for loop to test whether a document is open
Let's now look at a more useful example: how a for loop can be used to check whether a
specific object exists within a collection—in this case, whether the documents collection (i.e.
all documents currently open in InDesign) includes a document called “Rep2010.indd".)
Listing 3-5: Testing whether a given document is open
1 var strName="REP_2010.INDD";
2 var blnOpen = false;
3 for(var i = 0; i < app.documents.length; i++)
4{
5 if (app.documents[i].name.toUpperCase() == strName){
6 blnOpen = true;
7 break;
8 }
9}
10 if(blnOpen == false){
11 alert("Sorry, the file " + strName + " is not open. Cannot continue.");
12 }
13 else{
14 alert("Here we go!");
15 }

On line 3 of listing 3-5, we use the statement app.documents.length to set the limits of our
counter variable i: the looping will continue until i reaches one less than the number of open
documents. This is because the first open document in the documents collection has an index of
zero rather than one. Thus, to refer to the first element in the documents collection, we would
say:
app.documents[0]
To refer to the last document, we can either say
app.documents[app.documents.length -1] or app.documents[-1]
On line 2, we declare a boolean variable and assign it a value of false; then, on line 5—inside
the for loop, we test to see whether the name of app.document[i] is the same as the document
name we are looking for. If we find a match, we set our boolean variable to true (line 6) and
we exit the for loop with a break statement (line 7).

56
After the for loop ends, we test the boolean value: if it is still set to false, then we can assume
that the document is not open and we display an error message (line 11); otherwise, we can
carry on with the rest of our operations.
Looping in reverse
Most of the time, when looping through collections, it is logical to start from the first item and
loop through to the last. However, there are times when we need to loop in reverse: starting
with the last item and looping through to the first. This is particularly true when the number of
items in the collection changes during execution of the loop, such as when items are being
removed.
For example, if we want to use a for loop to remove all topics in an index and we looped from
the first to the last item; let's say there were 100 topics in the index: when we got to item 51,
we would get an error; because we would already have deleted 50 items, leaving only 50. So,
instead, we would start with item 100 and delete down to item 1.
Listing 3-6: Looping in reverse when deleting items
1 var doc = app.activeDocument;
2 for(var i = doc.indexes[0].topics.length - 1; i >= 0; i--){
3 doc.indexes[0].topics[i].remove();
4}

Break and continue


JavaScript provides two very useful statements for interrupting a loop before it reaches its
natural conclusion: break, which causes the for loop to end completely; and continue, which
interrupts the current iteration and moves onto the next. Thus for example, if we only wanted to
delete index topics beginning with the letters “M” to “Z", we could use the following code:
Listing 3-7: Using break to exit a for loop
1 var doc = app.activeDocument;
2 for(var i = doc.indexes[0].topics.length - 1; i >= 0 ; i--){
3 if(doc.indexes[0].topics[i].name.toUpperCase() >= "M") {
4 doc.indexes[0].topics[i].remove();
5 } else {
6 break;
7 }
8}
In listing 3-7, once again, we are looping in reverse to avoid generating errors as we delete
items. On line 3, we test to see whether the name of each topic is alphabetically “M” or later;
and, if it is, we delete it. As soon as this test fails, we use break (line 6) to exit the for loop
and no further topics are deleted.
To illustrate the use of continue, let's now say we want to delete all topics apart from those
whose name contains the word “Asia".
Listing 3-8: Using continue in a for loop
1 var doc = app.activeDocument;
2 for(var i = doc.indexes[0].topics.length - 1; i >= 0 ; i--){
3 if(doc.indexes[0].topics[i].name.toUpperCase().indexOf("ASIA") > -1){
4 continue;

57
5 }
6 doc.indexes[0].topics[i].remove();
7}
8
On line 3 of listing 3-8, we test to see whether the name of each topic contains the string
“Asia", using the JavaScript String function indexOf()—which returns a number representing
the position of “Asia” within the topic name (zero being the very start of the name). If the word
“Asia” is not present in a name, the indexOf() function returns -1. Thus the continue statement
on line 4 will only execute if the word “Asia” is present in the name of a topic, which will
cause the current iteration of the for loop to abort and prevent the topic from being deleted—
though the for loop will then continue to execute until the counter reaches its limit.
While loops
For loops are useful for working within limits which can be set numerically. However,
sometimes you simply need to repeat a given set of statements while or until a certain
condition is true. The solution for this requirement is the while loop, which carries out a series
of steps as long as a given condition is true. It is important that the condition will, at some
point, cease to be true; otherwise you will have an endless loop.
For example, if we want to display a dialog for the user until they either enter information or
cancel the operation, we could use the following while loop:
Listing 3-9: A while loop
1 var str_size = "";
2 while (str_size == "" && str_size != null){
3 str_size = prompt ("Please enter the required page size.", "", "Page Size");
4}
Pressing the Cancel button returns null; so the only permitted actions are either to enter a value
or press Cancel.
Functions
At its most basic level, a JavaScript function is similar to a variable. However, whereas a
variable stores a value, a function stores an unlimited number of lines of code. Just as you
retrieve the value in a variable by using its name, so you invoke a function by using the name
you assign it when you define the function. When you do so, all of the code inside the function
will be executed.
Defining and calling a function
To define a function, you use the keyword function followed by a descriptive name you assign
to the function. You then must add opening and closing parentheses which can optionally
contain one or more arguments—variables which allow you to modify the way the function
works or the result it produces. Then, enclosed in curly braces, you add your code—the
statements which comprise the function.
Let's take an example.
Listing 3-10: Defining and calling a function
1 displayDialog();

58
2
3 function displayDialog(){
4 var arrSizes = ['A4', ‘A3', ‘Letter', ‘Legal'];
5 var win = new Window('dialog');
6 var stxPrompt = win.add('statictext', [0, 0, 200, 20], ‘Please choose a size');
7 var ddlChoice = win.add('dropdownlist', [50, 30, 150, 50], arrSizes);
8 var btnOK = win.add('button', [0, 60, 100, 80], ‘OK');
9 win.show();
10 var strChoice = ddlChoice.selection.text;
11 alert(strChoice + ” it is!");
12 }
On line 1 of listing 3-10, we make a function call to the function displayDialog(). The
definition of this function then follows on lines 3 to 12. Calling a function before you have
defined it is not a problem and, if anything, is probably the norm. Functions provide your script
with modularity, the call to the function serving a similar function to an entry in a table of
contents. This means that you can get an overview of what a script does just by looking at the
function calls at the start of the script and any comments which accompany them. To see the
nuts and bolts of how the script works, you then examine the lines inside the function
definitions.
Don't worry too much about fully understanding the code inside the function at this point: it
displays a simple dialog box. We will look at the creation of ScriptUI dialogs in detail in
chapter 5.
On line 4, we declare an array of paper sizes. On line 5, we create an instance of the ScriptUI
Window object. On line 6, we add a control to the dialog called a statictext control—similar
to a <label> element on an HTML form. On line 7, we add a dropdownlist control to the
dialog—similar to a <select> element on an HTML form. On line 8, we add an OK button and
on line 9, we display the form.
On line 10, we read the value selected from the dropdownlist into a variable called strChoice
and finally, on line 11, we use the value in that variable in an alert statement which simply
confirms the user's choice.
Returning a value from a function
As well as simply executing a block of code, functions can also return a value. When calling a
function that returns a value you normally use a statement like the following:
var variableName = functionName ([arguments]);
The value returned by the function will then be placed inside the variable. To return a value,
somewhere inside the function, you must use the return statement:
return any Javascript statement;
The most frequent element that follows the return statement is simply the name of a variable
containing the value to be returned. Although only one value can be returned in this way, thanks
to arrays, you can return as many pieces of information as you want. Simply load all of the
values to be returned into an array and then return the array.
Let's modify our listing so that the function returns a value.
Listing 3-11: A function that returns a value

59
1 var arrSizes = ['A4', 'A3', 'Letter', 'Legal'];
2 var strChoice = displayDialog();
3 alert(strChoice + " it is!");
4
5 function displayDialog(){
6 var win = new Window('dialog');
7 var stxPrompt = win.add('statictext', [0, 0, 200, 20], 'Please choose a size');
8 var ddlChoice = win.add('dropdownlist', [50, 30, 150, 50], arrSizes);
9 var btnOK = win.add('button', [0, 60, 100, 80], 'OK');
10 win.show();
11 return ddlChoice.selection.text;
12 }
On line 3 of listing 3-11, when we make the function call, we place the result it returns into a
variable called strChoice. On line 11, inside the function, we have the matching return
statement which passes the value selected by the user from the dropdownlist back to the
strChoice variable used in the function call. Then, on line 3, we use the value returned in our
alert statement.
Passing parameters to a function
Having a function return a value is useful: it means that our function produces some output.
However, additionally, we can supply one or more input values when we call the function: this
is where the parentheses come in. When defining the function, you place the name that you want
to give each parameter inside the obligatory parentheses which follow the function name.
When you call the function, you supply—again inside parentheses, and in the same order—a
value for each parameter defined.
In listing 3-12, below, we have modified the function definition and function call to include
two arguments.
Listing 3-12: Passing arguments to a function
1 var arrOutput = ['Print', 'Interactive PDF'];
2 var arrSizes = ['A4', 'A3', 'Letter', 'Legal'];
3
4 var strOutput = displayDialog("Please specify required output.", arrOutput);
5 var strSize = displayDialog("Please specify paper size.", arrSizes);
6
7 alert("Producing " + strOutput + ", " + strSize + " document.");
8
9 function displayDialog(strPrompt, arrChoice){
10 var win = new Window('dialog');
11 var stxPrompt = win.add('statictext', [0, 0, 200, 20], strPrompt);
12 var ddlChoice = win.add('dropdownlist', [50, 30, 150, 50], arrChoice);
13 var btnOK = win.add('button', [0, 60, 100, 80], 'OK');
14 win.show();
15 return ddlChoice.selection.text;
16 }
On lines 1 and 2 of listing 3-12, we define two array variables: arrOutput and arrSizes,
which will be used inside the function to generate the values displayed on the dropdownlist.
On line 4, we call the displayDialog() function and pass it two arguments: the fixed string
“Please specify required output” and the arrOutput array variable. We place the result of this

60
function call inside strOutput.
On line 5, we again call the displayDialog() function, passing it two new arguments: the fixed
string “Please specify paper sizes” and the arrSizes array variable. We place the result of this
function call inside strSizes.
On line 7, we use the two values returned by our two function calls in an alert statement
confirming the choices made by the user.
On line 9, when we define the function, we specify that it requires two arguments which will
be referred to inside the function as strPrompt and arrChoice. This is similar to using the var
keyword when declaring a variable. However, here, the values that populate strPrompt and
arrChoice are supplied via the function call. The line
4 var strOutput = displayDialog("Please specify required output.", arrOutput);
implicitly states:
strPrompt = “Please specify required output.";
arrChoice = arrOutput;
So when the code inside the function executes, using the name strPrompt is the same as using
the fixed string “Please specify required output.".
On line 11, the strPrompt parameter variable is used as the third argument of the add() method
of the Window object—the text which will appear in the statictext control.
On line 12, arrSizes is used as the third argument of the add() method—this time, to specify
the list of items which will appear in the dropdownlist control.
What the parameters do for us is that, each time we call the function, we display the same
dialog box; but the text in the statictext control and the items in the list box can be different
with each function call.
Local and global variables
Even with the small amount of code we have so far discussed, you should now be able to see
that variables play a very important role in the creation of scripts. The use of functions adds
another level of complexity to variables: the question of scope —the locations within your
script from which the data in your variable can be accessed. In JavaScript, there are two things
which determine the scope of a variable: the position at which you declare it and whether or
not you use the var keyword.
If you declare a variable outside of all functions, it becomes a global variable and its values
can be read and modified from anywhere in your script. If you declare a variable inside a
function using the var keyword, its values can only be read and set from within that function.
However, if you omit var, you are, once more, declaring a global variable.
Listing 3-13 shows an example of creating two global variables—filePath and doc—and then
using them inside functions.
Listing 3-13: Using global variables
1 // Declare global variables
2 var filePath;

61
3 var doc;
4
5 main(); // Call main function

62
6
7 function main(){
8 userInput();
9 otherStuff();
10 outputDoc();
11 }
12
13 function userInput(){
14 filePath = File.saveDialog("Please specify location of output file.");
15 // ...
16 }
17 function otherStuff(){
18 doc = app.documents.add();
19 // ...
20 }
21
22 function outputDoc(){
23 doc.save(filePath);
24 // ...
25 }
The listing shows a typical structure for a script: first we define a main function which calls
all other functions; then we have the definition of these other functions. Note that, on line 5, we
have a function call to the main function: defining a function is not enough; each function must
be called in order for the code inside it to run.
On line 2 and 3, we declare our two global variables, using the optional keyword var. Since
the declaration is in the global scope—outside all functions—the variable we declare will be
global with or without var. However, in the interests of clarity, I would recommend using the
var keyword with every variable declaration. On line 14, inside the userInput() function, we
populate the filePath global variable by getting the user to choose a location and name for the
document our script will create via the saveDialog() method of the File object. On line 18,
inside the otherStuff() function, we create a new document inside the doc global variable.
Then, finally, in the outputDoc() function, we save the document, using the value inside
filePath as its location.
Using namespaces with variables
As your scripts get longer and more ambitious, the number of variables you use will increase
and it is possible that you will end up accidentally using the same variable name twice in two
different locations and with different scopes. Prefixing your variables with a namespace can
lessen the risk of this happening. Namespaces are a mechanism used in XML and some
programming environments for avoiding ambiguity when two elements in the same scope share
the same name.
There is no formal support for namespaces available in JavaScript; but the same result can be
achieved by using object variables. Simply define a JavaScript object which will be the only
global variable in your application and then assign properties to it which will then hold your
data. We shall be using this simple mechanism throughout the book and for consistency and
brevity, we will be calling our global variable g.

63
Using this technique, it becomes easy to distinguish between local and global variable names.
Basically, any variable whose name does not begin with g. is a local variable.
Placing all global information in one variable offers another important advantage: you can
destroy all of your global data simply by setting the value of the global variable, that contains
them all, to null.
Listing 3-14 shows another version of the previous script, modified to use the technique of
placing global data in a single object variable.
Listing 3-14: Using a namespace global variable
1 var g = {}; // Declare global variable
2
3 main(); // Call main function
4
5 g = null; // Destroy global data
6
7 function main(){
8 userInput();
9 otherStuff();
10 outputDoc();
11 }
12
13 function userInput(){
14 g.filePath = File.saveDialog("Please specify location of output file.");
15 // ...
16 }
17 function otherStuff(){
18 g.doc = app.documents.add();
19 // ...
20 }
21
22 function outputDoc(){
23 g.doc.save(g.filePath);
24 // ...
25 }
On line 1, we declare the global variable g as an empty JavaScript object; on line 3, we call
the main function; then, on line 5, we set the global variable to null. It may seem strange that
the global variable is nullified so quickly after being created; but remember that the main()
function is the indirect container for the entire script.
On line 14, instead of declaring a variable, we place the file chosen by the user in a property
of the global object variable called g.filePath. Similarly, on line 18, we place the new
document inside g.doc. Both of these bits of data are now global because their container is
global. So, on line 23, we are able to access the data from inside a separate function as we
save the file.
If your script is fairly long and complex, you may need to create more than one namespace
variable to hold different types of data. To do this, simply create further objects inside your
main global object.
For example, let's say you are building a script which will require several dialog windows

64
and involve the manipulation of several documents and the storing of key pieces of data that
you need to access from several different functions. You would create your main global
namespace object; but then you might create several namespace objects inside it: win, doc and
data, etc. You would then create properties inside each of these sub objects, adding your
dialog boxes, documents and data as values g.win, g.doc and g.data, respectively. This is
illustrated in listing 3-15, below.
Listing 3-15: Using multiple namespace objects
1 var g = {};
2 g.win = {};
3 g.doc = {};
4 g.data = {};
5
6 main();
7 g = null;
8
9 function main(){
10 dialogSetup();
11 docSetup();
12 dataSetup()
13 // ...
14 }
15
16 function dialogSetup(){
17 g.win.wiz1 = new Window('dialog');
18 g.win.wiz2 = new Window('dialog');
19 g.win.wiz3 = new Window('dialog');
20 // ...
21 }
22
23 function docSetup(){
24 g.doc.input = app.activeDocument;
25 g.doc.output = app.documents.add();
26 // ...
27 }
28
29 function dataSetup(){
30 g.data.filePath = File.openDialog("Please specify location of output file.");
31 // ...
32 }
Note that there is still only one global variable being declared: win, doc and data are
properties of this single object and will be destroyed along with it on line 7.
ExtendScript engines and variables
The software environment in which InDesign scripts are executed is referred to as an engine.
By default, scripts are executed in the main engine which is created automatically when script
execution is requested and reset when the script terminates, with the loss of all data.
Some scripting operations require persistence of data. For example, if you wish to create a
non-modal dialog—one which stays open while the user interacts with InDesign—you could
not run the script in the main engine, since whatever follows the display of the dialog would

65
cause it to simply disappear.
To run such scripts, use the #targetengine directive at the top of your script, for example:
#targetengine "session";
The engine name that you specify is unimportant: ExtendScript will create a persistent engine,
with that name, which lasts until the user quits InDesign.
Any global variables defined when a persistent engine is running will continue to exist until the
application is quit. Thus, if you have two global variables with the same name in two different
scripts, they will be treated as the same variable. This can obviously be used to your
advantage. To prevent it happening accidentally, use namespacing techniques like the ones
described in the previous section.
CHAPTER 4. Creating dialogs
We have encountered JavaScript's built-in dialogs including prompt(), which enables you to
ask the user to enter information. However, this is only suitable for obtaining really simple
data. ExtendScript offers you two methods of creating an interface for users of your solutions:
dialogs and ScriptUI.
Dialog controls
The dialog object contains a good variety of controls and enables fairly sophisticated
communication with those running your scripts. Controls include the following objects:
• Dialog—The dialog box itself: the container for all the other controls.
Layout controls
• DialogColumn—A container which can be used to create multi-column layouts. The
dialogColumn is the only control which can be placed directly inside the dialog object
itself.
• DialogRow—A container which can be used to create multi-row layouts.
• BorderPanel—A layout container with a visible border. Inside it, you can place
dialogColumn and dialogRow objects to create multi-column or multi-row layouts.
Text controls
• StaticText—A simple text label control used to annotate other controls. It cannot be
used to input user text.
• TextEditbox—A text box via which the user can input a text string.
Self-validating text controls
The UI suite of objects also includes a number of text boxes with built-in validation which
would be quite time-consuming to code. If inappropriate data is entered in these boxes, it is
refused and an error message is automatically displayed to the user.
• AngleEditbox—A text box which will only accept numeric values, after which a degree
symbol is automatically inserted.
• IntegerEditbox—A text box which will only accept whole numbers. Non-numeric values
are refused: decimal values are allowed and are simply truncated when the user clicks
OK or moves to another field.

66
• PercentEditbox—A text box which will only accept numeric values, after which a
percentage sign is automatically inserted.
• RealEditbox—A text box which will only accept numeric values including decimals.
The system accepts as many decimals as the user cares to enter; but the number is then
rounded to three decimal places.
Controls which offer the user a choice
Text controls are great for asking the user to enter names, measurements and the like; but,
wherever possible, it is usually best to get them to choose from a series of preset options. This
helps to reduce the risk of erroneous input. There are three controls available for offering your
users a choice:
• RadioButton—Classic input control consisting of a mutually exclusive series of circular
buttons.
• Checkbox—Controls which are also normally arranged in a group but can be activated
and deactivated independently of each other.
• Dropdown—A dropdown menu containing a series of options only one of which can be
selected by the user. Since the options are only displayed when the user clicks on the
control, it provides an ideal method of offering a large range of options in a small space.
Self-validating combobox controls
We also have comboboxes with built-in validation—each of which is equivalent to one of the
specialized text boxes we saw earlier. Thus we have angleCombobox, integerCombobox,
percentCombobox and realCombobox. A combobox is a combination of a dropdown and a
text box; so the user can either choose a value from the dropdown or enter a value in the text
box.
In the case of the angleCombobox and percentCombobox controls, a degree symbol and
percentage sign are automatically displayed after the numbers in both the dropdown and the
text box.
As well as validating numbers entered by the user, these controls also validate the data that
you attempt to display in the dropdown. Any inappropriate values will lead to a runtime error.
(We will discuss techniques for populating dropdowns shortly.)
Button controls
There are no button controls at your disposal when creating dialog boxes: an OK and Cancel
button are automatically added to each dialog. However, you do have the option of suppressing
the Cancel button when necessary.
Referring to controls
After the user has clicked OK, you will want to check the values entered by the user. This
means that you need to have a way of referring to each control. There are two methods
available: firstly, you can place every control into a variable as you create it, as we have been
doing with other InDesign objects—for example:
var dlg = app.dialogs.add();
Alternatively, you can assign the control a name when you create it—for example:

67
app.dialogs.add({name:"docSetup"});
The variable route is usually less verbose: you simple use the name of the variable to invoke
the control. If you name the control, you would refer to it thereafter by using the name as an
index—for example:
app.dialogs.item("docSetup")

Displaying a dialog
Dialogs are a property of the application object, so the code for creating a dialog is simply:
app.dialogs.add(). The basic code for displaying a dialog and capturing the result is something
along the following lines:
Listing 4-1: Displaying a dialog
1 var dlg_blank = app.dialogs.add({name:"An empty dialog"});
2 var bln_result = dlg_blank.show();
3 alert(bln_result);
4 dlg_blank.destroy();
The resulting dialog is shown in figure 4-1, below.

Figure 4-1: A blank dialog with no contents

On line 1 of listing 4-1, we create the dialog and, at the same time, place it inside a variable:
this will enable us to refer to the dialog later. Note that when you create any dialog object, you
have the opportunity of setting some of its properties at the same time. This is done via the
creation properties object: the properties are entered as a series of name-value pairs inside
braces, between the parentheses following the add() method. Thus, on line 1 of listing 4-1, we
set the name of the dialog being created: this will appear in its title bar.
On line 2, we display the dialog and capture the value of the button clicked by the user, in the
variable bln_result. If the user clicks the OK button, the value true is returned; if they click
Cancel, false is returned.
On line 4, we use the destroy() method to remove the dialog from memory.
Creating static labels and text boxes
Let's look now at placing some content inside our dialog to start interacting with the user. The
first element in any dialog layout is obligatory: a dialogColumn. Inside this, you can place
further dialogColumn objects—to obtain a columar layout, or dialogRows—to obtain a single
column with multiple rows. To divide the dialog into clearly demarcated zones—each
surrounded by a visible border, use borderPanel objects. DialogColumns and dialogRows can

68
both be contained inside a borderPanel... So, as you can see, there is a fair amount of
flexibility in how these objects can all be combined.
A basic dialog layout, containing staticLabel and textEditbox objects, is shown in figure 4-2,
below: we have labels on the left and controls on the right. Each label/control pair is
contained within a dialogRow element.

Figure 4-2: A dialog box containing staticText and textEditbox controls, each inside a dialogRow

The code for creating this basic layout is shown listing 4-2, below.
Listing 4-2: Creating a dialog with staticText and textEditbox controls
1 var dlg_details = app.dialogs.add({name:"Operator Details", canCancel:false});
2 var dlc_details = dlg_details.dialogColumns.add();
3 var dlr_name = dlc_details.dialogRows.add();
4 dlr_name.staticTexts.add({staticLabel:"Name:", minWidth:100});
5 var txt_name = dlr_name.textEditboxes.add({minWidth:100});
6 var dlr_num = dlc_details.dialogRows.add();
7 dlr_num.staticTexts.add({staticLabel:"PLNX Number:", minWidth:100});
8 var txt_PLNX = dlr_num.textEditboxes.add({minWidth:100})
9 dlg_details.show();
10 var str_name = txt_name.editContents;
11 var str_PLNX = txt_PLNX.editContents;
12 var str_Output = "Details received:- \rName: " + str_name;
13 str_Output += "\rPLNX Number: " + str_PLNX;
14 alert(str_Output);
15 dlg_details.destroy();

Placing dialog objects inside variables


Having created dialog elements, you will often need to refer to them again during your code.
One of the simplest methods of doing this is through the use of object variables. When the
object is created, a reference to it is placed inside a variable. To refer to that object in the
future, we simply use the variable name.
If you will not need to refer to a control in you code after you have created it, no benefit is
derived from placing it in a variable. Thus, on lines 4 and 7, when we create staticText
controls, no reference to the objects being created is placed in any variable.
In contrast, we need to refer to the dialog box itself and the two editTextbox controls after they
have been created. So we place them inside variables as we create them, on lines 1, 5 and 8,
respectively. Later, on lines 10 and 11, when we want to capture the contents of the two text
boxes, we simply use the variable names followed by .editContents (lines 14 and 15).

69
The canCancel property
On line 1 of listing 4-2, when we create the dialog, we define two properties: name and
canCancel—which we set to false. The canCancel property determines whether the dialog
has a Cancel button; setting it to false means that the dialog will have only an OK button.
When the dialog box only has an OK button, there is no real need to capture the result of the
dialog in a variable, since the only value we could capture would be true. Hence, on line 9,
we simply say: dlg_details.show(), as opposed to:
var bln_result = dlg_details.show();

The minWidth property


The size of a control is normally dictated by its contents: a static text control displaying 12
characters will occupy more space than one containing 6 characters. The minWidth property
offers a simple method of aligning elements. Thus, in listing 4-2, each of the elements created
is given a minWidth value of 100 (pixels). This applies both to the staticText controls in the
left column and the textEditbox controls in the right column.
Dropdown controls
A drop-down control affords us a very efficient way of offering the user a series of choices.
The control occupies a single line; but, when the user clicks on it, the dropdown expands to
reveal the choices. The two main things you need to do, when creating this element, are to
create an array containing the choices you wish to display to the user and to specify the default
value which will be displayed when the dropdown loads.
The stringList property
To set the list of choices associated with the dropdown, if the list of items is short, you can use
the following syntax:
ddlExample.stringList = ["Portrait", “Landscape"];
If there are lots of choices, it is more efficient to create an array variable, populate it with the
necessary list of items and then attach the array to the dropdown.
arrExample = [];
// Populate array
ddlExample.stringList = arrExample;
The selectedIndex property
The selectedIndex property determines which item within the stringList associated with a
dropdown is currently selected. It is a read/write property which has a zero based index: thus,
setting the selectedIndex property to 0 will cause the first item in the stringList to be displayed
in the dropdown.
ddlExample.stringList = ["Portrait", “Landscape"];
ddlExample.selectedIndex = 0;
The selectedIndex property is purely numeric: the dropdown object does not have a value
property corresponding to the text it is currently displaying. However, you can target this value

70
by combining the stringList and selectedIndex properties—for example:
ddlExample.stringList[ddlExample.selectedIndex]
In listing 4-3, below, we create a dialog which contains a single dropdown control, with the
standard OK and Cancel buttons. The dropdown displays the names of two templates: “2
column newsletter” and “3 column newsletter"—as well as an additional option
“Unspecified", which is the default value displayed when the dialog loads, as shown in figure
4-3.

Figure 4-3: A dialog box containing a single dropdown control. It is useful to give dropdowns a neutral
default value which reminds the user to make a choice.

The idea is that the user can either choose the name of a template and click OK or click the
Cancel button to abort the operation. It they leave the default value “Unspecified” in place and
click OK, an error message is displayed reminding them to choose a template.

Figure 4-4: The error message displayed when the user clicks OK without first choosing a template.

Listing 4-3: Creating a dropdown control


1 //Create dialog
2 var dlgTemplate = app.dialogs.add({name:"Choose a template"});
3 var dlcTemplate = dlgTemplate.dialogColumns.add();
4 var ddlTemplate = dlcTemplate.dropdowns.add({minWidth:100});
5 ddlTemplate.stringList = ["Unspecified", "2 column newsletter", "3 column newsletter"];
6 ddlTemplate.selectedIndex = 0;
7 // Display dialog
8 var blnResult;
9 var strChoice;
10 do{
11 alert("Please choose a template");
12 blnResult = dlgTemplate.show();
13 strChoice = ddlTemplate.stringList[ddlTemplate.selectedIndex];
14 }
15 while(strChoice =="Unspecified" && blnResult == true)
16 // Evaluate result

71
17 if(blnResult == false){
18 alert("Operation cancelled by user.");
19 }
20 else{
21 alert("The " + strChoice + " template will now be created.");
22 }
23 // Destroy dialog
24 dlgTemplate.destroy();
On line 4 of listing 4-3, we set the stringList property of the dropdown (ddlTemplate) and on
line 5, we set its default selection to zero—the first item: “Unspecified".
The boolean variable blnResult is used to check which button is clicked by the user—true for
OK and false for Cancel, while strChoice is used to store the template chosen by the user.
Since both of these variables are tested right at the top of the do...while loop, they must first be
declared—testing the value of an undeclared variable generates an error.
On line 15, the condition which controls the do...while loop tests to see whether the user has
left the default “Unspecified” selected on the dropdown and clicked the OK button. If they
have, the statements inside the loop execute again (the statements in a do...while loop always
execute at least once).
Inside the loop—on line 11, we display our “Please choose a template” message. Naturally, if
the user doesn't change the value chosen on the dropdown and clicks OK, the test on line 15
will continue to be true and the dialog will pop up again.
Once the user has interacted with our dialog in a sensible manner, on line 17, we test the value
of blnResult to see which button they have clicked. If blnResult is false—in which case the
Cancel button was clicked, we display “Operation cancelled by user": if blnResult is true
—OK button clicked—we display “Template X will now be created".
At this point, the dialog has served its purpose and can be destroyed (line 24). However, the
choice made by the user is still preserved in the variable strChoice and can be later used to
enable us to create a document using the appropriate template.
Creating radiobutton and checkbox controls
Radiobutton and checkbox controls both allow the user to make choices. However, radio
buttons behave as a mutually exclusive group: if you activate one member of the group, all
other members are automatically deactivated. This distinction is reflected in the way in which
radiobutton and checkbox controls are created: radiobuton controls are always children of a
radiobuttonGroup object; checkbox controls are independent of each other.
Radiobutton controls
To create a radiobuttonControl object, you must first create a radiobuttonGroup object. For
example, you might say:
var rbg_example = dlc_example.radiobuttonGroups.add();
Inside the radiobuttonGroup control, you can then create as many radiobutton controls as
required; and, because they are children of the same radiobuttonGroup, they will automatically
behave as a mutually exclusive group. For example:

72
var rbc_example1 = rbg_example.radiobuttonControls.add();
var rbc_example2 = rbg_example.radiobuttonControls.add();
var rbc_example3 = rbg_example.radiobuttonControls.add();
RadiobutttonControl objects have a staticLabel property which is used to automatically
position text next to the button. They also have a checkedState property which takes a boolean
value: if this is set to true, the control will be activated by default when the dialog loads.
To verify which radiobutton has been selected by the user, simply examine the selectedButton
property of the parent radiobuttonGroup—a zero based numeric index. If you want to pick up
the staticLabel associated with the selected button, you can say:
rbg_example.radiobuttonControls[rbg_example.selectedButton].staticLabel
In listing 4-4, we have a dialog offering choices relating to the creation of an ad. The dialog
contains a radiobuttonGroup containing two radiobuttonControls labelled “Full page” (the
default choice) and “Half page". We also have a series of checkboxes controlling some of the
items which can be included in the ad: they are all activated by default. The resulting dialog is
shown in figure 4-5, below.

Figure 4-5: A dialog box containing radiobuttonControls and checkboxControls.

Listing 4-4: Radiobutton and checkbox controls


1 var dlgAd = app.dialogs.add({name:"Ad settings"});
2 var dlcAd = dlgAd.dialogColumns.add();
3
4 // Ad size section
5 var bpnAd = dlcAd.borderPanels.add();
6 bpnAd.staticTexts.add({staticLabel:"Size:"});
7 // Size radiobuttonGroup
8 var rbgSize = bpnAd.radiobuttonGroups.add();
9 var rbcFullPage = rbgSize.radiobuttonControls.add({staticLabel:"Full page"});
10 var rbcHalfPage = rbgSize.radiobuttonControls.add({staticLabel:"Half page"});
11 rbcFullPage.checkedState = true;
12
13 //Ad options section
14 var bpnAd = dlcAd.borderPanels.add();
15 bpnAd.staticTexts.add({staticLabel:"Include:"});
16 // Checkboxes
17 var chkLogo = bpnAd.checkboxControls.add({staticLabel:"Logo", checkedState:true});
18 var chkWebsite= bpnAd.checkboxControls.add({staticLabel:"Website", checkedState:true});
19 var chkEmail = bpnAd.checkboxControls.add({staticLabel:"Email", checkedState:true});
20 var chkAddress = bpnAd.checkboxControls.add({staticLabel:"Address", checkedState:true});
21

73
22 // Display dialog and capture user choices
23 blnResult = dlgAd.show();
24 if (blnResult == false){
25 alert("Operation cancelled by user.");
26 }
27 else{
28 var strSize = rbgSize.radiobuttonControls[rbgSize.selectedButton].staticLabel;
29 var blnLogo = chkLogo.checkedState;
30 var blnWebsite = chkWebsite.checkedState;
31 var blnEmail = chkEmail.checkedState;
32 var blnAddress = chkAddress.checkedState;
33 }
34 dlgAd.destroy();
The dialog uses two borderPanels to separate the controls into two logical groups with a static
label as the first item in each (lines 6 and 15).
On lines 8 to 11, we create a radiobuttonGroup and add two radiobuttonControls to it, setting
the checkedState of the first—rbc_fullPage—to true, thus making it the default (line 11).
On lines 17 to 20, we create four checkboxes and set the checkedState of each of them to true
—this time within the creation property record. This means that, by default, all of these items
will be included in our ad.
Finally, we display the dialog and check to see which button the user clicks. If they click
Cancel, we display “Operation cancelled by user" (line 25); otherwise, we capture the static
label of the radio button selected by the user (line 28), as well as the checkedState of each of
the checkboxes (lines 29 to 32).

Try it for yourself!


TUTORIAL: Using self-validating controls
The dialog object offers three flavours of self-validating control: integer, degree and
measurement; and, furthermore, each type is available as an editbox or combobox. The benefit
of these controls is that they behave like their InDesign equivalents and automatically verify
the type of data being entered. The integer type will only accept whole numbers; the degree
type will only accept real numbers and will also display a degree symbol next to the figure
entered; while the measurement type will accept either integers or a combination of an integer
and a measurement abbreviation recognized by InDesign, such as “mm” or “in”.
In this tutorial, we will create a script which allows the user to add a customized watermark to
every master page in the current document, on a new layer below all existing layers.

74
The script will display a dialog box containing three input controls. Firstly, there is a
textEditbox into which they enter the text of the watermark—which must be a maximum of 15
characters. Secondly, we have an angleComboBox control from which they can specify the
angle of the text. There are several preset options available in the combo box and the default
angle is 45 degrees. The user can either choose a value from the list or enter a new one.

As you might expect, programmatically, the comboBox control has some properties of the
dropdown control and some of the angleEditbox control. Thus, it has a stringList property
which requires an array of values; but it does not use a selectedIndex property to reveal the
currently selected item. Instead, it has an editContent property, like a textEditbox control.
The third control will be a regular combo box which displays a list of all the fonts currently
available on the user's system and allows them to set the font used for the watermark text.

When the user makes her choices and clicks OK, the script creates the watermark in a large

75
font and centres it on the page.
1. The main function
To begin this exercise, you will need an InDesign document to play around with—a blank new
document will suffice. In these tutorials, we will build a script section by section, testing the
code as we go. (The completed version of the script can always be found in the folder of the
current chapter and will have a name ending “_completed". If you have difficulty entering the
code manually—or just want to speed things up—feel free to copy the code from the
completed version rather than typing it. Just look inside the “chapter04” folder and open “05-
self-validating-controls_completed”.)
In the ESTK, choose File > New JavaScript.
Choose File > Save, navigate to the “chapter04” folder and save
the file there, with the name “05-self-validating-controls.jsx”.
Although this is a short script and could easily be written without creating any functions,
scripts have a habit of expanding; and having a coherent structure in place from the outset is
therefore useful.
Enter the following code.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 blnResult = createDialog();
7 // if(blnResult){addWatermark();}
8}
On line 1, we declare our global object variable (g). On line 2, we call the main function
which will execute all our code. Then, on line 3, we destroy all the data in the global variable.
In this short script, this will consist of the three pieces of information entered by the user via
the dialog: the text, the angle and the font.
Inside the main function, we call the two functions into which the script will be divided:
createDialog() and addWatermark(). We have temporarily commented out the
addWatermark() function, so that we can test the code as we go.
2. Building the dialog
2a. The createDialog() function shell

Let's begin by creating the shell of the createDialog() function.


Add the following code to the end of your script.
9
10 function createDialog(){

76
11 var dlgWatermark = app.dialogs.add({name:"Add watermark"});
12 var dlcWatermark = dlgWatermark.dialogColumns.add();
13
14 // Watermark text
15
16 // Angle combobox
17
18 // Fonts dropdown
19
20 // Display dialog and process results
21
22 }

Test your code from the InDesign Scripts panel or the ESTK,
setting InDesign as the target application. It should display the
following dialog.

On line 11, we create the dialog, setting the name property—the text which will appear in the
title bar—to “Add watermark". Next, inside the dialog, we create a dialogColumn which will
act as the container for the three controls we will need.
Back in the ESTK, choose File > Save .
2b. The watermark text editTextbox

Now let's add the statictext and editTextbox controls which will
enable users to enter the text they want to use as a watermark.
Position the cursor in the blank line after the //Watermark text
comment and enter the following code.
14 // Watermark text
15 var dlrWatermark = dlcWatermark.dialogRows.add();
16 dlrWatermark.staticTexts.add({staticLabel:"Text (max 15 letters):", minWidth:150});
17 var txtWatermark = dlrWatermark.textEditboxes.add({minWidth:200, editContents:"Confidential"});
18
19 // Angle combobox
20

77
21 // Fonts dropdown
22
23 // Display dialog and process results
24
25 }
If we placed the staticText and editTextbox controls directly inside the dialogColumn, they
would end up one above the other. Since we want them to have a horizontal orientation, on line
15, we create a dialogRow control in which we then place both the staticText and editTextbox
controls.
On line 16, when we create the staticText control, we use the creation properties object to set
the text and minimum width. Since we will not need to retrieve any user input from this
control, we simply create it, without placing a reference to it in a variable.
By contrast, on line 17, when we create the textEditbox control, we simultaneously place it
inside a variable called txtWatermark. We use the creation properties object to set both the
minimum width of the control and the default text which will appear inside it—the word
“Confidential".
If you run the code now, your dialog should resemble the one
shown below.

Save your changes.


2c. Angle comboBox
Now we need a similar setup for the comboBox control which will enable the user to either
choose a preset value or enter a different one. Again we will need a static text field to the left
of the user input; so we will place both controls in a dialogRow element.
Enter the following code below the //Angle combobox comment.
19 // Angle combobox
20 var dlrWatermark = dlcWatermark.dialogRows.add();
21 dlrWatermark.staticTexts.add({staticLabel:"Angle of rotation:", minWidth:150});
22 var txtAngle = dlrWatermark.angleComboboxes.add({minWidth:100});
23 txtAngle.stringList = ["0", "45", "-45", "90", "-90"];
24 txtAngle.editContents = "45";
25
26 // Fonts dropdown
27
28 // Display dialog and process results
29
30 }

78
We create an angleComboBox control then set its stringList property to contain a choice of
several angles (line 23). We also use the editContent property to set the default angle to 45
degrees (line 24).
Run the code from the ESTK, setting InDesign as the target
application and try entering a text value—like “Forty-five”. Each
time you attempt to click OK, an error message will be
automatically displayed, thanks to the validation built into the
control, which forces the user to enter numeric values.

Save your changes.


2d. Fonts dropdown
Our final control is the most sophisticated of the three: it needs to display a list of all the fonts
available on the user's machine. In the InDesign object model, the fonts collection is a member
of both the application and document objects. However, the fonts in the document object are
simply those which have been used in that particular file. So, naturally, to offer the user a
decent choice, we need to extract a list of the fonts in the application object. Font names are
stored in the following format:
Trajan Pro -> Regular
Trajan Pro -> Bold
with the name of the font separated from the style by a tab. To make our list of fonts more user
friendly, we will replace the tab character with a space.
Insert the following code after the //Fonts dropdown comment.
26 // Fonts dropdown
27 var dlrWatermark = dlcWatermark.dialogRows.add();
28 dlrWatermark.staticTexts.add({staticLabel:"Font:", minWidth:150});
29 var arrFonts = app.fonts.everyItem().name;
30 var ddlFonts = dlrWatermark.dropdowns.add({minWidth:100, stringList:arrFonts});
31
32 // Display dialog and process results
33
34 }
On line 29, we create an array of font names—arrFonts—by using the statement
app.fonts.everyItem().name. The everyItem() method can be used with most InDesign object
collections and creates a reference which treats the whole collection as a single entity. We can

79
therefore address the name property of this single entity; but, because it is really a whole
collection of objects, it returns an array of names rather than a single item.
On line 30, when we create the dropdown control, we set the stringList property to the
arrFonts array that we have just created.
Test your code and look at the list of fonts produced.
If you are using a Windows machine, you will see a strange-looking character—a small square
—between the name of each font and the font style.

On a Mac, you will just see a large gap.

This is the tab character I mentioned.


Let's modify the code so that we can offer the user a less untidy-
looking list of fonts.
Insert the following lines of code after line 29—var arrFonts =
app.fonts.everyItem().name;—as shown below.
29 var arrFonts = app.fonts.everyItem().name;
30 var arrFontsApply = app.fonts.everyItem().name;
31 for(var i = 0; i < arrFonts.length; i ++){
32 arrFonts[i] = arrFonts[i].replace("\t", " ");
33 }
34 var ddlFonts = dlrWatermark.dropdowns.add({minWidth:100, stringList:arrFonts});
35
36 // Display dialog and process results
37
38 }
On line 30, we create a second array variable called arrFontsApply which will be used later
when we want to apply the font selected by the user to the watermark text. This means that the
original arrFonts can now be modified and made more user-friendly.
On lines 32, inside the for loop, we use the replace() function to change the tab character in
each font name to a space.

80
Test your code again and click on the fonts dropdown. This time,
there should be a space in place of the little square or large gap—
much more user-friendly and aesthetically pleasing.

Save your changes.

SYNTAX myArray = app.fonts.everyItem().name


SUMMARY ... InDesign object collection method
Quick and easy way of generating an array of all the font names on the system running the script.

2e. Validating user input


Now that we have all of our controls in place, we can think about validating the data entered
by the user. The angle of rotation is not a problem, since this is automatically validated by
virtue of being entered via an angleCombobox control. Also, if the user doesn't choose a font,
we can simply use the default font. So, all we really need to do is to ensure that the user does
not leave the watermark text field blank. We can do this with a do ... while loop.
Add the following lines below the // Display dialog and process
results comment.
36 // Display dialog and process results
37 do{
38 var blnResult = dlgWatermark.show();
39 }while(txtWatermark.editContents == "")
40
41 if(blnResult == true){
42 g.watermark = txtWatermark.editContents.substring(0, 15);
43 g.rotation = parseInt(txtAngle.editContents);
44 g.font = arrFontsApply[ddlFonts.selectedIndex];
45 dlgWatermark.destroy();
46 }
47 else{
48 alert("Operation cancelled.");
49 }
50 return blnResult;
51 }

81
Save your changes.
On line 37-39, we place the display of the dialog inside a do ... while loop. The while
statement—while(txtWatermark.editContents == "")—ensures that the user has to enter
some text into the txtWatermark textEditbox before they can dismiss the dialog with the OK
button.
On line 41, we test whether the user has clicked the OK button by checking the value of
blnResult. If the OK button has been clicked, we capture the values in each of the input
controls into variables and then remove the dialog from memory. (We will then move on to
adding the watermark to the master pages.)
On line 42, we use the substring() method of the JavaScript String object to place the first 15
characters of the text entered by the user in the textEditbox into the global variable, as the
property g.watermark.
On line 43, we place the value entered or chosen by the user into the global variable as
g.rotation. Since the value will contain the degree symbol, it will be a string. We therefore
use the parseInt() function—which is built into JavaScript—to convert it into an integer value.

SYNTAX parseInt (input string[, number system])


SUMMARY ... Global JavaScript function
Converts a string to its integer equivalent.

Input string The string to be converted.


Number Optional. The number system to use: 10 for decimal (the default); 16 for hexadecimal,
system etc.

On line 44, we use the selectedIndex property of the fonts dropdown to retrieve the
corresponding font name from the array variable arrFontsApply. (If you remember, arrFonts
has the user-friendly font names with the tab converted into a space, while arrFontsApply has
the raw font names retrieved with the statement app.fonts.everyItem().name.) We place it
inside g.font and will use this name later when we format the watermark text on each master
page.
That completes the display of the dialog and the capturing of the data entered by the user. Let
us now move on to the business of adding the watermark text to each of the master pages in the
document.
3. Adding the watermark text to the master pages
3a. Creating the “watermark” layer

Our first task is to create a layer called “watermark” and to make it

82
the bottom layer, so that the text will be displayed behind
everything else.
Add the following function skeleton at the end of your script.
52
53 function addWatermark(){
54 var doc = app.activeDocument;
55
56 // Delete layer if exists
57
58 // Create layer
59
60 for(var i =0; i < doc.masterSpreads.length; i++){
61 for(j=0; j < doc.masterSpreads[i].pages.length; j ++){
62 // Create and position text box
63
64 // Format text
65
66 }
67 }
68 }

On line 54, place the active document in a local variable called doc. We then have comments
for the various operations we will be performing.
On lines 60, we use the length property of the masterSpreads collection of the active document
to limit the number of iterations in our outer loop—using the counter i. Each master spread will
have either one or two pages; so, on line 61, we use the length property of the pages collection
of each masterSpread object to control the inner loop—using the counter j.
Before we create the new layer, let's attempt to delete it, in case the
user has run the script before and the layer already exists. Add the
following lines after the comment // Delete layer if exists.
56 // Delete layer if exists
57 try{
58 doc.layers.itemByName("watermark").remove();
59 }
60 catch(err){}
61
62 // Create layer
Since this layer may or may not exist, on lines 57 to 60, we attempt to delete it inside a try ...
catch block. This means that, if it exists, it will be deleted; if it does not exist, line 58 will not
generate an error.
Now that we know that no layer called “watermark” exists in the
83
active document, we can create it. Add the following code after the
// Create layer comment.
62 // Create layer
63 var layWatermark = doc.layers.add({name:"watermark"});
64 layWatermark.move(LocationOptions.atEnd);
On line 63, we can safely create—or recreate—the watermark layer. Then, on line 64, we
move the layer to the end—meaning the bottom of the pile. (LocationOptions.atBeginning
means the top layer and LocationOptions.atEnd means the bottom layer.)
Test your code and then open your Layers panel in InDesign. You
should see a new layer called “watermark” below all the others.

Run the code a second time, just to ensure that you do not get any
errors. It should look as though nothing has happened; but in
reality, the old “watermark” layer will have been deleted and a new
one created.
Save your changes.

SYNTAX myLayer.move (where to[, reference])


SUMMARY Layer object method
... Moves a layer to the specified location.

Enumeration specifying new location:


LocationOptions.before
LocationOptions.after
Where to
LocationOptions.atEnd
LocationOptions.atBeginning
LocationOptions.unknown
The layer referred to when the where to parameter is set to LocationOptions.before or
Reference
LocationOptions.after.

Within the inner loop, we will now create and position a text frame bearing the watermark text

84
entered by the user, and then format the text.
3c. Creating the watermark text frame
When we create the text frame, we will centre it on the page and vertically centre the text
inside it. We will then rotate it to the angle specified by the user.
Enter the following code inside the inner for loop, after the
comment // Create and position text box.
65
66 for(var i =0; i < doc.masterSpreads.length; i++){
67 for(j=0; j < doc.masterSpreads[i].pages.length; j ++){
68 // Create and position text box
69 var userPref = doc.viewPreferences.rulerOrigin;
70 doc.viewPreferences.rulerOrigin = RulerOrigin.pageOrigin;
71 var txfWatermark = doc.masterSpreads[i].pages[j].textFrames.add(layWatermark, {contents:g.watermark});
72 var intOffset = doc.documentPreferences.pageHeight/10;
73 var y1 = (doc.documentPreferences.pageHeight/2)-intOffset;
74 var x1 = 0;
75 var y2 = (doc.documentPreferences.pageHeight/2)+intOffset;
76 var x2 = doc.documentPreferences.pageWidth;
77 txfWatermark.geometricBounds=[y1, x1, y2, x2];
78 txfWatermark.textFramePreferences.verticalJustification = VerticalJustification.centerAlign;
79 app.activeWindow.transformReferencePoint = AnchorPoint.centerAnchor;
80 txfWatermark.rotationAngle = g.rotation;
81 doc.viewPreferences.rulerOrigin = userPref;
82
83 // Format text
84
85 }
86 }
87 }

Save your changes.


On line 70, we set the rulerOrigin to pageOrigin which makes it easier to express page
coordinates—it means that left and right pages can be treated the same. This is equivalent to
going into the Units & Increments section of the InDesign preferences and choosing Page
from the Origin dropdown.

However, to avoid upsetting our users, we first capture the existing settings in a variable

85
called userPref (line 69). After we have finished position the text box, we restore the original
settings (line 81).
On line 71, when we create the text frame, we use the optional first parameter—layer—to
specify that it should be placed on layWatermark—the layer we have just created. We also
use the creation properties object to specify the contents of the text frame—i.e. the text inside
it.

SYNTAX container.textFrames.add([layer, at, reference, creation properties])


SUMMARY TextFrames object collection method
... Creates a text frame on the specified layer.

Layer Optional. The layer on which the text frame should be placed. If omitted, the active layer is used.
Enumeration specifying new location relative to the reference object:
LocationOptions.before
LocationOptions.after
At within its container:
LocationOptions.atEnd
LocationOptions.atBeginning
LocationOptions.unknown
Required if the at parameter is set to LocationOptions.before or LocationOptions.after. (Usually
Reference
another textFrame object but can also be pageItem, layer, page or document.)
Creation
Optional. The attributes which the text frame will possess when it appears.
properties

On line 72, set the variable intOffset to a tenth of the page width. We then calculate the four
coordinates which are used with the geometricBounds property of a text frame: y1, x1, y2 and
x2. Basically we are creating a text frame which has the same width as the page, a height
which is one fifth of the page height and is centred on the page.

SYNTAX textFrame.geometricBounds = [y1, x1, y2, x2]


SUMMARY ... TextFrames object collection method
Creates a text frame on the specified layer.

y1, x1, y2, x2 Position of top, left, bottom and right of text frame.

On line 78, we set the vertical justification to center—the equivalent of clicking on Object >
Text Frame Options and then choosing Center from the Vertical Justification dropdown.

86
SYNTAX textFrame.textFramePreferences.verticalJustification = enumeration
SUMMARY ... TextFrame preference object property
Sets the vertical position of the contents of a text frame.

VerticalJustification.TOP_ALIGN
VerticalJustification.CENTER_ALIGN
Enumeration
VerticalJustification.BOTTOM_ALIGN
VerticalJustification.JUSTIFY_ALIGN

On line 79, before rotating the text frame, we do the scripting equivalent of clicking on the
centre reference point on the left of the InDesign Control panel.

SYNTAX app.activeWindow.transformReferencePoint = enumeration


SUMMARY ... Layout window object property
Sets the reference point to be used when page items are transformed.

AnchorPoint.BOTTOM_CENTER_ANCHOR
AnchorPoint.BOTTOM_LEFT_ANCHOR
AnchorPoint.BOTTOM_RIGHT_ANCHOR
AnchorPoint.CENTER_ANCHOR
Enumeration AnchorPoint.LEFT_CENTER_ANCHOR
AnchorPoint.RIGHT_CENTER_ANCHOR
AnchorPoint.TOP_CENTER_ANCHOR
AnchorPoint.TOP_LEFT_ANCHOR
AnchorPoint.TOP_RIGHT_ANCHOR

3d. Formatting the watermark text


Formatting the text inside the frame is pretty straightforward. We know that it will contain a

87
single paragraph of text; so we can simply target the required formatting attributes of this
paragraph object.
Enter the following code inside the inner for loop, after the
comment // Format text.
83 // Format text
84 var paraWatermark = txfWatermark.paragraphs[0];
85 if(g.font != undefined){
86 paraWatermark.appliedFont = app.fonts.itemByName(g.font);
87 }
88 paraWatermark.pointSize = "60 pt";
89 paraWatermark.fillColor = doc.colors.itemByName("Paper");
90 paraWatermark.strokeColor = doc.colors.itemByName("Black");
91 paraWatermark.justification =Justification.CENTER_JUSTIFIED;
92 }
93 }
94 }

Save your changes.


On line 84, to save ourselves repeating txfWatermark.paragraphs[0], we place this
paragraph object into a variable called paraWatermark which now points to the first (and
only) paragraph in the text frame txfWatermark.
On line 85, we check to make sure that the value of the font dropdown is not undefined—which
would indicate that the user did not make a choice—before using g.font as the index which
specifies the appliedFont property of the paragraph (line 86). If you remember, the global
variable property g.font contains the name of the font specified by the user via the dropdown
in the dialog—thanks to line 44 of our script:

44 g.font = arrFontsApply[ddlFonts.selectedIndex];
It is usually a good idea to set the font in a try ... catch block, in case the font is not present;
but, here, the user has just made a selection from a list of the fonts currently on his system; so
we can assume it is still present.
Finally, on lines 88 to 91, we set the pointSize, fillColor, strokeColor and justification
properties of the paragraph.

SYNTAX textObject.justification = enumeration


SUMMARY ... Text object (paragraph, line, word, character, etc.) property
Sets the horizontal justification of paragraph containing the text object.

Justification.LEFT_ALIGN
Justification.CENTER_ALIGN
Justification.RIGHT_ALIGN
Justification.LEFT_JUSTIFIED

88
Enumeration Justification.RIGHT_JUSTIFIED
Justification.CENTER_JUSTIFIED
Justification.FULLY_JUSTIFIED
Justification.TO_BINDING_SIDE
Justification.AWAY_FROM_BINDING_SIDE

Run the code again, enter some text, choose a font and click OK.
The watermark should appear on all pages in the active document
which are based on a master.

That completes this Try it for yourself! tutorial.

The dialog object allows you to create basic user interfaces for capturing and validating
information. In the next chapter, we will move on to look at creating dialog windows and
controls using ScriptUI, an ExtendScript component which allows you to build more
sophisticated user interfaces with a greater degree of interactivity.
CHAPTER 5. ScriptUI Dialogs
Dialog boxes are quick and easy to create and are excellent for capturing simple information
from the user. The self-validating controls are particularly useful. However, they offer only a
basic level of interactivity: there can never be any interaction between the various controls

89
within the dialog itself. For example, if one control changes, you couldn't have another control
within the dialog appear, disappear or become enabled or disabled.
Fortunately, InDesign offers a second way of building dialogs: ScriptUI—a powerful
component included with ExtendScript which enables the creation of sophisticated, interactive
user interfaces. Although its sophistication means that it is a little more difficult to master than
the dialog objects we encountered in the last chapter, it is definitively worth the effort.
ScriptUI allows you to create dialog boxes which have a similar functionality to the forms
which you can create for websites. This is mainly becaause ScriptUI controls share one
important ability with web form controls: they can both respond to events.
The key to mastering ScriptUI dialogs is to understand the hierarchy of objects required to
build a particular user interface and then to translate this into the equivalent hierarchy of
ScriptUI objects.
The Window object
The root container for ScriptUI objects is the window object, which serves the same purpose
as the dialog object we encountered in chapter 4. There are two variants: modal (dialog) and
modeless (window or palette). A modal window is a dialog box which demands the user's
attention and prevents any interaction with the InDesign application. The modeless window is
basically a floating palette which coexists with the InDesign interface and allows the user to
interact with other elements.
Creating a window
In ExtendScript, windows are created with the new Window() constructor method; for
example:
var win = new Window('dialog','Simple dialog box');
win.show();
The new Window() method creates the window in memory. The show() method is then used to
actually display it. The small, empty dialog produced by this code is shown in figure 5-1,
below.

Figure 5-1: An empty ScriptUI window

SYNTAX myVariable = new Window(type[, title, bounds, creation properties])


SUMMARY Window object constructor method
... Creates a new ScriptUI window.

The only obligatory argument, which can take one of three values:

90
Dialog—a modal window which prevents any other interaction with InDesign until it is dismissed.
Type Window—a modeless dialog window which can be moved out of the way while the user continues
to interact with InDesign.
Palette—also modeless and, in InDesign, pretty much the same as window.
Title Optional. The text which will appear in the top left of the window.
Optional. An array of four coordinates, in pixels, which can be used to size and position the dialog,
Bounds in the order: [left, top, right, bottom]. Since the eventual size of the window will be automatically
determined by the controls inside it, you can usually omit this argument.
Creation Optional. Provides a way of setting several creation properties—properties which can only be set
properties while the window is being created.

Container objects and the add() method


Inside the main window object, there are three types of container available for organizing and
grouping the various controls: tabbed panels, panels and groups.
Script UI controls are essentially a hierarchy of objects within objects and can be created and
referenced using dot syntax. With the exception of the window control—which is at the top of
the hierarchy, each new control that you create must go inside a container. This is achieved by
using the add() method of the item which is to be the container of the new control.

SYNTAX container.add(type[, bounds, text, creation properties])


SUMMARY ScriptUI container object method
... Creates a new ScriptUI control inside the container object.

Type This is the only obligatory argument and specifies the type of control to be created.
Optional. An array of four coordinates, in pixels, which can be used to size and position the control
within the coordinates of its container, in the order: left, top, right, and bottom. You will probably
Bounds find it easier to omit this argument and set the minimumSize property of the control later. This
allows you to specify only the width and height of the object while preventing such things as
truncation of text—as implied by the “minimum” in the name of the property.
Optional. The default text which you would like to appear in the control—if applicable. Not all
Text
controls display text: for example, panels do but groups don't.
Optional. Object literal containing a set of control property settings. This may not seem quite as
neat a feature as it should be, since the properties which can be set vary from control to control.
Creation
However, creation properties are very useful, since they allow you to set properties which cannot
properties
be set subsequently. These will be discussed later when we examine each of the main ScriptUI
controls in detail.

Tabbed panels
Tabbed panels allow you to organize related controls into separate overlapping areas which
are accessed by clicking on a particular tab. (To justify its existence, a tabbed panel must
contain at least two tabs.) Tabbed panels add clarity to a complex interface by separating

91
related controls into their own isolated zones, only one of which can be visible at any one
time.
To create a tabbed panel, containing three tabs, inside our basic window—which is being held
in a variable called win—we could modify our code thus:
Listing 5-1: Creating a dialog with a tabbed panel
1 var win = new Window('dialog', 'Simple dialog box');
2 win.tpn = win.add('tabbedpanel');
3 win.tpn.tabOperator = win.tpn.add('tab', undefined, 'Operator');
4 win.tpn.tabJob = win.tpn.add('tab', undefined, 'Job Details');
5 win.tpn.tabClient = win.tpn.add('tab', undefined, 'Client');
6 var intResult = win.show();
This produces the dialog shown in figure 5-2, below. To dismiss the dialog on a Mac, press
the Escape key in the top left of your keyboard.

Figure 5-2: A window containing a tabbed panel with three tabs

Panels
Within a window, or within each tab of a tabbed panel, panels can be used to create logical
divisions among the controls within a dialog. A panel provides a logical divider with a border
and an optional title in which related controls can be placed. Its role is similar to that of the
<fieldset> element used as container for related elements within an HTML form. Thus, instead
of the three tabs shown in figure 5-1—which would only be used if we have a decent number
of controls in each tab, we could simply create three panels, as shown in figure 5-3, below.

92
Figure 5-3: A window containing three panels

The code is shown in listing 5-2.


Listing 5-2: Creating a dialog with three panels
1 var win = new Window('dialog', 'Simple dialog box');
2 win.pnlOperator = win.add('panel', undefined, 'Operator');
3 win.pnlJob = win.add('panel', undefined, 'Job Details');
4 win.pnlClient = win.add('panel', undefined, 'Client');
5 var intResult = win.show();

In the above illustration, you will notice that, by default, panels are horizontally centred within
their container and that they appear in a column—one below the other. Both of these defaults
can be overridden by setting the alignChildren and orientation properties of the appropriate
container, respectively.
Orientation
The orientation property of a container determines whether its child objects repeat in a row or
column. It can take one of three values: row, column or stack. Row and column are self-
explanatory: stack causes the children to overlap—on top of each other—and is useful when
creating interactive controls which show and hide other controls. (You will see a practical
example of the use of the stacked orientation in one of the tutorials in chapter 7, starting on
page 153.)
In listing 5-3, below, we override the default columnar orientation by setting the orientation

93
property of the window object which contains the panels to row (line 2). The resulting dialog
can be seen in figure 5-4.
Listing 5-3: Creating a row of panels overriding the default columnar orientation
1 var win = new Window('dialog', 'Simple dialog box');
2 win.orientation = 'row';
3 win.pnlOperator = win.add('panel', undefined, 'Operator');
4 win.pnlJob = win.add('panel', undefined, 'Job Details');
5 win.pnlClient = win.add('panel', undefined, 'Client');
6 var intResult = win.show();

Figure 5-4: Setting the orientation of the window object to “row” causes the panels inside it to repeat
in a row instead of a column

SYNTAX container.orientation = string value


SUMMARY ... ScriptUI container object property
Determines whether elements within the container are arranged in rows or columns.

String value Permitted values are: row, column, or stack (overlapping).

AlignChildren
In a similar way, we can change how our panels align within the window by setting the
alignChildren property. In listing 5-4, we change the default centre alignment by setting the
alignChildren property to fill, a useful option, which makes all three panels the same width, by
causing them to fill their container—a bit like setting the width of a table or DIV to 100%, in
HTML. The resulting dialog is shown in figure 5-5.
Listing 5-4: Setting the alignment of the panels to “fill”
7 var win = new Window('dialog', 'Simple dialog box');
8 win.alignChildren = 'fill';
9 win.pnlOperator = win.add('panel', undefined, 'Operator');
10 win.pnlJob = win.add('panel', undefined, 'Job Details');
11 win.pnlClient = win.add('panel', undefined, 'Client');
12 var intResult = win.show();

94
Figure 5-5: Setting the alignChildren of the window object to “fill” causes the panels inside it to
become the same width.

SYNTAX container.alignChildren = string value


SUMMARY ScriptUI container object property
... Determines the alignment of elements within their container.

String If orientation is set to row, permitted values are: top, bottom, center and fill. For columnar orientation,
value permitted values are: left, right, center and fill. For stacked: top, bottom, left, right, center and fill.

Groups
Groups are like panels with two key features removed: they cannot have a visible border and
the cannot display a heading. They have two key uses: firstly, they are used to align related
controls in a row or column—using the same orientation property we encountered just a
moment ago; and, secondly, they can be used to organise panels (or other groups) into rows or
columns.
Aligning controls with groups
A classic use of groups is to act as a container for a data control (one via which the user will
input data) along with its associated label. In listing 5-5, inside the first of our panels

95
(win.pnlOperator), we have used the add() method to create two groups and placed a
statictext control and an edittext control inside the first; and statictext and dropdownlist
controls in the second.
Listing 5-5: Using groups as containers for label/control pairs
1 var win = new Window('dialog', 'Simple dialog box');
2 win.alignChildren = 'left';
3
4 // Operator panel (Contains two groups)
5 win.pnlOp = win.add('panel', undefined, 'Operator');
6 // Name Group
7 win.pnlOp.grpName = win.pnlOp.add('group');
8 win.pnlOp.grpName.orientation = 'row';
9 win.pnlOp.grpName.stx = win.pnlOp.grpName.add('statictext', undefined, 'Name:');
10 win.pnlOp.grpName.stx.minimumSize = [60, 20];
11 win.pnlOp.grpName.txt = win.pnlOp.grpName.add('edittext');
12 win.pnlOp.grpName.txt.minimumSize = [100, 20];
13
14 // Branch Group
15 win.pnlOp.grpBranch = win.pnlOp.add('group');
16 win.pnlOp.grpBranch.orientation = 'row';
17 win.pnlOp.grpBranch.stx = win.pnlOp.grpBranch.add('statictext', undefined,'Branch:');
18 win.pnlOp.grpBranch.stx.minimumSize = [60, 20];
19 var arrBranch = ['Glasgow', 'Leeds', 'London', 'Manchester'];
20 win.pnlOp.grpBranch.ddl = win.pnlOp.grpBranch.add('dropdownlist', undefined, arrBranch);
21 win.pnlOp.grpBranch.ddl.minimumSize = [100, 20];
22
23 // Job panel (Empty)
24 win.pnlJob = win.add('panel', undefined, 'Job Details');
25
26 // Client panel (Empty)
27 win.pnlClient = win.add('panel', undefined, 'Client');
28
29 // Show dialog
30 var intResult = win.show();

The resulting dialog is shown in figure 5-6.

96
Figure 5-6: Using groups to lay out data controls. Group 1 contains statictext and edittext controls and
group 2 a statictext and a dropdownlist.

When creating a group control, only one parameter is usually needed for the add() method of
its container: the type of element being created—namely, ‘group'. Thus, in line 5 of listing 5-5,
we have:
win.pnlOp = win.add('panel', undefined, 'Operator');
The same is true of edittext controls—line 11:
win.pnlOp.grpName.txt = win.pnlOp.grpName.add('edittext');
Since statictext controls are used to output text rather than for user input, they need the third
parameter (the text which will appear inside the control). If the second parameter is being
omitted, it must be replaced with the keyword "undefined". Thus, on line 9, we have:
win.pnlOp.grpName.stx = win.pnlOp.grpName.add('statictext', undefined, 'Name:');
When creating dropdownlists, we use the third (items) parameter to supply a list of the textual
items which will appear in the control. Thus, on line 19, we define an array called arrBranch;
then, on line 20, we use it as the items parameter of the add() method.
var arrBranch = ['Glasgow', 'Leeds', 'London', 'Manchester'];
win.pnlOp.grpBranch.ddl = win.pnlOp.grpBranch.add('dropdownlist', undefined, arrBranch);
It is interesting to note that it is only while the control is being created that the items property
of a dropdownlist control can be set. After the control has been created, this property becomes
read-only. Thus, if we tried to use the following code:
win.pnlOp.grpBranch.ddl = win.pnlOp.grpBranch.add('dropdownlist');
win.pnlOp.grpBranch.ddl.items = arrBranch;
we would get an ExtendScript error telling us: “items is read only”.
Coding styles for ScriptUI
Note the way in which dot syntax is used to create object inside object inside object. This
leads to code which is verbose but clear. An alternative method is to place elements in
variables as they are created. For example, instead of:
win.pnlOperator = win.add('panel', undefined, ‘Operator');
we could simply say:
var pnlOperator = win.add('panel', undefined, ‘Operator');
This techniques leads to much shorter lines of code; but the relationship of elements to one
another is not emphasized quite as well as with the other technique. Both techniques are useful

97
and you will encounter a mixture of the two in the scripts within this book.
Placing containers inside containers
The other main use of groups is to organize panels (or other groups) into rows and columns.
ScriptUI allows you to use groups and panels to group controls in any way you see fit. The
relationship between the various containers is illustrated in figure 5-8, below.

Figure 5-8: ScriptUI containers can be used in a very flexible manner.

For example, if we had four panels and we wanted to organise them into two rows and two
columns, we could do the following:
• Leave the orientation property of the window set to its default value “column"
• Place the first two panels inside a group and leave its orientation set to the default of
“row"
• Place the third and fourth panels inside a second group which also has the default
orientation property of “row".
An example is shown in listing 5-6, below.
Listing 5-6: Using groups to control the layout of panels
1 var win = new Window('dialog', 'Simple dialog box');
2 win.alignChildren = 'left';
3 win.grp1 = win.add('group');
4 win.grp1.pnlOperator = win.grp1.add('panel', undefined, 'Operator');
5 win.grp1.pnlJob = win.grp1.add('panel', undefined, 'Job Details');
6 win.grp2 = win.add('group');
7 win.grp2.pnlClient = win.grp2.add('panel', undefined, 'Client');
8 win.grp2.pnlResources = win.grp2.add('panel', undefined, 'Resources');
9 var intResult = win.show();
The resulting dialog is shown in figure 5-7.

98
Figure 5-7: Using groups to arrange panels in rows and columns.

Text Controls
ScriptUI offers two controls for displaying and inputting text: StaticText for text output and
EditText for input.
StaticText
The StaticText control is used to display text within the dialog window. It's most frequent role
is to provide labels for other controls. It can also be used to display instructions for users. The
most important property of the StaticText control is therefore the text property: this is
normally set via the third parameter of the add() method which is used to create the control.
We have encountered a couple of examples of its use, such as:
win.pnlOp.grpBranch.stx = win.pnlOp.grpBranch.add('statictext', undefined,'Branch:');
win.pnlOp.grpBranch.stx.minimumSize = [60, 20];
Here, we are setting the text to be displayed within the control to “Branch:". The second
parameter is bounds which allows you to specify the size and position of the control. This is
left undefined and on the following line, we use the more flexible minimumSize property
which will be automatically overridden by the ScriptUI Layout Manager, if necessary, to
prevent characters being truncated.
StaticText creation properties
The StaticText control allows you to set a couple of useful properties via the creation
properties parameter—the optional fourth parameter of the add() method.

SYNTAX
StaticText creation properties
SUMMARY
...
When set to true, causes text to wordwrap within the StaticText control. (The default is false.) This
Multiline
is useful when adding instructions to a dialog.
Allows you to go even further down the road of text-intensive dialogs. When this property is set to

99
Scrolling true, a vertical scrollbar becomes available for scrolling the text. This can be useful when displaying
very detailed instructions or help screens. (Again, the default is false.)

Using the multiline and scrolling properties


Figure 5-9 shows an example of using the multiline and scrolling properties. Users are invited
to read the terms and conditions of using the application. They can then click either the Agree
or Disagree button. The code that produces the dialog shown in listing 5-7.

Figure 5-9: Setting the multiline and scrolling properties of a StaticText control to true allows you to
present screens containing large amount of text.

Listing 5-7: Using the statictext multiline and scrolling properties


1 var strConditions = "Cienisque pressin totaquidenda nonsequiatam eaque non pelloris \
2 dolorit aturepra pa que digendelest hario ipsunt volorio quo vel illacea dolupta \
3 ecepro quod es quunt es invel ipsum cus ipsapelitio quo officiendel iur site noneces \
4 dellupt iberchitati debistr umquis exero blab il idi doluptam quis sundi berisqui \
5 dolorem olupienimint aliqui con re... "
6
7 var win = new Window('dialog', 'Terms and Conditions');
8 win.alignChildren = 'left';
9
10 win.grpMain = win.add('group', undefined, 'Terms and conditions');

100
11 win.grpMain.orientation = 'column';
12 win.grpMain.stxConditions = win.grpMain.add('statictext', undefined, strConditions, {multiline:true, scrolling:true});
13
14 win.grpMain.btnAgree = win.grpMain.add('button', undefined, 'Agree');
15 win.grpMain.btnDisagree = win.grpMain.add('button', undefined, 'Disagree');
16
17 win.defaultElement = win.grpMain.btnAgree;
18 win.cancelElement = win.grpMain.btnDisagree;
19 var intResult = win.show();
On lines 1 to 5, we place a four line string into a variable called strConditions. (The
backslash character is used to cause the JavaScript interpreter to ignore line endings.) On line
12, when we create the StaticText control, we use strConditions as the third (text) parameter
of the add() method. We also include the fourth (creation properties) parameter, in which we
set the multiline and scrolling properties to true.
On lines 17 and 18, the defaultElement and cancelElement properties of the window object
are used to specify which of our buttons will behave like an OK button and which like a
Cancel button. This is explained in detail in the section on button controls later in this chapter.
EditText
The EditText control is the input equivalent of the StaticText control.
Creating a default value
When it is being created, the third (text) parameter of the add() method can be used if you wish
to place a default entry in the text field when the dialog appears. For example, in the dialog
shown in figure 5-10, the current date has been automatically inserted into the “Start Date”
EditText field.

Figure 5-10: Parameter 3 of the add() method allows you to set a default value inside an EditText
control

101
The code that creates this functionality is shown in listing 5-8, below.
Listing 5-8: Inserting a default value into an EditText field
1 // Construct today's date (UK format)
2 var datToday = new Date();
3 var strToday = datToday.getDate() + "/" + (datToday.getMonth() +1) + "/"+ datToday.getFullYear();
4
5 // Construct window
6 var win = new Window('dialog', 'Enter Job Details');
7
8 // Job Number
9 win.grpJob = win.add('group');
10 win.grpJob.orientation = 'row';
11 win.grpJob.stx = win.grpJob.add('statictext', undefined, 'Job Number:');
12 win.grpJob.stx.minimumSize = [75, 20];
13 win.grpJob.txt = win.grpJob.add('edittext');
14 win.grpJob.txt.minimumSize = [100, 20];
15
16 // Start Date
17 win.grpDate = win.add('group');
18 win.grpDate.orientation = 'row';
19 win.grpDate.stx = win.grpDate.add('statictext', undefined, 'Start Date:');
20 win.grpDate.stx.minimumSize = [75, 20];
21 win.grpDate.txt = win.grpDate.add('edittext', undefined, strToday);
22 win.grpDate.txt.minimumSize = [100, 20];
23

102
24 // Buttons
25 win.grpButtons = win.add('group');
26 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
27 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
28
29 // Display window
30 var intResult = win.show();
On line 2, we place a copy of the Date object into a variable called datToday: this is
automatically set to the current date. On line 3, we create a variable called strToday into
which we extract the date (day), month and year components from our Date object,
concatenating the necessary “/” separators. (The getMonth() function returns a number
between 0 and 11, rather than 1 and 12; hence the need to say (datToday.getMonth() +1).)
On line 21, when we create our date EditText field, we use the strToday variable as the third
(text) parameter of the add() method, thus making it the control's default value.
Since the text property is not a creation property, it can of course be set after the EditText
field has been created. Thus, we could also use:
win.grpDate.txt = win.grpDate.add('edittext');
win.grpDate.txt.text = strToday;
EditText creation properties
The editText control has two creation properties: multiline and noEcho.

SYNTAX
EditText creation properties
SUMMARY
...
Like its sibling StaticText, the EditText control has a multiline property which causes text entered
into the field—either by the user or as default text—to wordwrap inside the control. Unlike
Multiline
StaticText controls, EditText fields do not have a scrolling property: scrollbars are automatically
created and become enabled as soon as the control is filled with text.
The noecho creation property of the EditText control determines whether text entered into the
NoEcho control is visible or masked. By default, this property is set to false: setting it to true creates a field
ideal for entering a password or other sensitive information.

The dialog box shown in figure 5-11 illustrates these two creation properties. Firstly, the
multiline property of the Job Description field has been set to true and, secondly, the password
field has had its noecho property set to true.

103
Figure 5-11: Using the multiline and noecho properties of the EditText control

The code behind the dialog is shown in listing 5-9, below.


Listing 5-9: Creating multiline and password EditText fields
1 // Construct window
2 var win = new Window('dialog', 'Enter Job Details');
3 win.alignChildren = 'left';
4
5 // Job Number
6 win.grpJob = win.add('group');
7 win.grpJob.orientation = 'row';
8 win.grpJob.stx = win.grpJob.add('statictext', undefined, 'Job Number:');
9 win.grpJob.stx.minimumSize = [100, 20];
10 win.grpJob.txt = win.grpJob.add('edittext');
11 win.grpJob.txt.minimumSize = [120, 20];
12
13 // Job Desc
14 win.grpDesc = win.add('group');
15 win.grpDesc.orientation = 'row';
16 win.grpDesc.alignChildren = 'top';
17 win.grpDesc.stx = win.grpDesc.add('statictext', undefined, 'Job Description:');
18 win.grpDesc.stx.minimumSize = [100, 20];
19 win.grpDesc.txt = win.grpDesc.add('edittext', undefined, undefined, {multiline:true});

104
20 win.grpDesc.txt.minimumSize = [120, 60];
21
22 // User Name
23 win.grpUser = win.add('group');
24 win.grpUser.orientation = 'row';
25 win.grpUser.stx = win.grpUser.add('statictext', undefined, 'User Name:');
26 win.grpUser.stx.minimumSize = [100, 20];
27 win.grpUser.txt = win.grpUser.add('edittext');
28 win.grpUser.txt.minimumSize = [120, 20];
29
30 // Password
31 win.grpPw = win.add('group');
32 win.grpPw.orientation = 'row';
33 win.grpPw.stx = win.grpPw.add('statictext', undefined, 'Password:');
34 win.grpPw.stx.minimumSize = [100, 20];
35 win.grpPw.txt = win.grpPw.add('edittext', undefined, undefined, {noecho:true});
36 win.grpPw.txt.minimumSize = [120, 20];
37
38 // Buttons
39 win.grpButtons = win.add('group');
40 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
41 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
42
43 // Display window
44 var intResult = win.show();
On line 19, when the Job Description EditText control is created, it's multiline creation
property is set to true. On line 20, it is given a minimum height of 60 pixels to enable the
display of several lines of text within the control.
Note also, on line 16, that the group which contains the Job Description field has had its
alignChildren property set to “top", so that the StaticText label aligns with the top of the
multiline field next to it, overriding the default middle vertical alignment.
As for the password field; on line 35, when it is being created, the noecho creation property is
simply set to true: {noecho: true}.
List Controls
List controls are basically a visual representation of an array of items. ScriptUI offers three
controls of this type: drop down list, list box and tree view. The Drop down list control
displays a single item and, when clicked, reveals a scrolling list from which the user can make
a single choice. The List box control displays multiple values in a scrollable list from which
the user may select one or more items. The tree view control is a hierarchical version of the
list box, in which each element can be either an item or a node. Node elements can have child
items—or child nodes—which can be displayed and hidden with intelligent controls which are
automatically displayed next to each node.
Drop down list
The Drop down list is probably the most frequently used of the ScriptUI list controls and is
equivalent to the HTML <select> element. It offers just two creation properties: name—a
unique name which can later be used to identify the control; and items—an array containing the

105
list of items to be displayed in the control. (Since this achieves the same result as using the
third parameter of the add() method, which we saw earlier, it is not usually implemented.)
After creation, the items in the list can be modified using the control's add(), remove() and
removeAll() methods. These are particularly useful when creating callback functions for
responding to events—for example, where the contents of a list are updated whenever the user
interacts with another control.
To illustrate the use of the items property and the add() method of the Drop down list control,
listing 5-10 creates the control by using a combination of both techniques.
The Drop down list shown in figure 5-12, below, has separators which divide the branches
according to the country in which they are located.

Figure 5-12: A Drop down list control which includes separators

Listing 5-10: Creating a Drop down list control which includes separators
1 var win = new Window('dialog', 'Operator Details');
2
3 // Name
4 win.Name = win.add('group');
5 win.Name.orientation = 'row';
6 win.Name.stxName = win.Name.add('statictext', undefined, 'Name:');
7 win.Name.stxName.minimumSize = [60, 20];
8 win.Name.txtName = win.Name.add('edittext');
9 win.Name.txtName.minimumSize = [100, 20];
10

106
11 // Branch
12 win.grpBranch = win.add('group');
13 win.grpBranch.orientation = 'row';
14 win.grpBranch.alignChildren = 'top';
15 win.grpBranch.stx = win.grpBranch.add('statictext', undefined, 'Branch:');
16 win.grpBranch.stx.minimumSize = [60, 20];
17 var arrBranch = ['Birmingham', 'Leeds', 'London', 'Manchester'];
18 win.grpBranch.ddl = win.grpBranch.add('dropdownlist', undefined, arrBranch);
19 win.grpBranch.ddl.minimumSize = [100, 20];
20
21 win.grpBranch.ddl.add('separator');
22 win.grpBranch.ddl.add('item', 'Belfast');
23 win.grpBranch.ddl.add('item', 'Dublin');
24 win.grpBranch.ddl.add('item', 'Cork');
25
26 win.grpBranch.ddl.add('separator');
27 win.grpBranch.ddl.add('item', 'Dundee');
28 win.grpBranch.ddl.add('item', 'Edinburgh');
29 win.grpBranch.ddl.add('item', 'Glasgow');
30
31 win.grpBranch.ddl.add('separator');
32 win.grpBranch.ddl.add('item', 'Cardiff');
33 win.grpBranch.ddl.add('item', 'Newport');
34 win.grpBranch.ddl.add('item', 'Swansea');
35
36 // Buttons
37 win.grpButtons = win.add('group');
38 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
39 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
40
41 // Display dialog
42 var intResult = win.show();
On line 18, when the dropdown is created, the first section of the list is created by using the
third (items) parameter of the add() method of the containing Group control, win.grpBranch.
We set the items parameter to arrBranch, an array which is created and populated on line 17.
On lines 21 to 39, we then add individual items to the list using the add() method and include
separators as required.
This technique is used for the purposes of illustration: in general, if you know the items which
need to appear in the list, use the items property; if you are populating a list dynamically, use
the add() method.

SYNTAX dropdownlist.add(type[, text])


SUMMARY Dropdownlist method
... Creates a new item within the dropdownlist control.

The type of element being added to the control: this may either be ‘item’ (i.e. text) or ‘separator', a non
Type selectable line which can be used to divide the list into sections. Separator elements can also be created
within the items creation property by including an item with the name ‘-’ (a hyphen).

107
Optional. If the type parameter is set to ‘item', this is the text which will appear within the control. (If
Text
the type parameter is set to ‘separator', the text parameter is ignored.)

Displaying images
ScriptUI offers the option of displaying an image next to each item in a list control. To
illustrate this feature, let's say we want to modify the previous example and display a small
flag next to the name of each branch to indicate the country in which it is based, as shown in
figure 15-13, below.

Figure 5-13: A drop down list control containing images

To display an image next to the text, first set the text property as normal when creating the
Drop down list control. Next, set the image property of each listItem object in the list to the
appropriate image file. The image will be displayed on the left of the text. This is illustrated in
listing 5-11, below.
Listing 5-11: Displaying images in a Drop down list control
1 var strPath = app.activeScript.parent + "/flags/";
2 var win = new Window('dialog', 'Operator Details');
3

108
4 // Name
5 win.Name = win.add('group');
6 win.Name.orientation = 'row';
7 win.Name.stxName = win.Name.add('statictext', undefined, 'Name:');
8 win.Name.stxName.minimumSize = [60, 20];
9 win.Name.txtName = win.Name.add('edittext');
10 win.Name.txtName.minimumSize = [150, 20];
11
12 // Branch
13 win.grpBranch = win.add('group');
14 win.grpBranch.orientation = 'row';
15 win.grpBranch.alignChildren = 'top';
16 win.grpBranch.stx = win.grpBranch.add('statictext', undefined, 'Branch:');
17 win.grpBranch.stx.minimumSize = [60, 20];
18 var arrBranch = ['Birmingham', 'Leeds', 'London', 'Manchester'];
19 win.grpBranch.ddl = win.grpBranch.add('dropdownlist', undefined, arrBranch);
20 win.grpBranch.ddl.minimumSize = [150, 20];
21 win.grpBranch.ddl.add('separator');
22 win.grpBranch.ddl.add('item', 'Belfast');
23 win.grpBranch.ddl.add('item', 'Dublin');
24 win.grpBranch.ddl.add('item', 'Cork');
25 win.grpBranch.ddl.add('separator');
26 win.grpBranch.ddl.add('item', 'Dundee');
27 win.grpBranch.ddl.add('item', 'Edinburgh');
28 win.grpBranch.ddl.add('item', 'Glasgow');
29 win.grpBranch.ddl.add('separator');
30 win.grpBranch.ddl.add('item', 'Cardiff');
31 win.grpBranch.ddl.add('item', 'Newport');
32 win.grpBranch.ddl.add('item', 'Swansea');
33
34 // Add flags
35 for(var i = 0; i <= 3; i++){
36 win.grpBranch.ddl.items[i].image = File(strPath + 'england.jpg');
37 }
38 win.grpBranch.ddl.items[5].image = File(strPath + 'n-ireland.jpg');
39 win.grpBranch.ddl.items[6].image = File(strPath + 'ireland.jpg');
40 win.grpBranch.ddl.items[7].image = File(strPath + 'ireland.jpg');
41 for(var i = 9; i <= 11; i++){
42 win.grpBranch.ddl.items[i].image = File(strPath + 'scotland.jpg');
43 }
44 for(var i = 13; i <= 15; i++){
45 win.grpBranch.ddl.items[i].image = File(strPath + 'wales.jpg');
46 }
47
48 // Buttons
49 win.grpButtons = win.add('group');
50 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
51 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
52
53 // Display dialog
54 var intResult = win.show();
On line 1 of listing 5-11, we set the variable strPath to app.activeScript.parent + "/flags/",
rather than setting the physical path to the “flags” folder inside the “chapter05” folder. The

109
statement app.activeScript.parent returns the path to the folder containing the script; then
adding "/flags/" implies that the "flags" folder is in the same folder as the script itself. If you
want to test a script containing the app.activeScript statement, you will need to test it from
InDesign rather than the ESTK.
On lines 35-46, we have added a series of commands which set the image property of each
item in the list to the correct flag image. Note that items are referenced using the built-in zero-
based index and that, in calculating the index number of each item, account has to be taken of
the separators—each of which counts as one item.
List box
The List box control is very similar to the Drop down list and identical in the way it is
constructed. It differs in two regards: firstly, it is capable of displaying multiple items; and,
secondly, it optionally allows the user to select more than one item by holding down Command
(Mac) or Control(Windows) and, of course, Shift on either platform.
Like the drop down list control, it allows you to specify the items which will be listed in the
control, either as the third parameter of the add() method of the parent object or as the items
parameter within the creation properties. There are also a number of other creation properties
which can be set.

SYNTAX
ListBox creation properties
SUMMARY
...
An array containing integers which specify the width of each column in a multi-column
ColumnWidths
list.
When the showHeader parameter is set to true, allows you to supply a title for each
ColumnTitles
column in a multi-column list.
Items An array containing the items to be displayed within the listbox control.
Multiselect Set this value to true to enable the user to select more than one item in the list.

Name A unique identifier which can subsequently be used to reference the object.
NumberOfColumns Listbox controls support multi-column lists.
ShowHeaders If set to true, enables the display of column headers in multi-column lists.

Figure 5-14 shows an example of a Listbox control with the multiSelect creation property set
to true. The code that generates the dialog is shown in listing 5-12.

110
Figure 5-14: A listbox control with multiSelect enabled

Listing 5-12: Creating a listbox control with multiSelect


1 var win = new Window('dialog', 'Job Details');
2 win.orientation = 'row';
3
4 // Job
5 win.grpJob = win.add('group');
6 win.grpJob.orientation = 'column';
7 win.grpJob.alignChildren = 'left';
8 win.grpJob.stx = win.grpJob.add('statictext', undefined, 'Job Description:');
9 win.grpJob.stx.minimumSize = [120, 20];
10 win.grpJob.txt = win.grpJob.add('edittext', undefined, undefined,
11 {multiline: true, scrolling: true});
12 win.grpJob.txt.minimumSize = [120, 100];
13
14 // Output
15 win.grpOutput = win.add('group');
16 win.grpOutput.orientation = 'column';
17 win.grpOutput.alignChildren = 'left';
18 win.grpOutput.alignChildren = 'top';
19 win.grpOutput.stx = win.grpOutput.add('statictext', undefined, 'Output required:');
20 win.grpOutput.stx.minimumSize = [120, 20];
21 win.grpOutput.ddl= win.grpOutput.add('listbox', undefined, undefined,
22 {multiselect: true, items:['Print', 'PDF', 'Interactive PDF', 'HTML', 'Kindle']});
23 win.grpOutput.ddl.minimumSize = [120, 100];
24
25 // Buttons
26 win.grpButtons = win.add('group');
27 win.grpButtons.orientation = 'column';
28 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
29 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
30
31 // Display dialog
32 var intResult = win.show();
On line 21 of listing 5-12, the creation properties of the add() method are used to specify that
the listbox control supports multiple selections and also the list of items which will appear
inside the control.
Treeview

111
The treeview works in a similar way to the Drop down list and list box controls. However, it
has one capability which the other two lack: any of the items placed inside a list view control
can be of the type ‘node’ and node items can act as containers for other items—including other
node items. It is this capability of one item to contain other items that gives the treeview its
expand/collapse capability.
Figure 5-15 shows an example of a treeview control containing two levels of items. The first
level (which has items of the node type) displays the country and expands to reveal the town at
level two.
An image is displayed next to the items on level one, using the same technique used in the
previous example.

Figure 5-15: A treeview control with two levels and images displayed at level one

Listing 5-13 shows how the two-tier treeview control is created.


Listing 5-13: Creating a two-level treeview control
1 var strPath = app.activeScript.parent + "/flags/";
2 var win = new Window('dialog', 'Operator Details');
3
4 // Name
5 win.Name = win.add('group');
6 win.Name.orientation = 'row';
7 win.Name.stx = win.Name.add('statictext', undefined, 'Name:');
8 win.Name.stx.minimumSize = [60, 20];
9 win.Name.txt = win.Name.add('edittext');
10 win.Name.txt.minimumSize = [250, 20];
11
12 // Branch
13 win.grpBranch = win.add('group');
14 win.grpBranch.orientation = 'row';
15 win.grpBranch.alignChildren = 'top';
16 win.grpBranch.stxBranch = win.grpBranch.add('statictext', undefined,'Branch:');

112
17 win.grpBranch.stxBranch.minimumSize = [60, 20];
18 win.grpBranch.trv = win.grpBranch.add('treeview');
19 win.grpBranch.trv.minimumSize = [250, 250];
20
21 win.grpBranch.trv.England = win.grpBranch.trv.add('node', 'England');
22 win.grpBranch.trv.England.image = File(strPath + 'england.jpg')
23 win.grpBranch.trv.England.add('item', 'Birmingham');
24 win.grpBranch.trv.England.add('item', 'Leeds');
25 win.grpBranch.trv.England.add('item', 'London');
26 win.grpBranch.trv.England.add('item', 'Manchester');
27
28 win.grpBranch.trv.Ireland = win.grpBranch.trv.add('node', 'Ireland');
29 win.grpBranch.trv.Ireland.image = File(strPath + 'ireland.jpg')
30 win.grpBranch.trv.Ireland.add('item', 'Belfast');
31 win.grpBranch.trv.Ireland.add('item', 'Dublin');
32 win.grpBranch.trv.Ireland.add('item', 'Cork');
33
34 win.grpBranch.trv.Scotland = win.grpBranch.trv.add('node', 'Scotland');
35 win.grpBranch.trv.Scotland.image = File(strPath + 'scotland.jpg')
36 win.grpBranch.trv.Scotland.add('item', 'Dundee');
37 win.grpBranch.trv.Scotland.add('item', 'Edinburgh');
38 win.grpBranch.trv.Scotland.add('item', 'Glasgow');
39
40 win.grpBranch.trv.Wales = win.grpBranch.trv.add('node', 'Wales');
41 win.grpBranch.trv.Wales.image = File(strPath + 'wales.jpg')
42 win.grpBranch.trv.Wales.add('item', 'Cardiff');
43 win.grpBranch.trv.Wales.add('item', 'Newport');
44 win.grpBranch.trv.Wales.add('item', 'Swansea');
45
46 // Buttons
47 win.grpButtons = win.add('group');
48 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');
49 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
50
51 // Display dialog
52 var intResult = win.show();
On line 18 of listing 5-13, the treeview control is created and given the name
win.grpBranch.trv. When each of the top level items is created (lines 21, 28, 34 and 40), the
first parameter of the add() method—the type property—is set to node, making it an
expand/collapse container. After creating each node, we set its image property to the
appropriate flag image.
We then use the add() method once more to attach town items to each of the nodes, this time
setting the type property to item, since the elements at the second level of the control will not
be acting as containers (lines 23-26, 30-32, 36-38 and 42-44).
Buttons
Buttons are an essential component in any dialog and are typically used to submit or dismiss
the dialog. Although button controls are not generated for you automatically by the ScriptUI
engine, it does provide a couple of nifty features for automatically setting the behaviour of the
buttons you create.

113
Creating Cancel and OK buttons
The show() method which is used to display dialogs returns an integer value into the variable
which references it. This is why, it is more useful to display a dialog with the line:
var intResult = win.show();
rather than simply:
win.show();
We are able to check the value returned into intResult to determine which button was clicked
and thus ascertain the user's intentions. By default, clicking the OK button causes the show()
method to return the number 1 while clicking Cancel generates 2.
There are two ways to automatically turn a button into either an OK or Cancel button: by
setting the cancelElement and defaultElement properties of the parent window; or by setting
the name creation property of the button control to either ‘OK’ or ‘Cancel'.
In the dialog shown in figure 5-16, we want the button marked “Continue with operation” to act
as the OK button while the one marked “Cancel operation” is to be the Cancel button. Listing
5-14 shows how this is accomplished using the cancelElement and defaultElement
properties of the window object. Listing 5-15 creates the same functionality by using the name
creation property.

Figure 5-16: Creating OK and Cancel buttons

Listing 5-14: Using cancelElement and defaultElement


1 var win = new Window('dialog', 'Cancel and OK buttons');
2
3 // Buttons
4 win.stxMessage = win.add('statictext', undefined, 'What would you like to do?');
5 win.btnOK = win.add('button', undefined, 'Continue with operation');
6 win.btnCancel = win.add('button', undefined, 'Cancel Operation');
7 win.defaultElement = win.btnOK;
8 win.cancelElement = win.btnCancel;
9
10 // Display dialog
11 var intResult = win.show();
12 (intResult === 1)? alert('Continuing operation'): alert('Operation cancelled');
On lines 5 and 6 of listing 5-14, the two buttons are created with nothing to specify their
intended roles. However, on lines 7 and 8, by setting the defaultElement and cancelElement
properties, we specify which button performs which function.

114
On line 12, we use the JavaScript ternary operator to check which button has been clicked: if
the win.show() command returned 1 into the variable intResult, then we know that the user
clicked the OK button.

SYNTAX condition ? result1: result2


SUMMARY ... JavaScript ternary operator
Shorthand version of if/else statement.

condition Any logical test.


Result1 The statement to perform if the condition proves true.
Result2 The statement to perform if the condition proves false.

Listing 5-15: Creating OK and Cancel buttons using the name creation property
1 var win = new Window('dialog', 'Cancel and OK buttons');
2
3 // Buttons
4 win.stxMessage = win.add('statictext', undefined, 'What would you like to do?');
5 win.OK = win.add('button', undefined, 'Continue with operation', {name:'OK'});
6 win.Cancel = win.add('button', undefined, 'Cancel Operation', {name:'Cancel'});
7
8 // Display dialog
9 var intResult = win.show();
10 (intResult === 1)? alert('Continuing operation'): alert('Operation cancelled');
In listing 5-15, we have amended the lines where the two buttons are created (lines 5 and 6) to
include the name creation property. Simply by naming one button ‘OK’ and the other ‘Cancel',
we are able specify which button is which and they automatically behave accordingly.
Events
In a similar manner to controls on web forms, ScriptUI controls respond to events triggered by
user interaction. Each time the user interacts with the controls in a window, an event takes
place: clicking a button, changing the text in an EditText field, selecting a value in a drop down
list, closing a window, etc. Your scripts can respond to such events by including callback
functions—event handlers which define the code that will execute when a given event occurs.
Button events
Among the various controls, buttons are the most obvious candidates for callback functions—
responding to the onClick event. These functions provide an ideal place for code which
validates and processes the data entered into the various controls by the user.
In the simple example shown in figure 5-17, we have created callback functions for the OK
and Cancel buttons of the dialog.

115
Figure 5-17: Using a callback function to force the user to complete required fields

The code is shown in listing 5-16.


• If the user clicks the OK button without entering a name or choosing a branch, an error
message is displayed and the window remains open.
• If the OK button is clicked and both fields have been completed, the data entered in the
two fields is recorded, the window closes and the message “Data received...” is
displayed.
• If the Cancel button is clicked, the window closes and the message “Operation
cancelled by user” is displayed.
Listing 5-16: Adding onClick callbacks to buttons
11 var win = new Window('dialog', 'Operator Details');
12
13 // Name
14 win.grpName = win.add('group');
15 win.grpName.orientation = 'row';
16 win.grpName.stx = win.grpName.add('statictext', undefined, 'Name:');
17 win.grpName.stx.minimumSize = [60, 20];
18 win.grpName.txt = win.grpName.add('edittext');
19 win.grpName.txt.minimumSize = [100, 20];
20
21 // Branch
22 win.grpBranch = win.add('group');
23 win.grpBranch.orientation = 'row';
24 win.grpBranch.alignChildren = 'top';
25 win.grpBranch.stx = win.grpBranch.add('statictext', undefined, 'Branch:');
26 win.grpBranch.stx.minimumSize = [60, 20];
27 var arrBranch = ['Birmingham', 'Leeds', 'London', 'Manchester'];
28 win.grpBranch.ddl = win.grpBranch.add('dropdownlist', undefined, arrBranch);
29 win.grpBranch.ddl.minimumSize = [100, 20];
30
31 // Buttons
32 win.grpButtons = win.add('group');
33 win.grpButtons.btnOK = win.grpButtons.add('button', undefined, 'OK');

116
34 win.grpButtons.btnCancel = win.grpButtons.add('button', undefined, 'Cancel');
35
36 // OK button callback
37 win.grpButtons.btnOK.onClick = function(){
38 if(win.grpName.txt.text == "" || win.grpBranch.ddl.selection == null){
39 alert("Please complete the name and branch fields.");
40 }
41 else{
42 var strName = win.grpName.txt.text;
43 var strBranch = win.grpBranch.ddl.selection.text;
44 win.close(1);
45 alert("Data received:\r Name: " + strName + "\r Branch: " + strBranch);
46 }
47 }
48
49 // Cancel button callback
50 win.grpButtons.btnCancel.onClick = function(){
51 win.close(2);
52 alert("Operation cancelled by user.");
53 }
54
55 // Display dialog
56 var intResult = win.show();
On line 38 of listing 5-16, inside the onClick callback of the OK button, we check to see
whether either the name or branch fields are still blank. (Since the name field is an EditText
control, it has a text property. The branch is a Drop down list and its equivalent is the
selection property which returns null if no item has been selected.)
If either field is blank, we display an error message (line 39); otherwise, we place the values
entered into the two fields into the variables strName and strBranch. We then close the
window (line 44), returning a value of 1 to identify the button clicked as being the OK button
—using the useful parameter provided by the close() method. Finally, we display an alert
containing the values held in strName and strBranch (line 45).

SYNTAX window.close(return value)


SUMMARY Window object method
... Closes the dialog window and optionally returns a value.

Return An integer to be returned to the variable to which the window.show() method was assigned. The
value number 1 normally indicates that the OK button was clicked and 2 that Cancel was clicked.

This chapter may not yet have convinced you that ScriptUI dialogs offer a superior
environment for communicating with the user than the dialog object. However, most of the
scripts we create from now on will involve the use of ScriptUI windows; so you will get
plenty of practice on this topic throughout the book.
Now that we know how to communicate with the user, we can start creating scripts which

117
manipulate InDesign objects; and what better place to start than with InDesign documents.
CHAPTER 6. Working with Files and Documents
About files and documents
There are two main ways in which InDesign documents are viewed: as files and as documents.
Basically, when a document is open, it is treated as a document object and all the properties
and methods of the object will apply to it. However, as soon as it is closed, it ceases to be a
document and becomes a plain old file. So, from the InDesign point of view, a closed InDesign
file is only a potential document: as soon as it is opened, a document object is generated which
can be stored in a variable and manipulated in the normal way.
Creating a new document
It follows that, in order to open a document, a reference has to be made to a file somewhere on
the file system; whereas, when creating a new document, there is no need to refer to a file,
since all new documents exist only in memory until saved. It is, however, always a good idea
to place a reference to the new document in a variable. Thus, rather than simply saying:
app.documents.add();
it is better to say something like:
var docNew = app.documents.add();
The variable docNew would then contain a reference to the new document and would inherit
all of the properties and methods of the document object.
Opening a document
By contrast, in order to open an InDesign file, we need to refer to the file object from which
the document object will be created. For example:
var docOpen = app.open(File("/c/training/chapter06/open.indd"));
or
var docOpen = app.open(File("/users/admin/desktop/training/chapter06/open.indd"));

Letting the user choose a file


As well as opening a document stored at a known location, you can also let the user select a
file, using the openDialog() method of the File object.
var docOpen = app.open(File.openDialog("Please select a file"));
This displays the standard dialog supplied by the operating system for opening a file.

118
Figure 6-1: The openDialog() method of the File object can be used to display a dialog inviting the
user to choose one or more files to be opened.

In the above example, the file argument of the open() method of the application object is
supplied by the openDialog() method of the File object.

SYNTAX File.openDialog(prompt[, filter, multiselect])


SUMMARY File class method
...
Displays a dialog allowing the user to choose one more files.

Prompt The prompt which will be displayed as the title of the dialog.
Optional. A string specifying the file type(s) to be opened (Windows) or a function that specifies
Filter
the files to be returned, based on the criteria you specify (Mac only).
Optional. If set to true, the user is able to select more than one file to be opened. (The default is
Multiselect
false.)

Using filter expressions (Windows only)


The filter parameter can be used to limit the files displayed in the Open dialog to those of a
certain file type. Thus to display only InDesign documents, on Windows, we could use:
var docOpen = app.open(File.openDialog("Please select a file", “InDesign Documents:*.indd"));

Figure 6-2: On Windows, the filter parameter of the openDialog() method can be used to force the
user's system to display only files of a certain type.

119
The second parameter, InDesign Documents:*.indd, displays a pop-up at the bottom of the
dialog containing only one choice: InDesign documents.
The filter parameter consists of a comma-separated list of file types. To limit the files
displayed to files of a given type, as in the above example, we put just one item in the list. The
item has two parts, separated by a colon: the text which will appear in the dropdown menu and
the file type associated with that choice. The asterisk is used as a wildcard character
—"*.indd” means any name followed by “.indd". We could also have used “*.ind*"—which
would be one way of picking up both InDesign documents (".indd") and InDesign templates
(".indt").
Here are a couple of examples of lists containing more than one file type and the drop-down
menus they would produce at the bottom of the dialog.
..."InDesign Documents:*.indd, InDesign Templates:*.indt"

Note the syntax: we have two items and they are separated by commas. It is also possible to
specify more then one file type within each item.
..."InDesign Documents:*.indd, InDesign Templates:*indt, InDesign Documents and Templates:*indd; *indt"

To specify more than one file extension for a given item, separate the file extensions with a
semi-colon.

SYNTAX Characters used in filter expressions with File.openDialog()


SUMMARY ...
Asterisk Wildcard representing one or more characters.
Colon Used to separate Menu text from associated file extension.
Comma Used as separator between multiple elements.
Semi-colon Used as separator between multiple file extensions.

Using Filter functions (Mac only)


To limit the files which can be opened using File.openDialog() on Mac, specify the name of a
function instead of using a filter expression; then define the function. Here is an example.
var docOpen = app.open(File.openDialog("Please select a file", limitFileType));
function limitFileType(currentFile){
var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));

120
if(strExt == ".indd" || strExt == ".indt"){return true;}
}

The second parameter of the openDialog() method becomes limitFileType and then we have
the definition of this function. Note the use of the parameter currentFile: this represents each
file instance being passed to the function. We then use three JavaScript String functions to
extract the file extension from the fullName of the file (the path and name) and then check to
see whether it is either ".indd" or ".indt". If it is either of these, the function returns true. (You
can find a description of the toLowerCase(), substr() and lastIndexOf() functions in chapter
2, starting on page 32.)
The result of applying this function is shown below.

Files that do not have the file extension ".indd" or ".indt" are visible but are greyed out and
cannot be selected. This includes InDesign files with no file extension—not uncommon on a
Mac. To get around this problem, it is possible to use the creator property of the File object.
This Mac-only property returns a three or four letter file-creator string which, in the case of
InDesign files is InDn. We can therefore alter our function as follows:
function limitFileType(currentFile){
var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
if(strExt == ".indd" || strExt == ".indt" || currentFile.creator == "InDn"){return true;}
}
Note that the creator property is metadata and may sometimes be lost—in which case the
creator will be ???? and not InDn; hence, it's still worth checking the file extension.

Creating a cross-platform mask

121
If you are creating a cross-platform solution and need to include masking when using
File.openDialog(), you can use the $.os function, which will return a string including either
"Macintosh" or "Windows". Here is an example of doing this.
if($.os.indexOf("Windows") > 0){
var docOpen = app.open(File.openDialog("Please select a file", “InDesign Documents:*.indd"));
}
else{
var docOpen = app.open(File.openDialog("Please select a file", limitFileType));
function limitFileType(currentFile){
var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
if(strExt == ".indd" || strExt == ".indt" || currentFile.creator == "InDn"){return true;}
}
}
We use the indexOf() function to see whether the word "Windows" is part of the string
returned by $.os; and, if it is, we use a mask expression with the openDialog() method.
Otherwise, we use a function name and then define the function.
Although we are discussing the openDialog() method in the context of opening InDesign
documents, it can be used in several different ways. The first thing to bear in mind is that it
simply returns either a single file object or an array of file objects—in other words, it doesn't
actually open any files. Thus, for example, it can be used to let the user choose one or more
text files which need to be imported into a document at a later stage. It's entirely up to you what
you actually do with the files returned by the openDialog() method. You can open them, place
them somewhere in the document, or just keep a record of them to which you can refer at some
point in the future—either in a variable or in a text file.
Allowing multiple selections
The openDialog() method can accept an optional multiSelect parameter which takes a boolean
value (true or false) and determines whether the user will be able to select more than one file
in the Open dialog box. If this parameter is set to true, then the openDialog() method returns
an array of File objects—even if the user only selects one item. If the parameter is set to false,
then a single file object is returned. In the following listing, we display a dialog in which the
user can select one or more InDesign files. We then open the file or files.
Listing 6-1: Using the OpenDialog() method
1 try{
2 if($.os.indexOf("Windows") > 0){
3 var docOpen = app.open(File.openDialog("Select file", "InDesign Documents:*.indd", true));
4 }
5 else{
6 var docOpen = app.open(File.openDialog("Select file", limitFileType, true));
7 function limitFileType(currentFile){
8 var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
9 if(strExt == ".indd" || strExt == ".indt" || currentFile.creator == "InDn"){return true;}
10 }
11 }
12 }
13 catch(errOpen){
14 alert("Error. Unable to open file.");

122
15 }
In listing 6-1, the attempt to open the document selected by the user has been enclosed in a try
... catch block. This means that, if the user presses the Cancel button—causing the
openDialog() method to return null instead of a File object—or if some other error occurs, a
user-friendly alert will be displayed.
Note also that, when the user chooses more than one file, we do not need to loop through the
array of files returned, opening each one. This is because app.open() will accept as its
primary argument either a single file or an array of files. If an array of files is supplied, it
automatically opens all of them.
The app.open() method also has two optional parameters: showingWindow and openOption.
The showing window parameter takes a boolean value and determines whether or not the
document opened is initially visible. If this parameter is omitted, the default value is of course
true. The openOption parameter takes an OpenOptions enumeration value which determines
whether the original document is opened or a copy. Thus, if we want to open copies of the
documents in the last example, we would add in the second and third parameters of the open()
method, thus:
var docOpen = app.open(File.openDialog("Please select a file", “InDesign Files:*.indd"), true,
OpenOptions.OPEN_COPY);
When InDesign opens the document, it would be an untitled copy rather than the original
document.

SYNTAX app.open(what[, show window, open options])


SUMMARY Application object method
... Opens the specified file or files.

What The InDesign document(s), book(s) or library/libraries to be opened.


Show
Optional. Specifies whether each item opened is initially visible.
window
Open Optional. Enumeration specifying whether to open the original document or a copy:
options OpenOptions.defaultValue, OpenOptions.openOriginal, OpenOptions.openCopy.

Testing whether a document is already open


If you attempt to open an InDesign document which is already open, you will not get an error:
the document will simply be activated. However, it is still useful to be able to check whether a
document you have been asked to open is already open, since the user may have forgotten that
she is working on it. To perform the check, you can use the fullPath property of the document
object. When using openDialog(), if the fullPath of any open document matches any of the files
returned by the method, then the file is already open.
Let's assume first of all that the multiSelect parameter of the openDialog() method has been

123
set to false. We need to loop through all of the currently open documents and compare their
fullPath properties with the file returned by the openDialog() method. As soon as we find a
match, we know that the file is already open; so we will exit the loop and display an alert
confirming that the file the user selected is already open.
Listing 6-2: Checking if a document is already open
1 // Obtain file path
2 if($.os.indexOf("Windows") > 0){
3 var fileChosenByUser = File.openDialog("Select file", "InDesign Documents:*.indd", true);
4}
5 else{
6 var fileChosenByUser = File.openDialog("Select file", limitFileType, true);
7 function limitFileType(currentFile){
8 var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
9 if(strExt == ".indd" || strExt == ".indt" || currentFile.creator == "InDn"){return true;}
10 }
11 }
12 if(fileChosenByUser == null){
13 alert("Error. No file selected.");
14 }
15
16 // Test whether file already open
17 var blnAlreadyOpen = false;
18 for (var i = 0; i < app.documents.length; i++){
19 if (app.documents[i].saved){
20 if (app.documents[i].fullName == String(fileChosenByUser)){
21 alert("Document " + app.documents[i].name + " is already open.");
22 blnAlreadyOpen = true;
23 break;
24 }
25 }
26 }
27
28 // Open file
29 if(blnAlreadyOpen == false){
30 try{
31 var docOpen = app.open(fileChosenByUser);
32 }
33 catch(errOpen){
34 alert("Error. Unable to open file.");
35 }
36 }
Note that, on lines 18 to 19 of listing 6-2, we check the saved property of the document before
testing whether the fullName (file path) property matches the path to the file chosen by the user
(line 9). The saved property of a document is false if it has never been saved. Such documents
do not have a fullName property. Hence, an error occurs if you test the fullName property of a
document whose saved property is false.
Testing whether an array of documents is already open
If the multiSelect parameter of the openDialog() method is set to true, we can use a nested

124
loop to allow us to compare each open document with each document selected by the user.
Listing 6-3: Checking if multiple documents are already open
1 // Read names of open documents into array
2 var arrInitiallyOpen = app.documents.everyItem().fullName;
3
4 // Obtain file path
5 if($.os.indexOf("Windows") > 0){
6 var arrFiles = File.openDialog("Select file(s)", "InDesign Documents:*.indd", true);
7}
8 else{
9 var arrFiles = File.openDialog("Select file(s)", limitFileType, true);
10 function limitFileType(currentFile){
11 var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
12 if(strExt == ".indd" || strExt == ".indt" || currentFile.creator == "InDn"){return true;}
13 }
14 }
15 if(arrFiles == null){
16 alert("Error. No files selected.");
17 }
18 else{
19 // outer loop - looping through array of files chosen by user
20 for (var h = 0; h < arrFiles.length; h ++){
21 var blnAlreadyOpen = false;
22 // inner loop - looping through names of files which were initially open
23 for (var i = 0; i < arrInitiallyOpen.length; i++){
24 if (String(arrFiles[h]) == arrInitiallyOpen[i]){
25 alert("Document " + app.documents[i].name + " is already open.");
26 blnAlreadyOpen = true;
27 }
28 }
29 if(blnAlreadyOpen == false){
30 try{
31 var docOpen = app.open(arrFiles[h]);
32 }
33 catch(errOpen){
34 alert("Error. Unable to open file " + arrFiles[h].name + ".");
35 }
36 }
37 }
38 }
On line 2 of listing 6-3, since the number of open documents will change once the script starts
running, we read the names of all of the initially open documents into the variable
arrInitiallyOpen before we do anything.
2 var arrInitiallyOpen = app.documents.everyItem().fullName;
The everyItem() method is available to most InDesign collections and returns a reference
which treats all of the objects in the collection as a single item. When we access the fullName
property of this item, we get an array containing the file path to each document currently open
in InDesign.
Then, on line 24, we use the String constructor as a function to convert each item in
arrFilesChosenByUser (the array containing the files selected by the user) into a string which

125
we then compare with each item in arrInitiallyOpen (the array of documents which were
initially open).
24 if (String(arrFiles[h]) == arrInitiallyOpen[i]){

Saving a document
The save() method is used to save a document and has two key parameters, both of which are
optional: the file path and whether to save as stationery.

SYNTAX document.save([to, stationery])


SUMMARY Document object method
... Saves a file at the location specified by the to parameter.

To Optional. The file location where the document is to be saved.


Optional. Whether or not to save the document as a template. (If left blank, the default is of
Stationery
course false.)

Testing whether a document has been saved


The saved property of the Document object lets you know whether a document has ever been
saved. If the saved property is false, the document is an untitled one which exists only in
memory and has yet to be saved. The modified property of the Document object can be
examined to test whether a document needs saving. If the value of the modified property is
true, then the document has been modified since it was last saved or—in the case of unsaved
documents—since it was created.
Saving the changes to a document
To save the changes to a document which has already been saved, simply use the save()
method without the first parameter (to).
if (docExample.saved){
docExample.save();
}
You can use the exact same statement to save a document that has never been saved and
InDesign will throw up the standard Save As dialog box for the user to enter a file name. If
you supply the to argument of the save method, no dialog will appear and the file will be
saved at the specified location—unless a file of that name already exists, in which case a
dialog will appear telling you that the file exists and asking whether you wish to replace it.
Using the saveDialog method
Like the openDialog() method we saw earlier, the saveDialog() method displays a dialog—
this time, for the user to navigate to his preferred location and enter a file name. As before, the
method returns a file object but does not actually save the file at the specified location. (As we
will see later, it can also be used when creating text files as well as when saving InDesign

126
files.)

SYNTAX File.saveDialog([prompt, filter])


SUMMARY ... File class method
Displays a dialog allowing user to choose file path and enter file name.

Prompt The text which will appear at the top of the dialog.
Optional. A string specifying the file type(s) to be opened, such as "InDesign files: *.indd".
Filter
(Windows only)

In the following example, we ask the user where they would like to save the file we are about
to create. The filter statement is used to limit the file type to InDesign files only. (This filtering
only works on Windows and is simply ignored on Macs.)
Listing 6-4: Using the saveDialog() method
1 var fileChosenByUser = File.saveDialog ("Specify file location", "InDesign Files:*.indd");
2 if (fileChosenByUser == null){
3 alert("Operation cancelled. No document created.");
4}
5
6 else{
7 var blnAlreadyOpen = false;
8 for (i = 0; i < app.documents.length; i++){
9 if (app.documents[i].saved == true){
10 if (app.documents[i].fullName == String(fileChosenByUser)){
11 alert("Can't overwrie document " + app.documents[i].name + " because it is open.");
12 blnAlreadyOpen = true;
13 }
14 }
15 }
16 if(blnAlreadyOpen == false){
17 try{
18 var docNew = app.documents.add();
19 //... SETUP DOCUMENT ...
20 docNew.save(fileChosenByUser);
21 }
22 catch(errOpen){
23 alert("Error. Unable to save file.");
24 }
25 }
26 }
If the user clicks the Cancel button, returning null, we display an error message (line 3).
Otherwise, we check to see whether the document specified by the user is currently open and,
if it is, we display another error message (line 11). If the document is not open, we create a
new document (line 18) and save it in the location specified by the user (line 20).
Closing a document
127
As you can guess, the document object also has a close() method. When it is used without any
parameters, the normal system behaviour applies: if the file has unsaved changes, the system
will prompt you to save it. If there are no unsaved changes, the document will simply close.
However, in addition, the close() method has parameters which allow you to control how the
system behaves.

SYNTAX
SUMMARY document.close([saving, saving in])
... Document object method
Closes the specified document.

Optional. Determines whether the document will be saved. The value of this parameter is one of the
following enumerations:
SaveOptions.NO—The document is closed without saving, even if there are unsaved changes.
Saving
SaveOptions.ASK—If there have been unsaved changes, the user is asked whether they should be
saved. If there are no unsaved changes, the file is closed with no prompt.
SaveOptions.YES—If there are unsaved changes, they are saved.
Saving
Optional. If SaveOptions.YES is used, specifies the location in which the file should be saved.
in

In the following code fragment, all open files are closed using different SaveOptions settings,
depending on the state of the document.
Listing 6-5: Closing all open document
1 for (var i = app.documents.length - 1; i >= 0; i--){
2 if(app.documents[i].modified == false){
3 app.documents[i].close(SaveOptions.NO);
4 }
5 else if(app.documents[i].saved == true){
6 app.documents[i].close(SaveOptions.YES);
7 }
8 else{
9 app.documents[i].close(SaveOptions.ASK);
10 }
11 }
In listing 6-5, if a document has not been modified (line 2), we close it using
SaveOptions.NO—since there is nothing to save. If a document has been modified, we do a
further check to see if it has ever been saved (line 5). If it has, we close it using
SaveOptions.YES; otherwise, we let the user decide whether to save by using
SaveOptions.ASK.
Note also that any loop which uses a numerical index and inside which removal or deletion of
items takes place, should always start with the highest value and loop towards the lowest
value (See line 1). Looping from lowest to highest will generate an error half way through the
loop when the targeted item is no longer there—having been removed or, in this case, closed.
Reading from a text file

128
The File object can be used to manipulate text files as well as InDesign documents. This can
be very useful for permanently storing data required by your scripts as well as writing to a log
file while a script is running. In addition, before placing text files in a publication, it is
sometimes useful to examine their contents.
This is accomplished with the read() method of the File object, which takes a single parameter
—the number of characters which should be read. If this optional parameter is omitted, the
entire file is read.
Before a file can be read, it must be opened using the open() method of the File object. The
open() method does not work in the same way as File > Open: the file does not open in a
document window but remains closed throughout the operation. However, its state changes
from closed to open. It takes a single parameter—mode—which specifies the condition of the
file while it is open—i.e. what can be done to it. When opening a file for reading, the mode
should be set to “r” (read).
Testing whether a file exists
Before attempting to open a file for reading or writing, it is useful to know whether it exists at
the expected location. This can be done using the File.exists property.

Figure 6-3: In listing 6-6, we use the File.exists property to test whether a file is at the expected
location then use its contents to populate a drop down list.

In listing 6-6, (“06-read-file-if-exists.jsx” in the “chapter06” folder), we check to see if the


file “settings.txt” exists at the expected location. If it does, we open it, read its contents into an
array variable and call the displayDialog() function which displays the dialog shown in figure
6-3. If the file does not exist, we display an error message.
Listing 6-6: Reading a file if it exists
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 var blnFile = readFile();
7 if (blnFile){var intResult = displayDialog();}
8 if (intResult == 1){openFile();}
9 else if (intResult == 2){alert("Operation cancelled.");}
10 else if (intResult == 3){printFile();}
11 }

129
12
13 function readFile(){
14 var filSettings = File(app.activeScript.parent + "/navigator/folder-list.txt");
15 if (filSettings.exists){
16 try{
17 filSettings.open ("r");
18 g.arrSettings = filSettings.read ().split("\n");
19 return true;
20 }
21 catch(errOpen){
22 alert("Error. The file could not be opened.");
23 return false;
24 }
25 }
26 else{
27 alert("The settings file could not be found");
28 return false;
29 }
30 }
31
32 function displayDialog(){
33 g.win = new Window('dialog', 'Navigator');
34 g.win.add('statictext', undefined, 'Choose a document then click a button');
35 // Label and dropdownlist
36 g.win.grpDoc = g.win.add('Group');
37 g.win.grpDoc.stx = g.win.grpDoc.add('StaticText', undefined, 'Document:');
38 g.win.grpDoc.stx.size=[60,15];
39 g.win.grpDoc.ddl = g.win.grpDoc.add('DropDownList', undefined, g.arrSettings);
40 g.win.grpDoc.ddl.size = [250,20];
41 g.win.grpDoc.ddl.selection =0;
42 // Buttons
43 g.win.grpButn = g.win.add('Group');
44 g.win.grpButn.Open = g.win.grpButn.add('Button', undefined, 'Open');
45 g.win.grpButn.Print = g.win.grpButn.add('Button', undefined, 'Print');
46 g.win.grpButn.Cancel = g.win.grpButn.add('Button', undefined, 'Cancel');
47 // Onclick event handlers for buttons
48 g.win.grpButn.Open.onClick = function(){g.win.close(1);}
49 g.win.grpButn.Print.onClick = function(){g.win.close(3);}
50 // Display dialog
51 return g.win.show();
52 }
53
54 function openFile(){
55 alert(app.activeScript.parent + "/" + g.win.grpDoc.ddl.selection.text);
56 try{
57 g.docOpen = app.open(File(app.activeScript.parent + "/open/" + g.win.grpDoc.ddl.selection.text));
58 return true;
59 }
60 catch(errOpen){
61 alert("Sorry, the file could not be opened.");
62 return false;
63 }
64 }
65

130
66 function printFile (){
67 if(openFile()){
68 g.docOpen.print(true);
69 g.docOpen.close(SaveOptions.no);
70 }
71 }

The file settings.txt contains a list of file names, each on a separate line, as shown in figure 6-
4, below.

open3.indd
open1.indd
open2.indd
open5.indd
open4.indd
Figure 6-4: This list of file paths uses the newline character as the separator. It can therefore be
converted into an array using the split() method of the String object with “\n” as the argument.

Inside the readFile() function , on line 15, we test whether the settings file exists and, if it
does, on line 17, we open the file in read ("r") mode then, on line 18, we use the JavaScript
split() function to convert the newline-separated list of items into an array.
15 if (filSettings.exists){
16 try{
17 filSettings.open ("r");
18 g.arrSettings = filSettings.read ().split("\n");
The split() function takes a text string and splits it into array items based on the specified
delimiter—in this case, the newline character. Each time it encounters a delimiter, it creates a
new array item.

Here's a reminder of the syntax.

SYNTAX myString.split ([delimiter, limit])


SUMMARY Method of String Object
... Creates an array by splitting a string using the specified delimiter.

The string to be used for the splitting operation. If this optional parameter is omitted, a single item
Delimiter
array containing the original text is produced.
Limit The number of items to be placed into the array. If omitted, all items are returned.

If the contents of the settings file are successfully read, the readFile() function returns true,
which triggers the displayDialog() function call—line 7. Inside the displayDialog() function,

131
on line 39, when we create the dropdownlist, we specify g.arrSettings—the array read in
from “settings.txt”—as the third (items) argument of the add() method, which supplies the
values displayed in the control.
39 g.win.grpDoc.ddl = g.win.grpDoc.add('DropDownList', undefined, g.arrSettings);
On lines 48 and 49, we specify callbacks for the Open and Print button which simply close the
dialog and return the numbers 1 and 3, respectively. There is no need to do the same for the
Cancel button. Since it's text property is set to the word "Cancel", it will automatically return
the number 2 when clicked. Finally, on line 51, we use the return statement when displaying
the dialog, so that the numeric value produced by the button click will end up in intResult, the
variable used in the function call on line 7.
Based on the number returned when the dialog is closed, the main function will either call the
openFile() or printFile() function or display an alert saying "Operation cancelled" (lines 8 to
10).
8 if (intResult == 1){openFile();}
9 else if (intResult == 2){alert("Operation cancelled.");}
10 else if (intResult == 3){printFile();}
The openFile() function uses a try ... catch statement to attempt to open the file. If the attempt is
successful, it returns the value true for the benefit of any functions which call it. If there is an
error, the catch section displays an error message for the user (line 61).
The printFile() function calls openFile() and, if it receives back the value true, indicating that
the file has been opened, it prints the file and then closes it (lines 68 to 69).

SYNTAX myDocument.print ([print dialog, using preset])


SUMMARY ... Method of document Object
Prints a document.

Print dialog Optional. boolean specifying whether or not the print dialog should be displayed.

Using preset Optional. The print preset to use.

Writing to a text file


In order to write to a text file, the file must be opened for writing using the open() method of
the File object with the mode parameter set to one of the following three options:
• Write ("w")—Overwrite any existing data with the new information being written.
• Edit ("e")—Insert text at any position within the file. This mode offers the most
flexibility when inserting text.
• Append ("a")—Add text to the end of the file.
To overwrite the current contents of a file with new information, use open("w") followed by
the write() method.
filExample.open("w");

132
filExample.write("new text");
filExample.close();
To insert text at the start of a document, use open("e") followed by seek(0) followed by the
write() method.
filExample.open("e");
filExample.seek(0);
filExample.write("new text");
filExample.close();
To add text to the end of the document, use open("a") followed by the write() method.
filExample.open("a");
filExample.write("new text");
filExample.close();
It's very important to close a file after each write operation, so that the next time you access it,
it is available to be opened.

SYNTAX myFile.open (mode)


SUMMARY Method of File Object
... Opens a text file with the privileges specified by the mode argument.

The reading/writing mode with which the file should be accessed:


"r" Read-only mode
Mode "w" Write mode (overwrites existing information)
"e" Read/write mode allowing text to be inserted. (Use seek() method to change cursor position.
"a" Read/write mode. Cursor automatically moved to end of file.
myFile.seek (position)
Method of File Object
Moves the cursor to a given character position within a text file.
Zero-based index representing character at which to start inserting text. Can only be used on files
Position
opened with a mode setting of "e".

133
myFile.read (characters)
Method of File Object
Reads contents of a text file.
Characters Optional. The number of characters to be read. If omitted, the entire file is read.
Also:
myFile.readln () Reads a line of text at current cursor position.
myFile.readch () Reads a character.
myFile.write (text)
Method of File Object
Writes a string to a text file.
Text The string to be written.
Also:
myFile.writeln () Writes a line of text at current cursor position and appends a line-feed character.

Creating a new file


ExtendScript does not offer a function for explicitly creating a file: instead, when the open()
method is used with the mode argument set to "w", "e" or "a", if the file specified does not
exist, it will be automatically created. Thus for example, the following statements will allow
the user to either specify an existing file or create a new one.
try{
var fleLog = File.saveDialog ("Log file location", "Text files: *.txt");
fleLog.open("a");
fleLog.write("\nStuff to be logged");
fleLog.close();
}
catch(err){
alert("Error. File could not be accessed.");
}
The file specified by the user is opened in append mode ("a") and then the appropriate text is
written to the file. If the user specifies a file that does not exist, it will automatically be
created.
Working with folders
There will inevitably be times when it is more convenient to work with folders rather than
with individual files. Sometimes you will need to process all files in a given folder;
sometimes you will need to create folders and then create files inside them, etc. To perform
these operations, you will use the properties and methods of the Folder object.
Creating folders
When scripting folders, you normally begin by creating a folder object. This is a JavaScript
object existing in memory representing a folder which may or may not exist somewhere on
disk. To create a folder, use the following constructor.
var myFolder = Folder(filePath)

134
If a folder exists at the location specified by the filePath parameter, the variable myFolder will
now represent that folder. If no folder exists at the specified location, one can be created by
using the create() method of the Folder object. However, if a folder with the same name
already exists at the location, no error is thrown, no folder is created and the create() method
returns true, as if it had just successfully created a new folder. It therefore makes sense to use
the exists property to test whether the folder exists before attempting to create it.
Listing 6-7: Creating a new folder
1 var strFolder = prompt("Enter a name for the new folder", "");
2 if(strFolder != null){
3 var fld = new Folder(app.activeScript.parent + "/" + strFolder);
4}
5 if(fld.exists){
6 alert("A folder named " + strFolder + " already exists.");
7}
8 else{
9 try{
10 var blnFolder = fld.create();
11 }
12 catch(err){
13 alert("Error. No folder has been created.");
14 }
15 if (blnFolder){alert("The new folder has been created.");}
16 }

We begin by asking the user to enter a name for the new folder (line 1).
On line 3, we create a new folder object pointing to a folder with the name entered by the user,
in the same folder as the active script. This folder may or may not exist.
On lines 5 to 7, we test to see if the folder exists and, if it does, we display an error message
and no further code is executed.
If the folder does not exist, we use a try ... catch block to attempt to create it (lines 9 to 14). If
the folder is successfully created, the value true will be placed in the variable blnFolder (line
10). On line 15, we test this variable and, if it contains true, we display an alert informing the
user that the folder has been created. (Note that if(blnFolder) is a shorthand way of writing
if(blnFolder == true). The two statements are equivalent in JavaScript.)
Finally, if there is an error, the catch block (lines 12 to 14) will display an error message.
Reading files in a folder
The getFiles() method of the Folder object is used to read the files in a given folder. It takes a
single, optional parameter: a mask or filter which can be used to limit the files retrieved. The
mask is a string which can include wildcard characters: a question mark represents a single
character and an asterisk represents any number of characters. Thus, if we only want to
retrieve InDesign files, we would use a mask like "*.indd" or "*brochure*.indd".
Very usefully, the mask parameter can also be the name of a function. This allows the setting of
fairly complex criteria in determining the files retrieved. The way it works is that the function

135
is automatically called each time a file is encountered: if it returns true, the file is included.
So, basically, you can create any function that evaluates whatever aspect of the file you want,
just as long as it returns true or false.
In listing 6-8 (“08-archive-files.jsx” in the “chapter06” folder), we ask the user to select a
folder using the selectDialog() method (which bears a striking resemblance to the
openDialog() method we encountered earlier). We then use a the getFiles() method with a
function as the mask argument. The function finds InDesign files whose creation date is more
than 30 days earlier than the current date and displays the results in a listbox.
Listing 6-8: Using a function parameter with the getFiles() method
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 getFiles();
7}
8
9 function getFiles(){
10 var sourceFolder = Folder.selectDialog("Please select a folder.");
11
12 if(sourceFolder == null){
13 alert("No folder selected.");
14 }
15 else{
16 try{
17 g.arrFiles = sourceFolder.getFiles(creationDate);
18 g.win = new Window ('dialog', 'InDesign files more than 30 days old.');
19 }
20 catch(err){}
21 }
22 if(g.arrFiles.length > 0){
23 g.win.add('listbox', [0, 0, 300, 150], g.arrFiles);
24 g.win.add('button', undefined, 'Cancel');
25 g.win.show();
26 }
27 }
28
29 function creationDate(currentFile){
30 var dateCreated = currentFile.created;
31 var dateToday = new Date();
32 var dateDifference = (dateToday - dateCreated) / (1000*60*60*24);
33 var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
34 return (dateDifference > 30 && strExt == ".indd");
35 }

When we use the getFiles() function (line 17), we specify the function creationDate() as the
mask parameter.
Notice how, on line 29, when we define the function, we use a parameter variable (in brackets
after the function name) to catch each file as it is passed to the function. We can then use this

136
variable name inside the function whenever we want to reference each file.
The function begins by creating two date variables: dateCreated and dateToday. It then
calculates the difference between these two dates (line 32). However, since JavaScript returns
this figure in milliseconds, we need to divide it by the number of milliseconds in a day
(1000*60*60*24) to convert the number to days.
On line 33, we then extract the file extension using a combination of the substr() and
lastIndexOf() functions. Substr() retrieves a subset of characters from a string, starting from
the character (numerically) specified in brackets after the function name. To specify this
number we use lastIndexOf(), which returns the position of the last occurrence of the
character used as its argument—the dot preceding the file extension.
Finally, the function needs to return true or false; so, on line 34, we use the keyword return
along with a boolean statement which will evaluate to either true or false: dateDifference >
30 && strExt == ".indd".

Try it for yourself!


TUTORIAL: Navigating folders and files recursively
The script in listing 6-8 works only with those files encountered directly within the specified
folder, ignoring the contents of any subfolders. However, it is often the case that one needs to
retrieve files in subfolders as well the main folder. This can be accomplished by using a
recursive function: a function which calls itself.
In this tutorial, we will create a function which explores the subfolders and files in a folder
designated by the user and then displays the results in a tree view control. The user then has the
option of clicking a Delete button to remove items from the list before clicking archive to
move the remaining items to an archive folder, also designated by the user. (The completed
script can be found in the "chapter06" folder and is called "09-recursive-
folders_completed.jsx".)

137
1. Creating the main function
Create a new script and save it in the "chapter06" folder under the
name "09-recursive-folders.jsx".
Enter the following code.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 var blnFolder = getFolder();
7 if(blnFolder){
8 var intResult = inputDialog();
9 if (intResult == 1){outputDialog();}
10 }
11 }
On line 1, we declare a global object variable (g) which will act as the namespace container
for any data that we need to access in more than one function; on line 2, we call the main
function which controls everything; and, on line 3, we delete all global data by setting the
value of the global variable to null.
The main() function calls a function named getFolder() which will prompt the user to select
both the source and destination folders; and we place the result of the function call in a
variable called blnFolder. If the getFolder() function returns true, the inputDialog() function
will be called, which will display a dialog for the user to enter the criteria for deciding which

138
files to archive.
If the user clicks the OK button, returning the value 1 into intResult, the outputDialog()
function will be called. This will display a second dialog containing a treeview control
displaying a hierarchical list of items matching the structure inside the source folder specified
by the user. The user will be able to remove any items they do not wish to archive and then
click the Archive button to move the remaining items to the archive folder.
2. The getFolder() function
In the getFolder() function, we will use the selectDialog() method to allow the user to browse
and select the source and destination folders.
Add the following function to the end of your script.
12
13 function getFolder(){
14 g.archiveFolder = Folder.selectDialog("Please select the archive folder.");
15 if(g.archiveFolder == null){return false;}
16
17 g.sourceFolder = Folder.selectDialog("Please select the source folder.");
18 if(g.sourceFolder == null){return false;}
19
20 if(g.sourceFolder.getFiles().length == 0){
21 alert("Source folder is empty.");
22 return false;
23 }
24 else{return true;}
25 }

Save your changes.


The function prompts first for the archive folder (line 14) and then for the source folder (line
17). In both cases, if the user clicks Cancel, returning the value null, the function returns false,
which means that line 7 will prove false and no more code will be executed.
On lines 20 to 25, we check to see whether there are any items in the source folder. If there are
not, we display an error message and return false, again bringing everything to a halt.
If the source folder is not empty, we return true which means that the conditional test in line 7
will be true and line 8 will call the inputDialog() function.
3. The inputDialog() function
In the inputDialog() function will create and display the following dialog.

139
The two date input fields will be pre-populated with the current date and the date 30 days
prior to the current date. When the user clicks the OK button, we need to verify that the first
date is earlier than the second and that both dates are valid.
Add the following function to the end of your script.
26
27 function inputDialog(currentFile){
28 g.win = new Window('dialog', "Retrieval criteria");
29
30 var dateToday = new Date();
31 var strToday = dateToday.getDate() + "/" + (dateToday.getMonth() + 1) + "/" + dateToday.getFullYear();
32 var dateFrom = new Date(dateToday.setDate(dateToday.getDate() -30));
33 var strFrom = dateFrom.getDate() + "/" + (dateFrom.getMonth() + 1) + "/" + dateFrom.getFullYear();
34
35 g.win.grpFrom = g.win.add('group');
36 g.win.grpFrom.add('statictext', [0, 0, 150, 20], 'Files modified on or after:');
37 g.win.grpFrom.txt = g.win.grpFrom.add('edittext', [150, 20, 250, 40], strFrom);
38
39 g.win.grpTo = g.win.add('group');
40 g.win.grpTo.add('statictext', [0, 40, 150, 60], 'and on or before:');
41 g.win.grpTo.txt = g.win.grpTo.add('edittext', [150, 60, 250, 80], strToday);
42
43 g.win.add('statictext', undefined, 'Please enter dates in the format \"dd/mm/yyyy\".');
44
45 g.win.grpBtn = g.win.add('group');
46 g.win.grpBtn.ok = g.win.add('button', undefined, 'OK');
47 g.win.grpBtn.ok.onClick = OKButtonClick;
48 g.win.grpBtn.cancel = g.win.add('button', undefined, 'Cancel');
49
50 return g.win.show();
51 }

140
Save your changes.
On line 30, we create a new JavaScript Date object; then, on line 31, we extract the day, month
and year from it into another the string variable called strToday, inserting forward slashes and
creating a UK formatted date. (The new Date() constructor function creates a Date object
automatically populated with the current date.)
On line 32, we want the variable dateFrom to contain a date 30 days before the current date.
We do this by taking the current date (which we have placed in the variable dateToday) and
using the setDate() function to modify the day portion. (The setDate() function sets the day of
the month—not the entire date.)

Setting and getting date components


SYNTAX myDate.getDate (), myDate.getMonth (), myDate.getFullYear ()
SUMMARY ... Methods of Date Object
Functions for retrieving the day, month and year portion of a given date.

myDate.setDate (day), myDate.setMonth (month), myDate.setFullYear (year [, month, day])


Methods of Date Object
Functions for setting the day, month and year portion of a given date.
Day Integer from 1 to 31.
Month Integer from 0 to 11.
Year Four digit integer representing year.

We then create two groups, each containing a statictext and edittext control which will allow
the user to enter a from and to date. When we create the edittext boxes (lines 37 and 41), we
use the third parameter of the add() method (the text to be displayed in the control) to enter the
values in the strFrom and strToday variables, respectively.
Finally, we define standard OK and Cancel buttons which will automatically return 1 and 2,
respectively, when the window is closed. Note the use of the return statement on line 50 when
the window is shown. This means that the value 1 or 2 will be returned to the calling statement
on line 8—var intResult = inputDialog().
When the OK button is clicked, we need to verify that the dates entered are in the correct
range. On line 47, we therefore assign a callback function to the button
—g.win.grpBtn.ok.onClick = OKButtonClick.
4. The OKButtonClick() callback function
To validate the dates entered by the user, we will attempt to convert them into Date objects
and verify that the from date is earlier than the to date.
Add the OKButtonClick() function to the end of your script.
52

141
53 function OKButtonClick(){
54 var arrFrom = g.win.grpFrom.txt.text.split("/");
55 g.userDateFrom = new Date();
56 g.userDateFrom.setYear(arrFrom[2]);
57 g.userDateFrom.setMonth(arrFrom[1] - 1);
58 g.userDateFrom.setDate(arrFrom[0]);
59 g.userDateFrom.setHours(0);
60 g.userDateFrom.setMinutes(0);
61 g.userDateFrom.setSeconds(0);
62
63 var arrTo = g.win.grpTo.txt.text.split("/");
64 g.userDateTo = new Date();
65 g.userDateTo.setYear(arrTo[2]);
66 g.userDateTo.setMonth(arrTo[1] -1);
67 g.userDateTo.setDate(arrTo[0]);
68 g.userDateTo.setHours(0);
69 g.userDateTo.setMinutes(0);
70 g.userDateTo.setSeconds(0);
71
72 if(g.userDateFrom < g.userDateTo){
73 g.win.close(1);
74 }
75 else{
76 alert("Please enter dates in the format d/m/yyyy with from-date earlier than to-date.");
77 }
78 }

Save your changes.


Provided that the user has followed the instruction to enter the date in the format
"dd/mm/yyyy", when we use the split("/") function on line 54, we should end up with an array
containing the day, month and year. (The split() function creates an array by using the character
specified as the delimiter.)
We then create a new Date object in the global variable and use the setDate(), setMonth()
and setFullYear() commands to populate it with the date entered by the user. The hours,
minutes and seconds will be set from the current time; so we then set each of them to zero to
avoid unpredictable results.
Having done this for both the from and to dates, on lines 72 to 78, we test whether the from
date is earlier than the to date. If it is, we close the window and return the value 1 (line 73); if
it isn't, the window stays open and an error alert is displayed (line 76).
5. The outputDialog() function
On line 9, in the main function, we checked to see whether the dialog returned the value 1.
9 if (intResult == 1){outputDialog();}
If it does, we call the outputDialog() function. This function needs to explore the source folder
specified by the user and display its contents in a tree view. Since we do not know how may
subfolders and sub-subfolders the source folder will contain, we will use a recursive function

142
to explore the folder structure.
Add the outputDialog() function to the end of your script.
79
80 function outputDialog(){
81 g.win = new Window("dialog", "Files to be archived");
82 g.win.trvFiles = g.win.add('treeview', [0, 0, 400, 200]);
83 var arrFiles = g.sourceFolder.getFiles(creationDate);
84 for(var i = 0; i < arrFiles.length; i ++){
85 addItem(g.win.trvFiles, arrFiles[i]);
86 }
87 g.win.btnDelete = g.win.add('button', undefined, 'Delete Item');
88 g.win.btnDelete.onClick = function (){
89 if(confirm("Are you sure you wish to delete the selected item?")){
90 g.win.trvFiles.remove(g.win.trvFiles.selection);
91 }
92 }
93 g.win.btnArchive = g.win.add('button', undefined, 'Archive');
94 g.win.btnArchive.onClick = archiveItems;
95 g.win.btnCancel = g.win.add('button', undefined, 'Cancel');
96 g.win.show();
97 }

Save your changes.


After creating the treeview control (line 82), we use the getFiles() method to retrieve the top
level contents of the source folder into the variable arrFiles (line 83). We then loop through
the array of items in arrFiles and call the recursive function addItem(), passing the treeview
control and the current value of arrFiles as arguments (line 85). We will create the addItem()
function next and we will then see how it executes recursively to explore the structure inside
each subfolder of the source folder.
On lines 87 to 92, then create the Delete button and write an inline callback function to handle
the onClick event. The function simply displays a confirm dialog asking the user if they are
sure they want to delete the item. If they respond by clicking the Yes button, we delete the
selected item using the remove() method of the treeview control, which takes as its parameter
the item to be deleted. In this case, we simply use the selection property of the treeview
control to return the currently selected node (line 90).
Next, we create the Archive button and, on line 94, we assign the function archiveItems() as
the onClick callback. This function will archive all the files corresponding to the items in the
treeview.
We then create a standard Cancel button and, on line 96, we display the window using the
show() method.
6. The recursive addItem() function
On line 85, the addItem() function is called from the outputDialog() function; but, in addition,

143
it will call itself repeatedly until it runs out of folders to process.
Insert the addItem() function at the end of your script.
98
99 function addItem(addTo, addWhat){
100 if(addWhat.constructor.name == "Folder"){
101 var currentNode = addTo.add('node', addWhat.name);
102 currentNode.fsItem = addWhat;
103 var arrInside = addWhat.getFiles(modifiedDate);
104 for(var i = 0; i < arrInside.length; i ++){
105 addItem(currentNode, arrInside[i]);
106 }
107 }
108 else if(addWhat.constructor.name == "File"){
109 var currentItem = addTo.add('item', addWhat.name);
110 currentItem.fsItem = addWhat;
111 }
112 }

Save your changes.


The following diagram illustrates how the recursive function works.

The addItem() function takes two parameters: addTo—the object to which each new item is to
be added, and addWhat—the item to be added, which will either be a file or folder reference.
Firstly we had the function calls, on line 85, from inside the outputDialog() function.
84 for(var i = 0; i < arrFiles.length; i ++){
85 addItem(g.win.trvFiles, arrFiles[i]);
86 }

This passes the tree view control to the addTo parameter and each item inside
g.arrSourceFolder to the addWhat parameter. The addItem() function will therefore create
an item or node on the tree view for each file or folder object within g.arrSourceFolder. This
takes care of the top level items which go directly on the tree view.

144
Inside the addItem() function, file and folder objects are treated differently. Firstly, to
distinguish between the two, we use the logical test addWhat.constructor.name == "Folder"
/ "File" (lines 100 and 108). When a file is encountered, we add an item element to the current
addTo object (line 109). Adding an item (as opposed to a node) to a tree view, creates a list
item which cannot act as a container for other items or nodes. The second parameter of the
add() method represents the text which will be displayed within the item or node just added.
By using add.what.name, we pick up the name of the file or folder—without the file path.
By contrast, when addWhat contains a folder, we add a node to the tree view (line 101).
Unlike items, tree view nodes can act as containers, both for items and for other nodes, and
will automatically have the collapse/expand functionality controlled by the plus and minus
icons.
We then loop through the contents of the folder and call the addItem() function from inside
itself, passing the newly created node as the addTo parameter and each item in the folder as
the addWhat parameter (lines 104 to 106). This process takes place indefinitely, with each
folder encountered triggering another recursive function call.
As each node or item is created, we also create a custom property inside it called fsItem and
assign the file or folder being processed as the value.
101 var currentNode = addTo.add('node', addWhat.name);
102 currentNode.fsItem = addWhat;
...
109 var currentItem = addTo.add('item', addWhat.name);
110 currentItem.fsItem = addWhat;
This means that each item in the tree view will contain a reference to one of the objects in the
source folder, which can be read and utilized later in the script. The fact that we are able to do
this with items and nodes in a tree view is not because they possess some special fsItem
property. We are inventing fsItem (short for file system item) and could use any other name.
Every JavaScript object has this useful ability to allow the creation of custom properties.
On line 103, when we use the getFiles() method of the Folder object to grab the items in the
folder being processed, we specify the function named modifiedDate() as the optional mask
parameter. This allows us to create a more sophisticated filtering mechanisem, rather than
being limited to wildcard-based criteria such as "*.indd". Let's now write this function.
Insert the modifiedDate() function at the end of your script.
113
114 function modifiedDate(currentFile){
115 if(currentFile.constructor.name == "Folder"){return true;}
116 var datemodified = currentFile.modified;
117 var strExt = currentFile.fullName.toUpperCase().substr(currentFile.fullName.lastIndexOf("."));
118 return (datemodified >= g.userDateFrom &&
119 datemodified <= g.userDateTo &&
120 (strExt == ".indd" || strExt == ".indt"));
121 }

145
When we define the function, we use the name currentFile as the parameter variable
representing the item currently being processed by the getFiles() method. On line 115, we
specify that if the item being processed is a folder, we return true, meaning that all subfolders
will be included in the array of items returned by getFiles().
If the item being processed by getFiles() is a file, it must satisfy 3 criteria:
• dateCreated >= g.userDateFrom
The creation date of the file must be later than or equal to the from date entered by the
user via the dialog
• dateCreated <= g.userDateTo
The creation date must be earlier than or equal to the to date entered by the user
• (strExt == ".indd" || strExt == ".indt")
The file extension must be either ".indd" or ".indt".
The file extension is calculated on line 117 using the substr() and lastIndexOf() functions:
117 var strExt = currentFile.fullName.toUpperCase().substr(currentFile.fullName.lastIndexOf("."));
LastIndexOf(".") retrieves the position of the final dot and is used as an argument for
substr() to specify the position within the file name at which to start extracting the substring.
Since only one parameter is supplied, extraction continues to the end of the file name.
7. The archiveItems() function
Since we added a custom property to each item in the tree view called fsItem which contains a
reference to the file or folder it represents, we can now navigate through these items, and
archive the corresponding files and folders. The process is once again recursive, since we
don't know how many levels of sub-nodes will have been created.
Add the archiveItems() function to the end of your script.
122
123 function archiveItems(){
124 for(var i = 0; i < g.win.trvFiles.items.length; i ++){
125 doArchive(g.win.trvFiles.items[i]);
126 }
127 g.win.close(1);
128 }
We use the same pattern as before: we loop through all of the top level items in the tree view
and call a recursive function named doArchive(). This time, we only need one parameter: the
item to be processed.
Now let's create the doArchive() recursive function.
129
130 function doArchive(what){
131 var currentPath = what.fsItem.toString().replace(g.sourceFolder.toString(), g.archiveFolder.toString());
132 if(what.fsItem.constructor.name == "Folder" && what.items.length > 0){

146
133 var currentFolder = Folder(currentPath);
134 currentFolder.create();
135 for(var i = 0; i < what.items.length; i ++){
136 doArchive(what.items[i]);
137 }
138 }
139 else if(what.fsItem.constructor.name == "File"){
140 var blnCopy = what.fsItem.copy(currentPath);
141 if(blnCopy){what.fsItem.remove();}
142 }
143 }

On line 131, we work out the path where the item should be archived inside a variable called
currentPath. We use three components to achieve this:
• what.fsItem.toString()—The file path of original item
• g.sourceFolder.toString()—The file path of the source folder specified by the user
• g.archiveFolder.toString()—The file path of the archive folder specified by the user
The archive location is created by taking the original file path and replacing the part of it
which corresponds to the source folder with the file path of the archive folder. The following
table shows an example of how the replace() function might change the original file path of an
item to create the file path to which it should be archived.

Original file path of item /c/indesign/clients/booklet-cmp-112.indd


Source folder /c/indesign/
Archive folder /c/archive/
Archive file path of item /c/archive/clients/booklet-cmp-112.indd

If the item being processed is a folder, on line 133, we create a folder object (in memory),
pointing to the archive file path in the currentPath variable. Then, on line 134, we create a
folder on disk at that location. We then loop through the items inside the folder and make a
recursive function call, passing each item as the function parameter.
Whenever the doArchive() function encounters a file, it attempts to copy it to currentPath
(line 140). If the copy method is successful, it returns true into the variable blnCopy and the
original item is deleted using the remove() method (line 141).
That completes the code. To test it, try using the folders called “archive” and “source” in the
“chapter06” folder. It contains a series of dummy files with modification dates in the range 1st
December 2010 to 28th February 2011. (You may find it convenient to copy the folder onto
your desktop for speedier access while testing.)
Save your changes and run the script.

147
CHAPTER 7. Document Layout
In this chapter, we will look at setting preferences for documents, setting up master pages and
placing text and images.
Document and default Preferences
Broadly speaking, there are two types of preferences in InDesign: document and default.
Document preferences are embedded in a particular document and cease to apply when the
document is closed. Default preferences apply whenever InDesign is used to create new
documents. From the user point of view, to set document preferences, you simply ensure that
the document in question is open and active when you specify the various settings. By contrast,
to set default preferences, you must ensure that all documents are closed when setting
preferences.
As regards scripting, the distinction between default and document is made by targeting either
the application object (to set defaults) or a particular document object (to set document
preferences).
There are three main preferences objects that we need to manipulate when initially setting up a
document: viewPreferences, documentPreferences and marginPreferences.
View Preferences
The viewPreferences object allows you to change specifications which are manually obtained
by choosing Edit > Preferences (Windows) or InDesign > Preferences (Mac) and selecting
the Units & Increments category. The first three options shown (See figure 7-1, below.) are
those which it is most useful to set when writing scripts.

Figure 7-1: The Ruler Units section of the Units & Increments category of InDesign's Preferences
contains the settings which are most important when scripting

SYNTAX
SUMMARY
Useful viewPreference properties
...
Can be set to one of three enumeration values:
RulerOrigin.spreadOrigin
RulerOrigin.pageOrigin

148
rulerOrigin RulerOrigin.spineOrigin
Generally speaking, RulerOrigin.pageOrigin is
usually the most convenient choice—especially
when calculating such things as text frame
coordinates.
Requires an enumeration, the most important of
which are:
MeasurementUnits.points
MeasurementUnits.picas
horizontalMeasurementUnits MeasurementUnits.inches
verticalMeasurementUnits
MeasurementUnits.inches_decimal
MeasurementUnits.millimeters
MeasurementUnits.centimeters
MeasurementUnits.pixels

Document Preferences
The documentPreferences object contains properties which are mainly equivalent to those
obtained by choosing File > Document Setup and also when creating a new document. They
are pretty straightforward and you will soon get used to the syntax. Some of the more important
properties are shown below.
Page attributes

SYNTAX
SUMMARY
Useful documentPreference properties
...
facingPages Boolean r/w—If true, the document has facing pages.

Boolean r/w—If true, the master spread A-Master has


masterTextFrame
automatic textframes on each of its pages.
Measurement Unit (Number or String) r/w—The height of the
pageHeight
page.
r/w—PageOrientation enumeration:
pageOrientation PageOrientation.landscape or
PageOrientation.portrait
pageSize String, r/w—The size of the page (e.g. “A4").
Measurement Unit (Number or String) r/w—The width of the
pageWidth page.

149
Integer r/w—The number of pages in the document. (Range:
pagesPerDocument
1 to 9,999)
Integer (range: 1 - 999,999) r/w—The starting page number
startPageNumber for a document. This is the same as the starting page number
for the first section of a document. The default value is 1.

Figure 7-2: The documentPreferences object has properties which are mainly equivalent to the
settings found using File > Document Setup

When supplying a value for any property which requires a measurement, you have two choices.
Firstly, you can specify the unit of measurement along with the number. In this case the value
needs to be a string—for example:
docExample.documentPreferences.pageWidth = “210 mm";
Secondly, you can set the unit of measurement in advance and then, whenever you supply a
measurement parameter it can then be purely numeric.
docExample.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.millimeters;
docExample.viewPreferences.verticalMeasurementUnits = MeasurementUnits.millimeters;
docExample.documentPreferences.pageWidth = 210;
Naturally, you can still override this default measurement by entering a string value whenever
it is useful to do so.
Bleed and slug

SYNTAX
SUMMARY
Useful documentPreference properties
...
Measurement Unit (Number or String) r/w—The distance to offset the bottom
documentBleedBottomOffset
document bleed.
documentBleedInside- Measurement Unit (Number or String) r/w—The distance to offset the inside
OrLeftOffset or left document bleed.
documentBleedOutside- Measurement Unit (Number or String) r/w—The distance to offset the outside
OrRightOffset or right document bleed.

documentBleedTopOffset Measurement Unit (Number or String) r/w—The distance to offset the top

150
documentBleedTopOffset document bleed.
Boolean r/w—When set to true, uses the document bleed top offset value for
documentBleedUniformSize bleed offset measurements on all sides of the document. The default setting is
true.
Boolean r/w—When set to true, uses the slug top offset value for slug
documentSlugUniformSize
measurements on all sides of the document. The default value is false.
Measurement Unit (Number or String) r/w—The distance to offset the bottom
slugBottomOffset
slug.
Measurement Unit (Number or String) r/w—The distance to offset the inside
slugInsideOrLeftOffset
or left slug.
Measurement Unit (Number or String) r/w—The distance to offset the outside
slugRightOrOutsideOffset
or right slug.
slugTopOffset Measurement Unit (Number or String) r/w The distance to offset the top slug.

MarginPreferences
InDesign's marginPreferences object has properties mainly equivalent to the settings found by
choosing Layout > Margins and Columns. Like their Layout menu equivalents,
marginPreferences settings can be applied both to document pages and—more typically—to
master spreads.

Figure 7-3: The marginPreferences object's properties equate to the options found by choosing
Layout > Margins and Columns

Margin settings

SYNTAX Useful marginPreference properties - margins


SUMMARY ...
bottom Measurement Unit (Number or String) r/w—The bottom margin setting.
left Measurement Unit (Number or String)r/w—The left margin setting.
right Measurement Unit (Number or String)—r/w The right margin setting.
top Measurement Unit (Number or String)r/w—The top margin setting.

151
Column settings

SYNTAX
Useful marginPreference properties - columns
SUMMARY
...
columnCount Number (range: 1 - 216) r/w—The number of columns to place on the page.
r/w—The direction of text in the column.
columnDirection HorizontalOrVertical.horizontal or
HorizontalOrVertical.vertical
Measurement Unit (Number or String)(a number from 0 - 1440) r/w—The gap between
columnGutter
columns.
An array of Measurement units (Number or String) r/w—The distance of each column
columnsPositions
guide from the left margin. An array in the format [g1, g2, etc.].
Boolean, read only—If false, columns must be evenly spaced. If true, column widths can
customColumns
be customized.

Implementing document setup


When setting up documents programmatically, you will need to change settings in all of the
three preference objects we have discussed. However, the manner in which you implement
those settings will depend on your situation in developing your scripts—for example, whether
you are creating a solution for your own use, for company use or for a client.
The first option is to embed all of the settings inside the various functions and routines within
your scripts. This will mean that if document parameters need to be changed, you (or some
other developer) will have to make the change. Depending on your situation, this may be
desirable or completely undesirable.
Another option would be to externalize all the settings in some way: the simplest being to
place them in a text file. The InDesign object names are pretty straightforward and human-
readable; so it would be easy for anyone using your script to open a text file and amend the
settings which your scripts reads when performing document setup routines.
You could also rely on the document presets which appear in the every user's File >
Document Presets sub-menu. Provided you could be confident that everyone using your
solution will have a given set of document presets, you could just choose the appropriate setup
for the appropriate operation.
Placing text and images
Once you have set up your document, you will want to add content and, when doing this via
scripting, your principal weapon will be the place command. As in the interface, this is used to
import both text and graphics and you have a fair amount of flexibility as to how and where
you place items.
Placing via the document object

152
Placing a file or files into the document is the least targeted method of importing text and
images. As when working with the InDesign interface, you can place one or more files. In
scripting this is determined by whether you use a file object or an array of file objects as the
argument of the place() command. For example, the following code places a single image.
app,activeDocument.place(File("~/desktop/projectx/logo.tif"));
To place a series of files, you would first create an array of files and then use this as the
argument of the place() command.
arrFiles = [];
arrFiles.push(File("~/desktop/projectx/logo.tif"));
arrFiles.push(file("~/desktop/projectx/products/bnl580x.tif"));
arrFiles.push(file("~/desktop/projectx/products/bnl680x.tif"));
arrFiles.push(file("~/desktop/projectx/products/bnl780x.tif"));
app.activeDocument.place(arrFiles);
When the place() method of the document object is used to import multiple files, the place gun
will appear, ready for the user to click or drag to place each item.

SYNTAX myDocument.place(file(s), [showing options])


SUMMARY Method of document object
... Places a file or array of files.

File(s) The file or array of files to be imported.


Showing Optional. Whether the Options dialog box appropriate for the item being imported should be
options displayed.

Placing items onto a page or spread


When using the place() method of the page object, you can only place one file: the first
argument cannot be an array. However, when placing text files, there is a useful autoflow
parameter which, if set to true, causes all of the text in the incoming file to be placed—
equivalent to the old trick of holding down the Shift key when manually placing text. Smart text
reflow makes this option less important; but it can still be useful.

SYNTAX myPage.place(file, [place point, layer, showing options, autoflowing])


SUMMARY Method of page and spread objects
... Places a file on the page or spread.

File The file to be imported.


Optional. The position at which the item should be placed on the page: an array containing [x,
Place point
y] coordinates.
Layer Optional. The layer on which to place the file.

Showing Optional. Whether the Options dialog box appropriate for the item being imported should be

153
options displayed.

Autoflowing Optional. When placing text files, if set to true, will cause all text within the file to be placed.

Placing items into a frame


Text files can also be placed directly into a text frame and graphic files directly into a
rectangle, polygon or oval. When placing text into a frame, any existing text is simply
overwritten. Only one file can be placed and there is no option to autoflow the incoming text.
The syntax for importing into a frame is identical to that for importing into a document, with the
exception that the first argument cannot be an array of files. Thus, to place a text file into the
only text frame on page 1 of the active document, we could use the following lines:
var frm = doc.pages[0].textFrames[0];
frm.place(File("~/desktop/indesign/articles/article124.doc"));
If a graphic is placed into a text frame, it is automatically converted into a graphic frame—as
if by choosing Object > Content > Graphic. When placing a graphic into a page item, the
graphic comes in at full size. One can then use the fit() method to resize it if necessary: this is
equivalent to choosing one of the options in the Object > Fitting submenu. For example, to
import an image into a rectangular frame and do the equivalent of choosing Object > Fitting >
Fill proportionally, we could say:
var frm = doc.pages[0].rectangles[0];
frm.place(File("~/desktop/projectx/products/bnl780x.tif"));
frm.fit(FitOptions.fillProportionally);

SYNTAX myGraphicFrame.fit(FitOptions)
SUMMARY Method of rectangle, oval, polygon, graphic and image
... Eqvuivalent to choosing options from the Object > Fitting submenu.

FitOptions enumarator: FitOptions.fillProportionally, FitOptions.proportionally,


FitOptions FitOptions.frameToContent, FitOptions.contentToFrame, FitOptions.centerContent,
FitOptions.applyFrameFittingOptions

Placing items inside a text object


It is also possible to place items inside a text object such as an paragraph, character or
insertion point. The rule here is that the item placed becomes the content of the text object,
replacing any text that was there previously. The insertionPoint object is therefore usually the
most useful option for this purpose. For example, if you want to insert an image between the
first and second paragraphs of a text box, you would first insert a new paragraph and then
place the image into it.
var txf = app.activeDocument.pages[0].textFrames[0];
var para = txf.paragraphs[0].insertionPoints[-1];
para.contents = "\r";
para.place(File("~/desktop/projectx/images/logo.tif"));

154
Using the statement txf.paragraphs[0].insertionPoints[-1] is equivalent to position the cursor
at the end of paragraph 1. Then, setting the insertion point to "\r" is the equivalent to pressing
Return at the cursor position. (When using negative numbers as an index, the starting point is
the end of the object: -1 means the last item; -2 the second to last, and so forth.)

Try it for yourself!


TUTORIAL: Automating document setup
In this tutorial, we will create a script which builds a document based on the text files found in
a folder specified by the user, who will also have the opportunity to choose some of the basic
attributes of the new document via the dialog shown below.

The radio buttons at the top of the dialog determine the options which are displayed in the drop
down list. (The completed script can be found in the "chapter07" folder and is called "01-
document-builder_completed.jsx".)
1. Creating the main function
In the ESTK, choose File > New JavaScript, enter the file name
"01-document-builder.jsx" and save the file in the "chapter07"
folder.
Enter the following code.
1 var g = {};
2 main();
3 g =null;
4
5 function main(){
6 var blnFiles = getFiles();
7 if(blnFiles){
8 createDialog();
9 eventSetup();
10 var intResult = g.win.show();

155
11 if(intResult == 1){
12 buildDocument();
13 }
14 }
15 }

Save your changes.


On line 1, we declare the global variable g which will be used as a container for all objects
and data which need to be referenced in more than one function; on line 2, we call the main()
function which will control the whole script; then, on line 3, we destroy the global information
by setting the object variable g to null.
The main function begins by calling the getFiles() function and obtaining a boolean result in
the variable blnFolder (line 6). The getFiles() function will ask the user to specify the folder
containing the text files needed to create the document.
If the getFiles() function returns true, we call the createDialog() and eventSetup() functions
which will build the dialog and set up callback functions, respectively.
Next, we display the dialog and, if the user clicks the OK button, returning the value 1 into
intResult, we call the buildDocument() function which will create the document and import
the text files found in the folder designated by the user.
2. The getFiles() function
In the getFiles() function, the user will be asked to choose the folder which contains the text
files to be imported into the new document. If the folder is missing or empty, an errror message
will be displayed. Otherwise, all files with the extensions “.txt”, “.rtf”, “.doc” and “.docx”
will be read into an array. Additionally, the first paragraph in each file will be read into a
second array and will later be used as the title of each section of the new document.
Add the getFiles() function to the end of your script.
16
17 function getFiles(){
18 g.sourceFolder = Folder.selectDialog("Select folder containing text files to be imported.");
19 if(g.sourceFolder == null){return false;}
20 else if(g.sourceFolder.getFiles(textFilesOnly).length == 0){
21 alert("Source folder contains no text files.");
22 return false;
23 }
24 else{
25 g.arrTitles = [];
26 try{
27 g.arrSource = g.sourceFolder.getFiles(textFilesOnly);
28 for(var i = 0; i < g.arrSource.length; i++){
29 g.arrSource[i].open ("r");
30 g.arrTitles[i] = g.arrSource[i].readln();
31 g.arrSource[i].close();

156
32 }
33 }
34 catch(err){}
35 if(g.arrTitles.length == 0){
36 alert("There was a problem reading the source files.");
37 return false;
38 }
39 else{
40 return true;
41 }
42 }
43 }

On line 18, we use the selectDialog() method of the folder class to display a dialog enabling
the user to choose the folder containing the source files. If the user clicks the Cancel button
instead of choosing a file, we exit the function (line 19) by returning the value false into
blnFiles, the variable used in the function call.
If the folder contains no text files, we also display a message and again return false (lines 21 to
22). To ascertain whether the folder contains text files, we use the getFiles() method,
specifying a function named textFilesOnly as the argument (line 20). This function will test
whether the file extension of each file in the folder is one of the four which are acceptable:
".txt", ".rtf", ".doc" and ".docx".
If all appears to be well, on line 27, we use the getFiles() method for real and place the files
retrieved in the array g.arrSource. We then loop through the array, open each file in read
mode (line 29), read the first line of the file into the array g.arrTitles (line 30) and then close
the file (line 31). In case problems are encountered when reading the files, we enclose these
steps in a try ... catch statement.
Finally, on lines 35 to 41, we check to see whether we have retrieved any titles inside
g.arrTitles. If we have not, we display an error message (line 36); if text has been read into
g.arrTitles, the function returns true (line 40).
Let's end this section by writing the textFilesOnly() function, which we specified as the
argument of the getFiles() method on lines 20 and 27.
Add the following code to your script.
44
45 function textFilesOnly(currentFile){
46 var strExt = currentFile.fullName.toLowerCase().substr(currentFile.fullName.lastIndexOf("."));
47 return strExt == ".txt" || strExt == ".rtf" || strExt == ".doc" || strExt == ".docx";
48 }

Save your changes.


On line 45, we use the name currentFile as a parameter variable representing each file
encountered by the getFiles() method. We then extract the file extension from the name of

157
currentFile by using the substr() function with lastIndexOf() supplying the position of the
final dot.
The value returned by the function is then determined by the result of the logical statement
strExt == ".txt" || strExt == ".rtf" || strExt == ".doc" || strExt == ".docx", which will
generate true provided one of the four permissible extensions is found.
3. The createDialog() function
In the createDialog() function, we will build a dialog containing two sets of radio buttons and
two drop down lists, one displaying US paper sizes and the other European. However, we will
set the alignment of the dropdowns to stacked, causing them to be placed on top of each other
—the idea being that only one of the two will be visible at any one time.
Let's begin by creating the dialog itself and the two radio buttons which will allow the user to
choose which set of paper sizes to display in the dropdown.
Add the following code to the end of your script.
49
50 function createDialog(){
51 g.win = new Window('dialog', 'Document builder');
52
53 // Page size radio buttons
54 var grp1 = g.win.add('Group');
55 g.win.radEuro = grp1.add('RadioButton', undefined, 'Euro');
56 g.win.radUSA = grp1.add('RadioButton', undefined, 'USA');
57 g.win.radEuro.value = true;
58
59 }

First we create a group control then we add the two radio buttons to it; then we set the value of
the "Euro" button to true, making it the default.
Now, let's create the two drop-down lists to go with these two options.
Add the following code to the end of your script.
57 g.win.radEuro.value = true;
58
59 // Page size drop downs
60 var grp2 = g.win.add('Group');
61 grp2.orientation = "stack";
62 g.win.ddlEuroSizes = grp2.add('dropdownlist');
63 g.win.ddlUSASizes = grp2.add('dropdownlist');
64
65 g.arrEuroPageSizes = [];
66 g.arrEuroPageSizes[0] = ["A4", 210, 297, "mm", 20];
67 g.arrEuroPageSizes[1]= ["A5", 148, 210, "mm", 12];
68 g.arrEuroPageSizes[2] = ["A6", 105, 148, "mm", 8];
69 g.arrUSAPageSizes = [];

158
70 g.arrUSAPageSizes[0] = ["Letter", 8.5, 11, "in", 0.8];
71 g.arrUSAPageSizes[1] = ["HalfLetter", 5.5, 8.5, "in", 0.5];
72 g.arrUSAPageSizes[2] = ["Executive", 7.25, 10.5, "in", 0.8];
73 for (i =0; i < g.arrEuroPageSizes.length; i++){
74 g.win.ddlEuroSizes.add('item', g.arrEuroPageSizes[i][0]);
75 g.win.ddlUSASizes.add('item', g.arrUSAPageSizes[i][0]);
76 }
77 g.win.ddlEuroSizes.selection = 0;
78 g.win.ddlUSASizes.selection = 0;
79 g.win.ddlUSASizes.visible = false;
80
81 }

Save the changes to your script.


Having created the group control which will act as the container for both dropdowns, on line
61, we set the orientation to stack.
On lines 62 and 63, we create the two dropdownlist controls but later, on line 79, we hide
g.win.ddlUSASizes, thus making g.win.ddlEuroSizes the default. When we create the onClick
callback for the two radio buttons, we will change the visible property of both items to match
the option selected by the user.
We then construct two arrays and populate them with all the information we will need relating
to the various paper sizes. Each array contains a series of nested arrays: the following table
shows the syntax for accessing each slot in the nested array and the information it contains.

Nested array item Example Usage


g.arrEuroPageSizes[0][0] A4 Text for dropdownlist
g.arrEuroPageSizes[0][1] 210 Page width of paper size
g.arrEuroPageSizes[0][2] 297 Page height of paper size
g.arrEuroPageSizes[0][3] mm Unit of measurement
g.arrEuroPageSizes[0][4] 20 Default margin width

On lines 73 to 76, we then loop through the two arrays—we have assumed that they will
always contain the same number of options—and add the first subitem of each item to the
appropriate dropdown.
Finally, on lines 77 and 78, we set the default on both controls to the first item, whose index is
of course zero.
Next up, we need two radio buttons to allow the user to choose the page orientation.
Add the following lines to your code.
78 g.win.ddlEuroSizes.selection = 0;

159
79 g.win.ddlUSASizes.selection = 0;
80
81 // Orientation radio buttons
82 var grp3 = g.win.add('Group');
83 g.win.radPortrait = grp3.add('RadioButton', undefined, 'Portrait');
84 g.win.radLandscape = grp3.add('RadioButton', undefined, 'Landscape');
85 g.win.radPortrait.value = true;
86
87 }
Straightforward enough: we create the two radio buttons inside a group control and then set the
"Portrait" option as default.
Our final controls will be the standard OK and Cancel buttons.
85 g.win.radPortrait.value = true;
86
87 // Buttons
88 g.win.grp4 = g.win.add('Group');
89 g.win.btnOK= g.win.grp4.add('Button', undefined, 'OK');
90 g.win.btnCancel = g.win.grp4.add('Button', undefined, 'Cancel');
91 }
That completes the createDialog() function.
Save you changes.
To test your code, you need to temporarily comment out line 9, as
shown below, since this function has not yet been written.
7 if(blnFiles){
8 createDialog();
9 //eventSetup();
10 var intResult = g.win.show();
11 if(intResult == 1){
12 buildDocument();
13 }
14 }
When you run the code, either from InDesign or from the ESTK, you should see the following
dialog which, as yet, shows no sign of the g.win.ddlUSASizes dropdown.

160
Click Cancel to dismiss the dialog.
Temporarily comment out line 64, where the g.win.ddlUSASizes
dropdown is made invisible.
62 g.win.ddlEuroSizes = grp2.add('DropDownList');
63 g.win.ddlUSASizes = grp2.add('DropDownList');
64 //g.win.ddlUSASizes.visible = false;

Run the code again and you will see just how setting the
orientation property of a dropdown to stacked actually works.

The two controls are superimposed and both functional.


Click Cancel to dismiss the dialog once more.
Choose Edit > Undo twice to reinstate lines 9 and 64; or simply
remove the two forward slashes from the start of each of those
lines.
161
4. The setupEvents() function
This section will be a very short one, because the only events we need to cater for are the
onClick events of the two radio buttons; and all we need to do is to show one dropdown and
hide the other.
Add the following function to the end of your script.
92
93 function eventSetup(){
94 g.win.radUSA.onClick = function(){
95 g.win.ddlUSASizes.visible = true;
96 g.win.ddlEuroSizes.visible = false;
97 }
98
99 g.win.radEuro.onClick = function(){
100 g.win.ddlUSASizes.visible = false;
101 g.win.ddlEuroSizes.visible = true;
102 }
103 }

Save your changes and run the script.


Test the two event handlers by selecting each of the two radio
buttons and ensuring that the correct dropdown becomes visible.

Click Cancel to close the dialog.

5. The buildDocument() function


The buildDocument() function will be the one that does most work for us. First, we will set up
the master pages; and then we will import each of the files in the source folder. The function
code is divided into a series of nested functions.

162
Add the following code to the end of your script.
104
105 function buildDocument(){
106 var docNew = app.documents.add();
107 viewPrefs();
108 docPrefs();
109 paraStyles();
110 createPages();
111
112 }

We begin by creating a new document inside the local variable docNew and then we make a
call to each of our four nested functions. Let's write the first of them: viewPrefs(), in which we
will set the relevant properties of the viewPreferences object.
Insert the viewPrefs() function just above the closing brace of the
buildDocument() function.
105 function buildDocument(){
106 var docNew = app.documents.add();
107 viewPrefs();
108 docPrefs();
109 paraStyles();
110 createPages();
111
112 function viewPrefs(){
113 if(g.win.radUSA.value == true){
114 docNew.viewPreferences.horizontalMeasurementUnits=MeasurementUnits.inches;
115 docNew.viewPreferences.verticalMeasurementUnits=MeasurementUnits.inches;
116 }
117 else{
118 docNew.viewPreferences.horizontalMeasurementUnits=MeasurementUnits.millimeters;
119 docNew.viewPreferences.verticalMeasurementUnits=MeasurementUnits.millimeters;
120 }
121 docNew.viewPreferences.rulerOrigin = RulerOrigin.pageOrigin;
122 }
123
124 }

Here, we set the unit of measurement to inches, if the user has clicked the g.win.radUSA radio
button, and to millimetres, if they have chosen g.win.radEuro. We then set the rulerOrigin
property to RulerOrigin.pageOrigin, since this makes it easier to specify the positions of text
frames.
Now on to the document preferences. Insert the docPrefs()
function at the end and inside of buildDocument().
163
123
124 function docPrefs(){
125 var euro = g.win.ddlEuroSizes.selection.index;
126 var usa = g.win.ddlUSASizes.selection.index;
127 var w = (g.win.radPortrait.value == true) ? 1 : 2;
128 var h = (g.win.radPortrait.value) ? 2 : 1; // == true can be omitted
129
130 if(g.win.radEuro.value){
131 g.intPageWidth = g.arrEuroPageSizes[euro][w];
132 g.intPageHeight = g.arrEuroPageSizes[euro][h];
133 }
134 else{
135 g.intPageWidth = g.arrUSAPageSizes[usa][w];
136 g.intPageHeight = g.arrUSAPageSizes[usa][h];
137 }
138
139 docNew.documentPreferences.pageWidth = g.intPageWidth;
140 docNew.documentPreferences.pageHeight = g.intPageHeight;
141
142 if(g.win.radPortrait.value){
143 docNew.documentPreferences.pageOrientation = PageOrientation.PORTRAIT;
144 }
145 else{
146 docNew.documentPreferences.pageOrientation = PageOrientation.LANDSCAPE;
147 }
148 }
149
150 }

On line 125 and 126, we retrieve integer values representing the zero-based index of the value
selected on the page size dropdowns, into the variables euro and usa, respectively. We then
want to use these integers as indexes to retrieve the width and height from the
g.arrEuroPageSizes and g.arrUSAPageSizes arrays, which have the format:
["Letter", 8.5, 11, "in", 1]
If the user has activated g.win.radPortrait, then the width is in slot 2 (index 1) and the height
is in slot 3 (index 2) : if he chooses g.win.radLandscape, then the height is in slot 2 (index 1)
and the width is in slot 3 (index 2).
On lines 127 and 128, we use JavaScript ternary statements to populate the variables w and h
with the appropriate indexes for width and height. (Line 128 contains a reminder that, in
logical tests == true can simply be omitted—(rad.value == true) evaluates to the same result
as (rad.value).
Once we know which index is width and which is height, on lines 130 to 137, we use an if
statement to test which array of paper sizes we should look in, and use the values we have
retrieved into the variables euro, usa, h and w as indexes. For example, if the user activates
the USA radio button, chooses the paper size "Letter" and clicks on the landscape radio button;
since "Letter" is the first item on the USA sizes dropdown, the usa variable would contain

164
zero; w would contain 2 and h would contain 1. So, we would end up with:
g.intPageWidth = g.arrUSAPageSizes[0][2];
g.intPageHeight = g.arrUSAPageSizes[0][1];
This would point us to the first item in the g.arrUSAPageSizes array:
g.arrUSAPageSizes[0] = ["Letter", 8.5, 11, "in", 0.8];
where g.intPageWidth needs the third subitem (index 2) and g.intPageHeight needs the
second (index 1). So the actual values going into our variables are:
g.intPageWidth = 11
g.intPageHeight = 8.5
On lines 139 to 140, we use g.intPageWidth and g.intPageHeight as values for the
appropriate properties of the documentPreferences object; then we set the orientation property
based on the radio button selected by the user.
Let's move on to the paragraph styles. Insert the paraStyles()
function as the last item inside buildDocument().
149
150 function paraStyles(){

165
151 g.stlNormal = docNew.paragraphStyles.add({name: "BodyText1"});
152 try{g.stlNormal.appliedFont = app.fonts.item("Garamond");}
153 catch(err){}
154 g.stlNormal.pointSize = "14pt";
155 g.stlNormal.spaceAfter = "6pt";
156
157 g.stlHeading1 = docNew.paragraphStyles.add({name: "Heading1"});
158 try{g.stlHeading1.appliedFont = app.fonts.item("Rockwell");}
159 catch(err){}
160 g.stlHeading1.pointSize = "16pt";
161 g.stlHeading1.spaceAfter = "9pt";
162
163 g.stlHeader = docNew.paragraphStyles.add({name: "Header"});
164 try{g.header.appliedFont = app.fonts.item("Rockwell");}
165 catch(err){}
166 g.stlHeader.pointSize = "12pt";
167 g.stlHeader.justification = Justification.rightAlign;
168 }
169
170 }

Save your changes.


Note that each attempt to set the font is enclosed in a try ... catch block; so that, if the font is not
present, there will be no error.
Now we are ready to write the createPages() function which will import the text files and
create document pages. We will split this function into three nested functions: marginSetup(),
headerSetup() and documentPages().
Insert the following code just above the closing brace of the
buildDocument() function.
169
170 function createPages(){
171 for(var i=0; i < g.arrSource.length; i++){
172 if(i > 0){
173 docNew.masterSpreads.add();
174 }
175 marginSetup();
176 headerSetup();
177 documentPages();
178
179 }
180 }
181 }
All of the code inside the createPages() function is inside a for loop which moves through
each of the text files we retrieved into g.arrSource on line 27, inside the getFiles() function.
27 g.arrSource = g.sourceFolder.getFiles(textFilesOnly);
The first time through the loop, we will not need to create a master page, since we already

166
have the default A-Master found in every new InDesign document: hence the if statement on
lines 172 to 174.
Let's create the first of our nested functions. Insert the
marginSetup() function just above the three closing braces of the
for loop, the createPages() function and the buildDocument()
function.
178
179 function marginSetup(){
180 if(g.win.radEuro.value == true){
181 g.normMarg = g.arrEuroPageSizes[g.win.ddlEuroSizes.selection.index][4];
182 g.units = g.arrEuroPageSizes[g.win.ddlEuroSizes.selection.index][3];
183 }
184 else{
185 g.normMarg = g.arrUSAPageSizes[g.win.ddlUSASizes.selection.index][4];
186 g.units = g.arrUSAPageSizes[g.win.ddlUSASizes.selection.index][3];
187 }
188 for (j= 0; j<= 1; j ++){
189 g.mPrefs = docNew.masterSpreads.lastItem().pages.item(j).marginPreferences;
190 g.mPrefs.left = g.normMarg + g.units;
191 g.mPrefs.right = (g.normMarg * 1.25) + g.units;
192 g.mPrefs.top = (g.normMarg * 1.5) + g.units;
193 g.mPrefs.bottom = g.normMarg + g.units;
194 }
195 }
196
197 }
198 }
199 }

Save your changes.


The last item in the nested arrays inside the variables g.arrUSAPageSizes and
g.arrEuroPageSizes is the value which we will use for the average margin (index 4) and the
second to last item is the unit of measuement (index 3). For example, the nested array for the
Letter page size is as follows:
g.arrUSAPageSizes[0] = ["Letter", 8.5, 11, "in", 0.8];
As you can see the unit of measurment is "in" and the average margin setting is 0.8. Thus, in the
if statement on lines 180 to 187, we use the selected index of the dropdown as the first index—
which gives us the correct paper size—then we use sub-index 4 to populate g.normMarg and
sub-index 3 for g.units.
On lines 188 to 194, we then loop through the pages in the master spread—a loop which we
know will always have just 2 iterations—setting the properties of the marginPreferences
object based on an adjusted g.normMarg combined with the unit of measurement held in

167
g.units. For example, if the user has chosen Letter as the page size, we would end up with the
values:
g.mPrefs.left = "0.8in"
g.mPrefs.right = "1in"
g.mPrefs.top = "1.2in"
g.mPrefs.bottom = "0.8in"

Let's move on to headerSetup(). Add the following code above the


closing brace of the for loop.
196
197 function headerSetup(){
198 // Header text frames
199 var arrBoundsLeft = [], arrBoundsRight = [];
200 var ms = docNew.masterSpreads[i];
201 arrBoundsLeft[0] = g.mPrefs.top * 0.5;
202 arrBoundsLeft[1] = g.mPrefs.right;
203 var intBoxH = (g.units == "in")? 0.2: 6;
204 arrBoundsLeft[2] = (g.mPrefs.top * 0.5) + (intBoxH);
205 arrBoundsLeft[3] = (g.intPageWidth - g.mPrefs.left);
206 arrBoundsRight[0] = g.mPrefs.top * 0.5;
207 arrBoundsRight[1] = g.mPrefs.left;
208 arrBoundsRight[2] = (g.mPrefs.top * 0.5) + (intBoxH);
209 arrBoundsRight[3] = (g.intPageWidth - g.mPrefs.right);
210 var txtHeadLeft = ms.pages[0].textFrames.add({geometricBounds:arrBoundsLeft});
211 var txtHeadRight = ms.pages[1].textFrames.add({geometricBounds:arrBoundsRight});
212
213 // Header text
214 txtHeadLeft.contents = SpecialCharacters.autoPageNumber;
215 txtHeadLeft.insertionPoints[0].contents = g.arrTitles[i] + "\t";
216 txtHeadLeft.paragraphs[0].appliedParagraphStyle = g.stlHeader;
217 txtHeadRight.contents = SpecialCharacters.autoPageNumber;
218 txtHeadRight.insertionPoints[-1].contents = "\t" + g.arrTitles[i];
219 txtHeadRight.paragraphs[0].appliedParagraphStyle = g.stlHeader;
220 }
221
222 }
223 }
224 }

Save your changes.


Our first objective is to create two text frames for our headers above the top margin of each
master spread page, aligned with the left and right margins, as shown below.

To do this, we create two array variables, arrBoundsLeft and arrBoundsRight, and assign
them values which can be used for the geometricBounds property of the text frames we are

168
about to create—in the format: [y1, x1, y2, x2]. Bear in mind that, when facing pages is active,
the right margin setting becomes the outside and the left inside.
On line 203, to set the height of the text frame (i.e., the difference between y1 and y2), we test
the value of g.units. If it is "in", we set the value of intBoxH to 0.2; otherwise we set it to 6
(millimetres). We then use intBoxH on lines 204 and 208 when setting the y2 value of
arrBoundsLeft and arrBoundsRight to, effectively, set the box height.
Having created the text frames, we add a page number and the first line of the document we are
just about to place, which we read into the g.arrTitles array earlier. The automatic page
number is a SpecialCharacters enumeration and cannot be concatenated with another string.
So, we insert it first and then add the title before it in the left frame and after it in the right
frame. Using insertionPoints[0] is equivalent to clicking at the start of the container; while
insertionPoints[-1] targets the end. We separate the page number and title with a tab character
("\t").
Finally, we apply the style g.stlHeader which included in its definition the line:
g.stlHeader.justification = Justification.rightAlign;
As you probably know, when two strings are separated by a tab and right aligned, one ends up
on the left and the other on the extreme right, as shown in the illustration above.
Our final step is to import the files and place all the text they contain, so that we are not left
with any overset text.
Insert the documentPages() function just before the closing brace
of the for loop inside the buildDocument() function.
221
222 function documentPages(){
223 if (i>0){
224 docNew.pages.add().appliedMaster = docNew.masterSpreads.lastItem();
225 }
226 var pgeStart = docNew.pages.lastItem();
227 (pgeStart.side == PageSideOptions.leftHand)?
228 pgeStart.place (g.arrSource[i], [g.mPrefs.right, g.mPrefs.top]):
229 pgeStart.place (g.arrSource[i], [g.mPrefs.left, g.mPrefs.top]);
230
231 pgeStart.textFrames[0].parentStory.texts[0].appliedParagraphStyle = g.stlNormal;
232 pgeStart.textFrames[0].parentStory.paragraphs[0].appliedParagraphStyle = g.stlHeading1;
233
234 while(docNew.pages[-1].textFrames[0].overflows){
235 var pgeNext = docNew.pages.add();
236 var txtPrevious = docNew.pages[-2].textFrames[0];
237 var bodyTextBounds = docNew.pages[0].textFrames[0].geometricBounds;
238 if(pgeNext.side == PageSideOptions.leftHand){
239 bodyTextBounds[1] = g.mPrefs.right;
240 bodyTextBounds[3] = g.intPageWidth - g.mPrefs.left;
241 }
242 txtNext = pgeNext.textFrames.add({geometricBounds: bodyTextBounds});

169
243 txtNext.previousTextFrame = txtPrevious;
244 }
245 }
246 }
247 }
248 }

Save your changes.


As with the master pages, we don't need to add a new page on the very first iteration inside the
for loop; hence the if statement on line 223. Note also that the statement on line 224 both
creates a new page and applies the most recently created master as its master page.
On lines 227 to 229, we use the JavaScript ternary statement to test whether the last page of the
document (on which we are about to place the file) is a left page. If it is, our second parameter
(place point—the [x, y] coordinates where the top left of the text frame will be created)
becomes [g.mPrefs.right, g.mPrefs.top]. This is because, on a left page, our outside (or right)
margin is always on the left.
On line 231, we change the paragraph style of all the imported text by targeting the texts[0]
property of the parentStory. Then, on line 232, we target the first paragraph only and set the
style to g.stlHeading1.
We then take care of overset text by using a while loop (lines 234 to 244). On line 235, we
add a new page and place it inside the variable pgeNext. On line 236, we place the second to
last page (docNew.pages[-2]) inside the variable pgePrevious.
We then need to create a text frame and set its geometricBounds to coincide with the margins.
However, left pages need to treated differently to right ones. So, on line 237, we set the
variable bodyTextBounds to match that of the text frame on the very first page of the document
—which we know to be a right-hand page. Of the four geometricBounds parameters [y1, x1,
y2, x2], only the second, index [1], and the fourth, index [3], need to be changed when we
encounter a left page. This is taken care of in the if statement on lines 238 to 241.
Test the script and specify the folder called "text" (inside the
"chapter07" folder) as the source folder. It contains five text files,
each of which start with a heading. The script should create a
document and import all of the text files, giving each one a
different master page and applying the three paragraph styles to
their designated text.

170
CHAPTER 8. Working with text
Understanding text objects
Any seasoned InDesign user will be familiar with the way in which you can select text by
clicking.
• One click simply positions the cursor.
• Two clicks select a word.
• Three clicks select a line.
• Four clicks select a paragraph.
• Five clicks select the whole story—including any overset text.
When scripting InDesign there are five corresponding objects—plus the character object to
represent any single character:
• InsertionPoint
• Character
• Word
• Line
• Paragraph
• Story.
However, InDesign offers a couple of further text objects to help us target the right piece of
text.
• Texts—any arbitrary range of characters
• Text Style Ranges—groups of characters with similar formatting
When manipulating documents, you can use whichever object is the most convenient at any
given point in time.

171
Overwriting text objects
To add text via scripting in InDesign, you basically overwrite the contents of a given object.
The most drastic thing you can do, is probably to overwrite an entire story. Any new text frame
that you create in InDesign contains a default story. If you link two or more frames to form a
thread, all the text they contain also constitutes a single story. Stories are entirely independent
of pages: you can have several stories on one page or a single story spanning hundreds of
pages.
For a bit of practice open the file called “01-single-story.indd” in the “chapter 8” folder. It
contains a single page with a series of linked text boxes. Let's write a script which will
replace the entire story.
In the ESTK, create a new file and enter the following code.
1 var doc = app.activeDocument;
2 var txt = doc.stories[0];
3 txt.contents = "The original story has been replaced."
Run the script, setting InDesign as the target application: figure 8-1 shows the before and after.

Figure 8-1: You can replace an entire story by setting its contents property to a new string

Now let's look at replacing the contents of a text frame. Text frames are usually targeted by an
index number—as in container.textFrames[0]. However, if there are several text frames on the
page, it is not easy to anticipate the index number which InDesign will assign to each one. The
general rule is that the most recently created will have the lowest index and the first to be
created the highest.
You will need to use a number of different techniques to identify individual text frames—
position, dimensions, position relative to other objects, and so forth. One thing you will need
to do quite frequently is loop through all of the text frames on a page testing for this and that.
For example, here, let's say that we want to target the frame that contains the word “replaced".
Modify your script to resemble the following.
1 var doc = app.activeDocument;
2 for(var i = 0; i < doc.pages[0].textFrames.length; i ++){
3 var txt = doc.pages[0].textFrames[i];

172
4 if(txt.contents != undefined){
5 if (txt.contents.indexOf("replaced") > -1){
6 txt.contents = "overwritten.";
7 }
8 }
9}
The script loops through all the textFrame objects on page 1 of the active document checking
whether they contain the word “replaced”. If it finds a match, it sets the contents of the
matching frame to the word “overwritten”.
In the same way, you can overwrite each of the other InDesign text objects—for example, let's
do one more quick exercise: let's look at overwriting a paragraph.
Delete all of the text frames on the page and replace them with a single text frame whose sides
coincide with the page margins.
Fill the new text frame with placeholder text—Type > Fill with placeholder text.
Now let's simply replace the third paragraph with the words “This is paragraph 3".
Back in the ESTK, enter the following script.

1 var doc = app.activeDocument;


2 txt = doc.pages[0].textFrames[0];
3 txt.paragraphs[2].contents = "This is paragraph 3.\r";
Note that, on line 3, we include a Return character at the end of the string. This is because each
paragraph object includes the closing Return; when we replace a paragraph, we therefore need
to ensure that our text also ends with a Return.
Inserting text
In InDesign scripting, inserting text still involves replacing the contents of a text obect: you
simply target an insertion point and set its contents to the desired text. You can target an
insertion point within any text object or text frame.
Inserting text before an object
To insert text in front of an object, you target its first insertion point—
myTextObject.insertionPoints[0]. Thus, for example, to insert a heading at the start of the page
we have just been using, we can target the first insertion point of the story or text frame or that
of the first paragraph, line, word or character. Text frame is probably the most logical in this
context.
4 var doc = app.activeDocument;
5 txt = doc.pages[0].textFrames[0];
6 txt.insertionPoints[0].contents = "Placeholder Text Strikes Again\r";

Working with Fonts


Fonts can be applied directly to any text object or to a paragraph or character style which can
then be associated with the text. InDesign offers two collections of fonts: application fonts and
document fonts: app.fonts or myDocument.fonts. Naturally, these two collections can be
different: any fonts in myDocument.fonts but not in app.fonts are missing and will be flagged

173
during preflighting.
The fastest method of accessing the entire fonts collection is to use the everyItem() method—
for example:
app.fonts.everyItem()
This syntax returns a reference to every font on the current system, treated as a single object.
Font names
You can also access the name of every font by using:
app.fonts.everyItem().name
which will give you an array of all font names. The format of each name will consist of the font
family and the font style separated by a tab character.

Try it for yourself!


TUTORIAL: Creating a font and style selection dialog
Offering the user a chance to choose a font is quite a common requirement in scripting; so let's
look at a script which displays two drop-down lists: one containing fonts and the other
displaying the available styles, as shown in figure 8-2, below.

Figure 8-2: A simple dialog which mimics InDesign's font and style dropdowns

Naturally, we will need to create an onChange callback function for the fonts dropdown
which displays the appropriate options on the styles dropdown.
Create a new JavaScript document and save the file in the “chapter08” folder under the name
“01-choose-font.jsx”. (You can also copy from the completed version (“01-choose-
font_completed”) at any time, if your own code stops working).
1. Creating an array of font names
Let's begin by creating an array containing the names of all the fonts on the current system. To
do this, enter the following code.
1 // Create array of every font on system
2 var appFonts = app.fonts.everyItem().name;
3 alert(appFonts.toString());
Run the code and you will get a huge dialog box, possibly too large to fit on your screen. When
you want to dismiss it, don't panic if you can't see the OK button—just say “Go away!” in a
clear, firm voice. If that doesn't work, press the Enter key.

174
Figure 8-3: Font names contain both the font family and the font style

If you look carefully at the data displayed in the alert box, you will see that the array is being
displayed as a comma separated list. Each item in the array consists of a font name, a tab
character and then a font style.
2. Creating separate arrays for font and style names
Our next task is to split this list into two separate arrays: one containing the font names and the
other, the font styles associated with that font. Naturally, we don't want the font names to be
repeated, as they are in the current list. We want each font name to occur only once. Also,
since there are usually two or more styles associated with each font, the array containing the
styles should be an array containing nested arrays.
The technique we will use is to loop through the fonts in the appFonts array we have created
and use the split command of the String object to convert each item into an array containing
two values: the font name and the style name. Each time we encounter a new font name, we
will do three things:
Add the font name to our font names array.
Create a new nested array in our font styles array.
Add the style name to this nested array.
As we continue to loop, if the font name is repeated, we will perform only step three. It is only
when a new font name is encountered that we will again perform all three steps.
In order to be flagged as a new font, an item must satisfy one of two criteria:
Item is the first font in the list
Item contains a different font name to that of its predecessor
Delete the alert statement on line 3 and add the following code.
1 // Create array of every font on system
2 var appFonts = app.fonts.everyItem().name;
3 // Create separate arrays for font and style
4 var displayFonts = [];

175
5 var displayStyles = [];
6 var counter = -1;
7 for (var i = 0; i < appFonts.length; i++){
8 appFonts[i] = appFonts[i].split("\t");
9 if(i == 0 || appFonts[i][0] != appFonts[i-1][0] ){
10 displayFonts.push(appFonts[i][0]);
11 counter ++;
12 displayStyles[counter] = [];
13 }
14 displayStyles[counter].push(appFonts[i][1]);
15 }
On lines 4-5, we define the two arrays which will hold the font name and font style nested
arrays, respectively: displayFonts and displayStyles.
On line 6, we initialize the counter which is used to identify the nested array within the
displayStyles array to which we need to add the latest font style retrieved. Since, inside the
loop, the counter will be incremented before being used as an index, we set it to an initial
value of minus one.
Inside the for loop, on line 8, we use the split() function with the tab character as its argument,
meaning that we will generate a two item array, with the first containing the font name and the
second the font style.
On line 9, we test to see whether this is the first iteration or whether the font name of the
current item of appFonts (currentFont) is different from that of the preceding item
(appFonts[i-1][0]). If either of these tests proves to be true, we add a new nested array inside
the displayStyles array by increasing the counter by one and using it as an index.
11 counter ++;
12 displayStyles[counter] = [];
On line 14, we add the current font style from appFonts (currentStyle) to the last created
nested array inside displayStyles (displayStyles[counter]). (Note that this step is not
conditional, since we always want it to be performed.)
3. Creating the dialog
Having created our two display arrays, we are ready to create our dialog.
Add the following code to your script.
16
17 // Window and controls
18 var win = new Window('dialog');
19 win.ddlFonts = win.add('dropdownlist', undefined, displayFonts);
20 win.ddlStyles = win.add('dropdownlist');
21 win.ddlStyles.minimumSize = [150,20];
22 win.btnClose = win.add('button', undefined, 'Close');
On line 19, when we create the font name dropdown, we specify the displayFonts array as the
third argument of the add() method—the items to be displayed within the control.
By contrast, on line 24, when we create the font styles dropdown, we simply omit this
argument: this means that the dropdown will be blank when it first loads.

176
On line 25, we give the control a minimum size: the actual width of the font name dropdown
will be determined by the longest font name.
4. onChange callback for the fonts dropdown
The interactive element of our dialog is that, when the user chooses a font, the appropriate font
styles are automatically displayed on the font styles dropdown. To do this, we create an
onChange callback function for the font names dropdown (win.ddlFonts).
Our callback function will need to do the following.
Clear out any existing values from the control.
Identify the nested array inside displayStyles which corresponds to
the font name chosen.
Loop through the nested font styles array, adding each item to the
font styles dropdown.
Since displayFonts and displayStyles were populated inside the same for loop, their index
numbers will correspond and of course displayFonts is controlling the items displayed in the
fonts dropdown. So if we use the index of the selected item of win.ddlFonts as an index for
displayStyles, we will end up with the correct nested array.
Add the following code to your script.
23
24 // Callback function - font dropdown
25 win.ddlFonts.onChange = function(){
26 win.ddlStyles.removeAll();
27 var arrStyle = displayStyles[this.selection.index];
28 for (var i = 0; i < arrStyle.length; i++){
29 win.ddlStyles.add('item', arrStyle[i]);
30 }
31 }
On line 26, we use the removeAll() method to clear out any existing values from inside
win.ddlStyles.
On line 27, we create a variable called arrStyle and place inside it the nested array which
contains the style names of the currently selected font. Since this is a callback function relating
to win.ddlFonts, the following statements are synonymous.
this.selection.index
win.ddlFonts.selection.index
We then loop through our nested array and, on line 29, we add each item to win.ddlStyles.
5. onClick callback for the Close button
Let's create one further callback function for the onClick event of the Close button. Our
callback will simply display an alert with the font name and style chosen then close the button.
Append the following code to your script.

177
32
33 // Callback function - close button
34 win.btnClose.onClick = function(){
35 var chosenFont = win.ddlFonts.selection.text;
36 var chosenStyle = win.ddlStyles.selection.text;
37 alert("Font chosen: " + chosenFont + "\rStyle chosen: " + chosenStyle);
38 this.parent.close(2);
39 }
To use the dialog to change the font of a text object, we would replace line 37 with the
following code:
someTextObject.appliedFont = chosenFont;
someTextObject.fontStyle = chosenStyle;

6. Displaying the dialog


Last but not least, let's add the following lines to display the dialog.
40
41 // Display window
42 var winResult = win.show();
Although we are not doing anything with the value returned by the dialog, it's always best to
use this style of coding when displaying dialogs. When we close the dialog (on line 40) , we
return a value of 2 with the close() statement: this value will end up inside winResult.
Run the script and choose a font and font style from the two dropdowns.

When you click the Close button, the alert should confirm the font and style you have chosen.

Click OK to dismiss the alert, save your changes then close the file.

Finding and replacing text


InDesign's Find/Change command is extremely powerful and now allows you to find and

178
change not only text but also GREP expressions, glyphs and object attributes. Scripting
Find/Change operations offers two advantages over performing Find/Change manually.
Firstly, if several standard changes need to be made to a file or group of files on a regular
basis—such as when performing clean-up operations, they can be made into a script.
Secondly, whereas there are strict limits to what you can put in the Change to box when
performing Find/Change manually, when scripting, the changes you can perform to the objects
found are limited only by your imagination.
To illustrate the automation of the Find/Change command, we will create a script which cleans
up the text in a document, searching for occurrences of usage which are inappropriate for our
publication—the use of two spaces after a full stop, tabs for indentation and straight quotes
instead of typographic quotes.
Clearing Find/Change preferences
If you are an experienced InDesign user, you will know how irritating it is when you perform a
Find/Change operation only to find that, as well as changing the text, you have inadvertently
changed the formatting due to settings that were left in the Change Format box from a
previous operation. You have to undo your changes, remove the formatting options, then try
again.
In scripting, the contents of both Find boxes are properties of the findTextPreferences object,
when working with text; findGrepPreferences, when working with GREP;
findGlyphPrefeences, when working with glyphs; and findObjectPreferences, when
working with object formats. The contents of both Change boxes are properties of the
changeTextPreferences, changeGrepPreferences, changeGlyphPreferences and
changeObjectPreferences.
To avoid unpredictable results, you should always programmatically clear out these objects
before performing a Find/Change operation in a script. When finding and changing text, the
code for doing this is as follows:
findTextPreferences = NothingEnum.Nothing;
changeTextPreferences = NothingEnum.Nothing;

179
Figure 8-3: Setting the findTextPreferences object (and the other variants) to nothing is the
programmatic equivalent of clicking on the wastebasket icons in the Find Format and Change Format
sections.

To avoid irritating anyone using your scripts—including yourself, you should also repeat this
clearance code after performing the Find/Change operation; otherwise, any settings used in
your code will stick around until the next time someone performs a Find/Change. Similar code
can be used to clear settings when performing other types of Find/Change operations other than
text.

Try it for yourself!


TUTORIAL: Creating a clean-up text script
For this example—a clean up text script, we will begin by creating an interface containing a
series of checkboxes which will allow the user to decide which operations will be performed.
The options are as follows:
• Two spaces after a full-stop (or period) to be replaced with a single space.
• Tabs used to indent start of paragraph to be removed.
• Two returns after each paragraph to be replaced with a single return.
• Two hyphens to be replaced with an em dash.
The drop-down menu at the top of the dialog (shown in Figure 8-4) allows the user to specify
the scope of the search. The choices are :
• Active document
• All open documents
• Selected text

180
Figure 8-4: A simple clean-up utility which tidies up undesirable text usage.

The only two event-handlers we will need to deal with are the onClick events of the Cancel
and Clean Up buttons.
1. Creating the skeleton of the script
In the ESTK, choose File > New JavaScript, enter the file name
“02-clean-up.jsx” and save the file in the “chapter08” folder of the
“indesigncs5js1” folder.
Enter the following code.
1 #targetengine "session"
2 var g = {};
3 createDialog();
4 g.win.show();
5
6 function createDialog(){
7
8}

Save your changes.


On line 2, we declare a variable called g: this will be used to contain the window we shall be
displaying. Since the window will be referenced from several different functions, it needs to
be a global variable and hence is declared outside of the function.
On line 3, we call the createDialog() function in which the main dialog window will be built

181
then, on line 4, we display the window and the script ends. The remaining functionality of our
script will be placed on the onClick callback of the OK button.
2. The createDialog() function
Now let's write the createDialog() function, beginning with the dropdownlist control from
which the user will choose the scope of the Find/Change operation.
2a. The scope dropdownlist

Position the cursor on line 7—the blank line inside the skeleton
createDialog() function.
Enter the following code.
6 function createDialog(){
7 g.win = new Window('palette', 'Clean up Typography');
8 g.win.pnlScope = g.win.add('panel');
9 g.win.pnlScope.add('statictext', undefined, 'Scope of Search');
10 var arrScope = ['Active document','All open documents', 'Selected text'];
11 g.win.pnlScope.ddl = g.win.pnlScope.add('dropdownlist', undefined, arrScope);
12 g.win.pnlScope.ddl.selection = 0;
13
14 }

Save your changes.


On line 7, when we create the window, we set the type to "palette" or non-modal. On line 8,
we create a panel and, on line 9, we insert the static text label "Scope of search".
On line 10, we define an array containing the three types of search the user will be allowed to
do; then, on line 11, we use the array as the third (items) parameter of the add() method which
creates the dropdownlist.
Finally, on line 12, we set the first item in the dropdownlist (item zero) as the default
selection.
Run the script from InDesign or from the ESTK, setting the target
application to InDesign. Your dialog should resemble the one
shown below.

182
2b. The checkboxes for choosing clean-up operations
Now let's create the checkboxes which will allow the user to choose which clean-up
operations they wish to perform.
Position the cursor on line 13—just above the closing brace of the
createDialog() function.
Press Return then enter the following code.
12 win.pnlScope.ddl.selection = 0;
13
14 g.win.pnlOps = g.win.add('panel', undefined, 'Operations');
15 g.win.pnlOps.alignChildren = 'left';
16 g.win.pnlOps.chkSpace = g.win.pnlOps.add('checkbox', undefined, '2 spaces --> 1.');
17 g.win.pnlOps.chkTab = g.win.pnlOps.add('checkbox', undefined, 'Remove tab as indent.');
18 g.win.pnlOps.chkRet = g.win.pnlOps.add('checkbox', undefined, '2 returns --> 1.');
19 g.win.pnlOps.chkQuot = g.win.pnlOps.add('checkbox', undefined, 'Straight quotes --> curly.');
20 g.win.pnlOps.chkEm = g.win.pnlOps.add('checkbox', undefined, '2 hyphens --> em dash.');
21
22 }

Save your changes.


On line 15, we change the orientation of items within the panel from the default "center" to
"left", so that the checkboxes will all be aligned.
If you run your script again, your dialog should now look like this.

183
Let's say that we anticipate that, most of the time, the person using the utility will want to
perform all of the clean-up operations. If this is the case, it would be convenient to have all of
the checkboxes activated by default when the dialog loads.
Position the cursor at the end of line 21—just above the closing
brace of the createDialog() function.
Enter the following code.
20 g.win.pnlOps.chkEm = g.win.pnlOps.add('checkbox', undefined, '2 hyphens --> em dash.');
21 for(i = 0; i < g.win.pnlOps.children.length; i ++){
22 g.win.pnlOps.children[i].value = true;
23 }
24
25 }

To save having to deactivate each checkbox individually, we simply loop through the children
collection of win.pnlOps and set the value of each element to true.

SYNTAX myControl.children
SUMMARY ... Read-only property of ScriptUI controls
Returns an array of the child controls contained within myControl. The array may contain controls of
various types.

2c. The Cancel and Clean up buttons


The final element in our window will be the buttons.

184
Position the cursor at the end of line 24—just above the closing
brace of the createDialog() function.
Press Return to leave a blank line and enter the following code.
22 g.win.pnlOps.children[i].value = true;
23 }
24
25 g.win.grpButs = g.win.add('group');
26 g.win.grpButs.cleanUp = g.win.grpButs.add('button', undefined, 'Clean Up');
27 g.win.grpButs.cancel = g.win.grpButs.add('button', undefined, 'Cancel');
28 g.win.grpButs.cleanUp.onClick = cleanUpText;
29 g.win.grpButs.cancel.onClick = function() {g.win.close();}
30 g.win.onClose = function(){g = null;}
31 }

Save your changes.


That completes the createDialog() function. On line 28, we specify the function
cleanUpText() as the callback of the Clean Up button which we will now be creating. The
inline callback of the Cancel button simply close the window; while the window itself is
assigned a callback which resets the global variable—g—to null.
3. The cleanUpText() function
The cleanUpText() function contains the code which is executed when the user clicks the
Clean Up button. In it, we will need to evaluate the choices made by the user and perform the
appropriate Find/Change operations.
3a. The function skeleton

Add the following function skeleton to your script, below all of the
existing code.
32
33 function cleanUpText()
34 {
35 var arrDocs =[];
36
37 // Detect scope chosen by user
38
39 // Perform operations chosen by user
40
41 }
As you can see from the comments, this function first needs to detect the scope chosen by the
user: “Active document”, “All open documents” or “Selected text”. We then need to perform
all of the operations requested by the user on the document, documents or selection specified.
The array variable arrDocs, which is declared on line 35, will be used to hold the document

185
or documents to be processed. If the user chooses “Active document” or “Selected text”, we
will simply add the active document to our array. If they choose “All open documents”, we
will add all of the currently open documents to the arrDocs array.
Later, when we come to perform the Find/Change operation(s), we will loop through arrDocs
and carry out the necessary changes to each of the documents it contains, looping just once if it
contains only the active document.
3b. Checking the scope of the Change/Find operations

Position the cursor on the blank line below the comment “// Detect
scope chosen by user”.
Insert the following code.
37 } //. Detect scope chosen by user
38 switch(g.win.pnlScope.ddl.selection.text){
39 case 'Active document':
40 arrDocs[0] = app.activeDocument;
41 break;
42 case 'All open documents':
43 arrDocs = app.documents;
44 break;
45 case 'Selected text':
46 if(app.selection.length == 1 && "contents" in app.selection[0]){
47 arrDocs[0] = app.activeDocument;
48 }
49 else{
50 alert('No text selected. Please select the text that you want to search.');
51 g.win.close();
52 }
53 }
54
55 // Perform operations chosen by user
56
57 }

Save your changes.


In lines 38 to 53, we use a switch statement to examine the choice made by the user in the
Scope dropdownlist. If they have chosen ‘Active document', we add the active document to the
arrDocs array variable (line 40). This means that we will have a single item array.
If they have chosen ‘All open documents', we add app.documents (i.e., all open documents) to
the arrDocs array variable (line 43). This means that we will have an unknown quantity of
items in our array.
If the user has chosen ‘Current Selection', we need to examine the validity of the selection. The
if statement on lines 46 to 52 checks to see whether a single item is selected. If more than one
item is selected, the selection cannot be a text selection; and if nothing is selected, then we

186
cannot proceed. Therefore, in the else section of the if statement on line, we display an alert
('No text selected. Please select the text that you want to search.'—line 50) and close the
dialog.
The if statement on line 46 also tests whether the selected item has a property called
"contents"—a simple way of ensuring that the item is a text object or text frame.

SYNTAX "myProperty" in myObject


SUMMARY ... Syntax for testing whether a property exists inside a given object
Returns true or false. Note that property name must be in quotes.

3c. Carrying out the selected clean-up operations


In the final part of the cleanUpText() function, we will use a series of if statements to test
which of the checkboxes is activated. The actual Find/Change operation will be peformed by a
separate function called doFindChange() which we will invoke repeatedly. The
doFindChange() function will require three arguments: the document or documents to be
searched (which will always be arrDocs), the text to be found and the text to which it should
be changed.
The following table shows the various text elements for which we will be searching and the
strings with which we will replace each one. They are all fairly straightforward, with the
exception of straight quotation marks. Here, we will take the simplified approach that, if
double or single straight quotation marks are preceded by a return or a space character, they
will be replaced by left curly quotation marks; and that, if they are followed by a return or
space, they will be replaced by right curly quotation marks.

Checkbox
Find what Change to
name
chkSpace ". " (Full stop, space, space) ". " (Full stop, space)
chkTab "\r\t" (Return, tab) "\r" (Return)
chkRet "\r\r" (Return, return) "\r" (Return)
" \"" (Space, straight double " “" (Space, double left curly
chkQuot
quotation marks) quotation marks.)
"\r\"" (Return, straight double "\r“" (Return, double left curly
chkQuot
quotation marks) quotation marks)
"\" " (Straight double quotation "” " (Double right curly quotation
chkQuot
marks, space) marks, space)

187
chkQuot "\"\r" (Straight double quotation "”\r" (Double right curly quotation
marks, return) marks, return)
" \'" (Space, straight single " ‘" (Space, single left curly quotation
chkQuot
quotation marks) marks.)
"\r\'" (Return, straight single "\r‘" (Return, single left curly
chkQuot
quotation marks) quotation marks)
"\' " (Straight single quotation "’ " (Single right curly quotation
chkQuot
marks, space) marks, space)
"\'\r" (Straight single quotation "’\r" (Single right curly quotation
chkQuot
marks, return) marks, return)
chkEm "--" (Hyphen, hyphen) "—" (Em dash)

Position the cursor on the blank line below the comment "//
Perform operations chosen by user".
Insert the following code. (If you find it difficult to read the second
and third arguments of the doFindChange() function calls, please
refer to the table above.)
55 // Perform operations chosen by user
56 if(g.win.pnlOps.chkSpace.value == true){doFindChange(arrDocs, ". ", ". ");}
57 if(g.win.pnlOps.chkTab.value == true) {doFindChange(arrDocs, "\r\t", "\r");}
58 if(g.win.pnlOps.chkRet.value == true) {doFindChange(arrDocs, "\r\r", "\r");}
59 if(g.win.pnlOps.chkQuot.value == true)
60 {
61 doFindChange(arrDocs, " \"", " “");
62 doFindChange(arrDocs, "\r\"", "\r“");
63 doFindChange(arrDocs, "\" ", "” ");
64 doFindChange(arrDocs, "\"\r", "”\r");
65 doFindChange(arrDocs, " \'", " ‘");
66 doFindChange(arrDocs, "\r\'", "\r‘");
67 doFindChange(arrDocs, "\' ", "’ ");
68 doFindChange(arrDocs, "\'\r", "’\r");
69 }
70 if(g.win.pnlOps.chkEm.value == true) { doFindChange(arrDocs, "--", "—");}
71 }

Save your changes.


On lines 56 to 70, we test each of the check boxes in the Operations section of the dialog. If a
check box is active, we make a call to doFindChange(), a function which takes three
arguments—the documents to be searched, the string to be found (strFindWhat) and the string

188
to change it to (strChangeTo).
In the case of the chkQuotes checkbox(lines 61 to 68), we perform 8 Find/Change
operations; since we have to replace opening double quotes, closing double quotes, opening
single quotes and closing single quotes.
3d. Creating the doFindChange() function
Finally, let's create the doFindChange() function.
Add the following code at the end of your existing script.
72
73 function doFindChange(arrDocs, strFindWhat, strChangeTo){
74 for (i = 0; i < arrDocs.length; i++){
75 app.findTextPreferences = NothingEnum.nothing;
76 app.changeTextPreferences = NothingEnum.nothing;
77 app.findTextPreferences.findWhat = strFindWhat;
78 app.changeTextPreferences.changeTo = strChangeTo;
79 if(g.win.pnlScope.ddl.selection.text == 'Selected text'){
80 app.selection[0].changeText();
81 }
82 else{
83 arrDocs[i].changeText();
84 }
85 app.findTextPreferences = NothingEnum.nothing;
86 app.changeTextPreferences = NothingEnum.nothing;
87 }
88 }

Save your changes.


The doFindChange() function loops through the items in the arrDocs array variable. With
each loop, we clear the findTextPreferences and changeTextPreferences objects (lines 75 to
76). We then set the findWhat and changeTo properties of these objects to the parameters
supplied when calling the function— strFindWhat and strChangeTo (lines 77 to 78).
We then use an if statement to check whether the user chose the “Selected Text" option (line
79). If they did, we use app.selection[0].changeText() to perform the clean-up; if they didn't
then we use arrDocs[i].changeText() (line 83).
To test the script, close down all of your own inDesign documents and open the files “02-
clean-up1.indd” and “02-clean-up2.indd” in the “chapter08” folder. Test the utility by
selecting different parameters in the dialog and clicking the Clean Up button. To try another
set of options, choose File > Revert to undo all of the changes.

Tables
InDesign tables are not treated as independent objects but rather as elements which can only
exist within a text frame, on the same level as the text. To create a table, the InDesign user

189
positions the cursor at the desired location and chooses Table > Insert Table. By default,
InDesign tables have 4 body rows and 4 columns with no header of footer rows. If you create a
table with a different number of rows and columns, these settings become the default. This
same default applies when scripting.
Creating tables
To create a table at a given location within a text frame, use the tables.add() method: it takes
three optional parameters.

SYNTAX myTextObject.tables.add([To, Reference object, Initial properties])


SUMMARY Method of tables collection
... Creates a table inside the specified text object—textFrame, story, etc.

Optional. The location of the table relative to a given reference object, using one of the following
enumeration options:
LocationOptions.BEFORE
To LocationOptions.AFTER
LocationOptions.AT_END
LocationOptions.AT_BEGINNING
LocationOptions.UNKNOWN (The default)
Reference Optional, but obligatory if the to parameter is set to LocationOptions.BEFORE or
object LocationOptions.AFTER. The reference object can be another table or any InDesign text object.
Initial
Optional. A JavaScript object containing table properties.
properties

Let's take a simple example. In listing 8-3, we create a new document, add a text frame on it's
first page, into which we insert a table. We then set the number of columns to 8.
Listing 8-3: Creating a basic table
1 var doc = app.documents.add();
2 var pg1 = doc.pages[0];
3
4 // Create textFrame
5 var y1 = pg1.marginPreferences.top;
6 var x1 = pg1.marginPreferences.left;
7 var y2 = doc.documentPreferences.pageHeight - pg1.marginPreferences.bottom;
8 var x2 =doc.documentPreferences.pageWidth - pg1.marginPreferences.right;
9 var frm1 = doc.textFrames.add({geometricBounds: [y1, x1, y2, x2]});
10
11 // Add table to text frame
12 var tbl1 = frm1.insertionPoints[-1].tables.add();
13 tbl1.bodyRowCount = 8;
14 tbl1.columnCount = 8;

The resulting table is shown in figure 8-5. Because no creation properties are used, the table is
created with the default 4 columns which are automatically sized to enable the table to fit

190
exactly into the text frame. When we specify the number of columns as 8, the 4 columns stay at
their original width and the other 4 spill over the edge of the text frame.

Figure 8-5: Tables created without creation properties have the default number of rows and columns.
Columns added subsequently spill over the edge of the container.

In lising 8-4, below, we have the same script; but, this time, we use the creation properties
parameter to set the number of columns and body rows.
Listing 8-4: Using creation properties to set the number of table rows and columns
1 var doc = app.documents.add();
2 var pg1 = doc.pages[0];
3
4 // Create textFrame
5 var y1 = pg1.marginPreferences.top;
6 var x1 = pg1.marginPreferences.left;
7 var y2 = doc.documentPreferences.pageHeight - pg1.marginPreferences.bottom;
8 var x2 =doc.documentPreferences.pageWidth - pg1.marginPreferences.right;
9 var frm1 = doc.textFrames.add({geometricBounds: [y1, x1, y2, x2]});
10
11 // Add table to text frame
12 var tbl1 = frm1.insertionPoints[-1].tables.add(undefined, undefined, {bodyRowCount: 8, columnCount: 8});

As we can see in figure 8-6, the width of the resulting table matches that of the containing text
frame.

Figure 8-6: Specifying the number of rows and columns as creation properties when using the
tables.add() method causes the table width the match that of its parent text frame.

Adding text to table cells


Writing a value in a cell is much the same as adding text to a text frame. Typically, you will
use the contents property of the target cell. In fact, it is possible to add text to table cells,
rows or columns. When targeting cells, you will typically want to loop through the cells,
reading data from one place and inputting it into the cells. When targeting rows or columns,

191
you also have the option of writing an array of strings: InDesign will automatically place each
item in the array into a different cell.
Writing a value into every cell
To write the same value to every cell in a table, you would use the everyItem() method to
target every cell and then set the contents property of the object returned to the required value.
Listing 8-5: Using everyItem() to target every cell in a table
1 var doc = app.documents.add();
2 var pg1 = doc.pages[0];
3
4 // Create textFrame
5 var y1 = pg1.marginPreferences.top;
6 var x1 = pg1.marginPreferences.left;
7 var y2 = doc.documentPreferences.pageHeight - pg1.marginPreferences.bottom;
8 var x2 =doc.documentPreferences.pageWidth - pg1.marginPreferences.right;
9 var frm1 = doc.textFrames.add({geometricBounds: [y1, x1, y2, x2]});
10
11 // Add table to text frame
12 var tbl1 = frm1.insertionPoints[-1].tables.add(undefined, undefined, {bodyRowCount: 8, columnCount: 8});
13
14 // Write value to all cells
15 var allCells = tbl1.cells.everyItem();
16 allCells.contents = "the same";

Adding the same value to every cell in a table isn't something you will want to do very often.
However, the cells.everyItem() method is worth bearing in mind; since it allows you to
quickly target any property of every cell in a given table.
Writing an array to a row
The ability to write an array to a table row is also very useful. Let's say, for example, we want
to create a table with 12 columns and a header row containing the months of the year. That's
quite a few columns, so we might also want to change the orientation of our headings to
vertical, as shown in figure 8-7, below, to reduce the overall table width.

Figure 8-7: When you set the contents of a table row equal to an array, each item in the array is
automatically inserted into a separate cell.

The code that creates the table is shown in listing 8-6, below.
Listing 8-6: Writing an array to a table row
1 var arrMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October",

192
"November", "December"];
2 var doc = app.documents.add();
3 var pg1 = doc.pages[0];
4
5 // Create textFrame
6 var y1 = pg1.marginPreferences.top;
7 var x1 = pg1.marginPreferences.left;
8 var y2 = doc.documentPreferences.pageHeight - pg1.marginPreferences.bottom;
9 var x2 =doc.documentPreferences.pageWidth - pg1.marginPreferences.right;
10 var frm1 = doc.textFrames.add({geometricBounds: [y1, x1, y2, x2]});
11
12 // Add table to text frame
13 var tbl1 = frm1.insertionPoints[-1].tables.add({headerRowCount: 1, bodyRowCount: 8, columnCount: 12});
14
15 // Add formatting and data to table
16 var allCells = tbl1.cells.everyItem();
17 var header = tbl1.rows[0];
18 allCells.verticalJustification = VerticalJustification.centerAlign;
19 header.writingDirection = HorizontalOrVertical.vertical;
20 header.height = "25 mm";
21 header.contents = arrMonths;

On line 1, we create an array called arrMonths containing the 12 months of the year.
On line 13, when we create the table, we use the creation property object to set the number of
header rows to 1, body rows to 8 and columns to 12.
On line 18, we set the vertical justification of all cells in the table to centerAlign. Then, on
line 19, we rotate the header text by setting the writingDirection property of the first row of the
table to HorizontalOrVertical.vertical.
Finally, on line 21, we set the contents of the header row to our array of months.

Try it for yourself!


TUTORIAL: Updating table data
Tables are often used in publications to hold such information as listings and specifications,
which may need updating when they become obsolete. Provided the publication uses the tables
concerned in a consistent manner, it is possible to read in the new data from a text file, match
each item of data to the contents of a table cell, then overwrite the current data with the values
in the text files.
In this example, we will work with a publication which uses tables to display a series of
dates. The publication lists the cooking classes of the fictitious Mountainwell Cooking School.
Each page contains a short description of one of the cuisines that they teach. At the top of the
page is a table which has the dates of the various short courses available, as shown below.

193
To update the publication, we need to import data from a tab-separated text file with the format
shown below.
American Beginners Wed 12 Jan
American Beginners Fri 11 Mar
American Beginners Tue 10 May
American Beginners Thu 07 Jul
American Beginners Mon 05 Sep
American Intermediate Tue 18 Jan
American Intermediate Thu 17 Mar
American Intermediate Mon 16 May
American Intermediate Wed 13 Jul
American Intermediate Fri 09 Sep
...
As you can see, each record contains a type of cuisine, a course as well as a date. We need to
use the cuisine column to target the right page, the course column to target the correct row in
the table and the entry in the date column needs to be written to the appropriate table cell.
However, the dates are separated into different records: it would be better if all the dates for
one course were in the same container—for example, an array. So, after we have read in the
data, we will modify its structure.
1. Variable declaration and function calls
The first thing you will need is the InDesign document.
Open the file called "07-update-tables-from-file.indd", in the folder
“chapter08".
Next, switch across to the ESTK and create a new file and save it
194
with the name "07-update-tables.jsx".
Enter the following code.
1 var g = {};
2 g.objDates = {};
3 importDates(); // Import dates and convert format
4 updateTables(); // Add new dates to table
5 g = null;
6
7 function importDates(){
8
9 // Read data file
10
11 // Convert to array
12
13 // Convert format to match tables in document
14
15 } // End function importDates
16
17 function updateTables(){
18
19 }
The variable g.objDates is the object literal in which we will hold the transformed version of
our tab-separated data. The structure we want to end up with is shown below.
{AmericanBeginners: ["Beginners, “Wed 12 Jan", “Fri 11 Mar", “Tue 10 May", “Thu 07 Jul", “Mon 05 Sep"],
AmericanIntermediate: ["Intermediate, “Tue 18 Jan", “Thu 17 Mar", “Mon 16 May", “Wed 13 Jul", “Fri 09 Sep"]...}
Each property in our object is a combination of the cuisine and the course and the value
associated with it is an array containing the course and all of the associated dates. The idea is
that each array within g.objDates contains exactly the data found within each row of the table.

We will therefore be able to set the contents of the row to the value of the appropriate array—
like we did in the last example, on page 191.
2. The importDates() function
2a. Reading the data file
The first thing we want to do is to import the file by reading its contents into a string variable.
Position the cursor below the comment "// Read data file", inside
the importDates() function.
Add the following code to your script.
7 function importDates(){
8
9 // Read data file

195
10 var strPath = app.activeScript.parent + "/food-dates.txt";
11 var fleDates = new File(strPath);
12 if(fleDates.exists){
13 fleDates.open("r");
14 var strDates = fleDates.read();
15 fleDates.close();
16 }
17 else{
18 alert("Sorry, unable to open data file.");
19 return;
20 }
21 alert(strDates);
22
23 // Convert to array
24
25 // Convert format to match tables in document
26
27 } // End function importDates()
On line 10, we specify the path to the data file using app.activeScript.parent to target the
folder containing the script, then concatenating the name of the text file.
On line 11, we create a new file pointing to this same path and, if the file exists, on lines 13 to
14, we open it in read ("r") mode and then read the entire file into the string variable strDates.
Once we have done that, we have finished with the file, so we can close it (line 15).
If the file does not exist, we display an error message and exit the function (lines 18 to 19).
The alert on line 21 is a temporary fixture enabling us to preview the data that we have read.
Making sure that the file "07-update-table-from-file.indd" is the
active InDesign document, run the script from the Scripts panel.
If the file path is correct, and "food-dates.txt" is in the same folder as your script, you should
see a large dialog containing the data that has been read in.

If there is an error on the file path, you will see the error message and will need to correct the
file path.

196
2b. Converting the data to an array
Now we want to convert our long string into a series of arrays. We do this by using the split()
method of the String object, which divides a string into a series of substrings using the
specified delimiter as a “splitter”. In our case, we want to use the line break at the end of each
line as the delimiter—in JavaScript, this is represented as “\n".

Delete the alert statement from line 21 of your script.


Position the cursor below the comment "// Convert to array"—
inside the importDates() function—and add the following lines of
code.
22 // Convert to array
23 var arrDates = strDates.split("\n");
24 arrLevels = [];
25 for (var i = 0; i < arrDates.length; i ++){
26 var currentDate = arrDates[i].split("\t");
27 arrLevels.push(currentDate);
28 }
29
30 // Convert format to match tables in document
31
32 }

Save your changes.


Line 23 will take the original data—which looks like this:
American Beginners Wed 12 Jan
American Beginners Fri 11 Mar
American Beginners Tue 10 May
...
and turn it into an array of strings called arrDates, looking some thing like this:
[American >> Beginners >> Wed 12 Jan, American >> Beginners >> Fri 11 Mar,
American >> Beginners >> Tue 10 May ...]
Note how each item in the array is still a tab-separated record (represented by ">>" above).
We will need to access the elements in each record individually; so we then perform a second

197
split, using the tab character as the delimiter—this is represented in JavaScript as “\t". We
need to keep the arrays we have; but instead of containing strings, we want them to contain
nested arrays each containing three strings.
On line 24, we create an empty array called arrLevels. Then, as we loop through our original
array—arrDates—we split each item we encounter using the Tab delimiter and place the
resulting array in a temporary local variable called currentDate (line 26).
Finally, on line 27, we add the array held inside currentDate to arrLevels using the code:
arrLevels.push(currentDate).
When the loop has finished, we have an array (arrLevels) containing nested arrays in the
format:
[ [American, Beginners, Wed 12 Jan], [American, Beginners, Fri 11 Mar], [American,
Beginners, Tue 10 May] ...]
2c. Formatting the data to resemble the publication
Our final step in preparing the data is to populate the JavaScript object g.objDates with a
series of arrays whose structure matches the data in each row of the tables in the publication.
To do this, we will loop through the outer array in arrLevels (arrLevels[0], arrLevels[1],
etc.) and create a key value (property name) by combining the first element in the inner array
(the cuisine—e.g. arrLevels[0][0]) with the second item (the course—e.g. arrLevels[0][1]).
We will then check to see if a property with that name already exists in g.objDates. If no item
with that key exists inside g.objDates, we will create one and set its value to an array
containing two items, the course and the date of the current item in arrLevels.
If a key with that name exists, this means that we have already created an array; so we simply
add the third element of the inner array of arrLevels (the course date—i.e. arrLevels[0][2])
to the existing array.
The roles of arrLevels and objCourses in this operation is illustrated in figure 8-8.

arrLevels
Outer array Inner array
arrLevels[0][0] arrLevels[0][1] arrLevels[0][2]
arrLevels[0] [ American Beginners Wed 12 Jan ]
Key for g.objDates = AmeicanBeginners

g.g.objDates
[0] [1]
g.objDates["AmericanBeginners"] [ Beginners Wed 12 Jan ... ]

198
Figure 8-8: The role of arrLevels g.objDates in our script

Let's say the first item in arrLevels is the array ["American", "Beginners", "Wed 12 Jan"], our
script will create a property inside g.objDates with the name "AmericanBeginners" and set its
initial value to the array ["Beginners", "Wed 12 Jan"]. At this point, g.objDates would look
like this:
{ AmericanBeginners: [ "Beginners", "Wed 12 Jan" ] }
If the second item in arrLevels is the array ["American", "Beginners", "Wed 12 Jan"], the
script will recognize that the property "AmericanBeginners" exists and add the date to the
array which is the value of that property. At this point g.objDates would look like this:
{ AmericanBeginners: [ "Beginners", "Wed 12 Jan", "Fri 11 Mar" ] }
Once we have processed all of the data in arrLevels, objData will contain a series of
properties whose names are a combination of each cuisine and level and whose values are
arrays ready to be written to the appropriate row of the table shown below.

The reason for repeating the level inside the array is so that the contents of each array will
precisely match the data which needs to be written to one table row.
Position the cursor below the comment "// Convert format to match
tables in document", inside the importDates() function and add the
following code.
30 // Convert format to match tables in document
31 for (var i = 0; i < arrLevels.length; i ++){
32 var currentCuisine = arrLevels[i][0];
33 var currentLevel = arrLevels[i][1];
34 var currentDate = arrLevels[i][2];
35 var currentKey = currentCuisine + currentLevel ;
36 if (currentKey in g.objDates){
37 g.objDates[currentKey].push(currentDate);
38 }
39 else{
40 g.objDates[currentKey] = [currentLevel, currentDate];
41 }
42 }
43 } // End of function importDates()
On line 35, we create a property name inside a temporary local variable called currentKey;
then, on line 36, we check to see whether a property with that name already exists—if
(currentKey in g.objDates). If the key exists, we simply add the Date element of the array

199
currently being processed to the item with that key, using the code:
g.objDates[currentKey].push(currentDate).
If a property with a name matching currentKey does not exist, we create one and
simultaneously enter, as the first two items in the array—the course name and the date:
g.objDates[currentKey] = [currentCourse, currentDate].
Let's take a look inside g.objDates to see what it contains: insert
the following code at the end of the importDates() function—just
above the closing brace.
43 for (var prop in g.objDates) {
44 alert(prop + ": " + g.objDates[prop]);
45 }
46 } // End of function importDates()

Run the script from InDesign and you should get a series of dialogs
like the one shown below. Keep your finger on the Enter key to
dismiss the remaining dialogs quickly once you've seen enough.

Delete the for loop from the script.


3. Updating the tables in the publication
Now that we have a data object which contains compatible data, we can loop through the
pages of the document, writing the appropriate information to the table on each listing page.
The pages which we want to process will be those that have a text frame containing a table.
The heading at the top of each page contains the cuisine; so, we can loop through the rows of
the table and create a temporary key for each row consisting of the cuisine and the entry in
column one of the table (the course level).

200
We can then check to see if a property with that name is present in g.objDates. If it is, we
simply set the contents of that row to the value of the array, overwriting any existing values in
the table.
Position the cursor on line 46—the blank line inside the skeletal
updateTables() function you created earlier.
Insert the code shown below.
43 } // End of function importDates()
44
45 function updateTables(){
46 var doc = app.activeDocument;
47 for (var i =0; i < doc.pages.length; i ++){
48 var currentPage = doc.pages[i];
49 var currentFrame;
50 if(currentPage.textFrames.length > 0 && currentPage.textFrames[0].tables.length > 0){
51 currentFrame = currentPage.textFrames[0];
52 }
53 else{
54 continue;
55 }
56 var tableCount = currentFrame.tables.length;
57 if(tableCount > 0){
58 var currentTable = currentFrame.tables[0];
59 var rowCount = currentTable.rows.length;
60 var currentCuisine = currentFrame.paragraphs[0].contents.replace("\r", "");
61 for (var j = 0; j < rowCount; j ++){
62 var currentRow = currentTable.rows[j];
63 var currentCourse = currentRow.cells[0].contents;
64 var currentKey = currentCuisine + currentCourse;
65 if(currentKey in g.objDates){
66 currentRow.contents = g.objDates[currentKey];
67 }
68 }
69 }
70 }
71 alert("Update complete.");
72 }

Save your changes.


The first thing we need to do inside our loop is to check whether there is a text frame on the
page which contains a table (line 50). If there is, we capture what we know will be the only
text frame in the variable currentFrame.
If there is no text frame, or if the text frame does not contain a table, we use the continue
statement to skip that iteration and move on to the next page (line 54).
Having established that we are not processing a blank page and that there is a table in the text

201
frame, we then grab the table, the number of rows and the page heading into variables.
57 if(tableCount > 0){
58 var currentTable = currentFrame.tables[0];
59 var rowCount = currentTable.rows.length;
60 var currentCuisine = currentFrame.paragraphs[0].contents.replace("\n", "");
The heading is the first paragraph on the page; but, in InDesign, all paragraphs include a
trailing carriage return. So, on line 60, we use the replace() function to get rid of the return and
place the resulting string into a variable called currentCuisine.
Next, we loop through the rows in the table, construct our property name and retrieve the data
contained inside the array which constitutes the value of the property, inside g.objDates.
61 for (var j = 0; j < rowCount; j ++){
62 var currentRow = currentTable.rows[j];
63 var currentCourse = currentRow.cells[0].contents;
64 var currentKey = currentCuisine + currentCourse;
65 if(currentKey in g.objDates){
66 currentRow.contents = g.objDates[currentKey];
67 }
68 }
On line 65, we test to see whether g.objDates contains a property with a name matching the
one we have just assembled in currentKey. If it does, on line 66, we set the contents of the
row to the value of that property.
Before testing your code, make sure that the file " 07-update-table-
from-file.indd", from the "chapter08" folder, is the active document
in InDesign.
Since we earlier used app.activeScript.parent + "/food-
dates.txt" to target the text file containing the course dates, you
will need to run the script from the InDesign Scripts panel.
The data should populate all of the tables. (Use File > Revert after
testing.)

202
CHAPTER 9. Working with Images
InDesign image objects
The image and its container
InDesign does not allow the user to place an image directly on the page; it has to be placed
inside a container—a graphic frame: rectangle, oval or polygon. Therefore, when you work
with images in scripting, the object your choose to initially target very much depends on what
you intend to accomplish.
Figure 9-1 shows the objects which you have at your disposal.

Figure 9-1: InDesign objects related to imported bitmapped and vector images

Targeting all graphic within a document


The allGraphics collection (an array of graphic objects) is a property of the document object
and contains all of the imported images within the document—irrespective of image format.
The page object does not have an allGraphics property. However, the graphic object does
have a parentPage property which returns the number of the page on which it is located.
If you need to examine or perform an operation on every image in a document, you would loop

203
through this collection and use the properties and methods of the graphic object as required.
For example, the script in listing 9-1 displays a series of alerts indicating the source file of
each image and the page on which it is located.
Listing 9-1: Looping through the allGraphics collection
1 var doc = app.activeDocument;
2 for (var i = 0; i < doc.allGraphics.length; i ++){
3 app.select(doc.allGraphics[i].parent);
4 var strPage = doc.allGraphics[i].parentPage.name;
5 if(doc.allGraphics[i].itemLink != null){
6 var strPath = doc.allGraphics[i].itemLink.filePath;
7 alert("Page " + strPage + ": " + strPath);
8 }
9 else{
10 alert("Page " + strPage + ": Embedded graphic with no linked file");
11 }
12 }

To test the script, open the file “01-allgraphics.indd” in the “chapter09” folder of the
“indesigncs5js1” folder or open one of your own files which contains images.
On line 5 of the script, we test whether each graphic in the allGraphics collection has an
itemLink property which is not null. Graphics pasted into InDesign, which have no associated
linked file, return an itemLink value of null. If the itemLink value is not null, we display the
page number and the filePath property of the itemLink; otherwise, we display the page number
and the fixed text “Embedded graphic with no linked file”.

SYNTAX myGraphic.itemLink.filePath
SUMMARY ... Property of link object
Syntax for locating file path of linked graphic. Returns null if graphic was pasted into InDesign.

Accessing graphics via the pageItems collection


If your script needs to have page logic rather than document logic (for example, if you need to
loop through every page in the document performing various operations including some that
apply to graphics), you will need to access graphic objects via the pageItems collection.
Since this is a generic collection which includes objects of various types, you will then need to
distinguish graphic objects from other types of pageItem. The main thing to check for is that the
contentType property of the pageIem returns ContentType.graphicType. However, since
movie and sound objects can also return a contentType of ContentType.graphicType
(because they can contain a poster image), you should then check that both the movies.length
and sounds.length properties of the page item return zero.
In listing 9-2, we have amended our previous script to loop through all the pages in the
document and then all the pageItems on each page. If a pageItem satisfies the criteria described

204
above, a descriptive alert is displayed as before.
Listing 9-2: Identifying graphics within the pageItems collection
1 var doc = app.activeDocument;
2 for(var h = 0; h < doc.pages.length; h++){
3 for (var i = 0; i < doc.pages[h].pageItems.length; i ++){
4 if(doc.pages[h].pageItems[i].contentType == ContentType.graphicType){
5 if(doc.pages[h].pageItems[i].movies.length == 0
6 && doc.pages[h].pageItems[i].sounds.length == 0){
7 app.select(doc.pages[h].pageItems[i]);
8 var strPage = doc.pages[h].pageItems[i].parentPage.name;
9 if(doc.pages[h].pageItems[i].allGraphics[0].itemLink != null){
10 var strPath = doc.pages[h].pageItems[i].allGraphics[0].itemLink.filePath;
11 alert("Page " + strPage + ": " + strPath);
12 }
13 else{
14 alert("Page " + strPage + ": Embedded graphic with no linked file");
15 }
16 }
17 }
18 }
19 }

On line 2, we have the outer loop (using counter variable h) which loops through all of the
pages in the document. Then, on line 3, we have a nested loop (using counter variable i) which
loops through all of the page items on each page.
We then check whether the contentType property of each page item is
ContentType.graphicType (line 4) and eliminate the possibility that the item is a sound or
movie (lines 5 to 6). If the page item passes both tests, then it is safe to assume that it is a
picture frame.
Note that, on line 8, we are able to check the parentPage property of the pageItem object to
ascertain the page number. However, the pageItem object does not have an itemLink property.
Therefore, on line 10, we need to drill down into the allGraphics collection of the page item
and look at the itemLink of the first (and only) graphic it contains.

Try it for yourself!


TUTORIAL: Relinking images
In this tutorial, we will create a script which allows the user to relink all graphics in a
document by specifying search and replace strings.

205
The user can place an entry into the Find and Change boxes either
by choosing a file extension from a drop down list or by entering a
string into the text box.
When the Add button is clicked, the Find and Change entries are
added to a listbox, thus allowing multiple Find and Change criteria
to be specified.
To remove an item from the listbox, the user simply selects it then
clicks the Delete Seleted button.
If the Manual radio button is selected, a JavaScript confirm dialog
allows the user to choose whether to relink or skip each image.

206
If the Automatic radio button is selected, the relinking process takes place without any user
intervention.
Finally, after the process is complete, a text file is created, and opened for the user to view,
containing a summary of the images (if any) which were relinked, showing the original file
path and the relinked one.

1. Creating the dialog


In the ESTK, choose File > New JavaScript.
Save the new file in the “chapter09” folder under the name “03-
relink-graphics.jsx”.
Enter the following code.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 if(app.documents.length == 0){
7 alert("Please open the g.document containing the images to be relinked.");
8 }
9 else{
10 intResult = buildDialog();
11 if(intResult == 1){
12 relinkGraphics();
13 }
14 }
15 }
On line 6, we make sure that there is at least one InDesign document open before proceeding to
call the buildDialog() function (line 10). If the function returns 1, indicating that the user has
clicked the Relink button, we then call the relinkGraphics() function (line 12).

207
Now let's create the dialog and insert the first panel which will allow the user to enter the Find
criteria.
Add the following code to your script.
16
17 function buildDialog(){
18 g.doc = app.activeDocument;
19 g.win = new Window("dialog", "Re-link Images");
20 g.win.add("statictext", undefined, "Choose file extension or enter text:");
21 var arrExtensions = [".ai", ".bmp", ".eps", ".gif", ".jpg", ".png", ".psd", ".tif"];
22
23 // From panel
24 g.win.pnlFrom = g.win.add("panel", undefined, "Find:");
25 g.win.pnlFrom.ddl = g.win.pnlFrom.add("dropdownlist", undefined, arrExtensions);
26 g.win.pnlFrom.txt = g.win.pnlFrom.add("edittext");
27 g.win.pnlFrom.txt.minimumSize = [200, 20];
28 g.win.pnlFrom.ddl.onChange = function(){
29 if(this.selection != null){g.win.pnlFrom.txt.text = this.selection.text;}
30 }
31
32 return g.win.show();
33
34 } // End function buildDialog

Save your changes.


On line 20, we create a statictext label which will display, at the top of the window, the
prompt "Choose file extension or enter text:".
On line 21, we define the array variable arrExtensions, which is then used to populate the
dropdownlist win.pnlFrom.ddl (line 25).
In the onChange callback on lines 28 to 30, we take the text of the currently selected item in
win.pnlFrom.ddl and set it as the text of the edittext control— win.pnlFrom.txt.
So that, we can test how the window is coming along, on line 32, we use the show() method to
make it visible.
Run the code from the InDesign Scripts panel or, in the ESTK, set
InDesign as the target application and click the Run button (or
choose Debug > Run).
When the window appears, choose a file extension from the drop
down list. The value you choose should then appear in the edittext
field.

208
Press the Escape key in the top left of your keyboard to close the
dialog and return to the ESTK.
The Change to panel will obviously be very similar to the From panel we have just created,
so feel free to use Copy and Paste when creating the code and then change all the “From”s to
“To”s.
Position the cursor above the return g.win.show() command and
insert the code shown below.
31
32 // To panel
33 g.win.pnlTo = g.win.add("panel", undefined, "Change to:");
34 g.win.pnlTo.ddl = g.win.pnlTo.add("dropdownlist", undefined, arrExtensions);
35 g.win.pnlTo.txt = g.win.pnlTo.add("edittext");
36 g.win.pnlTo.txt.minimumSize = [200, 20];
37 g.win.pnlTo.ddl.onChange = function(){
38 g.win.pnlTo.txt.text = this.selection.text;
39 }
40
41 return g.win.show();
42
43 } // End function buildDialog
The next control we need is the listbox, which will have a two column display showing the
criteria specified by the user via the From and To boxes. Above the listbox, we will need an
Add button to move the current values from the From and To boxes into the listbox. Below the
listbox, we need a Delete button to allow removal of criteria.
Position the cursor above the return g.win.show() command and
insert the code shown below.
40
41 // Add button
42 g.win.btnAdd = g.win.add("button", undefined, "Add");

209
43 g.win.btnAdd.onClick = updateListBox;
44
45 // Criteria list box
46 g.win.lstChanges = g.win.add("listbox", undefined, undefined,
47 {numberOfColumns: 2,
48 showHeaders: true,
49 columnTitles: ['Find:', 'Change to:'],
50 columnWidths: [100, 100]
51 });
52 g.win.lstChanges.minimumSize = [200, 100];
53
54 // Delete button
55 g.win.btnDelete = g.win.add("button", undefined, "Delete Selected");
56 g.win.btnDelete.onClick = deleteListItem;
57
58 return g.win.show();
59
60 } // End function buildDialog

On lines 46 to 51, since we want the listbox control to have two columns, we need to use the
fourth parameter of add() method—the creation properties—to supply the necessary attributes.

SYNTAX myContainer.add("listbox"[, bounds, items, creation properties])


SUMMARY ... Method of scriptUI container control (window, panel, group)
Creates a lisbox inside the specified container.

Listbox The type of control to be added—a listbox.


Bounds Optional. Position and size of object.
Items Optional. Array of strings to be displayed within the listbox.
Creation
An object specifying initial properties of the control.
properties
Listbox creation properties
A boolean value which determines whether the user can select more than one list
multiselect
item.
numberOfColumns An integer representing the number of columns of text to be displayed.
showHeaders A boolean value which determines whether column headings are displayed.
columnWidths An array of integers specifying the widths of all columns.
ColumnTitles An array of strings which will be displayed as column headings.

On lines 43 and 56, we specify callbacks for the two buttons associated with the listbox
—updateListBox and deleteListItem. We will create these callbacks in section two of this

210
tutorial.
The final controls in our window are the radio buttons which determine the mode in which we
do our relinking—manual or automatic—and the button that starts the relink process.
Position the cursor above the return g.win.show() command and
insert the code shown below.
57
58 // Manual/Auto radio buttons
59 g.win.radManual = g.win.add("radiobutton", undefined, "Manual");
60 g.win.radAuto = g.win.add("radiobutton", undefined, "Automatic");
61 g.win.radAuto.value = true;
62
63 return g.win.show();
64
65 } // End function buildDialog
On lines 59 and 60, we create our two radio buttons. Remember that, in ScriptUI, we do not
need to do anything to get the two radio buttons to work in a mutually exclusive fashion.
Simply listing them next to each other in the code, with no interruptions, is sufficient. On line
61, we make the Automatic radio button the default by setting its value property to true.
That just leaves the Relink and Cancel buttons.
Position the cursor above the return g.win.show() command and
insert the following code.
62
63 // Relink and Cancel buttons
64 g.win.btnRelink = g.win.add("button", undefined, "Relink");
65 g.win.btnRelink.onClick = function(){
66 if(g.win.lstChanges.items.length == 0){
67 alert("Please specify the relink criteria.");
68 }
69 else{
70 g.win.close(1);
71 }
72 }
73 g.win.btnCancel = g.win.add("button", undefined, "Cancel");
74
75 return g.win.show();
76
77 } // End function buildDialog

Save your changes.


On lines 65 to 72, we specify an inline onClick callback for the Relink button, which verifies
that the user has added at least one item to the criteria list box (line 66). If they have not, an
error is displayed (line 67): if they have, we close the dialog, returning the value 1 (line 70).

211
That completes the buildDialog() function.
2. Callback functions
We now have two callbacks to write: updateListBox()—which we attached to g.win.btnAdd;
and deleteListItem()—which we attached to g.win.btnDelete.
Our updateListBox() function will need to take the values in the From and To boxes and add
them as a single item to the listbox with the From value in column 1 and the To value in column
2.
Insert the following function at the end of your script, below the
buildDialog() function.
78
79 function updateListBox(){
80 if(g.win.pnlFrom.txt.text == "" || g.win.pnlTo.txt.text == "" ){
81 alert("Please enter text in both boxes.");
82 }
83 else{
84 var itm = g.win.lstChanges.add("item", g.win.pnlFrom.txt.text);
85 itm .subItems[0].text = g.win.pnlTo.txt.text;
86 g.win.pnlFrom.txt.text = "";
87 g.win.pnlTo.txt.text = "";
88 g.win.pnlFrom.ddl.selection = null;
89 g.win.pnlTo.ddl.selection = null;
90 }
91 }

Save your changes.


On lines 80 to 82, we deal with the possibility that one or both criteria text boxes will be
blank; in which case we display an error message.
If both boxes contain text, we add the value in the From box—g.win.pnlFrom.txt.text—to the
first column of our listbox and the value in the To box to the second column.
Note that, to write to the first column of a multi-column list control, you use the add() method
of the control, with "item" as the first parameter (the type of control to be added) and the text to
be displayed in the listbox as the second parameter.
To write to the second column, you set the text property of the subItems property of the item
you have just created: myItem.subItems[0].text writes text to column 2,
myItem.subItems[1].text writes text to column 3, and so forth.
On lines 86 to 89, we reset both dropdownlist controls and both edittext controls, ready for
reuse.
Run the script and try clicking the Add button without completing

212
the text fields. You should see the following error message.

Now try adding a couple of legitimate items to the listbox.


Close the dialog and return to the ESTK.
The deleteListItem() function will be fairly straightforward. First we need to verify that there
is indeed an item selected: if there isn't, we display an error message; if there is, we delete the
selected item. (Since we did not enable the multiselect creation property when we created the
item, there can only ever be one item selected.)
Add the following function at the end of your script, below the
updateListBox() function.
92
93 function deleteListItem(){
94 if(g.win.lstChanges.selection == null){
95 alert("Please select the item to be deleted");
96 }
97 else{
98 g.win.lstChanges.remove(g.win.lstChanges.selection);
99 }
100 }

Save your changes.


3. The relinkGraphics() function
Finally, we have the relinkGraphics() function, which needs to examine every item in the
allGraphics collection of the active document testing whether any of the Find criteria specified
by the user occur in its file path. If it does, we make the necessary changes and relink the image
to the modified file path.
To begin, add the following skeleton function at the end of your
script, below the deleteListItem() function.
101

213
102 function relinkGraphics(){
103 var strFeedback = "";
104 var blnRelink;
105 for(i = 0; i < g.doc.allGraphics.length; i ++){
106
107 // Exclude embedded items
108
109 // Apply all changes specified by user to filepath of graphic
110
111 // Relink image if applicable
112
113 } // End for loop
114
115 // Display user feedback
116
117 }

We start by initializing a couple of variables which we will use later in the function (line 103
and 104). We will use strFeedback to build a list of all the files that have been relinked,
which we will write to a text file when the process is complete. The boolean variable
blnRelink will be used to determine whether the relink operation should be performed on a
particular graphic.
Next, we loop through the allGraphics collection and the first thing we need to do is to skip an
iteration every time we encounter an embedded graphic with no linked file.
Insert the following if statement after the comment "// Exclude
embedded items".
107 // Exclude embedded items
108 if(g.doc.allGraphics[i].itemLink == null){
109 continue;
110 }
111
112 // Apply all changes specified by user to filepath of image

The if statement simply tests whether the itemLink property of any graphic in the allGraphics
collection is null. If it is, then relinking does not apply to that image and so the continue
statement is used to skip the current iteration of the for loop and move on to the next one.
Next, we need to use the JavaScript replace() function to apply the changes specified by the
user to the file path of the graphic.
Insert the following if statement after the comment "// Apply all
changes specified by user to filepath of image".
112 // Apply all changes specified by user to filepath of image
113 strBefore = doc.allGraphics[i].itemLink.filePath.toLowerCase();

214
114 currentLink = doc.allGraphics[i].itemLink.filePath.toLowerCase();
115 for(j = 0; j < win.lstChanges.items.length; j++){
116 currentFind = win.lstChanges.items[j].text.toLowerCase();
117 currentChange = win.lstChanges.items[j].subItems[0].text.toLowerCase();
118 currentLink = currentLink.replace(currentFind, currentChange);
119 }
120
121 // Relink image if applicable

On lines 113 and 114, we set two separate variables to the same value—the file path of the
current graphic, converted to lower case. This is to provide us with a mechanism for testing
whether the replace function has any effect on each file path.
On lines 115 to 119, we loop through the items in the listbox, setting the value in column one
as the find what argument of the replace() function and the value in column 2 as the change to
argument, then placing the resulting string in the variable currentLink.
Once the loop is complete, if strBefore is the same as currentLink, we know that the
replace() function has not changed the file path and so no relinking needs to be done.
Insert the following if statement after the comment "// Relink image
if applicable".
121 // Relink image if applicable
122 blnRelink = false;
123 if(strBefore != currentLink){
124 blnRelink = true;
125 if(g.win.radManual.value == true){
126 app.select(g.doc.allGraphics[i].parent);
127 blnRelink = confirm("Relink this image?");
128 }
129 }
130 if(blnRelink == true){
131 try{
132 g.doc.allGraphics[i].itemLink.relink(File(currentLink));
133 strFeedback += "Relinked: " + strBefore + " ->\nto: " + currentLink + "\n\n";
134 }
135 catch(err){
136 strFeedback += "Could not relink: " + strBefore + "\n-> to: " + currentLink + "\n\n";
137 }
138 }
139 } // End for loop
140
141 // Display user feedback
142
143 }

On line 122, we begin by setting blnRelink to false, meaning that the image does not need to be
relinked. Then, if strBefore is not the same as currentLink, we change blnRelink to true (line
124).

215
Next, if the user has chosen manual mode using the radio button g.win.radManual, we display
a JavaScript confirm to check whether they want this particular graphic relinked. To help them
identify the graphic we select it for them (line 126). When the confirm dialog appears, if they
click the Yes button, blnRelink will remain set to true; if they click No, blnRelink will be
changed to false (line 127).
The if statement on lines 130 to 138 tests whether blnRelink is true. If it is, we attempt to
relink the graphic to the new file path using a try ... catch block. If the relink succeeds, we add
a message to that effect to strFeedback (line 133); if it fails, we add a "Could not relink..."
message to strFeedback (line 136).
The final thing we need is to provide a feedback message for the user. If strFeedback is blank,
this indicates that no relinking took place: if it is not blank, then we will attempt to create a text
file in the same folder as the script, write the contents of strFeedback to the file, then open it
for the user to read. If we are unable to create the file, we will simply display an error
message in an alert dialog.
Insert the following if ... else statement after the comment "//
Display user feedback".
141 // Display user feedback
142 if(strFeedback == ""){
143 alert("No images were relinked.");
144 }
145 else{
146 var fleFeedback = File(app.activeScript.parent + "/relink-log.txt");
147 try{
148 fleFeedback.open("w");
149 fleFeedback.write(strFeedback);
150 fleFeedback.close();
151 fleFeedback.execute();
152 }
153 catch(err){
154 alert("Sorry. Due to an error, the log file could not be created.");
155 }
156 }
157 }

Save your changes.


If strFeedback is blank, we display an error message (line 143). Otherwise, on line 146, we
create a File object pointing to a file named "relink-log.txt" in the same folder as the script,
which may or may not exist.
On line 148, we open the file in write mode ("w"). Thus, if the file exists, its contents will be
overwritten; if it does not exist, it will be created. We then write to the file, close it and use
the execute() method to open it with the default editor for ".txt" files. All this takes place in a

216
try ... catch block: if there is an error, inside the catch section, we simply display and error
message informing the user that the log file could not be created.
That completes the function, and the script.
To test it, try opening the file called "03-relink-images.indd" in the
"chapter09" folder. It contains links to JPEG images in a folder
called "low-res". The "chapter09" folder also contains a folder
called "hi-res" which contains the same images in TIFF format.
Run the script from the Scripts panel, leaving "03-relink-
images.indd" active.
Enter "low-res" in the From box and "hi-res" in the To box.
Click the Add button.
Choose ".jpg" from the From dropdownlist and ".tif" from the To
dropdownlist.
Click the Add button.

Click the Relink button.


A text file should open with the default editor displaying the strFeedback text, indicating that
all of the JPEG images in the "low-res" folder have been replaced by TIFF images in the "hi-
res" folder.

217
If you try the script on any of your own documents, be sure to
choose File > Revert afterwards.

Working with links


Links are basically, the stuff that gets listed in the InDesign Links panel and which may be one
of four types: graphic, story, movie or sound. We briefly encountered links when we accessed
the itemLink property of the graphic object. Accessing graphic links in this way can be useful
because you are always sure that you are dealing with a graphic link. However, you can also
work directly with the links collection of a document.
Ascertaining the link type
You can verify the link type by using the syntax myLink.parent.constructor.name. However,
this statement does not just return the four options listed above (graphic, story, movie or
sound). Instead of the generic graphic object, you will get a graphic sub-type, such as EPS,
Image, PDF, PICT, WMF. To identify graphic links, therefore, you may find it easier just to
say that if myLink.parent.constructor.name does not return "Story", "Movie" or "Sound", it
is a graphic—as shown in listing 9-4, below.
Listing 9-4: Isolating graphics within the links collection
1 var doc = app.activeDocument;
2 for (i = 0; i < doc.links.length; i ++){
3 if(doc.links[i].parent.constructor.name != "Movie"
4 && doc.links[i].parent.constructor.name != "Sound"
5 && doc.links[i].parent.constructor.name != "Story"){
6 var strPage =doc.links[i].parent.parentPage.name;
7 var strPath = doc.links[i].filePath;
8 app.select(doc.links[i].parent.parent);
9 alert("Page " + strPage + ": " + strPath);
10 }
11 }

The if statement in lines 3 to 10, above, first checks that myLink.parent.constructor.name is

218
not equal to "Story", "Movie" or "Sound", then selects the parent of the parent of the link—the
graphic frame that contains it—and displays an alert detailing the page on which the graphic is
located and its file path. (The parent of a link is the graphic; the parent of the graphic is the
containing frame.)
Identifying poster images
Poster images associated with movies and sound will also be listed in the Links panel. They
can be identified using the conditional syntax:
myLink.parent.parent.constructor.name == "Movie"
myLink.parent.parent.constructor.name == "Sound"
In the case of a poster image, the parent of the link is the graphic (the poster image itself) and
the parent of the parent is the sound or movie to which the poster image is attached.
In listing 9-5, below, we loop through all of the links in a document, displaying an alert
whenever a poster image is encountered.
Listing 9-5: Identifying poster images within the links collection
1 var doc = app.activeDocument;
2 for(var i = 0; i < doc.links.length; i ++){
3 if(doc.links[i].parent.parent.constructor.name == "Movie"
4 || doc.links[i].parent.parent.constructor.name == "Sound"){
5 var strPage =doc.links[i].parent.parentPage.name;
6 var strPath = doc.links[i].filePath;
7 app.select(doc.links[i].parent);
8 alert("Poster image, page " + strPage + ": " + strPath);
9 }
10 }

Excluding both media and posters


If you were doing an operation which needed to process images but wanted to completely
ignore poster images and the media that contain them, you could simply use a conditional
statement that combined the two just used in listing 9-4 and listing 9-5.
Listing 9-6: Excluding poster images and media
1 var doc = app.activeDocument;
2 for (i = 0; i < doc.links.length; i ++){
3 if(doc.links[i].parent.constructor.name != "Movie"
4 && doc.links[i].parent.constructor.name != "Sound"
5 && doc.links[i].parent.constructor.name != "Story"
6 && doc.links[i].parent.parent.constructor.name != "Movie"
7 && doc.links[i].parent.parent.constructor.name != "Sound"
8 && doc.links[i].parent.parent.constructor.name != "Story"
9 ){
10 var strPage =doc.links[i].parent.parentPage.name;
11 var strPath = doc.links[i].filePath;
12 app.select(doc.links[i].parent.parent);
13 alert("Page " + strPage + ": " + strPath);
14 }
15 }

FilePath versus name

219
As we have seen, the link object has a filePath property which returns the full path to the
linked file. In addition, there is also a name property which returns just the file name.
Naturally, if you subtract name from fullPath, you get the folder that contains the linked image.
In listing 9-7, below, we loop through the allGraphics collection of the active document and
populate three variables with the file path, file name and folder which we then display in an
alert.
Listing 9-7: Isolating the file path, file name and folder of a linked graphic
1 var doc = app.activeDocument;
2 for (i = 0; i < doc.allGraphics.length; i ++){
3 if( doc.allGraphics[i].itemLink != null){
4 var strPage = doc.allGraphics[i].parentPage.name;
5 var strPath = doc.allGraphics[i].itemLink.filePath;
6 var strName = doc.allGraphics[i].itemLink.name;
7 var strFolder = strPath.replace(strName, "");
8 app.select(doc.allGraphics[i].parent);
9 strAlert = "Page " + strPage + "\n\nFile path: " + strPath;
10 strAlert += "\nFile name: " + strName;
11 strAlert += "\nFolder: " + strFolder;
12 alert(strAlert);
13 }
14 }

Embedding and unembedding linked graphics


Images can be embedded in two ways in InDesign. Firstly, you can copy an image from any
location and paste it into an InDesign document. This creates an embedded image with no
linked disk file. Secondly, you can use the Links panel menu to embed a linked image—one
imported using File > Place. When you embed a linked graphic, InDesign remembers the
location of the original linked file, enabling you to unembed the image at a later stage.
We have encountered the syntax for identifying the first type of embedded image. If the
statement myGraphic.itemLink == null is true, then we know that the graphic is embedded
and there is no linked file. To identify an embedded linked graphic, we use the conditional
syntax:
myGraphic.itemLink.status == LinkStatus.LINK_EMBEDDED
The status property is read-only. However, the link object does have a couple of methods
which allow you to embed and unembed linked graphics.
• To embed a linked graphic, use myGraphic.unlink()
• To unembed, use myGraphic.unembed()
The script in listing 9-8, below, displays a dialog which allows the user to choose whether
they wish to embed or unembed linked images.

220
Figure 9-2: A simple script for embedding and umembedding linked graphics.

Listing 9-8: Embedding and unembedding images


1 #targetengine session
2 if(app.documents.length ==0){
3 alert("Please open a document in order to continue.");
4}
5 else{
6 var doc = app.activeDocument;
7 var win = new Window("window", "Embed/Unembed linked images");
8 win.alignChildren = "left";
9 win.radEmbed = win.add("radiobutton", undefined, "Embed all linked images");
10 win.radUnembed = win.add("radiobutton", undefined, "Unembed all embedded linked images");
11 win.btnContinue = win.add("button", undefined, "Continue");
12 win.btnContinue.onClick = function(){
13 for (i = 0; i < doc.allGraphics.length; i ++){
14 if(doc.allGraphics[i].itemLink != null){
15 if(win.radEmbed.value == true){
16 doc.allGraphics[i].itemLink.unlink ();
17 }
18 else if(win.radUnembed.value == true){
19 doc.allGraphics[i].itemLink.unembed ();
20 }
21 }
22 }
23 }
24 win.show();
25 }

On lines 7 to 11, we construct a simple window with two radio buttons and a button. The
onClick callback loops through all of the linked graphics in the document, tests whether either
of the radio buttons has been activated and uses the unlink() or unembed() method as
appropriate.

Try it for yourself!


TUTORIAL: Unembedding unlinked images
As we have seen, when an image is pasted into InDesign, it does not appear in the Links panel
and is not therefore checked during preflighting. The itemLink property of the graphics object

221
can be used to identify such images: if the itemLink property is null, then the image is purely
embedded and there is no linked file.
In this tutorial, we will create a script which scans the active document looking for embedded,
unlinked images. If it does not find any, it displays a message to that effect.

If any embedded files are found, the JPEG export dialog shown below is displayed, allowing
the user to choose the settings for the files being exported.

The dialog contains a subset of the options found in InDesign's Export JPEG dialog which
allows you to export either a selected image or an entire page as a JPEG file. However,
whereas using File > Export, you can only export one selected image at a time; with scripting
you can loop through all of the images in a document and export all of the unembedded,
unlinked ones.
1. Creating the main script
(As always, the completed version of this script can be found in the "chapter09" folder. It is
called "09-unembed_completed.jsx")
In the ESTK, choose File > New JavaScript.
Save the new file in the "chapter09" folder under the name "09-
unembed.jsx".
Enter the following code.

222
1 var g ={};
2 main();
3 g = null;
4
5 function main(){
6 var blnLinks = findEmbedded();
7 if(blnLinks){
8 var intResult = createDialog();
9 }
10 if(intResult == 1){
11 exportImages();
12 }
13 else if(intResult == 2){
14 alert("Operation cancelled by user.");
15 }
16 }
In the main() function, we call the function findEmbedded(), which will scan the document
looking for unembedded, unlinked images. If the function returns true, indicating that such
images were found, we then call createDialog() (line 8), which will enable the user to set the
export parameters. If they click the OK button (returning the value 1), we call the function
exportImages() (line 11); if they click Cancel, we display an alert (line 14).
2. The findEmbedded() function
Now let's create the findEmbedded() function.
Add the following code at the end of your script.

17
18 function findEmbedded(){
19 if (app.documents.length ==0){
20 alert("Please open a document before continuing.");
21 }
22 else{
23 var doc = app.activeDocument;
24 g.arrEmbedded = [];
25 for (var i = 0; i < doc.allGraphics.length; i ++)
26 {
27 if (doc.allGraphics[i].itemLink == null)
28 {
29 g.arrEmbedded.push(doc.allGraphics[i]);
30 }
31 }
32 if (g.arrEmbedded.length < 1)
33 {
34 alert("The document contains no embedded, unlinked images");
35 }
36 else{
37 g.fldBasePath = Folder.selectDialog("Please select a folder for the extracted images.");
38 if(g.fldBasePath != null){
39 return true;

223
40 }
41 else{
42 alert("Operation cancelled");
43 return false;
44 }
45 }
46 }
47 }

Save your changes.


On line 24, we create the array variable called g.arrEmbedded then, on lines 25 to 31, we loop
through all of the graphics in the allGraphics collection of the active document, testing for
unembedded and unlinked images:
27 if (doc.allGraphics[i].itemLink == null)
Whenever we find such an image, we add it to the g.arrEmbedded array:
29 g.arrEmbedded.push(doc.allGraphics[i]);
Once we have processed all of our graphics, we test to see whether g.arrEmbedded is still
empty. If it is, we display and error message and the script ends (line 34). If there are any
graphics in g.arrEmbedded, we display a dialog for the user to choose the folder in which the
extracted images should be placed.
37 var fldBasePath = Folder.selectDialog("Please select a folder for the extracted images.");
If the user chooses a folder, the function returns true (line39). If, instead, the user clicks the
Cancel button, an error alert is displayed and we end the script by returning false (lines 42-
43).
3. The createDialog() function
Add the following function skeleton at the end of your script,
below all of the existing code.
48
49 function createDialog(){
50 g.win = new Window('dialog', 'Extract embedded files');
51
52 // Colour space dropdown
53
54 // Resolution dropdown
55
56 // Quality dropdown
57
58 // Rendering style dropdown
59
60 // File name prefix edittext
61
62 // Buttons
63

224
64 // display dialog
65 g.win.show();
66
67 }

Having created the dialog, we need to insert seven groups, the first six each containing a static
text label and a control and the seventh two buttons. The first group of controls allows the user
to choose the colour space.
Position the cursor on line 53, below the comment "// Colour space
dropdown".
Insert the code shown below.
52 // Colour space dropdown
53 g.win.grpSpace = g.win.add('Group');
54 g.win.grpSpace.stx = g.win.grpSpace.add('StaticText', undefined, 'Colour Space:');
55 g.win.grpSpace.stx.size=[100,20];
56 g.win.grpSpace.ddl = g.win.grpSpace.add('DropDownList', undefined, ["CMYK", "RGB"]);
57 g.win.grpSpace.ddl.size = [100,20];
58 g.win.grpSpace.ddl.selection = 0;
59
60 // Resolution dropdown

Save your changes.


Since the dropdownlist control only has two items, when we create it (on line 56), we use the
third agurment (items) to specify the values it should display by supplying an array of two
values. Then, on line 58, we set the default item to zero—the first item in the list.
If you wish to test your script, open the file called "09-
unembed.indd" in the "chapter09" folder which contains a series of
unlinked images. When you run your script, you should see the
following dialog.

Press the Escape key on your keyboard to dismiss the dialog.


Position the cursor below the comment "// Resolution dropdown",
225
and enter the following code.
60 // Resolution dropdown
61 g.win.grpRes = g.win.add('Group');
62 g.win.grpRes.stx = g.win.grpRes.add('StaticText', undefined, 'Resolution:');
63 g.win.grpRes.stx.size=[100,20];
64 g.win.grpRes.ddl = g.win.grpRes.add('DropDownList', undefined, ["300", "150", "72"]);
65 g.win.grpRes.ddl.size = [100,20];
66 g.win.grpRes.ddl.selection = 0;
67
68 // Quality dropdown

This is a similar setup to the first group and, as before, when we create the dropdown (on line
64), we use the items argument to specify an array of 3 values. Again, on line 66, we set the
default item to zero—the first item in the list.
Position the cursor on what should be line 69, below the comment
"// Quality dropdown", and enter the following code.
68 // Quality dropdown
69 g.win.grpQuality = g.win.add('Group');
70 g.win.grpQuality.stx = g.win.grpQuality.add('StaticText', undefined, 'Quality:');
71 g.win.grpQuality.stx.size=[100,20];
72 g.win.grpQuality.ddl= g.win.grpQuality.add('DropDownList', undefined, ["Maximum", "High", "Medium", "Low"]);
73 g.win.grpQuality.ddl.size = [100,20];
74 g.win.grpQuality.ddl.selection = 0;
75
76 // Rendering style dropdown

Position the cursor on line 77, below the comment "// Rendering
style dropdown", and enter the following code.
76 // Rendering style dropdown
77 g.win.grpRender = g.win.add('Group');
78 g.win.grpRender.stx = g.win.grpRender.add('StaticText', undefined, 'Rendering:');
79 g.win.grpRender.stx.size=[100,20];
80 g.win.grpRender.ddl= g.win.grpRender.add('DropDownList', undefined, ["Baseline", "Progressive"]);
81 g.win.grpRender.ddl.size = [100,20];
82 g.win.grpRender.ddl.selection = 0;
83
84 // File name prefix edittext

Position the cursor on line 85, below the comment "// File name
prefix edittext", and enter the following code.

226
84 // File name prefix edittext
85 g.win.grpPrefix = g.win.add('Group');
86 g.win.grpPrefix.stx = g.win.grpPrefix.add('StaticText', undefined, 'File prefix:');
87 g.win.grpPrefix.stx.size=[100,20];
88 g.win.grpPrefix.txt= g.win.grpPrefix.add('edittext', undefined, 'extracted');
89 g.win.grpPrefix.txt.size = [100,20];
90
91 // Buttons
Note that, on line 88, when we create the edittext control, we use the third argument of the
add() method to supply a default value prefix "extracted".
Position the cursor on line 92, below the comment "// Buttons",
and enter the following code.
91 // Buttons
92 g.win.grpBut = g.win.add('Group');
93 g.win.grpBut.export = g.win.grpBut.add('Button', undefined, 'Export Files');
94 g.win.grpBut.export.onClick = function (){g.win.close(1);}
95 g.win.grpBut.cancel = g.win.grpBut.add('Button', undefined, 'Cancel');
96
97 // display dialog
98 g.win.show();
99
100 } // End createDialog function

Save your changes.


That completes the dialog.
On line 94, we assign an inline onClick callback function to the Export Files button which
simply closes the window, returning the value 1. Back in the main function, this will cause the
function exportImages() to be executed. Let's move on to writing the exportImages()
function.
4. The exportImage() function
When the user clicks the Export Files button, we need to do two things. Firstly, we need to set
the jpegExportPreferences to match the choices made by the user; and, secondly, we need to
loop through all of the images in the g.arrEmbedded array and export each one to the folder
designated by the user at the start of the script.
Add the following function skeleton at the end of your script.
98 g.win.show();
99
100 } // End createDialog function
101
102 function exportImages(){
103
104 // Set export preferences

227
105
106 // Export images
107
108 }
The jpegExportPreferences object is a property of the application object which allows the
programmer to predetermine the settings used when exporting a graphic in JPEG format. We
will need to use the settings specified by the user via our dialog to change each of the relevant
jpegExportPreferences. To make life easier, we will be taking advantage of the fact that object
properties can be expressed in two ways; either:
myObject.propertyX
or:
myObject["propertyX"]
where propertyX is written as a string value. Where appropriate, we will read string values
from the controls in our dialog and use them as property names between square brackets.
Position the cursor on line 105, below the comment "// Set export
preferences", and enter the following code.
104 // Set export preferences
105 with (app.jpegExportPreferences){
106 jpegExportRange = ExportRangeOrAllPages.EXPORT_ALL;
107 exportResolution = parseInt(g.win.grpRes.ddl.selection.text);
108 jpegColorSpace = JpegColorSpaceEnum[g.win.grpSpace.ddl.selection.text];
109 jpegQuality = JPEGOptionsQuality[g.win.grpQuality.ddl.selection.text.toUpperCase()];
110 var strRender = g.win.grpRender.ddl.selection.text .toUpperCase()+ "_ENCODING";
111 jpegRenderingStyle = JPEGOptionsFormat[strRender];
112 }
113
114 // Export images
115
116 }

Save your changes.


To avoid having to repeat app.jpegExportPreferences. at the start of each line, we enclose
our code in a with statement. The statement following the keyword with will be automatically
inserted at the start of each line within the code block following the with.
On line 106, since we want to process all pages in the document, we set the exportRange
property of the jpegExportPreferences object to the preset enumeration
ExportRangeOrAllPages.exportAll.
On line 107, since the resolution has been entered as a string value, we use the parseInt()
function to convert it to an integer, which we assign as the value of the exportResolution
property.
On line 108, when we set the jpegColorSpace property, we assign the value by placing the
option chosen by the user via the win.grpSpace.ddl dropdownlist in square brackets after the

228
prefix JpegColorSpaceEnum. Since the dropdown list items were generated from the array
["CMYK", "RGB"], this will give use one of the following two values:
JpegColorSpaceEnum["CMYK"]
JpegColorSpaceEnum["RGB"]
which are equivalent to:
JpegColorSpaceEnum.CMYK
JpegColorSpaceEnum.RGB
On line 109, we use the same technique to set the jpegQuality property to one of the following
values:
JPEGOptionsQuality["Maximum"]
JPEGOptionsQuality["High"]
JPEGOptionsQuality["Medium"]
JPEGOptionsQuality["Low"]
And we do the same again to set the rendering style (lines 110 to 111). This time, to create a
legitimate value, we need to read the selected text from the dropdownlist win.grpRender.ddl
and then append the string "_ENCODING". We assemble the string in the variable strRender
and then use strRender in square brackets to generate the correct value for the
jpegRenderingStyle property.

SYNTAX
SUMMARY ... Properties of the JPEGExportPreference object

The range of pages to be exported.


jpegExportRange ExportRangeOrAllPages.EXPORT_RANGE
ExportRangeOrAllPages.EXPORT_ALL
exportResolution The resolution at which to export the image. A real number between 0 and 2400.
JpegColorSpaceEnum.RGB,
jpegColorSpace JpegColorSpaceEnum.CMYK,
JpegColorSpaceEnum.GRAY
JPEGOptionsQuality.LOW,
JPEGOptionsQuality.MEDIUM,
jpegQuality
JPEGOptionsQuality.HIGH,
JPEGOptionsQuality.MAXIMUM
JPEGOptionsFormat.BASELINE_ENCODING
jpegRenderingStyle
JPEGOptionsFormat.PROGRESSIVE_ENCODING

Position the cursor on line 115, below the comment "// Export
images", and enter the following code.
114 // Export images
115 for (var i = 0; i < g.arrEmbedded.length; i ++)

229
116 {
117 var img = File(g.fldBasePath + "/" + g.win.grpPrefix.txt.text + (i +1) + ".jpg");
118 g.arrEmbedded[i].exportFile(ExportFormat.JPG, img);
119 g.arrEmbedded[i].parent.place(img);
120 }
121 alert(g.arrEmbedded.length + " images exported and linked.");
122 }

Save your changes.


As we are looping through the graphics in the g.arrEmbedded array, on line 117, we create a
File object and assemble a file path for it consisting of g.fldBasePath (the folder selected by
the user at the start of the script), the "/" separator, the current value of the counter variable i
(plus 1, since it is zero-based) followed by the file extension ".jpg".
On line 118, we use the exportFile() method to export the file in JPEG format to the file
location specified on line 117 using the variable img.
On line 119, we import the exported file back into the parent frame containing the image, using
the place() method.
Finally, we close the window and display an alert confirming the number of images exported
and linked.
Test the script using the file "09-unembed.indd" in the "chapter09"
folder. (It contains a series of embedded, unlinked images.)
Choose the folder called "extracted" in the "chapter09" folder, enter
a prefix for the file names in the dialog, then click the Export
button.
Check that the files are being exported to the designated folder.
Look in the Links panel to verify that the images are now linked to
the exported files.

The image object


If you are interested in targetting specific types of imported images, you can make a reference
to one of the child objects of the graphic object. Image is perhaps the most important child,
including as it does the key bitmapped file formats: TIFF, PSD, PNG, JPEG and GIF. You
would therefore work with the image collection rather than the graphics collection when you
want to target bitmapped images rather than vectors.
A good way of accessing the image object is via the allPageItems collections which differs
from the pageItems collection in that it includes the children of each page item, the children of

230
their children, and so on, all the way to the end of each branch in the object hierarchy. Thus to
access all of the image objects on a page, we would loop through myPage.allPageItems and
use myPage.allPageItems[x].constructor.name == "Image" to identify each image object.
Listing 9-10, below, illustrates this procedure.
Listing 9-10: Targeting the image object via the allPageItems collection
1 var doc = app.activeDocument;
2 for(var h = 0; h < doc.pages.length; h++){
3 for (var i = 0; i < doc.pages[h].allPageItems.length; i ++){
4 var strPage = doc.pages[h].allPageItems[i].parentPage.name;
5 if(doc.pages[h].allPageItems[i].constructor.name == "Image"){
6 if(doc.pages[h].allPageItems[i].itemLink != null){
7 var strPath = doc.pages[h].allPageItems[i].itemLink.filePath;
8 app.select(doc.pages[h].allPageItems[i].parent);
9 alert("Page " + strPage + ": " + strPath);
10 }
11 else{
12 alert("Page " + strPage + ": Embedded graphic with no linked file");
13 }
14 }
15 }
16 }
On line 2, we begin a loop through all of the pages in the document using the counter variable
h. On line 3, we begin a nested loop (using counter variable i) which loops through the
allPageItems collection of each page.
On line 5, we test whether the current member of alIPageItems is an image. If it is and it is also
a linked image, we select it (line 8) and display its file path (line 9).
Independent and anchored graphics
We have discussed four different object collections through which it is possible to reference
the graphics within a document: allGraphics, links, pageItems and allPageItems. The pageItems
collection is the only one of these which completely ignores anchored graphics. In fact, the
only object it will recognize is the text frame which contains the image.
When using the allGraphics collection, you can distinguish between independent and anchored
graphics with the conditional statement:
myGraphic.parent.parent.constructor.name == "Character".
The parent of the graphic is the frame that contains it and, if the graphic is anchored, the parent
of the frame will be a Character object.
The script in listing 9-11, below, shows how to convert anchored graphics into independent
ones.
Listing 9-11: Changing anchored images to independent
1 #targetengine = session
2 if(app.documents.length == 0){
3 alert("Please open the document containing the images to be relinked.");
4}
5 else{
6 var doc = app.activeDocument;

231
7 var win = new Window("window", "Move Anchored Graphics");
8 win.add("statictext", undefined, "Text wrap style:");
9 arrWrap = ["NONE"];
10 arrWrap.push("JUMP OBJECT TEXT WRAP");
11 arrWrap.push("NEXT COLUMN TEXT WRAP");
12 arrWrap.push("BOUNDING BOX TEXT WRAP");
13 arrWrap.push("CONTOUR");
14 win.ddlWrap = win.add("dropdownlist", undefined, arrWrap);
15 win.ddlWrap.selection = 3;
16 win.add("statictext", undefined, "Text wrap offset (mm):");
17 win.txtOffset = win.add("edittext", undefined, "5");
18 win.txtOffset.minimumSize = [50, 20];
19 win.btnMove = win.add("button", undefined, "Move Anchored Graphics");
20 win.btnMove.onClick = function(){
21 for (var i = 0; i < doc.allGraphics.length; i ++){
22 var strPage = doc.allGraphics[i].parentPage.name;
23 if(doc.allGraphics[i].parent.parent.constructor.name == "Character"){
24 app.select(doc.allGraphics[i].parent);
25 alert("Page " + strPage );
26 var x = doc.allGraphics[i].parent.geometricBounds[1];
27 var y = doc.allGraphics[i].parent.geometricBounds[0];
28 var graphOrig = doc.allGraphics[i].parent;
29 var graphNew = doc.allGraphics[i].duplicate([x , y]);
30 graphOrig.remove();
31 graphNew.bringToFront();
32 var strWrap = win.ddlWrap.selection.text.replace(/ /g, "_");
33 graphNew.textWrapPreferences.textWrapMode = TextWrapModes[strWrap];
34 intOffset = parseInt(win.txtOffset.text) + "mm";
35 switch(win.ddlWrap.selection.text){
36 case "JUMP OBJECT TEXT WRAP":
37 graphNew.textWrapPreferences.textWrapOffset = [intOffset, intOffset];
38 case "BOUNDING BOX TEXT WRAP":
39 graphNew.textWrapPreferences.textWrapOffset = [intOffset, intOffset, intOffset, intOffset];
40 break;
41 case "NEXT COLUMN TEXT WRAP":
42 case "CONTOUR":
43 graphNew.textWrapPreferences.textWrapOffset = intOffset;
44 break;
45 }
46 win.close();
47 }
48 }
49 }
50 win.show();
51 }

On lines 7 to 19, we construct the basic ScriptUI window shown below.

232
It allows the user to choose a text wrap mode equivalent to the five icons at the top of the Text
Wrap panel and to specify the text wrap offset measurement which, here, is assumed to be in
millimetres.
On lines 20 to 49, we define a callback function for the Move Anchored Graphics button.
Inside the function, we loop through the allGraphics collection and test each one to see if it is
anchored (line 23). If it is, then we create a copy of the graphic at the same coordinates as the
original. This is done by capturing the x1 and y1 coordinates of the graphic from its
geometricBounds property into variables called x and y.
26 var x = doc.allGraphics[i].parent.geometricBounds[1];
27 var y = doc.allGraphics[i].parent.geometricBounds[0];
(The geometricBounds of an image are expressed in the format [y1, x1, y2, x2].)
On line 29, we duplicate the image and supply the optional first parameter—the location of the
new image—using the x and y values we took from the original image.
29 var graphNew = doc.allGraphics[i].duplicate([x , y]);
We then delete the original image and bring the duplicate to the front (lines 30 and 31).
On lines 32 and 33, to set the text wrap mode, we read the value chosen by the user and
replace all spaces with underscores to make it code-ready.
32 var strWrap = win.ddlWrap.selection.text.replace(/ /g, "_");
33 graphNew.textWrapPreferences.textWrapMode = TextWrapModes[strWrap];
We then use this value inside square brackets after TextWrapModes to generate a value in the
format:
TextWrapModes["BOUNDING_BOX_TEXT_WRAP"]
which, as we saw earlier, is equivalent to:
TextWrapModes.BOUNDING_BOX_TEXT_WRAP .
Our final task is to set the text wrap offset. We begin by capturing the value entered by the user
inside a variable called intOffset and converting it into an integer.
34 intOffset = parseInt(win.txtOffset.text) + "mm";
However, since the format of the text wrap offset depends on the text wrap mode, we use a
switch statement to dictate the value to which the text wrap offset is set (lines 35 to 45).
If you would like to quickly test out this script, try opening the file called "11.anchored.indd"
in the "chapter09" folder. It contains a few anchored graphics.

233
SYNTAX
SUMMARY Properties of the TextWrapPreference object
...

Programmatic equivalents of the five icons at the top of the Text Wrap panel.
1. TextWrapModes.NONE,
2. TextWrapModes.JUMP_OBJECT_TEXT_WRAP,
textWrapMode
3. TextWrapModes.NEXT_COLUMN_TEXT_WRAP,
4. TextWrapModes.BOUNDING_BOX_TEXT_WRAP,
5. TextWrapModes.CONTOUR
The distance between the graphic and surrounding text.
If textWrapMode is set to 3 or 5 above, a single value is required—e.g. 5 or "5mm".
If textWrapMode is set to option 2 above, two values are required, in the order top, bottom
textWrapOffset
—e.g. [0.5, 0.75] or ["0.5in", "0.75in"].
If textWrapMode is set to option 4 above, four values are required in the order top, left,
bottom, right—e.g. [6, 12, 6, 12] or ["6pt", "12pt", "6pt", "12pt"].

Try it for yourself!


TUTORIAL: Finding stretched images
Let's end this chapter with a tutorial.
When images are scaled, it is always possible for their aspect ratios to be compromised and
this can be very difficult and time-consuming to detect. With scripting, it becomes easy: you
compare the horizontalScale value with the verticalScale value; if they are not identical, then
the image has been stretched.
The script in this tutorial loops through all the images in the current document, comparing their
horizontal and vertical scale. If no stretched images are found, an alert box is displayed giving
a clean bill of health.

If any stretched files are found, the Fix stretched images dialog shown below is displayed.

234
It allows the user to choose a method for fixing the problem:
• Use 100% for both
• Use horizontal scale for both
• Use vertical scale for both
Changing the scaling of an image will almost certainly affect the way in which the image sits in
its container and parts of the image may become hidden. So, the second drop-down in the
dialog asks the user to choose a fitting method:
• Fit frame to content
• Fill frame proportionally
• Fit content proportionally
When the user clicks on the Fix Images button, the script checks the value of the two
dropdowns and applies the necessary changes to each stretched image in the document.
1. Creating the main() function
(As always, the completed version of this script can be found in the "chapter09" folder. It is
called "12-stretched-images_completed.jsx")
In the ESTK, choose File > New JavaScript.
Save the new file in the "chapter09" folder under the name "12-
stretched-images.jsx".
Enter the following code.
1 var g = {};

235
2 main();
3 g = null;
4
5 function main(){
6 blnStretched = findStretched();
7 if(blnStretched){
8 var intResult = createDialog();
9 }
10 if(intResult == 1){
11 fixImages();
12 }
13 }
In the main() function, we call the function findStretched(), which will scan the document
looking for stretched images. If the function returns true, indicating that some stretched images
have been found, we then call createDialog() (line 8). If the user clicks the Fix Images button,
the callback function for the button will return 1, meaning that we call the function fixImages()
(line 11).
2. The findStretched() function
Now on to the findStretched() function.
Add the following code at the end of your script.
14
15 function findStretched(){
16 if(app.documents.length == 0){
17 alert("No documents are open.");
18 }
19 else{
20 var doc = app.activeDocument;
21 g.arrStretched = [];
22 for (var i = 0; i < doc.allGraphics.length; i ++){
23 if (doc.allGraphics[i].horizontalScale != doc.allGraphics[i].verticalScale){
24 g.arrStretched.push(doc.allGraphics[i]);
25 }
26 }
27 if (g.arrStretched.length == 0){
28 alert("No stretched images were found.");
29 return false;
30 }
31 else{
32 return true;
33 }
34 }
35 }

Save your changes.


On line 16, we test that there is a document open before placing the active document in a
variable called doc (line 20).

236
On lines 21 to 26, we then create an array inside the global variable called g.arrStretched
and loop through the graphics in the document, testing whether the horizontal and vertical
scales are the same. When we find a graphic where this is not the case, we add it to
g.arrStretched (line 24).
After completing the loop, we test the length of g.arrStretched. If it still zero, we know that no
stretched images have been found and display an alert to this effect then return false (lines 28
and 29); otherwise, the function returns true back to the main() function, triggering the
execution of the createDialog() function.
3. The createDialog() function
Add the following function skeleton at the end of your script,
below all of the existing code.
36
37 function createDialog(){
38 g.win = new Window('dialog', 'Fix stretched images');
39
40 // Fix method dropdown
41
42 // Fitting dropdown
43
44 // Buttons
45
46 // display dialog
47 return g.win.show();
48 }

Now position the cursor on the line below the comment "// Fix
method dropdown" and insert the code shown below.
40 // Fix method dropdown
41 g.win.grpScale = g.win.add('Group');
42 g.win.grpScale.stx = g.win.grpScale.add('StaticText', undefined, 'Fix Method:');
43 g.win.grpScale.stx.size = [75, 20];
44 g.win.grpScale.ddl = g.win.grpScale.add('DropDownList');
45 g.win.grpScale.ddl.size = [200,20];
46 g.win.grpScale.ddl.add('item', "Use 100% for both");
47 g.win.grpScale.ddl.add('item', "Use horizontal scale for both");
48 g.win.grpScale.ddl.add('item', "Use vertical scale for both");
49 g.win.grpScale.ddl.selection = 0;
50
51 // Fitting dropdown

Save your changes.

237
On line 44, we create the drop down list without specifying the items it will display; then, on
lines 46 to 48, we use the control add() method to insert the 3 entries it will contain. Finally,
on line 49, we specify the first item (index zero) as the default selection.
To test the script, open the file called "12-stretched-images.indd" in
the "chapter09" folder which contains a series of discernibly
stretched images. When you run your script, you should see the
following dialog fragment.

Press the Escape key on your keyboard to dismiss the dialog.


Position the cursor below the comment "// Fitting dropdown", and
enter the following code.
51 // Fitting dropdown
52 g.win.grpFit = g.win.add('Group');
53 g.win.grpFit.stx = g.win.grpFit.add('StaticText', undefined, 'Fitting:');
54 g.win.grpFit.stx.size=[75,20];
55 g.win.grpFit.ddl = g.win.grpFit.add('DropDownList');
56 g.win.grpFit.ddl.size = [200,20];
57 g.win.grpFit.ddl.add('item', "Fit frame to content");
58 g.win.grpFit.ddl.add('item', "Fill frame proportionally");
59 g.win.grpFit.ddl.add('item', "Fit content proportionally");
60 g.win.grpFit.ddl.selection = 0;
61
62 // Buttons

To complete the createDialog() function, position the cursor on


the line below the comment "// Buttons", and enter the following
238
code.
62 // Buttons
63 g.win.grpBut = g.win.add('Group');
64 g.win.grpBut.fix = g.win.grpBut.add('Button', undefined, 'Fix Images');
65 g.win.grpBut.fix.onClick = function(){g.win.close(1);}
66 g.win.grpBut.cancel = g.win.grpBut.add('Button', undefined, 'Cancel');
67
68 // display dialog
69 return g.win.show();
70 }

Save your changes.


On line 65, we assign an inline onClick callback function to the Fix images button which
simply closes the window, returning the value 1. This value ends up in the variable intResult
back in the main() function and triggers the execution of the fixImages() function.
4. The fixImages() function
Since the method used to fix the images is determined by the user, we will use a couple of
switch statements to check which options they have chosen and use the appropriae methods.
Add the following function to the end of your script.
71
72 function fixImages(){
73 for (var i = 0; i < g.arrStretched.length; i++){
74 switch(g.win.grpScale.ddl.selection.index)
75 {
76 case 0:
77 g.arrStretched[i].horizontalScale = 100;
78 g.arrStretched[i].verticalScale = 100;
79 break;
80 case 1:
81 g.arrStretched[i].verticalScale = g.arrStretched[i].horizontalScale;
82 break;
83 case 2:
84 g.arrStretched[i].horizontalScale = g.arrStretched[i].verticalScale;
85 break;
86 }
87 switch(g.win.grpFit.ddl.selection.index)
88 {
89 case 0:
90 g.arrStretched[i].fit(FitOptions.FRAME_TO_CONTENT);
91 break;
92 case 1:
93 g.arrStretched[i].fit(FitOptions.FILL_PROPORTIONALLY);
94 break;
95 case 2:

239
96 g.arrStretched[i].fit(FitOptions.PROPORTIONALLY);
97 g.arrStretched[i].fit(FitOptions.FRAME_TO_CONTENT);
98 break;
99 }
100 }
101 alert(g.arrStretched.length + " stretched images fixed.");
102 }

Save your changes.


The function loops through the items inside g.arrStretched. Inside the loop we have our two
switch statements The first one (lines 74 to 86) tests which of the three options in the scale
dropdown was chosen by the user and makes the appropriate adjustment to the
horizontalScale and verticalScale values.
The second (lines 87 to 99) tests which fit options settings the user has chosen and applies the
appropriate one.
Finally, on line 101, we display an alert which confirms the number of stretched images which
have been fixed, based on the number of items in g.arrStretched.
Test the script, either from the Scripts panel or the ESTK using the
file "12-stretched-images.indd" in the "chapter09" folder.
Experiment with the different options, using File > Revert after
each test.

240
CHAPTER 10. Page Items and Layers
The term pageItem covers a multitude of sins in InDesign: everything from a line to a movie.
The simplest way to decide what is and isn't a pageItem is this: can you select the item with the
selection tool. If you can, then it's a pageItem. So, basically, that's everything you can possibly
put on an InDesign page apart from text and tables—which can only be selected with the text
tool.
Given that the object is so wide ranging, why do we need it? Surely it makes more sense to
target each element as a specific object type, such as textFrame, graphic, oval or rectangle?
Well, sometimes it does; but sometimes you want to target items just as objects on the page;
and it's here that the pageItem object becomes useful.
Working with layers is one example of where page items are the most useful object. It is
usually the pageItem that you want to move to between layers. Other objects may be nested
inside containers and therefore not capable of being transferred independently.
The pageItems and allPageItems collections
InDesign offers two object collections relating to the pageItem object: pageItems and
allPageItems. The pageItems collection contains only the top level page items without
reference to any pageItem objects each may contain.
By contrast, the allPageItems collection incorporates a drill-down mechanism and contains not
only all pageItem objects within the targeted container but also any objects they or their
children contain.
To illustrate this difference, in InDesign, open the file “01-pageitems.indd” in the "chapter10"
folder. (The page contains a text frame in which we have a rectangular picture box, in which
we have an image.) Next, in the ESTK, open the file pageItems01.jsx and run the script with
InDesign as the target application.
The script loops through all of the pageItems on the first page of the active document. Inside
the loop, the script displays an alert declaring the type of each item. The user can choose
whether the script loops through the pageItems or allPageItems collections by clicking one of
two radio buttons.
Run the script and click the Page Items radio button: you will notice that only one alert is
displayed—for the textFrame. Now click on the All Page Items button: this time there will be
three alerts—one for the text frame, one for the rectangle and one for the image. In other
words, there is only one object in the pageItems collection; but the allPageItems collection
contains three objects.

241
Figure 10-1: The pageItems collection sees only top level objects and ignores all descendants. The
allPageItems collection sees not only child objects but also all of their descendants.

The script is shown in listing 10-1, below.


Listing 10-1: The difference between pageItems and allPageItems
1 var win = new Window('dialog', 'pageItems vs allPageItems');
2 win.grpItems = win.add('Group');
3 win.grpItems.radPg = win.grpItems.add('RadioButton', undefined, 'pageItems');
4 win.grpItems.radAllPg = win.grpItems.add('RadioButton', undefined, 'allPageItems');
5 win.grpItems.btnCancel = win.grpItems.add('button', undefined, 'Cancel');
6 win.grpItems.radPg.onClick = displayItems;
7 win.grpItems.radAllPg.onClick = displayItems;
8 function displayItems(){
9 var pg = app.activeDocument.pages[0];
10 var pgItems =[];
11 (win.grpItems.radPg.value == true)? pgItems = pg.pageItems: pgItems = pg.allPageItems ;
12 for (var i = 0; i < pgItems.length; i++){
13 alert(pgItems[i].getElements()[0].constructor.name);
14 }
15 }
16 var intResult = win.show();
On lines 6 and 7, the same callback function (displayItems) is assigned to the onClick events
of both radio buttons.
On line 11, inside the displayItems() function, we check to see whether the value of the
pageItems radio button has been set to true.
11 (win.grpItems.radPg.value == true)? pgItems = pg.pageItems: pgItems = pg.allPageItems ;
If it is true, we set the pgItems array to the pageItems collection of page 1 of the current
document; otherwise, we set it to the allPageItems collection. On lines 12 to 14, we loop
through the resulting collection displaying the object type of each element within it, using the
syntax pgItems[i].getElements()[0].constructor.name. (In fact, getElements()[0] is only
necessary for the pageItems collection: pgItems[i].constructor.name would work fine for the
allPageItems collection; but, if we used it for the pageItems collection, all objects would be of
the type PageItem.)
(To test whether a page item is a particular type of object, such as a text frame, you can use the
JavaScript instanceOf operator. See below.)

242
SYNTAX myPageItem.getElements()[0].constructor.name
SUMMARY ... Syntax for retrieving object type of a pageItem
Returns a string containing the name.

myPageItem.getElements()[0] instanceof ObjectType


Syntax for testing whether a pageItem is a particular type of object. Returns true or false.

Try it for yourself!


TUTORIAL: Navigating all page items in a document
In the following tutorial, we will build an object Explorer script which presents the user with
a hierarchical display of all the pageItem objects in the active document. The treeview control
is ideal for displaying this kind of hierarchical structure.

Depending on the complexity of the page, it may take our script a while to build a node or item
corresponding to every pageItem in the document. A ProgressBar control would therefore be a
useful addition to the interface.
The hierarchical structure of the allPageItems collection
Naturally, to build our structure, we will need the allPageItems—rather than the pageItems—
collection. So, let's look a bit more closely at what it contains.
The allPageItems collection is basically an array containing each pageItem object in the
specified container as well as all of its descendants. This means that a page item which is
nested inside another page Item will occur at least twice: once as a child inside its parent and
once as a page item in its own right.
For example, let's say we have a page containing a text frame inside of which we have a nested
rectangular picture frame, inside of which there is an image. The top level of the allPageItems

243
collection of that page would be as follows.
TextFrame
Rectangle
Image
However because the collection contains child objects as well, the true nature of the structure
would be:
TextFrame
Rectangle
Image
Rectangle
Image
Image
As you can see, child objects are listed twice, grandchild objects are listed three times, and so
forth. To populate our treeview with the appropriate hierarchy, we will not need to include
child items when the occur on the top level. Thus, in the above example, we would only be
interested in the first item; because we can access the nested items inside it and add them to the
correct node of our treeview.
Once an item has been added to the treeview, we will need to ensure that it is ignored if it
occurs elsewhere within the allPageItems structure. Fortunately, InDesign assigns a unique ID
to each pageItem; so, we can simply add the ID of each element we process to an array which
we can then check before we add each new item.
So, let's begin our script, the completed version of which is called “02-object-
explorer_completed .jsx” and is in the “chapter10” folder.
1. The main() function
In the ESTK, choose File > New JavaScript.
Save the new file as “02-object-explorer.jsx” and is in the
“chapter10” folder.
Enter the following code.
1 #targetengine "session"
2 var g = {};
3 main();
4
5 function main(){
6 if(app.documents.length ==0){
7 alert("No documents are open.");
8 }
9 else{

244
10 createDialog();
11 buildTreeView();
12 }
13 }

Save your changes.


Since we want the user to interact with the document while the dialog is open, it needs to be a
modeless “palette” and this requires a persistent session. Therefore, on line 1, we use the
#targetengine directive to make the script runs in a persistent engine. (See page 58.)
We have two main tasks to perform: firstly, defining the window, its controls, their properties
and callback functions; and, secondly, looping through the allPageItems objects on each page
and creating an item in the treeview control corresponding to each one. We have named these
functions createDialog() and buildTreeView() (lines 10 and 11).
2. Creating the window
The first thing we need to do inside the createDialog() function is to define the window itself
inside the global variable g declared on line 2. Let's give the window the title ‘Object
Explorer'.
Add the following code at the end of your script.
14
15 function createDialog(){
16 g.win = new Window('palette', 'Object Explorer');
17 g.doc = app.activeDocument;
18 }
We will only be placing three elements inside the window: the ProgressBar, the treeview and
a Cancel button; so there is no need to create a panel or group—we can just accept the default
columnar, centred layout. Let's begin with the progress bar.
Position the cursor at the end of line 17, just above the closing
brace of the createDialog() function.
Press Return and insert the code shown below.
16 win = new Window('palette', 'Object Explorer');
17 g.doc = app.activeDocument;
18 // Progress bar
19 g.win.prg = g.win.add('progressBar');
20 g.win.prg.maxvalue = g.doc.pages.length;
21 g.win.prg.value = 0;
22 g.win.prg.size = [300, 20];
23 }
On line 20, we set the maxvalue property of the treeview (its upper limit) to match the number
of pages in the active document. On line 21, we set the value property of the treeview to zero.

245
After each page has been processed, we will add one to the value. The value property will
therefore match the maxvalue after the last page has been processed.
Now for the all-important tree view.
Position the cursor at the end of line 22, just above the closing
brace of the createDialog() function.
Press Return and enter the following code.
22 win.prg.size = [300, 20];
23 // Tree view
24 g.win.trv = g.win.add('treeview');
25 g.win.trv.size = [300, 200];
26 g.win.trv.onChange = selectPageItem;
27 }
Note that we have used the size property rather than minimumSize (line 21). This is to prevent
the treeview control from expanding uncontrollably if the active document contains a lot of
pages with complex layouts. If we fix the size, scrollbars will appear if and when they become
necessary, but the size of the control will not increase.
The tree view event-handler function defined on line 26 (selectPageItem ) will enable the
user to click on any item in the treeview and have the corresponding item selected.
Finally, let's add a Cancel button and, having built all of the controls, let's show the window.
Position the cursor at the end of line 26.
Press Return and enter the following code.
26 win.trv.onChange = selectPageItem;
27 // Buttons
28 g.win.btnClose = g.win.add('button', undefined, 'Close');
29 g.win.btnClose.onClick = function(){
30 this.parent.close();
31 g = null;
32 }
33 // Show window
34 g.win.show();
35 }

Choose File > Save to save your changes.


The inline callback function (lines 29 to 32) closes the window and then sets the global
variable g to null.
Here is the complete createDialog() function.
15 function createDialog(){
16 g.win = new Window('palette', 'Object Explorer');

246
17 g.doc = app.activeDocument;
18 // Progress bar
19 g.win.prg = g.win.add('progressBar');
20 g.win.prg.maxvalue = g.doc.pages.length;
21 g.win.prg.value = 0;
22 g.win.prg.size = [300, 20];
23 // Tree view
24 g.win.trv = g.win.add('treeview');
25 g.win.trv.size = [300, 200];
26 g.win.trv.onChange = selectPageItem;
27 // Buttons
28 g.win.btnClose = g.win.add('button', undefined, 'Close');
29 g.win.btnClose.onClick = function(){
30 this.parent.close();
31 g = null;
32 }
33 // Show window
34 g.win.show();
35 }

3. Constructing the treeview control


Our buildTreeView() function will loop through the pages in the document and the
allPageItems collection on each page. We thus need a loop inside a loop. In addition, when we
loop through each element in the allPageItems collection, we need to loop through its child
objects—if they exist—as well as their children, and so forth.
To achieve this, our inner loop will be placed in an external function which will be called
both from buildTreeView() function and also recursively—in other words, the external
function will call itself. This mechanism is commonly used in programming when exploring
this kind of branching structure.
Add the following code at the end of your script.

36
37 function buildTreeView(){
38 for (var h = 0; h < g.doc.pages.length; h++){
39 currentChildren = g.doc.pages[h].allPageItems;
40 var currentPage = g.doc.pages[h].name;
41 var currentNode = g.win.trv.add('node', "Page " + currentPage);
42 displayPageItems(currentNode, currentChildren);
43 g.win.prg.value ++;
44 }
45 g.win.prg.visible = false;
46 }

Save your changes.


On line 38, we set the upper limit of our for loop counter (h) to match the number of pages in
the active document.

247
On line 39, we place all of the page items on each page in a variable called currentChildren.
On line 41, we add a new node to the treeview control and label it “Page ” followed by the
number of the current page.
On line 42, we call the recursive function, passing as arguments the node we have just created
and the currentChildren variable which contains all of the pageItem objects on the current
page.
On line 45, outside the for loop and, hence, after all pages have been processed, we hide the
progress bar.
Now let's examine the recursive function to see how it works.
Add the following code at the end of your script.
47
48 function displayPageItems(oldNode, pItems){
49 for (var i = 0; i < pItems.length; i++){
50 var currentItem = pItems[i];
51 var currentID = currentItem.id;
52 var CurrentNodeText = currentItem.constructor.name + " id " + currentID;
53 var currentChildren =currentItem.allPageItems;
54 var currentChildCount = currentChildren.length;
55 if(!(currentID in g.win.trv)){ // If node does not already exist
56 var nodeType;
57 (currentChildCount > 0)? nodeType = 'node': nodeType = 'item';
58 g.win.trv[currentID] = oldNode.add(nodeType, CurrentNodeText);
59 displayPageItems(g.win.trv[currentID], currentChildren);
60 }
61 }
62 }

Save your changes.


The recursive function loops through the page items passed to it via the pItems parameter. The
first thing it does is to check whether each page item has already been processed (line 55).
This is done by testing whether an item exists inside g.win.trv with the same name as the id of
the page item: if(!(currentID in g.win.trv)).
For example, if we are processing a page item with an id of 211 and g.win.trv.211 (which can
also be written g.win.trv["211"]) exists, this means that we must have already processed this
node as a child node and no further action needs to be taken.
If the item has not already been processed, we want to add a node whose text label contains
the pageItem type (ascertained with constructor.name) as well as the unique id automatically
assigned by InDesign to each page item. We will simultaneously place a reference to the item
inside a property of the tree view, with the same name as the id of the page item. We also need
to add the right type of element to the treeview—'node’ if the page item contains other
pageItem objects and ‘item’ (non-expandable) if it doesn't.

248
56 var nodeType;
57 (currentChildCount > 0)? nodeType = 'node': nodeType = 'item';
58 g.win.trv[currentID] = oldNode.add(nodeType, CurrentNodeText);
Note that when we create the node, we assign it to win.trv[currentID]. In other words, we are
adding our own custom property to the tree view object and giving it a name that matches the id
of the page item which this node represents. This property will contain a reference to the node
being created.
Once we have created the node, we call the function from inside itself, passing the node we
have just created, along with the allPageItems collection of the page item we have just
processed, as function arguments.
59 displayPageItems(g.win.trv[currentID], currentChildren);
What happens when the function displayPageItems() is called recursively depends on the
number of items in currentChildren (the allPageItems collection of the page item just
processed). When there are no further child items to be processed, the function call produces
no result and no further elements are added to the node represented by g.win.trv[currentID].
Since this script makes no changes to the document, it is safe to test it with any InDesign
document.
Run the script with any InDesign document open and then explore
the resulting treeview structure.
Click the Close button when you have finished.
Our final step is to write the onChange callback function for the treeview control. When an
item in the control is clicked, we want to select the corresponding element within the InDesign
document.
Enter the following function at the end of your script.
63
64 function selectPageItem(){
65 var arrAllItems = g.doc.allPageItems;
66 for (var i = 0; i < arrAllItems.length; i ++){
67 if (this.selection == g.win.trv[arrAllItems[i].id]){
68 arrAllItems[i].select();
69 }
70 }
71 }

Save your changes.


The tree view event-handler loops through all of the page items in the document and takes
advantage of the fact that the treeview object has a series of custom properties with names
matching the id of each page item in the document, each pointing to the treeview node
representing that same page item.

249
In the statement this.selection == win.trv[arrAllItems[i].id], on line 67, we use the id of the
page item currently being processed in the for loop as an index to target one of the custom
properties of the tree view. If we find a match, the item is selected (line 68).
Run the script again with any InDesign document open and then try
clicking on some of the items in the treeview.
Each time, you click on a node item (the ones with the plus sign), nothing should happen (apart
from expand/collapse) since these items are merely containers sub-dividing the elements by
page. However, when you click on one of the sub-items, it's id property should match one of
the page item ids and that page item will therefore be selected.

Ascertaining pageItem type


Using constructor.name
Although we can get a description of the pageItem type using the constructor.name statement,
this technique does not tell us the whole picture. For example, a rectangular picture frame
containing an image, an empty rectangular picture frame and a plain rectangle will all have the
constructor.name value of “Rectangle". To find out more about an object, we need to test a
combination of other properties.
Using instanceof
The JavaScript operator instanceof can also be used to test whether an object is of a particular
type. Thus the statement:
if(myPageItem.getElements()[0].constructor.name == “TextFrame")...
is equivalent to:
if(myPageItem.getElements()[0] instanceof TextFrame)...
The instanceof statement has the reputation of being faster to execute than constructor.name.
Identifying textFrames, groups and buttons
TextFrame, Group and Button page items are easy to identify: either constructor.name or
instanceof will return the object type with no ambiguity.
Some of the other page item types need a bit more fine-tuning, since constructor.name and
instanceof return Rectangle, Oval and Polygon for several different types of object.
Identifying picture frames
If you want to test whether a page item is a picture frame, you will need to check two things.
Firstly, you need to check that constructor.name returns a rectangle, oval or polygon.
Secondly, you need to verify that the contentType property returns
ContentType.graphicType:
if(myPageItem.getElements()[0].contentType == ContentType.GRAPHIC_TYPE)...

250
SYNTAX PageItem property
SUMMARY ... Equivalent to selecting a pageItem and choosing Object > Content in InDesign.

ContentType.UNASSIGNED
Possible values ContentType.GRAPHIC_TYPE
ContentType.TEXT_TYPE

Identifying movies and sound


Constructor.name also returns a rectangle, oval or polygon for both movies and sound. In
addition, since they can contain a poster image, their contentType will be
ContentType.GRAPHIC_TYPE. To distinguish them from picture frames, it is therefore
necessary to check whether they contain a Sound or Movie object. For example:
if(myPageItem.getElements()[0].movies.length > 0)...
if(myPageItem.getElements()[0].sounds.length > 0)...
Identifying type on a path
Although type on a path is not a page item, it is sometimes useful to be able to find instances of
it within a document. We can do this by using a similar technique to the one we have just seen:
if(myPageItem.getElements()[0].textPaths.length > 0)...
Identifying regular graphic objects
If we start with a Rectangle, Oval, Polygon (or Line) object and exclude all of above pageItem
types, then we can conclude that the object is just a plain old graphic. We could also
additionally test its contentType property:
if(myPageItem.getElements()[0].contentType == ContentType.UNASSIGNED)...

PageItems and layers


When working with layers, we are principally concerned with the pageItems—rather than the
allPageItems—collection. Only top level objects can be placed on a particular layer: for
example, if you have a group containing an image and its caption—in other words, a Rectangle
and a TextFrame object—you will not be able to place the rectangle and the text frame on
separate layers unless you ungroup them.
Scripting allows you to make changes to pageItems and layers throughout an entire document.
For example, we could assign all objects of a given type to a particular layer. We can, equally,
move any arbitrary selection of objects—across multiple pages—to a given layer. The
following Syntax Summary shows some of the key methods and properties of the layer object
and the layers collection.

myPageItem.itemLayer = myLayer
SYNTAX PageItem property
SUMMARY ... Used to move an item to a given layer.
myDocument.layers.add([Creation properties])
Layers collection method
Used to create a new layer.

251
myDocument.layers.remove()
Layers object method
Used to delete a layer.
myDocument.layers.name = layerName
Layer object property
Changes the name of a layer.
layerName String: the name of the new layer.

Try it for yourself!


TUTORIAL: Creating a layer manager utility

The following tutorial offers practice on working with page items and layers by building the
dialog-based application shown above.
The dialog displays all of the page items in the active document, with each item identified by
its page number, object type and layer. On the left of the dialog are a series of checkboxes
which provide an interactive filter, allowing the user to determine which object types are
included in the list.
On the right of the dialog, the Select by type button allows the user to select all objects of one
(or more) particular type(s). The Move to layer button transfers the selected item(s) to the
specified layer or to a new layer. Finally, the Delete Selected button will remove the selected
item(s) from the document.
You will find the completed script in the “chapter10” folder under the name "03-layer-
manager_completed.jsx". Feel free to copy code from this source as you work through the
tutorial rather than typing it yourself.
1. Creating the main() function
In the ESTK, create a new file and save it with the name " 02-
object-explorer.jsx".
252
Let's begin by entering the main function which will control the
program flow. Enter the following code.
1 #targetengine "session"
2
3 var g = {};
4 main();
5
6 function main(){
7 if(app.documents.length== 0){
8 alert("Please open the document you wish to browse.");
9 }
10 else{loadPageItems(); buildDialog(); updateListbox();}
11 }
The application requires a non-modal dialog in order to enable the user to interact with page
items; so, on line 1, we have to set the target engine. Otherwise, the dialog would appear and
instantly disappear. For the same reason, we do not set the global variable g to null after the
main() function call, as we normally do. Instead, we will perform this step when the user
clicks the Close button to dismiss the dialog.
Inside the main() function, on line 7, we check to see whether there is a document open: if not,
we display an error message (line 8).
If there is a document open we make our function calls (line 10):
• loadPageItems() is where we will add a reference to each page item in the document to
an array (g.arrPageItems), as well as displaying a progress bar.
• In the buildDialog() function, we will create the dialog and its controls. We will also
need to define callback functions for each of the controls in the dialog that need to be
responsive to the user's actions.
• In the updateListbox() function, we will update the page items listed in the dialog box,
based on the currently selected object type checkboxes on the left of the dialog. This
function will be called when the application first loads and also each time the user
activates or deactivates a checkbox.
2. The loadPageItems() function
Whilst it is possible for us to obtain a reference to every page item in the current document
with the statement app.activeDocument.pageItems, the order of the items will be
unpredictable. Since, we want the list to be ordered by page number, it makes more sense to
loop through all the pages in the document, addding the items on each page to an array
variable. To keep users informed, while this operation is going on, we will display a progress
bar.
Add the following lines to your script.
12

253
13 function loadPageItems(){
14 displayProgressBar();
15 docPageItemsList();
16 masterPageItemsList();
17
18 } // End function loadPageItems
In this function, we make function calls to what will be three nested functions, the names of
which are fairly self-explanatory.
Position the cursor on line 17, above the closing brace of the
loadPageItems() function.
Press Return to leave a blank line, then enter the following nested
function.
13 function loadPageItems(){
14 displayProgressBar();
15 docPageItemsList();
16 masterPageItemsList();
17
18 function displayProgressBar(){
19 g.doc = app.activeDocument;
20 g.winProg = new Window('palette', undefined, undefined, {closeButton:false});
21 g.winProg.prg =g.winProg.add('progressbar', [0, 0, 400, 20]);
22 var stxPrg = g.winProg.add('statictext', [100, 30, 300, 50], 'Updating list of page items');
23 var numItems = g.doc.pageItems.length;
24 g.winProg.prg.value = 0;
25 g.winProg.prg.maxvalue = numItems;
26 g.winProg.show();
27 }
28
29 } // End function loadPageItems
On line 20, when we create the progress bar window, we set the closeButton creation
property to false to make it look less window-like.
The key fact about displaying a progress bar is mapping the maxvalue property to some action
or event in the program. Thus, on line 23, we put the number of page items in the active
document into a variable called numItems. On line 24, we set the (starting) value of the
progress bar to zero and, on line 25, we make its upper limit—maxvalue—equivalent to
numItems.
Position the cursor on line 28, above the closing brace of the
loadPageItems() function.
Leave a blank line then enter the second nested function.
28
29 function docPageItemsList(){

254
30 g.arrPageItems = [];
31 for(var i = 0; i < g.doc.pages.length; i ++){
32 var currentPage = g.doc.pages.item(i);
33 for (var j = 0; j < currentPage.pageItems.length; j ++){
34 var currentPageItem = currentPage.pageItems.item(j).getElements()[0];
35 g.arrPageItems.push(currentPageItem);
36 g.winProg.prg.value ++;
37 }
38 }
39 }
40
41 } // End function loadPageItems
On line 30, we create an array inside our global object variable called g.arrPageItems. To
create the list of page items inside the array, we will first loop through the document pages and
then (in our third nested function) through the master pages. When we loop through document
pages, we then use a nested loop to traverse the page items on each page.
As we encounter each page item, we add it to g.arrPageItems (line 35) and then add one to
the value of the progress bar (line 36).
Position the cursor on line 40, above the closing brace of the
loadPageItems() function.
Enter the third and final nested function.
40
41 function masterPageItemsList(){
42 for(var i=0; i < g.doc.masterSpreads.length; i ++){
43 var currentMaster = g.doc.masterSpreads[i];
44 for (j = 0; j < currentMaster.pages.length; j++){
45 var currentPage = currentMaster.pages.item(j);
46 for (var k = 0; k < currentPage.pageItems.length; k ++){
47 currentPageItem = currentPage.pageItems.item(k).getElements()[0];
48 g.arrPageItems.push(currentPageItem);
49 g.winProg.prg.value ++;
50 }
51 }
52 }
53 g.winProg.prg.value = 0;
54 g.winProg.close();
55 }
56
57 } // End function loadPageItems
When we loop through the master spread, our first level nested loop (using counter variable j)
goes through the pages in each master spread and then we have a second level nested loop
(using counter variable k) to loop through the page items on each individual master page.
Once again, when we encounter a page item, we add it to g.arrPageItems (line 48)
Finally, on lines 53 to 54, we close the progress bar dialog window after resetting its value to
zero, ready for the next time it is displayed.

255
Save your changes.
To test your script, you will need to temporarily disable the
function calls to the functions we have not yet created. To do this,
modify line 10 of the script as shown below.
10 else{loadPageItems();} // buildDialog(); updateListbox();}

Open any InDesign document that has some content. When you run
your code, you should see the progress bar move from zero to its
maximum value before disappearing.

Use Edit > Undo to restore line 10 to the way it was.


10 else{loadPageItems(); buildDialog(); callbackFunctions(); updateListbox();}

3. Creating the dialog box


In the buildDialog() function, we will create the dialog box and its various controls. We will
follow the same pattern of subdividing the function into a series of nested functions.
3a. Creating the window itself

Begin by entering the skeleton of the buildDialog() function—the


calls to the nested functions.
58
59 function buildDialog(){
60 createWindow();
61 dataTypesCheckboxes();
62 pageItemsListbox();
63 selectItemsGroup();
64 moveLayerGroup();
65 deleteAndCloseButtons();
66 g.winMain.show();
67
68 } // end function buildDialog
The function calls a series of nested functions, each of which will create one key area of the

256
dialog, then, once everything has been built, we display the window (line 66). (The window
will be created in the first nested function, createWindow().)
Position the cursor on line 67, above the closing brace of the
buildDialog() function and key in the createWindow() nested
function which defines the dialog itself.
59 function buildDialog(){
60 createWindow();
61 dataTypesCheckboxes();
62 pageItemsListbox();
63 selectItemsGroup();
64 moveLayerGroup();
65 deleteAndCloseButtons();
66 g.winMain.show();
67 function createWindow(){
68 g.winMain = new Window("palette", "Layer Manager");
69 g.winMain.orientation = "row";
70 g.winMain.alignChildren = "top";
71 }
72
73 } // end function buildDialog
On line 68, we create a window of the “palette” type: this is because we will need to carry out
operations which cannot take place while the modal “dialog” type window is active, such as
moving items to a new layer.
The layout of items in the window will consist of three columns, each contained in a separate
group control. This requires the window itself to have its orientation set to “row” (line 69) to
override the default “column”.
3b. Creating the object type checkboxes
The first group control within the window will contain the checkboxes which allow the user to
determine which types of page item are displayed.
To create this control, place the cursor on what should be line 72—
the line above the closing brace of the buildDialog() function,
leave a blank line then enter the second nested function.
72
73 function dataTypesCheckboxes(){
74 g.grpTypes = g.winMain.add('group');
75 g.grpTypes.orientation = "column";
76 g.grpTypes.alignChildren = "left";
77 g.arrTypes =['Text frame', 'Picture frame', 'Group', 'Shape', 'Line', 'Type on a path', 'Movie', 'Sound', 'Button'];
78 for(var i = 0; i < g.arrTypes.length; i ++){
79 var currentChk = g.grpTypes.add('checkbox', undefined, g.arrTypes[i], {name: g.arrTypes[i]});
80 currentChk.value = true;

257
81 currentChk.onClick = updateListbox;
82 }
83 }
84
85 } // end function buildDialog

Save your changes.


If you test your code at this point (temporarily disabling the
function call to the updateListBox() function, on line 10), after
the progress bar disappears, you should see a tall, thin dialog like
the one shown below.

On line 75, since we want the checkboxes to form a column, we override the default “row”
orientation of the g.grpTypes group control by setting it to “column”.
Since checkboxes are not mutually exclusive, we do not need to give them any special
attributes. Therefore, on line 77, we define an array and place the text for each checkbox
inside it. We then loop through the array, creating a checkbox and assigning each array item as
the checkbox text (lines 78 to 82).
Since we want all types of pageItem to be listed when the dialog box appears, on line 80, we
set the value of all checkboxes to true.
Finally, on line 81, we set the onClick callback of each checkbox to the updateListbox()
function which will rebuild the list of pageItems to reflect the currently activated checkboxes.
The main script already contains a call to this function, which we will create later.
3c. Creating the multi-column listbox
In the central column of the dialog box, we need a listbox control with three columns: page
number, item type and layer.

258
Position the cursor on line 84— the line above the closing brace of
the buildDialog() function and enter the following nested
function.
84
85 function pageItemsListbox(){
86 var grpItems = g.winMain.add('group');
87 grpItems.orientation = "column";
88 grpItems.alignChildren = "top";
89 g.lstPageItems = grpItems.add('listbox', undefined, undefined,
90 {multiselect:true, numberOfColumns: 3,
91 showHeaders:true,
92 columnTitles:['Page', 'Item', 'Layer'],
93 columnWidths:[50, 125, 125]
94 });
95 g.lstPageItems.size = [300,250];
96 g.lstPageItems.onDoubleClick = lstPageItems_onDoubleClick;
97 }
98
99 } // end function buildDialog

Save your changes.


Run your script (temporarily disabling the function call to the
updateListBox() function, on line 10). Your dialog should now
resemble the one below.

In order to create a multi-column listbox, we set several creation parameters when the listbox
is added to the grpItems group control (lines 89 to 94).

259
SYNTAX Listbox control creation properties
SUMMARY ...

Multiselect A value of true allows user to select more than one item in list. Default false.

Number of columns Specifies the number of columns to display in the list.


Show headers Determines whether column headers are displayed.
ColumnTitles An array of strings specifying the titles to be displayed at the top of each column.
ColumnWidths An array of integers specifying the width of each column.

Please note that, although we have set the numberOfColumns, showHeaders, columnTitles
and columnWidths properties, these will not have any effect until we add at least one item to
the listbox. (This will be done in the updateListbox() function.)
Note also that, on line 96, we assign the function lstPageItems_onDoubleClick() as the
callback for the listbox. This will allow the user to select any page item in the document by
double-clicking the node or item that represents it.
3d. Creating the selectItems dropdown and button
The first item in the third column of the dialog will be a dropdownlist which allows the user to
select all page items of a given type.
Position the cursor on line 98— the line above the closing brace of
the buildDialog() function and enter the following nested
function.
98
99 function selectItemsGroup(){
100 // Select dropdown
101 g.grpButtons = g.winMain.add('group');
102 g.grpButtons.orientation = "column";
103 var pnlType = g.grpButtons.add('panel');
104 g.ddlTypes = pnlType.add('dropdownlist', undefined, g.arrTypes);
105 // Select button
106 var btnSelect = pnlType.add('button', undefined, 'Select by type');
107 btnSelect.size = [120, 20];
108 btnSelect.onClick = btnSelect_onClick;
109 // Check box
110 g.chkAdd= pnlType.add('checkbox', undefined, 'Add to selection');
111 }
112
113 } // end function buildDialog

260
Save your changes.
Run your script (temporarily disabling the function call to the
updateListBox() function, on line 10). The three item selection
controls should look like the ones shown on the right.
The item selection controls consist of a dropdownlist, button and checkbox. Therefore, having
created the group control which will house all of the controls in column three of the dialog
(line 101), we add a panel to act as the container for the three controls (line 103).
On line 104, when we add the dropdownlist, we re-use, as the items parameter, the
g.arrTypes array which we created earlier and used to provide the text of the checkboxes.
On line 110, we create a checkbox which will allow the user to determine whether a new
selection is made when the button is clicked or whether the items specified are added to any
existing selection. The idea is that, if the checkbox is activated, any existing selection in the
page items listbox will remain in place. If the checkbox is unchecked, a new selection will be
made in the listbox.
3e. Creating the move layer dropdown and button
The second item in the third column of the dialog will be a dropdownlist which allows the
user to choose a layer, and a button to move the selected items to the layer specified.
Position the cursor on line 112—above the closing brace of the
buildDialog() function and enter the following code.
112
113 function moveLayerGroup(){
114 // Move layer dropdown
115 var pnlMove= g.grpButtons.add('panel');
116 var arrLayers = [];
117 for(var i = 0; i < g.doc.layers.length; i ++){
118 arrLayers.push(g.doc.layers[i].name);
119 }
120 arrLayers.push("-");
121 arrLayers.push("New Layer");
122 g.ddlLayers = pnlMove.add('dropdownlist', undefined, arrLayers);
123 // Move layer button
124 var btnMove = pnlMove.add('button', undefined, 'Move to layer');
125 btnMove.size = [120, 20];

261
126 btnMove.onClick = btnMove_onClick;
127 }
128
129 } // end function buildDialog

Save your changes.


Run your script (temporarily disabling the function call to the
updateListBox() function, on line 10). When you click on the
dropdownlist, you should see a list of the layers in the document, a
separator line, followed by the New Layer option.
On lines 116 to 119, we create an array called arrLayers and add to it the names of all the
layers in the active document to it. We then add a separator item—a hyphen (line 120),
followed by the fixed text item “New Layer” (line 121).
Finally, when we create the dropdownlist, on line 122, we use arrLayers as the third
argument of the add() method—the items which will appear in the list.
3f. Creating the Delete and Close buttons
The final items in the dialog are two buttons: one to delete the currently selected pageItems and
one to close the dialog.
Position the cursor on line 128—above the closing brace of the
buildDialog() function and add the code shown below.
128
129 function deleteAndCloseButtons(){
130 // Delete button
131 var btnDelete = g.grpButtons.add('button', undefined, 'Delete Selected');
132 btnDelete.size = [120, 20];
133 btnDelete.onClick = btnDelete_onClick;
134 // Close button
135 var btnClose = g.grpButtons.add('button', undefined, 'Close');
136 btnClose.size = [120, 20];
137 btnClose.onClick = btnClose_onClick;
138 }
139
140 } // end function buildDialog

262
Save your changes.
That completes the construction of the dialog; so now we can turn our attention to making it
interactive by creating callback functions for the various controls.
4. Creating the updateListbox() function
The updateListbox() function is called once in the main script and also whenever the user
checks or unchecks any of the object type checkboxes on the left of the dialog. This function
needs to loop through all the page items stored inside g.arrPageItems and add each one to the
listbox, provided its object type checkbox has been activated by the user.
Add the updateListbox() function to the end of your script.
141
142 function updateListbox(){
143 app.scriptPreferences.enableRedraw = false;
144 g.lstPageItems.selection = null;
145 g.lstPageItems.removeAll();
146 g.winProg.show();
147 for(var i = 0; i < g.arrPageItems.length; i ++){
148 var currentPageItem =g.arrPageItems[i];
149 var currentPage = currentPageItem.parentPage;
150 var currentItemType = detectType(currentPageItem);
151 currentCheckBox = g.grpTypes.children[currentItemType];
152 if(currentCheckBox.value == true){
153 var currentListItem = g.lstPageItems.add('item', currentPage.name);
154 currentListItem.subItems[0].text = currentItemType;
155 currentListItem.subItems[1].text = currentPageItem.itemLayer.name;
156 currentListItem.id = currentPageItem.id;
157 }
158 g.winProg.prg.value ++;
159 }
160 g.winProg.prg.value =0;
161 g.winProg.close();
162 app.scriptPreferences.enableRedraw = true;
163 }

Save your changes.


The code app.scriptPreferences.enableRedraw = false (line 143) can be used during
repetitive procedures when updating the screen will considerably increase the time taken. At
the end of the function, on line 162, we use app.scriptPreferences.enableRedraw = true to
reinstate screen updating.
The function begins by clearing out the existing values from the listbox (lines 144 to 145) and
displaying the progress bar window (line 146).
We then loop through all of the page items inside g.arrPageItems, updating the three columns
of the page items listbox with appropriate values (lines 147 to 159). Inside the loop (on line

263
150), we capture the object type of the current page item inside a variable called
currentItemType, by obtaining a return value from a function called detectType()—which
we will write shortly.
The detectType() function will determine the object type of the page item and return a value
which precisely matches the text of one our checkboxes. So, on line 151, we are able to use the
value returned by the detectType() function (currentItemType) as an index to identify the
checkbox which indicates whether objects of that type should be added to our list.
On line 152, we test whether the matched text box is checked and, if it is, we update the three
columns of the listbox. First, we create a new listItem using the add() method of the listbox
object and setting the second parameter—the text will appear in the first column of the item—
to the name (i.e. page number) of the current page (line 153). Next, we set the text property of
the second column to currentItemType (line 154) and that of the third column to name of the
layer on which the current page item is located (line 155).
When setting the text of a multi-column list item, the text property of the item refers to the first
column; subItems[0] refers to second column; subItems[1] targets the third column; and so
forth.
On line 156, we create a property called id inside the currentItem variable, taking advantage
of the fact that, in JavaScript, you are able to add your own properties to those already built
into a particular object. We set the id to match that of the pageItem being processed. This id is
assigned to the pageItem automatically by InDesign and provides a convenient way of targeting
page items without having to refer to page numbers, location or attributes. Thus, when someone
double-clicks on a list item, we will be able to read the id property of the selected item and
look for a page item with that same id.
The final thing we do inside the loop is to update the value of the progress bar to keep it
moving along (line 158). Then, once the loop finishes, we reset and close the progress bar then
reinstate screen updating.

myListbox.text = myString
SYNTAX Property of listbox object
SUMMARY ... Sets the text of the first column of a multi-column listbox control.
myListbox.subItems[0].text = myString
Property of listbox object
Sets the text of the second column of a multi-column listbox control.
myListbox.subItems[n].text = myString
Property of listbox object
Sets the text of the column n-1 of a multi-column listbox control.

Our next task in this tutorial is to create the detectType() function which we referred to inside
updateListbox(). In it, we will use some of the techniques discussed earlier in the chapter for
detecting the object type of a page item. The function will use a series of conditional

264
statements to determine the object category in which a given page item should be placed.
Enter the following function below all of your existing code.
164
165 function detectType(currentPageItem){
166 if(currentPageItem instanceof TextFrame){ return "Text frame"};
167 if(currentPageItem instanceof Button){ return "Button"};
168 if(currentPageItem instanceof Group){ return "Group"};
169 if(currentPageItem instanceof Rectangle
170 || currentPageItem instanceof Oval
171 || currentPageItem instanceof Polygon){
172 if(currentPageItem.movies.length > 0){return "Movie";}
173 else if(currentPageItem.sounds.length > 0){return "Sound";}
174 else if(currentPageItem.contentType == ContentType.GRAPHIC_TYPE){return "Picture frame";}
175 else if(currentPageItem.textPaths.length > 0){return "Type on a path";}
176 else{return "Shape";}
177 }
178 if(currentPageItem instanceof GraphicLine){
179 if(currentPageItem.textPaths.length > 0){
180 return "Type on a path";}
181 else{return "Line";}
182 }
183 }
The following diagram illustrates the conditional logic used in the function.

265
On line 165, when we define the function, we specify a single parameter called
currentPageItem: the page item which will be examined. (You will remember that, when we
called the function from inside updateListbox(), we supplied the required parameter:
150 var currentItemType = detectType(currentPageItem);
On lines 166-168, we eliminate the three page items which are the easiest to identify: text
frames, buttons and groups—all three return an unambiguous object name to the instanceof
statement and we simply use the return statement to supply the appropriate value for the
currentItemType variable used in the function call.
We then deal with the three more ambiguous object types: rectangle, oval and polygon (lines
169 to 177). On line 172 and 173, we test to see whether there are any movie or sound objects
inside the page item; if there are, we identify the object as a movie or sound.
On line 174, we test the contentType property of the page item: if it is
ContentType.GRAPHIC_TYPE, we return the object type “Picture frame". Note that we
only do this test after eliminating movies and sounds, since their poster images mean that their
contentType property can also return ContentType.GRAPHIC_TYPE.
On line 175, if the page item contains a textPath object, we return “Type on a path” as the
object type.

266
If none of the foregoing tests are true, we conclude (on line 176) that the object is just a plain
old vector shape.
After dealing with rectangles, ovals and polygons, we are left with graphic lines (178-182). If
the page item is a graphic line and contains a textPath object, we return “Type on a path” as the
object type (line 180). If not, we return the word “Line" (line 181).
Save your changes.
Before running your code, open the document called “03-
pageitems.indd” in the “chapter10” folder.

Test the buttons on the left of the dialog. Each time you activate or
deactivate a button, the list of items should be refreshed to reflect
the current choices.
5. Creating callback functions
Our final task is to define the callback functions which will allow the user to interact with the
other controls in the dialog. The callback functions all have names which combine the name of
the control and the event—for example:
126 btnMove.onClick = btnMove_onClick;
The only exception is the checkboxes which all call the updateListbox() function which we
have just completed:
81 currentChk.onClick = updateListbox;
Let's now add the ability to activate a page item by double-clicking on any item in the
g.lstPageItems listbox control.
5a. Page items listbox onDoubleClick

Enter the following function at the end of your script, below all of
the existing code.
184

267
185 function lstPageItems_onDoubleClick(){
186 var idClicked = this.selection[0].id;
187 var item2Select = g.doc.pageItems.itemByID(idClicked);
188 item2Select.select();
189 }

Save your changes.


On line 186, we put the value of the id property of the double-clicked item into a variable
called idClicked. If you remember, the id property is not built-in but, rather, one which we
artificially added to each list item while we populated the list in the updateListbox() function
and which matches the built-in id of the corresponding page item.
On line 187, we then use this id as an index to capture the corresponding page item inside a
variable called item2Select.
Finally, on line 188, we select the page item.
Run the script and try double-clicking items in the list. You should
automatically be taken to the page containing the item which
should then be selected.
5b. Select by Type button onClick
When the user clicks the Select by Type button—btnSelect in our code, all items of the
specified type will be selected. If the Add to selection checkbox is checked, we need to leave
any selection in place; if not, we must first deselect all items.
Enter the following code at the end of the script.
190
191 function btnSelect_onClick(){
192 if(g.ddlTypes.selection == null){
193 alert("Please specify the type of item to be selected");
194 }
195 else{
196 if(g.chkAdd.value == false){g.lstPageItems.selection = null;}
197 // Show progress bar
198 app.scriptPreferences.enableRedraw = false;
199 g.winProg.prg.value = 0;
200 g.winProg.show();
201 for(var i = 0; i < g.lstPageItems.items.length; i ++){
202 var currentItem = g.lstPageItems.items[i];
203 if(currentItem.subItems[0].text == g.ddlTypes.selection.text){
204 currentItem.selected = true;
205 }
206 g.winProg.prg.value ++;
207 }
208 g.winProg.hide();
209 app.scriptPreferences.enableRedraw = true;
210 }

268
211 }

Save your changes.


On line 192, we start by checking that the user has made a selection from the dropdownlist: if
they have not, we display an error message.
If a selection has been made, we then look at the value of the Add to selection checkbox (lines
196): if it is set to false (unchecked), we deselect all of the items in the listbox by setting the
selection property of the listbox to null.
Since we are selecting item by item, selection may take a while in a large list. It is therefore a
good idea to switch off screen updating (line 198) and display the progress bar (line 200) until
the selection process is complete.
We then loop through all of the items in the listbox and compare the entry in the second column
(currentItem.subItems[0].text)to the selection made by the user: if they are the same, we set
the selected property of that item to true (lines 201 to 205).
5c. Move to Layer button onClick
When the user clicks the Move to Layer button—btnMove in our code—one of two things
needs to happen. If they have chosen the name of an existing layer, we need to move each of the
selected pageItems to that layer. If, instead, they have chosen the option New Layer, we need
to first create a layer—allowing the user to choose the name—and then move the selected
items onto it.
Since this function is rather long, begin by entering the following
skeleton code at the end of your script.
212
213 function btnMove_onClick(){
214 if(g.lstPageItems.selection == null || g.ddlLayers.selection == null){
215 alert("Please specify the item(s) to be moved and the target layer");
216 }
217 else{
218 // Target new layer?
219
220 // Target existing layer?
221
222 // Move items
223
224 }
225 }
On line 214, we start by checking whether the user has neglected to make a selection either
from the listbox or from the layers dropdownlist. If they have made the necessary selections,
we will then target either a new layer or an existing one and then move the items to the target
layer.
Let's enter the code which will create a new layer.

269
Position the cursor on line 219, below the comment “// Target new
layer?” and enter the following lines.
218 // Target new layer?
219 if(g.ddlLayers.selection.text == "New Layer"){
220 var newLayer = prompt("Please enter name of new layer", "");
221 if(newLayer == "" || newLayer == null){
222 alert("No layer created");
223 return;
224 }
225 try{
226 var targetLayer = g.doc.layers.add();
227 targetLayer.name = newLayer;
228 g.ddlLayers.selection = null;
229 g.ddlLayers.removeAll();
230 for(var i = 0; i < g.doc.layers.length; i ++){
231 g.ddlLayers.add('item', g.doc.layers[i].name);
232 }
233 g.ddlLayers.add('separator');
234 g.ddlLayers.add('item', "New Layer");
235 }catch(err){
236 alert("Sorry a layer by that name already exists.");
237 targetLayer.remove();
238 return;
239 }
240 // Target existing layer?
241
242 // Move items
243
244 }
245 }
On line 219, we test to see whether the user has selected the option New Layer. If they have,
we use a JavaScript prompt to invite them to enter a name for the new layer (line 220).
If they leave the text box in the prompt window blank or click the Cancel button (causing the
prompt to return null), we display an error message and exit the callback function by using the
return statement (lines 221 to 224).

270
It the user has entered a layer name, on lines 225 to 239, we use a try…catch block to create a
new layer and then assign it the name entered by the user. In the try section (which will
execute if a layer by the name entered does not already exist and the name is valid) we rename
the layer then refresh the items in the ddlLayers dropdownlist (lines 227 to 232).
In the catch section (which will execute if the name already exists or is invalid), we display
an error message (line 236) and then delete the new layer that we have just created (line 237).
Position the cursor on line 241—below the comment “// Target
existing layer?” and enter the code which will execute if the user
chooses an existing layer.
240 // Target existing layer?
241 } else{
242 var newLayer = g.ddlLayers.selection.text;
243 var targetLayer = g.doc.layers.itemByName(newLayer);
244 }
245 // Move items
246
247 }
248 }
On line 242, we read the text of the item selected on the ddlLayers dropdownlist into a
variable called newLayer. Then, on line 243, we use that variable as an index to target the
layer which bears the same name.
Now that we have targeted the layer to which the user wants the
selected items moved, we can go ahead and perform the move.
Position the cursor on line 246—below the comment “// Move
items” and enter the lines shown below.
245 // Move items
246 for(var i = 0; i < g.lstPageItems.selection.length; i ++){
247 var currentItem = g.lstPageItems.selection[i];
248 var item2Move = g.doc.pageItems.itemByID(currentItem.id);
249 item2Move.itemLayer = targetLayer;
250 currentItem.subItems[1].text = targetLayer.name;
251 }
252 }
253 }

Save the changes to your script.


In this section, as we loop through the selected items in g.lstPageItems, our objective is to
identify each page item that needs to be moved by its unique id. The strategy being employed is
that, when we created the list (in the updateListbox() function), we assigned each list item an

271
id property with a value which matches the id of the pageItem it represents.
Thus, on line 247, we place each list item into a variable called currentItem. We then use the
id of the currentItem as an index to identify the pageItem and place it inside a variable called
item2Move (line 248). To move the pageItem, we simply set its itemLayer property to the
targetLayer we identified earlier (line 249).
Finally, on line 250, we update the object name associated with the list item by changing the
text of the third column to match the name of the target layer.
5d. Delete and Close buttons onClick
When the user clicks the Delete button, after verifying that they really do want to delete the
selected items, we need to loop through our list of items and delete the corresponding page
items from the document. The Close button simply needs to close the dialog box and set the
global variable to null.

Enter the following onClick callback for the Delete button at the
end of your script.
254
255 function btnDelete_onClick(){
256 var blnConfirm = confirm("Delete selected items from document?");
257 if(blnConfirm == true){
258 var arrDelete = g.lstPageItems.selection;
259 for(var i = 0; i < arrDelete.length; i ++){
260 var currentItem = arrDelete[i];
261 var item2Delete = g.doc.pageItems.itemByID(currentItem.id);
262 item2Delete.remove();
263 g.lstPageItems.remove(currentItem);
264 }
265 }
266 }
On line 256, we use the JavaScript confirm function to allow the user a chance to change their
mind and not delete the item. If they click OK—thus setting blnConfirm to true, we place all of
the selected items into an array called arrDelete (line 258). We then loop through the array
and process each item in much the same way as we did in the Move button callback.
On line 260, we place each item in arrDelete into a variable called currentItem. We then use
the id of currentItem as an index to identify the page item and place it inside a variable called
item2Delete (line 261). On line 262, we delete the page item using the remove() method of
the pageItem object. We then remove the item from the list in our dialog box with the remove()
method of the listbox object (line 263).

SYNTAX myListbox.remove (item)


SUMMARY ... Method of listbox object
Removes specified item from a listbox control.

272
Item The item to be removed. Can be specified as an integer, string or listitem object.

Finally, enter the following simple Close button callback below all
of your existing code.
267
268 // Callback for btnClose
269 btnClose.onClick = function(){
270 g.win.close();
271 g = null;
272 }

Save your changes.


That completes our callbacks and our script.
6. Testing the script

Before running your code, you might like to open the document
called “03-pageitems.indd” in the “chapter10” folder.
Test the buttons on the left of the dialog. Each time you activate or
deactivate a button, the list of items should be refreshed.
Try double-clicking any item in the list. You should automatically
jump to the page containing the corresponding page item, which
should also be selected.
Choose Picture frame from the selection dropdownlist control and
click the Select by Type button. Now choose Group, activate the
Add to selection checkbox and click the button again.
Choose New layer from the second dropdown, enter a layer name,
click OK then click Move to layer.
Now click the Delete Selected button to delete the items.
Test the dialog on some copies of your own documents. Be sure to
use File > Save As to create a copy to mess about with, since the
changes being made to your documents are fairly drastic and may

273
accidentally get saved.

CHAPTER 11. Error handling and debugging


Creating scripts for different InDesign versions
This book deals with writing scripts for InDesign CS5. However, it is possible that you will
also need to write scripts which are compatible with previous versions of InDesign. The
degree of compatibility you can achieve in your scripts will depend on the InDesign objects
they involve. If these objects have undergone changes across the different versions, then, by
and large, your script will not be backward compatible. This means that, if you want users of
previous versions of InDesign to use your script, you will need to create different versions.
Detecting the InDesign version
When scripting, InDesign itself is the application object—app—and the property app.version
will indicate which InDesign version is running the current script. To convet the version to an
integer, use the parseInt() function.
var intVersion = parseInt(app.version);
This returns the underlying version number, not the name CS4, CS5, etc., as shown in the
following table.

Version name Version number


InDesign CS 3
InDesign CS2 4
InDesign CS3 5
InDesign CS4 6
InDesign CS5 7

Creating conditional code


When creating scripts which can be run on multiple InDesign versions, you will employ three

274
main techniques:
• Use conditional statements within the script which execute different lines of code
depending on the version

if (parseInt(app.version) > 5){


// Code for CS4 and later
}
else{
// Code for CS3
}
• Use conditional statements within your script which execute different functions

if (parseInt(app.version) > 5){


CS4andCS5Code();
}
else{
CS3Code();
}

function CS4andCS5Code(){
// Code for CS4 and later
}

function CS3Code(){
// Code for CS3
}

• Write different scripts and run the appropriate one, depending on the InDesign version

if (parseInt(app.version) > 5){


app.doScript(File(app.activeScript.parent + "/cs4andcs5code.jsx"));
}
else{
app.doScript(File(app.activeScript.parent + "/cs3code.jsx"));
}

SYNTAX app.doScript (Script)


SUMMARY ... Method of application object
Runs the code in the specified file as if it were part of the current script.

The script file to be executed. For example:


Script
File(app.activeScript.parent + "/cs4andcs5code.jsx")

Running old scripts with InDesign CS5


If a script was created with an older version of InDesign, it may run under CS5 with no
problems. However, it may also contain code which is no longer compatible with CS5 and

275
which causes errors. Luckily, ExtendScript includes a mechanism for allowing such scripts to
run without returning errors.
The scriptPreferences object, a property of the application object, contains a property called
version which can be used to specify the object model which will be used when the
ExtendScript engine interprets the code. To override the default—which, naturally, matches the
application being run—set the version to the required number. Thus, for example, to run a CS4
script from inside CS5, you insert the line:
app.scriptPreferenes.version = 6.0;
at the top of your script.
Detecting the platform
Although JavaScript is cross-platform and can be used to create solutions which will run on
both Mac and Windows, there will be times when you wish to differentiate between the
platforms. Working with files is one typical example. The syntax for detecting the platform is
simple: $.os. This will generate details of both the operating system and the version being
used. Thus for example, the code:
alert($.os);
will generate the dialogs shown in figure 11-1, below, on Mac and Windows platforms,
respectively.

Figure 11-1: The $.os property returns both the operating system and version.

To use this information to decide which platform we are dealing with, the JavaScript String
function indexOf() can be used. It returns the position of the string specified as the main
parameter of the function: if the string is not found, it returns -1.
if($.os.indexOf("Windows") > -1){
var strOS = "Windows";
}

276
else if($.os.indexOf("Macintosh") > -1){
var strOS = "Macintosh";
}
alert(strOS);

Basic error handling techniques


It is to be expected that when someone is running a script that you have written, conditions will
arise which you have not anticipated, execution of the code will cease and an error message
will be displayed. Whilst it is not possible to anticipate all of the areas where this might be the
case, there are usually a few situations that are a safe bet. Code relating to the manipulation of
specific resources will be prone to error whenever these resources are not present or not
accessible. Anything involving files or file access is a good candidate to throw up errors for
some users.
The best way to minimize the risk of errors is to make your code as flexible as possible. After
stating what should happen in an ideal situation, your script should then say what should
happen under less than ideal circumstances. For example, if your script requires a template
called "leaflet-a5-512.indt" which is supposed to be in the My Documents folder for
Windows users, the Documents folder for Mac users and which, of course, may be missing;
you would use conditional statements to cater for each of the relevant possible outcomes.
Listing 11-1: Anticipating errors with conditional logic
1 if($.os.indexOf ("Macintosh") > -1){
2 var fleTemplate = File("~/Documents/indesign/leaflet-a5-512.indt");
3}
4 else if($.os.indexOf ("Windows") > -1){
5 var fleTemplate = File("~/My%20Documents/indesign/leaflet-a5-512.indt");
6}
7 if (fleTemplate.exists){
8 var doc = app.open(fleTemplate);
9}
10 else{
11 fleTemplate = File.openDialog ("Please locate template leaflet-a5-512.indt", "*.indt");
12 if(fleTemplate != null){
13 var doc = app.open(fleTemplate);
14 }
15 }

On lines 1 to 6, we use conditional statements to populate the variable fleTemplate based on


the operating system. Then, if the file exists on that machine, we open it (line 8).
If the file does not exist, we display a dialog allowing the user to locate the file for us (line
11). Then, provided they choose a file, we open it (line 13).
Using try ... catch
In addition to employing conditional logic, errors can also be prevented by using try ... catch
statements. Try and catch are the JavaScript statements used to shield the user from alarming
and confusing error messages by allowing you to suppress built-in error handling and replace
it with something more user-friendly. The format is shown below.

277
try{
// Code which may trigger errors
}
catch(errorVariable){
// Code to be executed if an error occurs
}
For example, if your script includes a section where the user has to create and name a new
layer and they enter a layer name which already exists, an error will occur and the script will
terminate. By placing the attempt to create a new layer in a try block, this error will be
eliminated and the code inside the catch section will be executed instead.
Listing 11-2: Using try ... catch statements
1 var doc = app.activeDocument;
2 var blnLayer = false;
3 while (blnLayer == false){
4 var strLayer = prompt("Name of new layer.", "");
5 if(strLayer != null){
6 try{
7 var newLayer = doc.layers.add({name: strLayer});
8 blnLayer = true;
9 }
10 catch(errLayer){
11 alert("Sorry, a layer by that name already exists.");
12 }
13 }
14 }

To oblige the user to enter a name which has not already been assigned to an existing layer, we
use a while block, which will keep executing as long as the variable blnLayer contains its
initial value of false.
Within the while loop, we prompt the user to enter the name of the new layer and capture the
result in strLayer (line 4).
If the user does not click the Cancel button (which would cause strLayer to be null), we
attempt to create a layer with the value returned into strLayer (line 7). If the attempt to create
the layer is unsuccessful, line 8 does not execute: instead, we go straight to the catch block and
the alert is displayed (line 11).
If the attempt is successful, we set blnLayer to true (line 8) causing the while loop to
terminate.
The error object
The error object is a global JavaScript object which contains the last error generated by the
active script. You will notice that the catch statement requires the use of a variable: this acts as
the container for the error object. When providing feedback to the user, we can use the
properties of the error object to provide information. Thus, in our alert on line 11, we could
use the description property.
11 alert(errLayer.description);

278
This would automatically generate a message not unlike the one we have manually constructed.

Figure 11-2: The properties of the error object can provide useful user feedback.

The properties of the error object are shown below.

SYNTAX Properties of error object


SUMMARY ...
Name The type of error: e.g. ReferenceError or IOError.
Number The error code or number.
Description A short description of the error.
Message The message which is displayed when the error occurs. Usually the same as the description.
Line The line number of the statement which triggered the error.
FileName The full path to the script including the file name.
Source The actual code of the problem script.

Throwing your own errors


It is also possible to use the try ... catch structure for your own purposes, throwing your own
errors, which will then be handled by the catch statement just like proper JavaScript errors.
This is particularly useful when performing operations like data validation where you want to
have the final say on what constitutes an error. The syntax for doing this is as follows:
throw new Error(message)
Listing 11-3 shows an example of using the throw statement.
Listing 11-3: Using the throw statement
1 win = new Window("dialog", "Advert Setup");
2
3 win.grpJob = win.add("group");
4 win.grpJob.add("statictext", [0, 0, 75, 20], "Job number");
5 win.grpJob.txt = win.grpJob.add("edittext", [75, 0, 150 , 20]);
6
7 win.grpW = win.add("group");
8 win.grpW.add("statictext", [0, 25, 75, 45], "Width:");

279
9 win.grpW.txt = win.grpW.add("edittext", [75, 25, 150, 45]);
10
11 win.grpH = win.add("group");
12 win.grpH.add("statictext", [0, 50, 75, 70], "Height:");
13 win.grpH.txt = win.grpH.add("edittext", [75, 50, 150, 70]);
14
15 win.grpButs = win.add("group");
16 win.grpButs.ok = win.grpButs.add("button", [0, 80, 75, 100], "OK");
17 win.grpButs.ok.onClick = validateData;
18 win.grpButs.add("button", [75, 80, 150, 100], "Cancel");
19
20 var intResult = win.show();
21
22 function validateData(){
23 strError = "";
24 try{
25 if (win.grpJob.txt.text == "" ||
26 win.grpW.txt.text == "" ||
27 win.grpH.txt.text == ""){
28 throw new Error("Please complete all fields.");
29 }
30 else if (isNaN(parseInt(win.grpW.txt.text)) ||
31 isNaN(parseInt(win.grpH.txt.text))){
32 throw new Error("Please enter numbers for width and height.")
33 }
34 }
35 catch(err){
36 strError = err.message + "\n";
37 }
38 if(strError != ""){alert(strError);}
39 else{win.close(1);}
40 }

Figure 11-3: Using throw statements with try ... catch for form validation.

The script creates the dialog shown in figure 11-3: it has three textedit fields, two of which—
width and height—must be completed with numeric values and none of which should be left
blank.
On lines 24 to 37, inside the try block, we test whether any of the fields are blank and whether
either the width or height contains a non-numeric value. If either of these tests is true, we use a
throw statement to roll our own error with the appropriate message (lines 25 and 29).

280
Inside the catch block, we use the parameter variable err to contain the error object and access
the message property with the statement err.message (line 36).
Finally, outside the try ... catch structure, we check the value of strError. If the variable is not
empty, we display the error message it contains; otherwise, we close the dialog (lines 38 to
39).
Eliminating simple errors
To err is human. Having the knowledge and ability to do something does not guarantee that we
will be able to do it perfectly every time. Therefore, when writing code, everyone will make
errors and many of these are pretty basic. First, we have typos and other such errors, where we
just enter the wrong piece of code. These are usually pretty easy to spot. As soon as you test
your script, it will freeze on the line containing the mistake and, as often as not, you will spot
your mistake straightaway.
For example, let's say you enter the wrong name for an object (like putting application instead
of app), as shown in the following code snippet.
for(var i = 0; i < application.documents.length; i ++){
alert(application.documents[i].name);
}
If you test your code by running the script in InDesign, the error message shown in figure 11-4
will be displayed.

Figure 11-4: The error message displayed by InDesign when it does not recognise an object name.

The error message identifies the type of error and the line where it occurred. Similarly, if you
test your code from the ESTK, you will be returned to the ESTK with the line containing the
error highlighted and the status bar will give details of the error.
Recognizing these simple errors for what they are is not always as straightforward as this. For
example, if we have a variable called strErrors and, at some stage in our script, we enter
strError by mistake, we won't get an error saying: "You entered 'strError'; did you mean
'strErrors'?". The message you see will be determined by what you were trying to do with the
contents of strErrors. However, by and large, fixing syntax errors will not occupy too much of
your time.
Creating scripts for other people
281
Far more of your time will be spent, anticipating and testing for runtime errors. In fact, there is
no real limit to how much time you can spend on this type of testing: it all depends on the
demands of your audience. If you are the only person who will be using a script, you can
anticipate the conditions under which the script will be running and get away with a minimal
level of error handling. However, as soon as you start thinking about distributing a script to
other people, you need to think about how the script will perform in their environment.
Avoiding references to specific locations
One of the key requirements in creating code for other people is to make references to file
locations as non-specific as possible.
Using app.activeScript
For example, if your script needs a particular file, instead of entering a literal file path:
var myDocument = app.open(File("/c/indesign/catalog.indd"));
you can enter a path relative to the location of your script:
var scriptFolder = app.activeScript.parent;
var filePath = scriptFolder + "/indesign/catalog.indd";
var myDocument = app.open(File(filePath));
or display a dialog allowing the user to specify the location of the file.
var filePath = File.openDialog("Please locate the catalog document.");
var myDocument = app.open(filePath);
Using Folder.current
The current property of the Folder class allows you to set an arbitrary starting point in relation
to which relative references can then made. Thus, for example, if your script needs to refer to a
folder whose structure is known but whose location may vary from user to user, you could set
Folder.current by asking the user to supply the location of the folder and then use relative
references to navigate within the folder structure, as shown in listing 11-4, below.
Listing 11-4: Using relative references
1 var userFolder = Folder.selectDialog("Please locate the clients directory.");
2 if(userFolder != null){
3 Folder.current = userFolder;
4 var settingsFile = File("scripting/settings.txt");
5 alert(settingsFile.fullName);
6 try{
7 settingsFile.open("r");
8 var strSettings = settingsFile.read();
9 alert(strSettings);
10 settingsFile.close();
11 }
12 catch(err){
13 alert("Settings file could not be opened.");
14 }
15 // ...
16 }
This example assumes that, inside the clients folder, we know that there will be a folder called
"scripting" and inside that will be a file called "settings.txt". On line 1, we use the

282
selectDialog() method to ask the user to locate the "clients" folder. Then, on line 3, we change
Folder.current to the location specified. This means that, on line 4, when we create our file
object, we can specify the relative location "scripting/settings.txt".
Debugging in break mode
The process of debugging is the tracking down of code which has errors that prevent your
scripts from doing what they are supposed to do. The ExtendScript Toolkit includes a
debugger which allows you to step through code line by line, inspect the contents of variables
and the values returned by statements while the script is running as well as inserting
breakpoints to interrupt code execution at strategic points.
Stepping through a script
In order to debug code, you must run the script you are testing from the ESTK in break mode.
The simplest method of working in break mode is to step through your code line by line.
Activate the script you wish to debug.
Select Adobe InDesign CS5 as the target application from the drop-
down menu in the top left of the document window.
Choose Debug > Step Into or press F11 (Windows) or Command-
Shift-I (Mac). You can also click on the Step Into button on the
toolbar in the top right of the document window.
Repeat step 3 as many times as necessary.
Examining variables, objects and statements
In break mode, your script is frozen in time and you can examine the contents of variables,
objects and evaluate statements. Firstly, you can simply position the cursor over specific
elements of your code and a tooltip will be displayed showing you the value which the element
currently contains. Secondly, you can enter a variable or object name or a statement in the
JavaScript console and press return to obtain the current value.
Using breakpoints
When using the technique outlined above, you enter break mode at the very start of the script
and execution begins at line 1. It is often more convenient to enter break mode at some midway
point within the script. This can be achieved by setting a breakpoint.
You can set breakpoints by clicking just to the right of the line number. A red circle appears
next to the line number to indicate that a breakpoint has been set.
You can also use the Breakpoints panel (Window > Breakpoints). Choose Add from the menu
in the top right of the panel.

283
Figure 11-5: Adding a breakpoint using the Breakpoints panel

Try it for yourself!


TUTORIAL: Working in break mode
In this tutorial, we will explore debugging techniques that can be used in break mode.
Begin, in InDesign, by opening the file called "05-delete-
buttons.indd" in the "chapter11" folder.
Switch over to the ESTK and open the file "05-delete-buttons.jsx",
again, in the "chapter11" folder. The code is shown in listing 11-5,
below.
Listing 11-5: The script used in this tutorial
1 for(var i = 0; i < app.activeDocument.pages.length; i ++){
2 var currentPage = app.activeDocument.pages[i];
3 for (var j= 0; j < currentPage.buttons.length; j ++){
4 checkButton(currentPage.buttons[j]);
5 }
6}
7
8 function checkButton(currentButton){
9 if (currentButton.behaviors.length == 0){
10 killButton(currentButton);
11 }
12 }
13
14 function killButton(currentButton){
15 app.select(currentButton);
16 var blnKill = confirm("Delete this button?");
17 if(blnKill){currentButton.remove();}
18 }

The purpose of the script is to delete all buttons in the active document that have no behaviors

284
attached to them. It loops through all of the pages in the current document and then all of the
buttons on each page. For each button, the main script calls the checkButton() function,
passing the button as a parameter (line 4).
The checkButton() function tests whether the current button has no behaviors attached to it. If
there are no behaviors, it calls the killButton() function, again passing the button as an
argument (line 10).
The killButton() function selects the button (line15) and displays a confirm dialog asking the
user whether to delete the button. If the user clicks "Yes" (causing the confirm function to
return true), the button is deleted (line 17).
1. Stepping through a script
Resize the application windows of InDesign and the ESTK so that
both are visible on screen in a vertical tile. (If you have the luxury
of a dual screen setup, display the ESTK on one screen and
InDesign on the other).

Activate the ESTK and click on the Step Into button on the toolbar
in the top right of the document window.

The script runs, but immediately enters break mode and pauses with the first line of the code
highlighted. Note that the highlighted line has not yet been executed.
Position the cursor over the variable name i on line 1 of the code. A
tooltip should appear bearing the message Object i = undefined.

285
The other way of examining the contents of variables and objects is to use the JavaScript
console.
If it is not already visible, choose Window > JavaScript Console.
Enter the line app.activeDocument.pages.length and press
Return.
The JavaScript console should display the text: Result: 1.

Click the Step Into button again or press F11 (Windows) or


Commaind-Shift-I (Mac).
This causes the highlighted line to execute and line 2 to be selected. As well as displaying
values, the JavaScript console also allows you to change the values of variables. Thus if, for
testing purposes, we wanted to jump to the point where i is equal to 5, we can simply
overwrite the current value of i.
In the JavaScript console, enter the line i = 4 and press Return. The
JavaScript console displays the confirmation line Number i = 4.
Click the Step Into button repeatedly to execute a complete
iteration of the outer for loop.
When the confirm dialog appears in the InDesign window, click the
No button.

286
Click the Stop button on the toolbar of the ESTK.

2. Setting breakpoints
By using the Step Into command at the outset, we enter break mode immediately. If we want to
enter break mode at a later point in the script, we can set a breakpoint on the desired line.
Breakpoints can be set either in the ESTK or in your code.
In the ESTK, choose Window > Breakpoints to display the
Breakpoints panel.
Click on the grey bar to the right of line number 17 to set a
breakpoint. A red circle appears next to the line number and a new
item appears in the Breakpoints panel.
Double-click on the item in the Breakpoints panel and the Modify
Breakpoint dialog appears.
At the top of the dialog is a confirmation of the line number, which can obviously be changed,
and a checkbox for activating and deactivating the breakpoint. Then there is a larger text box
into which you can enter a conditional statement.
The final text field—Hitcount—allows you to enter the iteration on which to activate the
breakpoint. Thus, if we enter the number 3, the breakpoint will only kick in the third time it is
encountered.
Finally, we have a button which allows us to remove the breakpoint. Breakpoints can also be
deleted by selecting them in the Breakpoints panel and choosing Remove from the menu in the
top right of the panel or by clicking twice on the red circle next to the line number—the first
click makes the breakpoint inactive (and changes its colour to a dark red) and the second
removes it.

287
Let's make the breakpoint conditional.
In the Condition box of the Modify Breakpoint window enter the
statement:
j == 5.
(We could achieve the same result by entering 5 in the Hitcount field.)

Click OK.
You will notice that , as a result of entering a condition in the Modify Breakpoint dialog, the
red circle next to the line number is now orange and the icon to the left of the item in the
Breakpoints panel is now diamond-shaped.
Click on the Run button on the toolbar.
The confirm dialog will appear five times before the breakpoint
interrupts the execution of the script. Each time it appears, click on
the No button.

When the script stops—with line 17 highlighted but not yet executed—the variable blnKill will
contain the value false, since you just clicked No when the confirm dialog appeared. Let's now
override this value.
In the JavaScript console, enter the line: blnKill = true and press

288
Return. The confirmation Result: true should then appear on the
line below.
Now click on the Step Into button to execute the line and the
selected button should be deleted from the InDesign document.

Click on the Stop button on the toolbar to exit break mode, or


choose Debug > Stop.
Return to InDesign and choose File > Revert to restore all of the
buttons.
Now let's try setting a breakpoint programmatically.
Click twice on the orange circle on line 17 to remove the
breakpoint.
Position the cursor at the end of line 16 and press Return to create
a blank line.
Enter the line shown below.
14 function killButton(currentButton){
15 app.select(currentButton);
16 var blnKill = confirm("Delete this button?");
17 $.bp(j == 5);
18 if(blnKill){currentButton.remove();}
19 }

SYNTAX $.bp([condition])
SUMMARY ... $ object function
Sets a breakpoint programmatically, with an optional condition attached.

289
Condition Optional. A conditional statement which will determine if and when the breakpoint is executed.

Click on the Run button on the toolbar of the ESTK.


The confirm dialog will again appear five times: click Yes each
time to delete the buttons.
When the break point interrupts code execution, click the Step
Into button three times to delete the button, exit the function and
return to the nested for loop on line 3.
Position the cursor over the statement
currentPage.buttons.length. A tooltip should appear with the
words Number currentPage.buttons.length = 6.

(If the tooltip trick does not work for you, just enter
currentPage.buttons.length in the JavaScript console and press
Return.)
At the start of the script, currentPage.buttons.length was 12; but since we have deleted 6
buttons, it is now 6 and, since currentPage.buttons.length represents the upper limit of our
for loop, the loop will now terminate and no more buttons will be processed.
click the Step Into button to exit the nested for loop and return to
the outer for loop.
Since there is only one page in the doucment, app.activeDocument.pages.length has also
reached its limit.
click the Step Into button once more to exit the outer for loop and
finish execution of the script.

290
The status bar in the bottom left of the document window now displays the text: Execution
finished. Result: undefined.

If you didn't remember the solution to the problem with this script straightaway, you probably
will have by now. The inner loop needs to start with the highest index and work its way down
to the lowest; so that the index being processed cannot cease to exist during the execution of
the script.
Delete line 17—$.bp(j ==5)—to remove the breakpoint, which has
now served its purpose.
Modify line 3 as shown below.
1 for(var i = 0; i < app.activeDocument.pages.length; i ++){
2 var currentPage = app.activeDocument.pages[i];
3 for (var j = currentPage.buttons.length - 1; j >= 0 ; j --){
4 checkButton(currentPage.buttons[j]);
5 }
6}

Run the script again, clicking Yes each time to delete the buttons.
This time, 10 buttons should be deleted and you should be left with
just the two that have behaviors attached to them.

Using alerts for debugging


One of the simplest and most effective methods of obtaining feedback from a problem script is
the strategic placement of debugging lines consisting of alerts which output the contents of
variables and the results of evaluating key statements. When using this technique, you should
try to make the output as detailed and clear as possible. Instead of using alerts to output the
contents of one variable at a time:
alert(var1)
try creating complex statements which output all relevant information in one alert:
var strDebug = "Var1 = " + var1 + "\n"
strDebug += "Var2 = " + var2 + "\n"
strDebug += "var3 object type = " + var3.constructor.name // etc
alert(strDebug);
Thus, in the example we have just looked at in the last tutorial, we could have used the
following alert to track the value of the counter variable j and the number of button objects in
the document.

291
1 for(var i = 0; i < app.activeDocument.pages.length; i ++){
2 var currentPage = app.activeDocument.pages[i];
3 for (var j= 0; j < currentPage.buttons.length; j ++){
4 checkButton(currentPage.buttons[j]);
5 strDebug = "Counter i = " + i + "\n";
6 strDebug += "Counter j = " + j + "\n";
7 strDebug += "Buttons left = " + currentPage.buttons.length;
8 alert(strDebug);
9 }
10 }
This would have produced alerts like the one shown in figure 11-6, below.

Figure 11-6: Using alert statements to output the value of variables and objects is a simple but useful
debugging technique

Writing values to the JavaScript console


Using alerts is useful in examples where there is not a lot of iteration. However, you can
imagine how tedious it would be to have to dismiss alert after alert if you had a script which
looped through the pages of a long document. An alternative method of outputting script data is
to write to the JavaScript console using the $.write() and $.writeln() functions.
In our last example, as an alternative to using an alert statement, we could have used
$.writeln() as shown below.

1 for(var i = 0; i < app.activeDocument.pages.length; i ++){


2 var currentPage = app.activeDocument.pages[i];
3 for (var j= 0; j < currentPage.buttons.length; j ++){
4 checkButton(currentPage.buttons[j]);
5 strDebug = "Counter i = " + i + "\t\t";
6 strDebug += "Counter j = " + j + "\t\t";
7 strDebug += "Buttons left = " + currentPage.buttons.length;
8 $.writeln(strDebug);
9 }
10 }

$.write(text)
SYNTAX Function of the $ object
SUMMARY ... Writes the specified text to the JavaScript console
$.writeln(text)

292
Function of the $ object
Writes a line to the JavaScript console followed by a newline character
Text The text to be written. Any valid JavaScript statement.

At the end of lines 5 and 6, two tab characters are inserted to enhance readability of the output.
(Each use of the $.writeln() function generates a new line automatically.)
The output produced by the code is shown in figure 11-7.

Figure 11-7: The $.writeln() function can be used to output a series of statements to the JavaSript
console.

Any text which appears in the JavaScript console can be copied and pasted into a word
processor or an InDesign file where it can be formatted, printed or made into a PDF.
CHAPTER 12. Interactive Documents
Overview
Traditionally, InDesign produced documents which were destined to be printed and the ability
to produce PDF files with bookmarks and links was just a useful bonus. Now that we have all
become used to the on-screen consumption of information, it is no surprise that the features
available in InDesign for the creation of interactive documents have become quite significant.
Setting preferences
When scripting interactive documents, there are a number of preferences that you will want to
set differently than when working with documents destined for print.
View preferences
When creating on-screen documents, pixels become the most convenient unit of measurement.
The viewPreferences object of the document in question contains the relevant properties.
var myDoc = app.documents.add();
myDoc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.pixels;
myDoc.viewPreferences.verticalMeasurementUnits = MeasurementUnits.pixels;

Document preferences
If your script is going to create a document which will be displayed at a particular monitor
resolution, you can simply enter this resolution as the document size. For documents which

293
will be distributed to a wider audience, a minimum size of 800 x 600 can be used, as the
lowest common denominator which will work on absolutely anyone's screen, or a more typical
size of 1024 x 768 will be compatible with most users' screens. You will also want to set the
orientation to landscape and switch off facing pages.
myDoc.documentPreferences.pageWidth = 1024;
myDoc.documentPreferences.pageHeight = 768;
myDoc.documentPreferences.pageOrientation = PageOrientation.landscape;
myDoc.documentPreferences.facingPages = false;

Transparency preferences
If you are using transparency effects, you will want to set the blendingSpace property of the
transparencyPreferences object to BlendingSpace.RGB.
myDoc.transparencyPreferences.blendingSpace = BlendingSpace.RGB;

Adding page transitions


When a document is being viewed in full screen mode in Acrobat, the page transitions feature
provides a basic form of animation as one page is replaced by another, such as a fade or wipe.
Page transitions are also shown when an interactive InDesign document is exported as a Flash
SWF document and offer extra options such as the popular page turn transition. Page
transitions are applied via the Page Transitions panel which contains the three dropdown
menus shown below.

Figure 12-1: The InDesign Page Transitions panel

When scripting, page transitions are applied to the spread (rather than the page) object. The
three page transition properties are outlined below.

294
SYNTAX Page transition properties of the spread object
SUMMARY ...
The type of transition, for example:
PageTransitionTypeOptions.
pageTransitionType dissolveTransition
fadeTransition
pageTurnTransition
The speed of the transtion:
PageTransitionDurationOptions.fast
pageTransitionDuration
PageTransitionDurationOptions.medium
PageTransitionDurationOptions.slow
The transition direction, for example:
PageTransitionDirectionOptions.
rightToLeft
pageTransitionDirection
leftDown
leftUp
leftToRight

To apply page transitions to a document, you would typically loop through the document pages
and target the page transition properties of the parent spread by using code like the following.
myPage.parent.pageTransitionType = PageTransitionTypeOptions.fadeTransition

Automatic layout adjustment


When adapting existing print documents for online delivery, you would almost certainly set all
of the preferences we have mentioned. In addition, it is useful to activate InDesign's automatic
layout adjustment feature (Layout > Layout Adjustment).

Figure 12-2: InDesign's layout adjustment dialog

To control layout adjustment with scripting, set the relevant properties of the
LayoutAdjustmentPreferences object of the document.
To activate layout adjustment, use the following code:

295
myDocument.layoutAdjustmentPreferences.enableLayoutAdjustment = true

Shortening a document
In many cases, the online version of a document needs to be shorter than the original print
version, particularly if the document needs to be converted into an on-screen presentation. If
the document uses styles, scripting can be used to remove all text in particular styles.
Listing 12-1 gives an example of preparing a document for online use. It displays a dialog
allowing users to select the styles they want to keep. Any text not in one of the styles selected
is then removed from the document.
The script then activates automatic layout and changes the orientation of the document to
landscape before adding a page transition to each of the document pages.

Figure 12-3: The dialog displayed by the code in listing 12-1

Listing 12-1: Converting a print document for online use


1 var g = {}; main(); g = null;
2
3 function main(){
4 if(app.documents.length == 0){
5 alert("No documents are open. Cannot continue.");
6 }
7 else{
8 var intResult = selectStyles();
9 if(intResult ==2){
10 alert("Operation cancelled.");
11 }
12 else if(intResult ==1){
13 removeUnwanted();
14 adjustLayout();
15 addTransitions();
16 }
17 }

296
18 }
19
20 function selectStyles(){
21 g.doc = app.activeDocument;
22 g.win = new Window("dialog", "Style selection");
23 g.win.add("statictext", undefined, "Styles to include in the output document:");
24 g.win.lstStyles = g.win.add("listbox", undefined, undefined, {multiselect:true});
25 g.win.lstStyles.size = [200, 100];
26 for (var i = 0; i < g.doc.paragraphStyles.length; i ++){
27 g.win.lstStyles.add("item", g.doc.paragraphStyles[i].name);
28 }
29 g.win.add("button", undefined, "Cancel");
30 g.win.btnOK = g.win.add("button", undefined, "OK");
31 return g.win.show();
32 }
33
34 function removeUnwanted(){
35 for (var h = 0; h < g.doc.stories.length; h ++){
36 var pageType = g.doc.stories[h].textContainers[0].parentPage.parent;
37 if(pageType.constructor.name != "MasterSpread"){
38 var myRanges = g.doc.stories[h].textStyleRanges;
39 for (var i = myRanges.length -1; i >=0; i --){
40 var blnKeep = false;
41 for (var j = 0; j < g.win.lstStyles.selection.length; j++){
42 if (g.win.lstStyles.selection[j].text ==myRanges[i].appliedParagraphStyle.name){
43 blnKeep = true;
44 }
45 }
46 if (blnKeep == false){
47 myRanges[i].remove();
48 }
49 }
50 }
51 }
52 }
53
54 function adjustLayout(){
55 with(g.doc.layoutAdjustmentPreferences){
56 allowGraphicsToResize = true;
57 enableLayoutAdjustment = true;
58 snapZone = "10px";
59 g.doc.documentPreferences.pageOrientation = PageOrientation.landscape;
60 g.doc.documentPreferences.facingPages = false;
61 }
62 for(var i = 0; i < g.doc.pages.length; i++){
63 var blnEmpty = true;
64 for (var j =0; j < g.doc.pages[i].textFrames.length; j ++){
65 if(g.doc.pages[i].textFrames[j].contents.length > 0){
66 blnEmpty = false;
67 }
68 }
69 if(blnEmpty == true){g.doc.pages[i].remove();}
70 }
71 }

297
72
73 function addTransitions(){
74 for (var i = 0; i < g.doc.spreads.length; i++){
75 g.doc.spreads[i].pageTransitionType = PageTransitionTypeOptions.dissolveTransition;
76 }
77 }

The script begins by checking that there is a document open: if there is, the selectStyles()
function is called, which displays the dialog. If the user clicks OK, thereby returning the
number 1 into the variable intResult, the removeUnwanted() function is called.
The removeUnwanted() function loops through all of the stories in the document and checks
that they are not on a master page (lines 36-37). It then loops through all of the textStyleRange
objects in each story and tests whether the text is in one of the styles selected by the user.
On line 40, we set the variable blnKeep to false then we loop through the items selected by the
user in the listbox control win.lstStyles comparing the text of the item to the name of the style
of the current textStyleRange object. If we find a match, then we know this section of text must
be kept, so we set blnKeep to true. After the loop has finished, on lines 46 to 48, we check the
value of blnKeept. If it is false, we delete the current textStyleRange object.
The adjustLayout() function activates InDesign's automatic layout feature, changes the page to
landscape and switches off facing pages. Also, on lines 62 to 70, we loop through the
document pages, deleting all pages whose text frames are all empty.
Finally, the addTransitions() function loops through all of the spreads in the document
applying the dissolve transition.
You can test the script by opening the file called “01-quick-interactive.indd” in the
“chapter12” folder and then running the script from the Scripts panel. Select the styles
Heading1, Subhead1 and firstPara then click OK.
Creating buttons
In InDesign, any page item can become a button: you simply right-click on it and choose
Interactive > Convert to button from the context menu. InDesign then creates a button and
makes the original page item the content of the default (normal) state of the button. (It's a bit
like converting artwork into a symbol in Flash.)
Button states
When scripting, you create a button by using the add() method of the buttons collection of the
page on which you want to place the button. Every newly-created button has a single state—the
normal state. To make the button visible, you then add a page item to the button's normal state.
var myButton = myPage.buttons.add();
var myPageItem = myPage.pageItems[0];
myButton.states[0].addItemsToState(myPageItem);
Note that myButton.states[0] exists as soon as the button is created. To add any further states to
the button, you use the states.add() method of the button object. When you do so, you need to
specify the state type. This can be done after the state has been added by setting stateType

298
property of the button state.
myButton.states.add();
myButton.states[1].stateType = StateTypes.rollover;
var myPageItem2 = myPage.pageItems[1];
myButton.states[1].addItemsToState(myPageItem2);
You can also specify the stateType while creating the button by using the creation properties
object.
myButton.states.add({stateType: StateTypes.rollover});

Behaviors
To specify what happens when the user interacts with the button, you add the appropriate type
of behavior to it. For simple navigation, you would use one of the go to page behaviors:
gotoFirstPageBehavior, gotoLastPageBehavior, gotoNextPageBehavior,
gotoPreviousPageBehavior or gotoPageBehavior. You would then specify the mouse event that
triggers the behavior: mouseUp, mouseDown, mouseEnter, mouseExit, onFocus or onBlur.
var myBehavior = myButton.gotoFirstPageBehaviors.add();
myBehavior.behaviorEvent = BehaviorEvents.mouseUp;
You can also specify the behaviorEvent while adding the behavior:
var myBehavior = myButton.gotoFirstPageBehaviors.add({behaviorEvent: BehaviorEvents.mouseUp});

Try it for yourself!


TUTORIAL: Creating an interactive presentation from images
In this Try it for yourself! tutorial, we will create an interactive presentation by importing all
the images in a folder specified by the user. After choosing the folder, the user will specify a
title and choose a page transition from the dialog shown in figure 12-4.

Figure 12-4: The dialog allows the user to enter a title and choose a page transition.

The title entered by the user is placed on the first page of the presentation and is also used as a
footer on subsequent pages, alongside navigation buttons which allow the user to move from

299
page to page. The script also imports all of the images in the folder and places each one on a
separate page, displaying metadata from each file as a title above the image, as shown in figure
12-5.

Figure 12-5: The script places an image on each page together with a title read in from the image's
metadata and also creates navigation buttons.

1. The main() function


Since the main element of this script will be building a document based on a series of images,
the first thing we need to do is to ask the user to specify the location of these images. Next, we
display a dialog allowing them to enter a title and choose a page transition. Then we build the
document and, finally, create an interactive PDF.
Create a new document in the ESTK and save it in the "chapter12"
folder as "02-interactive-from-images.jsx".
Enter the main script shown below.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 const minImages = 4;
7 findImages();
8
9 if(g.imageFiles.length < minImages){
10 alert("Not enough images to create document.");
11 }
12 else{
13 var intResult = createDialog();

300
14 if(intResult == 2){
15 alert("Operation cancelled.");
16 }
17 else{setupDocument(); setupTitle(); addNavigation(); importImages(); createPDF();}
18 }
19 }
On line 6, we create a constant called minImages: this stores the minimum number of images
which the folder designated by the user must contain before we will allow our script to
produce a document.
On line 7, we call the function findImages() which we hope will return an array of images
inside g.imageFiles. If the number of images inside g.imageFiles is less than g.minImages, we
display an error message (line 10) and no further processing takes place.
If sufficient images have been retrieved, we call the createDialog() function, which will
display the dialog shown in figure 12-4 (on page 301), and capture the result returned inside
the variable intResult. If the dialog returns 2 (indicating that the user clicked the Cancel
button), we display an alert and no further code is executed. Otherwise, we call the various
functions which will create the new document and generate an interactive PDF file.
2. Retrieving the image files
The findImages() function will use the Folder.selectDialog() method to allow the user to
specify a folder. It will then read all the files in that folder, which have a suitable file
extension, into the array g.imageFiles, which is then checked in the main() function to see if it
contains sufficient files to proceed.
Add the following function to the end of your script.
20
21 function findImages(){
22 var fldImages = Folder.selectDialog("Please select the images folder.");
23 g.imageFiles =[];
24 if(fldImages != null){
25 g.imageFiles = fldImages.getFiles(imagesOnly);
26 }
27 function imagesOnly(currentFile){
28 var intDot = currentFile.fullName.lastIndexOf(".");
29 var ext = currentFile.fullName.toLowerCase().substr(intDot);
30 switch(ext){
31 case ".tif": case ".eps": case ".ai": case ".psd" : case ".jpg": case ".gif" : case ".png":
32 return true;
33 break;
34 default:
35 return false;
36 }
37 }
38 }

301
Save your changes.
On line 22, we use Folder.selectDialog() to invite the user to specify the folder that contains
the image and capture the result inside fldImages.
Provided the user does not click the Cancel button (which returns null), on line 25, we use the
getFiles() method to retrieve all of the files in the folder. GetFiles() takes an optional filter
argument which may either be a string containing wildcards— for example, "*.jpg"—or, as in
our script, the name of a function— imagesOnly. Using a function offers more flexibility and
allows you to test for multiple file types. The function will automatically be called once for
each file in the folder and will determine whether the file will be retrieved or ignored.
When we define the imagesOnly() function, on line 27, we specify a single parameter
—currentFile—which will capture each file the folder contains, as the function is repeatedly
called.
On line 28, we place the position of the last dot within the file name inside the variable
intDot; then, on line 29, we use intDot as the argument of the substr() function to retrieve all
characters in the file name, starting from the last dot and reading to the end of the name, into the
variable ext.
Having extracted the file extension from the file name, we then use a switch statement to see if
it matches any of the image file types we want to import into the document. On lines 31 to 32,
we compound all the case statements which should return true: on lines 34 to 35, we specify
that, for every other file extension, the function will return false.
3. Creating the dialog
The createDialog() function builds and displays a simple dialog with an edittext control for
the user to enter a title and a dropdownlist from which they can choose a page transition.
Add the following function to the end of your script.
39
40 function createDialog(){
41 g.win = new Window ("dialog", "Document Setup");
42 g.win.add("statictext", undefined, "Title:");
43 g.win.txtTitle = g.win.add("edittext");
44 g.win.txtTitle.minimumSize = [200,20];
45 g.win.add("statictext", undefined, "Transition:");
46 arrTransitions = ["Blinds", "Box", "Comb", "Cover", "Dissolve", "Fade", "Push", "Split", "Uncover", "Wipe", "Zoom in", "Zoom
Out"];
47 g.win.ddlTransitions = g.win.add("dropdownlist", undefined, arrTransitions);
48 g.win.ddlTransitions.selection = 5;
49 g.win.add("button", undefined, "OK");
50 g.win.add("button", undefined, "Cancel");
51 return g.win.show();
52 }

302
Save your changes.
On line 46, we create and array called arrTransitions and populate it with the names of the
various transitions; then, on line 47, when we create the dropdownlist (g.win.ddlTransitions)
we use the array as the third parameter of the add() method—the items which will appear in
the list. On line 48, we set the default item of the dropdownlist as index 5—the sixth item:
"Fade".
On lines 49 and 50, we create standard OK and Cancel buttons which will return 1 and 2,
respectively when the dialog is closed. On line 51, we show the dialog, using the return
statement to send back the 1 or the 2 to the variable used in the function call:
13 var intResult = createDialog();

4. Document setup
The setupDocument() function will create a new InDesign file and set preferences suitable for
an interactive document.
Add the setupDocument() function to the end of your script.
53
54 function setupDocument(){
55 g.doc = app.documents.add();
56 // View Prefs
57 g.doc.viewPreferences.horizontalMeasurementUnits=MeasurementUnits.pixels;
58 g.doc.viewPreferences.verticalMeasurementUnits=MeasurementUnits.pixels;
59 // Doc Prefs
60 g.doc.documentPreferences.facingPages = false;
61 if(g.doc.masterSpreads[0].pages.length > 1){
62 g.doc.masterSpreads[0].pages[1].remove();
63 }
64 g.doc.documentPreferences.pageWidth = 800;
65 g.doc.documentPreferences.pageHeight = 600;
66 g.doc.documentPreferences.pageOrientation = PageOrientation.landscape;
67 // Margin prefs
68 g.master = g.doc.masterSpreads[0].pages[0];
69 g.master.marginPreferences.left = 25;
70 g.master.marginPreferences.top = 25;
71 g.master.marginPreferences.right = 25;
72 g.master.marginPreferences.bottom =75;
73 }

Save your changes.


On line 57-58, we change the unit of measurement of the new document to pixels, the most
suitable setting for an on-screen document.
On lines 60 to 63, we get rid of facing pages and the extra master page if it exists. When the
document is created, on line 55, it will use the default document preset which, unless the user
has changed it, will have facing pages switched on and therefore create two master pages.

303
Since switching off facing pages subsequently does not remove the extra master page, we have
to delete it ourselves.
5. Setting up the title page
The setupTitle() function adds an image to the first page of the document as well as the title
entered by the user via the dialog.
Add the setupTitle() function to the end of your script.
74
75 function setupTitle(){
76 var pgCover = g.doc.pages[0];
77 var fileName = File(app.activeScript.parent + "/nav-images/cover.jpg");
78 var imgCover = pgCover.place(fileName, [0,0]);
79 imgCover[0].parent.geometricBounds = [0, 0, 600, 800];
80 imgCover[0].fit(FitOptions.fillProportionally);
81
82 var txtTitle = pgCover.textFrames.add();
83 txtTitle.geometricBounds = [200, 800-25, 400, 25];
84 txtTitle.contents = g.win.txtTitle.text;
85 txtTitle.paragraphs[0].justification = Justification.centerAlign;
86 try{txtTitle.paragraphs[0].appliedFont = app.fonts.item("Franklin Gothic Heavy");} catch(err){}
87 txtTitle.paragraphs[0].pointSize = "48pt";
88 txtTitle.paragraphs[0].fillColor = g.doc.colors.itemByName("Paper");
89 txtTitle.paragraphs[0].strokeColor = g.doc.colors.itemByName("Black");
90 }

Save your changes.


On line 77, we create a file path relative to the location of our script inside the variable
fileName then, on line 78, we use the fileName variable as we place the file into the variable
imgCover. We also use the second parameter of the place() command: the place point or x and
y coordinates where the item will be placed.
Since place() can be used to import several items at once (as is also the case when useing File
> Place in the InDesign interface), the method returns an array of graphic objects—even when
only one file is being imported. Therefore, on lines 79 and 80, we use imgCover[0] to target
the (single) graphic inside the array. Having used the geometricBounds property to make the
rectangle containing the image (imgCover[0].parent) full size, we use the fit() method with
the parameter FitOptions.fillProportionally to scale the image to the size of its container
without distortion or gaps.
On lines 82 to 84, we create a text frame, position it and set it to display the title entered by the
user in the dialog. Then, on lines 85 to 89, we format the box and the text.
On lines 86, when setting the font, we use a try .. catch block, since an error will be generated
if the font is not available on the user's system. With this approach, the attempt will fail
without an error and the text will remain in the default font.

304
6. Adding navigation buttons
The main role of the addNavigation() function is to place four navigation buttons on the master
page and, hence, on every page in the document. For each button we need to do the following:
• Create the button
• Add an image to the normal state
• Add an image to the mouseOver state
• Add the appropriate behavior to the button
Since we will need to perform these steps four times (using virtually identical code), we will
create a for loop which will execute four times. To enable us to process a different button with
each iteration, we will loop through an array containing the four strings "Last", "Next",
"Previous" and "First".
Insert the following code at the end of your script.
91
92 function addNavigation(){
93 g.doc.layers.add();
94 var arrButtons = ["Last", "Next", "Previous", "First"];
95 for (var i = 0; i <=120; i +=40){
96 var btn = g.master.buttons.add(g.doc.layers.item("Layer 2"),
97 {geometricBounds: [600-60, 800-65-i, 600-20, 800-25-i]});
98 var arrIndex;
99 (i == 0)? arrIndex = arrButtons[0]: arrIndex = arrButtons[i/40];
100 var fileName = File(app.activeScript.parent + "/nav-images/" + arrIndex + ".png");
101 var img = g.master.place(fileName, [800-65-i, 600-60]);
102 btn.states[0].addItemsToState(g.master.pageItems[0]);
103 btn.states.add({stateType: StateTypes.rollover});
104 var fileName = File(app.activeScript.parent + "/nav-images/" + arrIndex + "2.png");
105 var img2 = g.master.place(fileName, [800-65-i, 600-60]);
106 btn.states[1].addItemsToState(g.master.pageItems[0]);
107 btn["goto" + arrIndex + "PageBehaviors"].add({behaviorEvent:BehaviorEvents.mouseUp});
108 }
109
110 // Add footer
111 }

Save your changes.


On line 93, we create a new layer for the navigation elements. There is no need to place it in a
variable. Since we created the document from scratch, it will initially have had a single layer
called "Layer 1". The new layer will be called "Layer 2" and be above "Layer 1".
On line 94, we define the array arrButtons with the four string values that we will loop
through. However, on line 95, we match the limits of the loop to the button dimensions.
Basically, the buttons each have a width of 40: so we start our counter (i) at zero and end at 3 x
40: 120.

305
On lines 96-97, we create the button on layer 2 and use the value of the counter i when setting
the geometricBounds property so that the buttons will be spaced 40 pixels apart. The
geometricBounds requires an array in the format [y1, x1, y2, x2]. By setting the
geometricBounds of each button to [600-60, 800-65-i, 600-20, 800-25-i], we are saying that
we want the top of each button to be 60 pixels from the bottom of our 600 pixel high page, and
that the buttons should start 65 pixels from the right of the 800 pixel wide page and then occur
at 40 pixel intervals. The following table shows the resulting x and y coordinates of each
button.

y1 x1 y2 x2
Button 0 (last) 540 735 580 775
Button 1 (next) 540 695 580 735
Button 2 (previous) 540 655 580 695
Button 3 (first) 540 615 580 655

Before we can use the counter as an index for arrButtons, we need to convert it. On line 99,
we use the JavaScript ternary operator, so that when i is zero, we use i as our index value but,
thereafter, we use i divided by 40 and the value retrieved from arrButtons is placed in a
variable called arrIndex.
On lines 100 and 104, we use arrIndex to complete the file path which is used to place the
image for the normal and mouseOver states of the button, respectively.
On line 107, we use arrIndex again to determine which behavior is added to each button.
Thus, for example, if i is 80 (third of four iterations), arrIndex will contain the word
"Previous"—the third item in arrButtons. The statement
btn["goto" + arrIndex + "PageBehaviors"].add(...)
will therefore evaluate to
btn["gotoPreviousPageBehaviors"].add(...)
which is an alternative method of writing
btn.gotoPreviousPageBehaviors.add(...)

Although it is not strictly related to navigation, let's add the page


footer as part of this function.
Insert the following lines of code below the line containing the
comment "// Add footer".
112 // Add footer
113 var txtFooter = g.master.textFrames.add(g.doc.layers.item("Layer 2"));
114 txtFooter.geometricBounds = [540, 615, 580, 25];
115 txtFooter.insertionPoints[0].justification = Justification.centerAlign;

306
116 txtFooter.textFramePreferences.verticalJustification = VerticalJustification.centerAlign;
117 try{txtFooter.insertionPoints[0].appliedFont = app.fonts.item("Franklin Gothic Heavy");}
118 catch(err){}
119 txtFooter.insertionPoints[0].pointSize = "18 pt";
120 txtFooter.contents = g.win.txtTitle.text;
121 }
On line 111, we create a text frame on Layer 2, alongside the navigation buttons.
On lines 113 and 114, we set both the horizontal and vertical alignment to "center".
On lines 115 to 116, we us a try block to set the font to "Franklin Gothic Heavy", so that the
user will not see an error if the font is not present.
7. Importing the images
Earlier in our script, we populated g.imageFiles with the image files contained in the folder
designated by the user. Our importImages() function will loop through these images and place
each one on a new page together with a text frame in which we will insert metadata read from
each image.
Add the importImages() function to the end of your script.
122
123 function importImages(){
124 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
125 for (var i = 0; i < g.imageFiles.length; i ++){
126 var currentPage = g.doc.pages.add();
127 currentPage.appliedMaster = g.doc.masterSpreads[0];
128 var currentImage = currentPage.place(g.imageFiles[i])[0];
129 currentImage.parent.itemLayer = g.doc.layers.item("Layer 1")
130 currentImage.parent.geometricBounds = [75, 100, 525, 700];
131 currentImage.fit(FitOptions.fillProportionally);
132 var trans = g.win.ddlTransitions.selection.text.replace(" ", "_").toUpperCase() + "_TRANSITION";
133 currentPage.parent.pageTransitionType = PageTransitionTypeOptions[trans];
134
135 var txtMeta = currentPage.textFrames.add(g.doc.layers.item("Layer 1"));
136 txtMeta.geometricBounds = [25, 800-25, 65, 25];
137 txtMeta.insertionPoints[0].justification = Justification.centerAlign;
138 try{txtMeta.insertionPoints[0].appliedFont = app.fonts.item("Franklin Gothic Heavy");}
139 catch(err){}
140 txtMeta.insertionPoints[0].pointSize = "36pt";
141 try{txtMeta.contents = currentImage.itemLink.linkXmp.documentTitle;}
142 catch(err){};
143 }
144 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
145 }

Save your changes.


Since applying page transitions is an operation which causes InDesign to display an alert, on
line 122, we set the user interaction level to neverInteract while the document pages are
being created and the transitions applied. Then, on line 142, once we have created all of the

307
pages, we set back to the normal interactWithAll.
On lines 124 and 125, we create a new page and apply the master page to it. Then we place the
next image from g.imageFiles on to the page and move it to Layer 1. (Line 126 ends with [0]
because the place command returns an array of images and we need to target the image inside
the array.)
On line 130, we read the value selected by the user from the dropdownlist into a variable
called trans. To convert the text into a syntactically correct pageTransionType value, we
replace space (" ") with underscore ("_"), convert the string to uppercase and add
"_TRANSITION" to the end. We then use trans in square brackets after
PageTransitionTypeOptions.
Thus, if the user were to choose "Zoom in" from the dropdownlist, trans would contain
"ZOOM_IN_TRANSITION" and we would end up with
currentPage.parent.pageTransitionType = PageTransitionTypeOptions["ZOOM_IN_TRANSITION"];
which is the same as
currentPage.parent.pageTransitionType = PageTransitionTypeOptions.ZOOM_IN_TRANSITION;
Since page transitions are applied to spreads rather than pages, we have to use
currentPage.parent.pageTransitionType.
Next, on lines 133 to 138, we create a text frame for the metadata and set the text format. Then,
on lines 139 to 140, inside a try ... catch block, we attempt to read the documentTitle property
of the linkXmp property of the itemLink property of the image. If we attempt to read the
documentTitle and the image has no metadata, the try .. catch statement will prevent an error.
(The itemLink property returns a link object and link objects have a linkXmp property, which
returns a LinkMetadata object, which, in turn, has a documentTitle property.)
A simple way of setting the documentTitle of an image is by choosing File > File Info in
Photoshop and filling out the Document Title field.
8. Exporting an interactive PDF
It's probably a bit premature to be generating a PDF file, especially if the images do not
contain any metadata, but let's do it just for practice. There are two key steps: setting the
relevant properties of the pdfExportPreferences and interactivePDFExportPreferences objects
and using the exportFile() method of the document object with the first parameter set to
ExportFormat.interactivePDF.
Enter the following createPDF() function to the end of your script.
146
147 function createPDF(){
148 var indFile = null;
149 while (indFile == null){
150 indFile = File.saveDialog("Specify name of output document, including file extension.");
151 }
152 var strExt = indFile.fullName.toLowerCase().substr(indFile.fullName.lastIndexOf("."));
153 if(strExt != ".indd"){

308
154 indFile = (File(indFile.toString() + ".indd"));
155 }
156 g.doc.save(indFile);
157
158 app.pdfExportPreferences.viewPDF = true;
159 app.interactivePDFExportPreferences.openInFullScreen = true;
160 var pdfFile = File(indFile.fullName.toString().replace(".indd", ".pdf"));
161 var pdfPreset = app.pdfExportPresets.itemByName("[Smallest File Size]");
162 g.doc.exportFile(ExportFormat.interactivePDF, pdfFile, false, pdfPreset);
163 }

Save your changes.


On lines 147 to 149, we use a while loop structure to oblige the user to specify a name and
location for the new InDesign document. Then, on line 150, we extract the file extension from
the specified file path. If the extension is not ".indd", we add this extension to indFile (line
152) before we save the file (line 154).
On line 156, we set the viewPDF property of the pdfExportPreferences object to true, which
will cause the document to open in Acrobat or Acrobat Reader after it has been produced.
On line 157, we set the openInFullScreen property of the interactivePDFExportPreferences
object to true, which will cause the document to open in full screen mode
On line 158, we obtain the file path to the PDF file by replacing the file extension ".indd" with
".pdf".
On line 159, we place the built-in Smallest File Size preset inside a variable called preset;
then, on line 160, we use it as the fourth argument of the exportFile() method.

SYNTAX myDocument.exportFile(format, location, [showing options, preset])


SUMMARY Method of document object
... Exports a file in the specified format.

The format in which to export the document: ExportFormat.epsType, ExportFormat.inCopyMarkup,


ExportFormat.indesignMarkup, ExportFormat.indesignSnippet, ExportFormat.interactivePDF,
Format ExportFormat.JPG, ExportFormat.packagedXFL, ExportFormat.pdfType, ExportFormat.pngFormat,
ExportFormat.RTF, ExportFormat.SWF, ExportFormat.taggedText, ExportFormat.textType or
ExportFormat.XML
Location Optional. Location where the PDF (or other file type) should be produced.
Showing
Optional. Whether the PDF Options dialog box should be displayed.
options
Preset Optional. The PDF Preset to use.

Test the script by right-clicking on it in the Scripts panel in


309
InDesign.
When the SelectDialog() window appears, choose the folder called
"images" in the "chapter12" folder.
When the ScriptUI dialog appears, enter the text "Meet our support
team" in the edittext box and click OK.

After the documents have been created, the PDF file should open in
Acrobat in full screen mode.
Click on the navigation buttons to make sure they all work.
Naturally, they should work in both normal and full screen mode.

310
Press Escape to exit full screen mode or use Control-L (Windows),
Command-L (Mac).

311
CHAPTER 13: XML Essentials
What is XML?
XML is a markup language used to contain, organize, describe and clarify information in a
deliberately neutral way. It can be used as a container for any information which can be
represented textually. It has been adopted by developers working in many different fields as a
kind of lingua franca, enabling communication between systems and environments which have
little or no common ground.
Markup languages have been with us for some time: they embed predetermined text strings
within a document to describe the data it contains. The most widely used markup language of
recent times has been HTML and the success of HTML—as well as some of its shortcomings
—played a significant role in the development of the XML specification.
Like HTML, XML uses named elements as containers for each piece of data within a
document. The term element refers to the logical container; tags are the visual markup used to
represent the element. Elements which contain other elements or text have an opening and a
closing tag: empty elements are written with a single tag.
One of the key differences between XML and HTML is that XML does not have a fixed
vocabulary. While HTML has a limited set of permissible elements which must be used for all
purposes, XML allows developers to define the markup elements that will be used for their
documents and the rules that govern their usage. This puts the onus of choosing the write
markup syntax on the person developing the XML—which is as it should be; since the person
who is most familiar with the data should be able to choose the most suitable markup for
describing and containing that data (with some professional help, if necessary).
If you are working with XML as a container for books, magazines or other publications, it is to
be expected that your XML vocabulary will make reference to such things as chapters,
headings and paragraphs. If your company sells DIY materials, your XML may contain tags
which intimately describe your store locations, your product lines, your delivery methods, and
so on.
The inherent flexibility of XML is reigned in by a set of stringent requirements placed on the
syntax which one is permitted to use. Element names must conform to certain standards. There
are restrictions on what tags should look like and where they should be placed. For example,
characters which clash with XML markup such as ">" must be replaced by special codes
called character entities.

Structure of an XML document


Most XML documents are easily readable by humans: it is easy to distinguish the data, the start
tags and closing tags and to get some idea of how the data is structured. XML documents have
a tree structure and it is a requirement that there is a root element which contains all other
elements. A simple XML document is shown in listing 13-1 and the structure of the document
is shown in figure 13-1.

312
Listing 13-1: An XML document containing details of staff members' IT Skills
1 <?xml version="1.0" encoding="utf-8"?>
2 <staff>
3 <staffmember dept="Accounts">
4 <firstname>Andrew</firstname>
5 <lastname>Gilbert</lastname>
6 <skills>Excel, Word, Sage</skills>
7 <photo href="file://images/andrew-gilbert.tif"/>
8 </staffmember>
9 <staffmember dept="Marketing">
10 <firstname>Cathy</firstname>
11 <lastname>Hargreaves</lastname>
12 <skills>Word, PowerPoint, Dreamweaver, QuarkXPress</skills>
13 <photo href="file://images/cathy-hargreaves.tif"/>
14 </staffmember>
15 <staffmember dept="Accounts">
16 <firstname>Alana</firstname>
17 <lastname>Jones</lastname>
18 <skills>Excel, Word, PowerPoint, PageMaker, Sage</skills>
19 <photo href="file://images/alana-jones.tif"/>
20 </staffmember>
21 <staffmember dept="Sales">
22 <firstname>Justine</firstname>
23 <lastname>Parry</lastname>
24 <skills>Excel, Word, PowerPoint</skills>
25 <photo href="file://images/joseph-parry.tif"/>
26 </staffmember>
27 <staffmember dept="Communications">
28 <firstname>Lynn</firstname>
29 <lastname>Spencer</lastname>
30 <skills>Dreamweaver, Mac OSX, InDesign, QuarkXPress</skills>
31 <photo href="file://images/lynn-spencer.tif"/>
32 </staffmember>
33 <staffmember dept="Marketing">
34 <firstname>Malcolm</firstname>
35 <lastname>Wallace</lastname>
36 <skills>Word, PowerPoint, Dreamweaver, FrameMaker, PageMaker</skills>
37 <photo href="file://images/malcolm-wallace.tif"/>
38 </staffmember>
39 </staff>

The two key elements of an XML file are the prolog and the root element. The prolog is
optional and, if used, must precede the root element. In listing 13-1, the prolog contains an
XML declaration (line 1), detailing the version and encoding. The root element (lines 2 to 39)
is called staff and contains the XML data which is arranged in a hierarchical structure. Within
the staff element, there are several staffmember elements and each staffmember element in
turn contains a firstname, lastname, skills and photo element.

313
Figure 13-1: The tree structure of a simple XML file

XML documents are made up of the following components: elements, attributes, entity
references, CData sections, comments and processing instructions.
Elements
Elements are the containers for the data stored in XML documents. However, because of the
hierarchical nature of XML, many elements actually contain other elements rather than data. In
the structure shown in figure 13-1, only the firstname, lastname, skills and photo elements
store information. The staff and staffmember elements play an organization role, sub-dividing
the data into sections.
Attributes
Attributes are optional name-value pairs which can be placed inside the start tag of an element
and which are normally used to supply metadata—information about the data. In the context of
InDesign XML workflows, it is important to be aware that information inside attributes cannot
be placed as text within a layout. If such information needs to appear on the page, the solution
is normally to use XSLT or scripting to transform the attribute into an element.
In the example shown in listing 13-1, the staffmember element has an attribute—dept, as does
the photo element—href. The href attribute has a special status in InDesign, in that it is
automatically recognized as the file path to an image.
Entity references
If your XML documents contain information on computing and on such topics as HTML, it is
inevitable that they will need to display characters which would lead to ambiguity, since they
are used in XML markup—"<" and ">", for example. These characters are banned in XML
and, wherever they occur, they must be replaced with entity references: symbolic
representations of a character or text string.
Character entity references consist of an ampersand, the name of the entity and a semi-colon.
They can be declared by the developer of the XML application and, in addition, XML contains
the following five built-in references:
< &lt;
> &gt;
& &amp;
" &quot;
' &apos;

314
CDATA sections
If you are creating a document where the five illegal characters mentioned above will occur
repeatedly, replacing every occurence with entity references may become a major headache.
So, instead, the XML specification allows you to place whole blocks of text inside CDATA
sections. CDATA stands for character data and contrasts with PCDATA which is parsed
character data. PCDATA is examined by XML parsers and may not contain illegal characters:
CDATA section are not parsed and may therefore contain as many illegal characters as you
like. An example of a CDATA section is shown below.
<![CDATA[
Private Sub frmReports_Initialize ()
dim strFile as string
Me.cmbFiles.Clear()
strFile = dir ("c:\excelfiles\*.xls*")
While strFile <> ""
Me.cmbFiles.AddItem(strFile)
Wend
end Sub
]]>
In this example, the illegal characters (the quotation marks and the less than and greater than
symbols) do not invalidate the XML: the CDATA section acts like an insulator, shielding its
contents from the parser.
Comments
Comments are a common feature of coding environments: they allow developers to embed
explanatory information which is not treated as part of the XML markup. XML comments are
identical to the comments used in HTML and have the following format:
<!-- This is a comment -->
Comments can be placed anywhere in an XML document, providing that they do not precede
the XML declaration. As with programming languages, comments may be used to "comment
out" or temporarily exclude an item—for example:
<?xml version="1.0"?>
<example>
<leavein>
This element remains active.
</leavein>
<!--
<takeout>
This element is inactive because it is inside a comment.
</takeout>
-->
</example>
Processing instructions
Processing instructions perform a similar function to comments. However, they are intended
for applications which process the XML document rather than for humans. The have a standard

315
format, beginning with "<?" and ending with "?>"; they can only be placed in the prolog; and
the XML declaration that begins most XML documents is itself a kind of processing
instruction.
The processing instruction which is most important to InDesign is the one which tells it to use
an XSL stylesheet to transform the XML document. This takes the following format:
<?xml-stylesheet href="example.xsl" type="text/xsl"?>
Applications will generally ignore processing instructions which are not aimed at them and
which they do not understand. This is the case with InDesign which imports and displays
processing instructions but ignores them all, bar the stylesheet declaration.

XML validation
In order for XML documents to be trusted by all parties using them to share information, it is
important that the integrity of a document can be gauged in some way: this is the process of
validation. In order to be trusted, an XML document must pass two tests: it must be well-
formed and it must be valid. A well-formed document is one which does not break any of the
fundamental syntactical rules to which every XML document must adhere. A valid XML
document is one which, in addition to being well-formed, adheres to a specific set of rules laid
down for that particular type of XML document.
Well-formedness
In order to be considered well-formed, an XML document must satisfy the following criteria:
• Element and attribute names must be legal in XML.
• Names cannot contain spaces.
• Names cannot start with a number or special character.
• Names cannot start with the letters "xml"—in any case combination.
• Names must contain at least one character.
• Element tags must be properly nested.
If tag2 is opened after tag1 then tag2 must be closed before tag1 closes: overlapping
element tags are not permitted.

Properly nested
<tag1>
<tag2>
</tag2>
</tag1>

Overlapping (illegal)
<tag1>
<tag2>
</tag1>
</tag2>

Figure 13-2: In a well-formed XML document, tags must be properly nested

316
• Every opening tag must have a matching closing tag.
In the old days, web browsers were fairly forgiving when closing HTML tags were
omitted, and the document would still be displayed: not so with XML.
This rule also applies to empty elements (those that contain no text or other elements).
However, there is a special format for indicating empty elements which combines the
opening and closing tags to form a single tag. Thus, in XHTML—the XML-compliant
version of HTML, the empty tag <br> may either be written <br></br>, with the closing
tag immediately following the opening tag, or as the single tag <br/>.
• All attributes must have values.
Thus, in XHTML, <option selected> (which indicates a pre-seleced, default item in a
drop-down menu, on a form) now has to be written:
<option selected = "selected">
• All attribute names must be quoted.
There may be an assumption that numerical values do not need quotes, as is the case in
many other environments; but in XML quotes are obligatory for all values. Single or
double quotes are permitted.
• The five characters which could be mistaken for XML markup must be replaced
with character entities:
< &lt;
> &gt;
& &amp;
" &quot;
' &apos;
InDesign's built-in validator checks an XML document for well-formedness before importing
and will display a helpful error message if it finds illegal markup. Importantly, the error
message will include the line number of the problem code.

Figure 13-3: An example of an XML parsing error message displayed by InDesign

Schema validation
Declaring that a document is well-formed is not exactly a huge vote of confidence; after all, it
simply mean that it satisfies the most basic requirements of an XML document. It may still
contain errors that render it less than useful. For example, in the staff document that we looked
at earlier, suppose we encountered two lastname elements inside one of the staffmember
elements:

317
...
<staffmember dept="Accounts">
<firstname>Gillian</firstname>
<lastname>Howard</lastname>
<lastname>Robertson</lastname>
<skills>PowerPoint, Word, Excel, Access</skills>
<photo href="file://images/gillian-howard.tif"/>
</staffmember>
...
how can we tell if this is simply an error or if the person has a double-barrelled surname? Is
this an isolated occurrence or can we expect to find other staff members with two surnames?
Clearly, we need to be able to lay down some ground rules to clarify exactly which elements
this type of XML document may contain and how these elements are supposed to be used.
This clarification is provided by the creation of a document type definition (DTD)—a formal
set of declarations regarding the creation of XML documents of a given type. A DTD would
quickly clear up the confusion caused by the two lastname elements in our example by stating
how many lastname elements were permitted. DTDs may be embedded within an XML
document or, more typically, exist as external documents with a ".dtd" file extension.
InDesign recognises DTDs and, whenever you import an XML document, if there is an
associated DTD, the program validates the content of the document against it and returns an
error if any rules are broken. Any XML document which satisfies the rules laid down by its
document type definition is treated as valid and will be imported into InDesign without any
problems.
DTDs versus XML schemas
DTDs work well with XML documents which mainly contain textual information. Where XML
is being used to store more varied types of data, DTDs may not be capable of providing a
precise enough definition of the XML data. To cater for such needs, the Worldwide Web
Consortium created the XML Schema Recommendation.
Like DTDs, XML schemas allow you to define the nature and structure of your XML data; but
they offer a more robust type of validation than DTDs. Schemas allow you to place very
precise restrictions on the content of the elements in your XML documents, such as the type of
data they may contain. Schemas are great for validating data-centric XML documents where
strict data-typing is important. However, InDesign does not work with schemas; so let's put
them to one side.
Creating XML
Just as PDF documents normally start life as native documents of a particular type of software
—InDesign, QuarkXpress, Microsoft Word, etc.—so too the information which ends up as
XML starts life in databases, spreadsheets and a variety of other documents. A number of
software programs have features which allow the exporting of data in XML format.
Additionally, programmers can create custom scripts and applications which extract
information from databases and other sources and turn it into XML.
Let's look at the XML export capabilities of a few popular programs, starting with Microsoft

318
Office. The bad news is that XML features are only available in the Windows versions of
Microsoft Office.
Microsoft Access
Microsoft Access enables you to export database tables, queries, forms and reports as XML
and will even generate an XML schema document and an XSLT stylesheet to enable the data to
be displayed on a web page.
In Access 2007 and 2010, the XML export features are located in the External Data tab.
In the navigation pane, highlight the Access object (table, query,
form or report) you would like to export as XML.
Highlight the External Data ribbon tab.

Figure 13-4: Exporting a Microsoft Access table as an XML file

In the Export group, click on the More button and choose XML
File.
This brings up a dialog box allowing you to choose the location
where you would like to save the XML file.
(In Access 2003, choose File > Export and Choose XML from the Save As drop-down.)

Figure 13-5: Choosing a location for the XML export

319
Click on the Browse button, open the folder in which you would
like to save the file and enter a file name. Click Save and then OK.
Access will now ask you to specify the documents you would like it to create: XML, XSD and
XSL.

Figure 13-6: Specifying which documents Access will produce

In the context of InDesign workflows, XML is the only format of interest. XSD is a schema
document and InDesign uses DTDs rather than schemas. Also, although InDesign understands
XSLT documents, it requires them to perform XML to XML transformations rather than the
XML to HTML transformation offered by Access.
When you click the OK button, Access produces an XML file at
the specified location.
The file produced has a fairly standard format. For example, the clients_sample query shown
in figure 13-7 retrieves the first five records from a clients table and has six fields: ID,
ClientName, Address1, Address2, Town and Postcode.

Figure 13-7: Exporting an Access query called clients_sample

Listing 13-2 shows the resulting XML file.


Listing 13-2: An XML file created by exporting an Access query
1 <?xml version="1.0" encoding="UTF-8"?>
2 <dataroot xmlns:od="urn:schemas-microsoft-com:officedata" generated="2011-12-30T09:07:14">
3 <clients_sample>
4 <ID>1</ID>
5 <ClientName>Allen, O&apos;Reilley &amp; Company Limited</ClientName>
6 <Address1>Yarvil Street</Address1>
7 <Address2>Barutley</Address2>

320
8 <Town>Sidkeltoess</Town>
9 <Postcode>BAR31 7RU</Postcode>
10 </clients_sample>
...
35 <clients_sample>
36 <ID>5</ID>
37 <ClientName>Ashlaeld Ashquisern Limited</ClientName>
38 <Address1>Yeoham Lane</Address1>
39 <Address2>Bidadham</Address2>
40 <Town>Southporsea</Town>
41 <Postcode>BID7 3AD</Postcode>
42 </clients_sample>
43 </dataroot>

On line 1 of the document, we have the standard XML declaration then, on line 2, we have the
opening tag of the root element—which Access normally names <dataroot>. The opening tag
of the <dataroot> element contains a namespace declaration. Namespaces are used in XML to
associate groups of elements and to distinguish them from elements with the same name. The
declaration
xmlns:od="urn:schemas-microsoft-com:officedata"
states that any element within this document whose name starts with the prefix "od" will be
treated as belonging the "urn:schemas-microsoft-com:officedata" namespace which is used by
Access when exporting Accesss-specific objects such as such as indexes and primary keys.
The XML document shown in this example does not include any elements in this namespace
and, hence, the prefix "od" is never used.
Within the <dataroot> element, we have the repeating <clients_sample> element which
contains each exported record. This is another standard naming feature used by Access when
exporting data: the table or query name is used as the name of the elements which occur
repeatedly inside the <dataroot> element.
Finally, inside each <clients_sample> element, we have what we might call the detail
elements, whose names correspond to the names of the columns in the Access table or query.
Microsoft Excel
Microsoft Excel also has XML export capabilities which allow you to export an Excel list as
XML. In Excel, a list is any body of data arranged like a database table, with column headings
and where each row represents one database record. However, in spite of this fairly clear-cut
and consistent requirement, you cannot simply export any old Excel list/database the way you
can in Access. You first need to specify an XML source.
In Excel 2007/2010, activate the Developer tab of the ribbon and,
in the XML group, click on the Source button to display the XML
Source task pane. (In Excel 2003, choose Data > XML > XML
Source.)

321
Figure 13-8: Displaying the XML Source task pane in Microsoft Excel

In the XML Source task pane, click on the XML Maps button.
When the dialog shown in figure 13-8 appears, click on the Add
button and locate either an XSD or XML document with a format
corresponding to the Excel list you wish to export.

Figure 13-9: Creating an XML map in Microsoft Excel

The file that you specify when adding an XML map is not imported: it is simply used as a
model from which Excel extracts schema data.
When you click OK, the XML map is displayed in the XML
Source task pane.
You can now drag elements from the map directly onto columns
within your Excel list to bind the list to the XML source. In the
example shown in figure 13-10, instead of dragging SalesID onto
the SalesID column, Salesperson onto the Salesperson column,

322
etc.; you can simply drag the container element branch_sales onto
the first column of the table to map all the columns in one hit.

Figure 13-10: Specifying which documents Access will produce

Once this mapping is in place, you can populate the Excel table in any way you like, for
example using formulas and consolidating information from other worksheets. Whenever you
export the data as XML, Excel will always use the mapping information to determine the
element names and the structure of the XML file.
To export an Excel list as XML, in Excel 2007 or 2010, click on
the Export button in the XML group of the Developer tab of the
Excel ribbon. In Excel 2003, choose Data > XML > Export.
Listing 13-3 shows the XML file produced by Excel from the data shown in figure 13-10.
Listing 13-3: The XML file produced from the data shown in figure 13-10
1 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2 <dataroot>
3 <branch_sales>
4 <SalesID>108</SalesID>
5 <Salesperson>Henry Clark</Salesperson>
6 <Branch>Birmingham </Branch>
7 <AX101>2</AX101>
8 <AX102>1</AX102>
9 <AX103>23</AX103>
10 <Total>26</Total>
11 </branch_sales>
...
696 <branch_sales>
697 <SalesID>170</SalesID>
698 <Salesperson>Bradley Khan</Salesperson>
699 <Branch>Swansea</Branch>
700 <AX101>5</AX101>
701 <AX102>12</AX102>
702 <AX103>4</AX103>
703 <Total>21</Total>
704 </branch_sales>

323
705 </dataroot>

FileMaker Pro
FileMaker Pro is a database application which is widely used on the Mac platform and is also
available on Windows. It allows you to export either a table or a layout as an XML file.
Activate a layout which contains the fields you wish to export or
which is based on the table whose data you wish to export.
Choose File > Export Records and choose XML from the Saves
as type drop-down.

Figure 13-11: Exporting FileMaker Pro data in XML format

FileMaker displays the Specify XML and XSL options dialog


shown in figure 13-10, below.

324
Figure 13-12: When exporting XML from FileMaker Pro, the FMPDSOResult grammar produces a
simpler and more InDesign-friendly document than the FMPXMLResult option

Choose FMPDSOResult from the Grammar drop-down. This


option produces a file which uses the field names of the table being
exported as the names of the elements in the XML file.
You are then asked to specify the fields which you want to export
and, if the active layout contains summary informaton, you are
given the option of exporting summary—rather than detail—
information.

325
Figure 13-13: Specifying which fields you wish to export

Listing 13-4 shows the XML file produced by FileMaker Pro from the same data which we
used in Excel, as shown in figure 13-10, on page 326.
Figure 13-4: The XML file produced using Filemaker's FMPDSOResult grammar
1 <?xml version="1.0" encoding="UTF-8" ?>
2 <!-- This grammar has been deprecated - use FMPXMLRESULT instead -->
3 <FMPDSORESULT xmlns="http://www.filemaker.com/fmpdsoresult">
4 <ERRORCODE>0</ERRORCODE>
5 <DATABASE>branch_sales.fp7</DATABASE>
6 <LAYOUT></LAYOUT>
7 <ROW MODID="0" RECORDID="1">
8 <SalesID>108</SalesID>
9 <Salesperson>Henry Clark</Salesperson>
10 <Branch>Birmingham</Branch>
11 <AX101>2</AX101>
12 <AX102>1</AX102>
13 <AX103>23</AX103>
14 <Total>26</Total>
15 </ROW>
...
106 <ROW MODID="0" RECORDID="75">
107 <SalesID>133</SalesID>
108 <Salesperson>Amber Thomas</Salesperson>
109 <Branch>Swansea</Branch>
110 <AX101>21</AX101>
111 <AX102>6</AX102>
112 <AX103>0</AX103>
113 <Total>27</Total>
114 </ROW>
115 </FMPDSORESULT>

The root element is called FMPDSORESULT and the container element for the data in each
column is called ROW. The field names used in the FileMaker table are used as the names of
the elements which contain the data. (FileMaker changes the names as necessary to make them
well-formed XML.)
You will notice that the XML file contains a warning comment that this grammar has been
deprecated.
2 <!-- This grammar has been deprecated - use FMPXMLRESULT instead -->
Nevertheless, as long as its available, it definitely produces output which is more useful for
InDesign XML workflows. For comparison, output produced by the FMPXMLResult grammar
is shown in figure 13-5.
Figure 13-5: The XML file produced using Filemaker's FMPXMLResult grammar
1 <?xml version="1.0" encoding="UTF-8" ?>
2 <FMPXMLRESULT xmlns="http://www.filemaker.com/fmpxmlresult">
...
15 <RESULTSET FOUND="78">
16 <ROW MODID="0" RECORDID="1">
17 <COL>

326
18 <DATA>108</DATA>
19 </COL>
20 <COL>
21 <DATA>Henry Clark</DATA>
22 </COL>
23 <COL>
24 <DATA>Birmingham</DATA>
25 </COL>
26 <COL>
27 <DATA>2</DATA>
28 </COL>
29 <COL>
30 <DATA>1</DATA>
31 </COL>
32 <COL>
33 <DATA>23</DATA>
34 </COL>
35 <COL>
36 <DATA>26</DATA>
37 </COL>
38 </ROW>
...
982 </RESULTSET>
983 </FMPXMLRESULT>

Here, the repeating element is again called ROW; but each data element is wrapped in an
element called COL, making it harder to distinguish one column from another.
If you find that exporting data from FileMaker involves quite a few preparatory steps and takes
a while, you might consider automating the procedure with scripting using FileMaker's user-
friendly ScriptMaker feature.
CHAPTER 14: InDesign XML Essentials
XML elements, tags and styles
When working with XML in InDesign, there are really three main parts to the puzzle: XML
elements, tags and InDesign styles. InDesign allows you to import XML data and use it as the
basis of document content and one of key mechanisms it uses to achieve this is the mapping of
XML elements to InDesign styles via the use of tags.
The two panels concerned with the creation of XML-based documents are the Structure pane
and the Tags panel. To make the Structure pane visible, choose View > Structure > Show
Structure. The Structure pane displays any XML which has been imported into—or created in
—InDesign.

327
Figure 14-1: Any XML which is imported into InDesign is displayed in the Structure pane

To make the Tags panel visible, choose Window > Utilities > Tags. The Tags panel allows
you to import, create and delete tags, which correspond to the XML elements whose content
you wish to display on your pages.

Figure 14-2: The tags panel offers a flexible and powerful way of working with XML elements and
associating them with document content

Tags are the representation of elements within XML markup. If we have an element called Ref
and it contains the text "NW1735", this would be written:
<Ref>NW1735</Ref>
InDesign uses tags in a similar way and, when XML-based content is placed in a layout, start
and end tags are placed around each element in much the same way as they are within the
original XML code. However, InDesign allows you the freedom to create tags at will and
associate them with the content of your document, even before the XML which they represent
has been imported—or even created.

328
Figure 14-3: When viewed in the Story Editor, tags resemble XML markup

Creating tags
Before XML can be added to any InDesign layout, a tag must be created to match every XML
element that you want to include in your layout. Luckily, although you can create tags one by
one, they can also be imported en masse. Firstly, whenever you import an XML file, all of the
elements imported will automatically create a matching tag in the InDesign document with the
same name as the element. Secondly, you can import tags from an XML file as a separate
operation by choosing Load Tags from the menu in the top right of the Tags panel. (This
operation imports only the tags: it does not import the XML file.) And, thirdly—in much the
same way, you can load tags from a DTD by choosing Load DTD from the Tags panel menu.
This command both loads the DTD—which is then displayed in the Structure pane—and
creates a tag for every element defined in the DTD.
You would typically create individual tags when you are only interested in using certain
elements within an XML file in your layout. Naturally, you must ensure that the names of the
tags match the names of the XML elements—remembering, as well, that XML is case-
sensitive. When you import the XML file, you would then activate the option Only import
elements that match existing structure in the XML Import Options dialog.
Mapping tags to styles
Much of the power of InDesign's XML workflows stems from style mapping—the association
of styles with specific tags and, hence, with the XML elements they represent. When importing
XML, the Map Tags to Styles command is used to associate tags with paragraph and character
styles. When XML is placed into a layout, the content of each element is then automatically
represented in the style that is mapped to its tag.
Importing XML
The key step in building an XML-driven publication is to import XML into InDesign. To do
this, choose File > Import XML, or choose Import XML from the menu in the top right of the
Structure pane, or right-click on an element in the Structure pane and choose Import XML
from the context menu. The Import XML dialog allows you to browse for the XML file and
features a checkbox—which is activated by default—marked Show XML import options. The
XML Import Options dialog is shown in figure 14-4: there follows a description of the options
it contains.

329
Figure 14-4: InDesign's XML Import Options dialog

• Mode—Merge Content causes the imported file to replace any existing XML
information; while Append Content will add the imported XML to existing content.
• Create link—forces InDesign to create a link to the imported XML file. If the XML file
is modified, then the next time the InDesign document is opened, a dialog like the one
shown in figure 14-5 is displayed.

Figure 14-5: The Create link import option causes InDesign to notify the user whenever an imported
XML file has been modified or is missing.

• Apply XSLT—allows you to specify an XSL stylesheet transformation to the XML file.
• Clone repeating text elements—causes elements which repeat within the structure of
the imported XML file to repeat within your InDesign layout.
• Only import elements that match existing structure—If an element within the
incoming XML is not already in place in the structure you have defined in the Structure
pane, it will be ignored.
• Import text elements into tables if tags match—allows you to import XML data into
table cells simply by tagging placeholder tables with tags which match the names of the
appropriate elements.
• Do not import contents of whitespace-only elements—tells InDesign to ignore
spaces, tabs and carriage returns between elements and import only the contents of the
elements themselves.
• Delete elements, frames, and content that do not match imported XML—cleans up
placeholder-based layouts by removing items from the layout which have no

330
corresponding content in the incoming XML data.
• Import CALS tables as InDesign tables—Converts CALS tables to the InDesign table
model. (The CALS table model is an SGML standard for table definition widely used in
XML.)

Try it for yourself!


TUTORIAL: Basic XML workflow
To give you a feel for the way that InDesign handles XML, in this Try it for yourself! tutorial,
you will set up a document for XML import and then import first a raw XML file and then one
which is controlled by an XSL stylesheet.
1. Renaming the Root tag
Create a new InDesign document, making sure to activate the
option Master Text Frame at the top of the New Document dialog,
then save the document as 01-health-articles.indd in the
"chapter14" folder.

Choose Window > Utilities > Tags and View > Structure >
Show Structure, if necessary.
The Tags window should contain a single, default tag called Root.
Double-click on this tag and, when the Tag Options dialog appears,
rename the tag “articles”—the root element of the file we will be
importing.

The Structure pane should now display a single root element, also
331
called “articles”.

2. Loading tags from an XML file


Choose Load Tags from the menu in the top right of the Tags panel
and double-click on the file 01-health-articles.xml in the chapter
14 folder.

Some of the tags loaded represent elements that contain other elements. The tags which
represent elements containing text and which will be placed on the page are head, author and
para. We must now create a paragraph style to control the appearance of each of these three
styles.
3. Creating paragraph styles
In the Paragraph Styles panel (Window > Styles > Paragraph
Styles), choose New Paragraph Style from the panel menu.
Enter the name head, all in lower case—to match the name of the
corresponding XML element. This facilitates the process of
matching tags to styles.
Choose some basic formatting suitable for a heading.
Click on the Keep Options category on the left of the Paragraph
Style Options dialog then choose On Next Page from the Start
332
Paragraph drop-down menu.

Create a style called author and assign it some attributes suitable


for a sub-heading.
Create a style called para and assign it some attributes suitable for
body text.
4. Mapping tags to styles
Choose Map Tags to Styles from the Tags panel menu.

Click on the Map by Name button and the benefit of giving your
styles exactly the same name as your tags becomes apparent.
InDesign automatically maps tags to styles with matching names,
saving you a few precious minutes of work.
Where the names do not match, you would simply choose a style name from the drop-down
menu next to each tag.
5. Importing the XML file
Choose File > Save to save the changes you have made so far.
333
(This is important, since we will be reverting the document after
performing the next few steps.)
Choose File > Import XML, highlight the file named 01-health-
articles.xml and click the Open button—making sure that Show
XML Import Options is checked.

In the XML Import Options dialog, choose Merge Content from


the Mode drop-down menu; activate Clone repeating text
elements and Do not import content of white-space only
elements. (Deactivate all the other options.)

6. Placing the XML content on the page


To place the entire XML document, drag the articles element (the
root) from the Structure pane onto the page.
Since you activated the Master Text Frame option when creating the document, InDesign's
Smart Text Reflow feature should autoflow the entire XML document, creating pages as
required. (If it doesn't, don't worry, since we won't be keeping the changes to the document.)
The resulting document has one striking and highly undesirable feature: there are no paragraph
breaks; the XML elements just follow one after the other, forming a continuous block.

334
It often surprises people that when you import XML elements which contain paragraphs,
InDesign doesn't treat them as paragraphs. However, when you think about it, why should it?
Your XML elements could contain any type of data; it would be inconvenient, if InDesign kept
inserting paragraph breaks into your content without being asked to. Instead, InDesign leaves it
up to us to insert a paragraph break at the end of each element which needs to be treated like a
paragraph.
As is often the case with InDesign XML workflows, there are two ways of accomplishing this:
using XSLT or through scripting. Let's look here at the XSLT solution. We will be discussing
XSL stylesheets in the next chapter: for the moment we will simply import the same XML file
and use an already prepared XSL file to insert paragraphs at the end of each head, author and
para element.
Choose File > Revert to go back to the last saved version of the
file.
Choose File > Import XML once more; but, this time, double-
click the file named 01-health-articles-paras.xml.
This file is identical to the one you imported before, but contains the following extra line
which links it to a stylesheet called health-articles.xsl.
<?xml-stylesheet href="health_articles.xsl" type="text/xsl"?>

In the XML Import Options dialog, choose Merge Content from


the Mode drop-down menu.
Activate the option Apply XSLT and leave the drop-down menu set
to Use stylesheet from XML. This will cause InDesign to use the
linked stylesheet (health_articles.xsl) to transform the XML
document.
Activate Clone repeating text elements and Do not import

335
content of white-space only elements: deactivate all other
options.
Click OK to import the XML document.
Finally, drag the articles element from the Structure pane onto the
page.
This time, thanks to the XSL stylesheet, each head, author and para element contains a
paragraph and so they each inherit the stylistic attributes you assigned to them using the Map
Tags to Styles command.

If InDesign's Smart Text Reflow feature is not active and your


document still only contains a single page, add a new page, click
on the overset text icon in the bottom right of the text frame on
page one; then, holding down the Shift key, click in the top left of
the new page to place the entire document.

CHAPTER 15: Working with DTDs


A DTD (Document Type Definition) is a document which stipulates the rules governing a
certain type of XML document. It identifies the elements and attributes which may be used in
the XML document and the relationship between them.
Using an internal DTD
A DTD may either be stored in a separate file or embedded within the XML document it
describes. Having the DTD as a separate file is the norm, since this allows the file to be
associated with many different XML files and makes it easier to maintain. Each XML file
referencing a DTD must contain a DOCTYPE declaration and, if the DTD is internal, it is

336
actually located within the body of this declaration, as shown in figure 15-1, below.
<?xml version="1.0" encoding="utf-8" standalone = "yes"?>
<!DOCTYPE staff[
<!ELEMENT staff (staffmember+)>
<!ELEMENT staffmember (firstname, lastname, skills, photo)>
<!ATTLIST staffmember dept CDATA #REQUIRED>
<!ELEMENT firstname (#PCDATA)>
<!ELEMENT lastname (#PCDATA)>
<!ELEMENT skills (#PCDATA)>
<!ELEMENT photo EMPTY>
<!ATTLIST photo href CDATA #REQUIRED>
]>
<staff>
...

Figure 15-1: An internal DTD

Using an external DTD


When using an external DTD, you create a reference to the external DTD file in the DOCTYPE
declaration using the keyword SYSTEM followed by the location of the DTD file relative to
the XML document. An example is shown in figure 15-2, below.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE staff SYSTEM "staff.dtd">
<staff>
...

Figure 15-2: An external DTD declaration

Note that the standalone attribute is included in the internal XML declaration, on the first line.
The default for this attribute is "no"; therefore, when the DTD is external, it can simply be
omitted. Having said that, InDesign will always import a DTD if one is declared, regardless of
the standalone attribute.
Declaring elements
As for the DTD document itself, it contains the XML declaration found in XML documents but
is not itself an XML document. Instead, it simply contains a list of definitions detailing the
elements, attributes and entity references which may be used in XML documents of this type.
The most important part of an XML file is normally the elements and their contents. Elements
are declared using the ELEMENT keyword followed by the name of the element and a
definition of its content—for example, in the following declaration:
<!ELEMENT staff (staffmember+)>
the element name is staff and staffmember element is what it is supposed to contain. (The
plus sign indicates that the staff element should contain one or more staffmember elements.)
There are three main types of element:
• Elements that contain other elements

337
• Elements that contain data
• Elements that contain a data and other elements
• Empty elements (which may still contain attributes)
Declaring elements that contain other elements
If an element contains other elements, the name of the child element or elements should follow
the name of the parent element in parentheses. We have just seen an example of an element that
is meant to contain a single element: here is an example of an element that is meant to contain
several child elements:
<!ELEMENT staffmember (firstname, lastname, skills, photo)>
Here the parent element, staffmember, is meant to contain four child elements: firstname,
lastname, skills and photo. In addition to this parent-child relationship between the elements,
the syntax also conveys two other important facts: sequence and occurrence. In this example,
the child elements must occur in the order listed in the declaration and there must be one
occurrence of each element and one only. We have seen how the plus sign can be used to
indicate the permitted number of elements, let's now examine the concepts of occurrence in
more detail.
Specifying the occurrence of child elements
One occurrence only
To specify that one and only one child element is permitted, we simply include the name of the
child element in parentheses after the name of the parent.
<!ELEMENT parent (child1, child2, child3, child4)>
Optional child elements
To change the number of occurrences, we place a qualifier immediately after the name of the
child in question. To make a child element optional (zero or one occurrence), use a question
mark.
<!ELEMENT parent (child1, child2?, child3, child4)>
Note that in the above example, only child2 has been made optional: the other three elements
are still obligatory.
Zero or more occurrences
To specify that an element must occur zero or more times, use an asterisk.
<!ELEMENT parent (child1, child2?, child3*, child4)>
Here, child3 can be omitted or can occur any number of times.
One or more occurrences
To specify that an element must occur one or more times, use a plus sign.
<!ELEMENT parent (child1, child2?, child3*, child4+)>
Here, child4 can occur any number of times but cannot be omitted.
Limiting occurrences to a choice
It is also possible to specify that any one of a choice of child elements may occur in a given
position. This is done by separating the child elements with a pipe character ("|") rather than a

338
comma—for example:
<!ELEMENT parent ((child1|child2), child3)>
Here, the parent element can contain either child1 or child2 (but not both) , followed by
child3. Note the use of parentheses to combine the child1 and child2 elements. As with
mathematical formulas, parentheses can be used to good effect to determine how element
declarations are parsed. Thus, for example, the statement:
<!ELEMENT parent (child1, (child2 |child3), child4)>
means that the parent element must have a child1 element, followed by either a child2 or a
child3 element and finally a child4 element. Whereas this statement:
<!ELEMENT parent ((child1, child2) | (child3), child4))>
implies that the parent element must have two children and that they must be either child1
followed by child2 or child3 followed by child4.
Declaring elements that contain only data
To specify that an element must contain text but may not contain any other elements, we declare
the content of the element as #PCDATA. Thus, in the example, we saw earlier, we have:
<!ELEMENT firstname (#PCDATA)>
<!ELEMENT lastname (#PCDATA)>
<!ELEMENT skills (#PCDATA)>

Declaring elements with mixed content


Sometimes, a parent element contains both character data and other elements. Thus, for
example, in an XHTML document, block elements like p or li will quite often include inline
elements like span. In an XML-based InDesign workflow, these embedded elements will often
be rendered using character styles.
Elements that are allowed to have this type of mixed content are declared as follows:
<!ELEMENT parent (#PCDATA|child1,child2)*)>
In this statement, #PCDATA has to come first and may be followed by any number of child
elements. The asterisk outside the brackets then indicates that these elements can be
interspersed with PCDATA in any combination and can occur any number of times. This may
seem a little imprecise but, unfortunately, DTDs do not allow for any easy way of precisely
determining how XML elements may be mixed with free text.
Declaring empty elements
Elements that contain no text are referred to as empty elements—which is not to diminish their
importance, since empty elements often contain attributes whose values convey important
information about the data.
To declare an empty element, simply use the keyword EMPTY after the element name. For
example:
<!ELEMENT photo EMPTY>

Declaring attributes
339
Element attributes are declared in a DTD using the keyword ATTLIST: the "list" part of
ATTLIST pointing to the fact that, if an element contains more than one attribute, they can all
be declared in a single statement. The basic format for attribute declaration is as follows:
<!ATTLIST element_name attribute_name data_type usage>
Thus, for example, we saw earlier the following:
<!ATTLIST photo href CDATA #REQUIRED>
Here, photo is the name of the element, href is the name of the attribute and its usage is
#REQUIRED—indicating that this attribute cannot be omitted without invalidating the XML
document.
The opposite of #REQUIRED is #IMPLIED which basically means “optional”.
If an element has several attributes, they can all be declared with one statement.
<!ATTLIST photo href CDATA #REQUIRED
resolution CDATA #REQUIRED
width CDATA #REQUIRED
height CDATA #REQUIRED >

Attribute data types


The type of data which an attribute can accept can be one of several different options—none of
which equate to the data types encountered in programming languages, such as string, integer
and real. Of these types, the ones which are most important when working with XML in
InDesign are as follows:
• CDATA—Character data which is treated as markup and therefore not parsed when the
file is being validated, unlike PCDATA (parsed character data).
• ID—The value of the attribute must be a unique ID.
• Enumerated value—The value must be one of a choice of predetermined options. For
example:
<!ATTLIST photo colourspace (RGB|CMYK) “CMYK”>

Here the permitted values are “RGB” and “CMYK” and “CMYK” is the default.

Try it for yourself!


TUTORIAL: Creating a DTD and using it for validation
In this tutorial, we will create a simple DTD and then use it in InDesign to validate an XML
file.
1. The XML file
Create a new InDesign document.
In the Tags panel (Window > Utilities > Tags), double-click on

340
the Root tag and rename it properties.

Choose View > Structure > Show Structure to open the


Structure pane.
In the Structure pane, right-click on the properties element and
choose Import XML from the context menu.

Navigate to the “chapter15” folder and double -click on the file


called “properties.xml” leaving the option Show XML Import
Options activated.
When the XML Import Options dialog appears, leave the Mode set
to Merge Content and activate the option Clone Repeating Text
Elements. Deactivate all of the other options.

341
If the Structure pane is not already visible, choose View >
Structure > Show Sructure.
Explore the structure of the file you have just imported by
expanding the container elements.

The structure of the XML file is illustrated in figure 15-3. The names of items representing
attribute nodes are prefixed with an @ sign.

342
Figure 15-3: The structure of the file "properties.xml"

The root element of the XML file is properties and it has two attribues: ListID and ListType
and one child element: branch_sales.
The branch_sales element is a repeating element with one attribute: name and a single child
element: property.
Property contains eight elements: Ref, Branch, Location, Type, Bedrooms, Price, Image,
Description and Details. Each of these child elements contains actual text with the exception
of Image which, instead, has a single attribute: href—the attribute recognized by InDesign as
denoting a graphic.
An extract from "properties.xml" is shown in listing 15-1, below.
Listing 15-1: An extract from the XML file for which we will create a DTD
1 <?xml version="1.0" encoding="UTF-8"?>
2 <properties ListID = "274931" ListType = "all">
3 <branch_sales name = "Burelmerth">
4 <property>
5 <Ref>BR1371</Ref>
6 <Branch>Burelmerth</Branch>
7 <Location>Broodionumigh Crescent, Burelmerth</Location>
8 <Type>House</Type>
9 <Bedrooms>4</Bedrooms>
10 <Price>382000.1</Price>
11 <Image href = "file:///images/br1371.jpg" />
12 <Description>This superbly presented period detached house with four bedrooms property doluptiis bright can&apos;t
eaquibusam then perhaps pounds together sliding doors store other point coast oditaspiet basement transport links lautem test
main onectem sand sight report newly sendio the triangle level alitatus call sae march nonsequatium cool quo flat sum dead
ommodi soon melody.</Description>
13 <Details>Metal residential accommodation assimilition facilities itatus evening voluptatia double glazing nim gated prorio
picture really of pretty laughed pratess record body est. Received popular drop ari killed magnietur property family ommolup
gardens delibeat sell ex fingers faceped communal circle utem plants foot reium lounge family ommolup.
14 Famous sam thin radiator ptatus second rest might effect sapeditatis create assimpelit market market ulpa act cuscipi
won&apos;t security dollars dolupti statement track. Entryphone torum follow imus everyone aruntia lifted eumentesed
stainless steel residents nus yes died picaecto sell ex tall radiator voluptatur legs plia direct nonecea miss.</Details>
15 </property>
...
16 </branch_sales>
...
17 </properties>

2. Creating the DTD

343
Now let's turn our attention to the DTD and I should warn you that we will be making a couple
of omissions in our code, so we can test out InDesign's validation capabilities.
Open the ESTK or, if you have a decent XML editor loaded on
your machine, open that—Adobe Dreamweaver, Microsoft Visual
Web Developer, XMLSpy, StylusStudio, oXygen, etc. (This
tutorial will assume that you are using the ESTK.)
Choose File > New JavaScript if necessary and then save the file
in the “chapter15” folder under the name “properties.dtd”.
The first thing we need is an XML declaration. Enter the following
lines:
1 <?xml version="1.0" encoding="utf-8"?>

Now let's declare the root element—properties—and its child elements and attributes.

Add the following lines to your markup.


2 <!ELEMENT properties (branch_sales)>
3 <!ATTLIST properties ListID CDATA #REQUIRED
4 ListType (all|flats|houses) #REQUIRED >
On line 2, when we declare the properties element, we specify that it has one child element:
branch_sales.
On lines 3 to 4, we declare the two attributes of the properties element: ListID and ListType.
The ListType attribute is specified as an enumerated list: in order to be valid the ListType
value must be “all”, “flats” or “houses”.
Now let's move on to the branch_sales element and its children.

344
Add the following lines to your markup.
5 <!ELEMENT branch_sales (property)>
6 <!ATTLIST branch_sales name CDATA #REQUIRED>
On line 5, when we declare the branch_sales element, we specify that it has one child
element: property. On lines 6, we declare the name attribute and set its data type to CDATA.
Finally, let's declare the property element and its children.

Add the following lines to your markup to complete the DTD.


7 <!ELEMENT property (Ref, Branch, Location, Type, Bedrooms, Price, Image, Description, Details)>
8 <!ELEMENT Ref (#PCDATA)>
9 <!ELEMENT Branch (#PCDATA)>
10 <!ELEMENT Location (#PCDATA)>
11 <!ELEMENT Type (#PCDATA)>
12 <!ELEMENT Bedrooms (#PCDATA)>
13 <!ELEMENT Price (#PCDATA)>
14 <!ELEMENT Image (#PCDATA)>
15 <!ATTLIST Image href CDATA #REQUIRED>
16 <!ELEMENT Description (#PCDATA)>
17 <!ELEMENT Details (#PCDATA)>

Save your changes then close the file.


On line 7, when we declare the branch_sales element, we specify that it has eight child
elements. On lines 8 to 17, we declare each of these elements and, since they are the elements
which will contain text, we set the data type to #PCDATA.
There are two omissions in the DTD we have just created. Let's see if InDesign will spot them.
Switch back to InDesign and choose Load DTD from the menu in
the top right of the Tags panel (Window > Utilities > Tags).

345
Double-click on the DTD file you have just created in the
“chapter15” folder.
When the DTD appears in the Structure pane, above the root
element, right-click on it and choose Validate from Root Element
from the right-click menu.

The lower half of the Stucture pane displays an error and the
problem elements are highlighted in red.

346
You will notice that the first problem is the second occurrence of a branch_sales element.
Similarly, all occurrences of the property element, with the exception of the first, are
highlighted in red. Clicking on any red item causes InDesign to update the error message in the
bottom half of the Structure pane.
Choose View List of Errors from the menu in the top right of the
Structure pane. The following dialog appears with a summary of
error messages.

Fortunately, we don't need to do any debugging to figure out what's wrong. All the error
messages state the same thing:
<branch_sales> This element is not valid at this position.
<property> This element is not valid at this position.
You have probably figured out that we have neglected to specify that these two elements
should be repeatable.

347
InDesign allows you to view a DTD—though not modify it.
Choose View DTD from the menu in the top right of the Structure
pane.

The first problem can be seen in the first line displayed in the dialog:
<!ELEMENT properties (branch_sales)>
This declaration states that the properties element is supposed to have one branch_sales child
element only. We need to add a plus sign after branch_sales to indicate that it should occur
one or more times. Then we need to indicate that the property element should occur one or
more times within each branch_sales element.
Switch back to the ESTK or to your preferred XML editor.
Open “properties.dtd” and modify the two lines shown below.
2 <!ELEMENT properties (branch_sales+)>
...
5 <!ELEMENT branch_sales (property+)>

Save your changes then return to InDesign.


Loading a DTD into InDesign is not a dynamic process, so the changes you have just made will
not register unless you reload the file.
Choose Load DTD from the menu in the top right of the Tags
panel.
In the Structure pane, right-click on the DTD and choose Validate

348
from Root Element from the right-click menu.
This time, you should be given a clean bill of health. The words “No know errors” should
appear in the bottom section of the Structure pane.

On this occasion, we loaded the XML file and the DTD into InDesign separately. Naturally, if
an XML file contains a DTD declaration, the associated DTD file will be loaded at the same
time as the XML. Let's end this tutorial by linking “properties.dtd” to “properties.xml”.
Open “properties.xml” in the ESTK (or other editor). Insert the
following line after the XML declaration.
6 <?xml version="1.0" encoding="UTF-8"?>
7 <!DOCTYPE properties SYSTEM "properties.dtd">

Save your changes then close the file.

CHAPTER 16: XSLT Essentials


When importing XML into InDesign, it will almost always be the case that the structure of your
XML documents needs some form of modification. For example, when placing XML into a
layout, the order in which elements are placed has to be the same as the order in which they
occur in the XML file. Thus, let's say you have an XML file containing information about an
organization's staff members in which elements occur in the following order.
<staff_member staff_id = "04490281">
<last_name>Lambert</last_name>
<first_name>Donte</first_name>
<middle_initial>S</middle_initial>

349
<start_date>01/08/2006</start_date>
<gender>Male</gender>
<profile>Tem initassit rem ent exped quos rem ratur. Sandelique nam. Vende re voluptatur. Is reriatquam.</profile>
<photo href="file://images/04490281.jpg"/>
</staff_member>

When placing the data into an InDesign layout, you would not be able to have the <first_name>
element preceding <last_name>, since they do not occur in that order in the XML file.
Another restriction which InDesign imposes on you is that attribute values cannot be accessed
and added to layouts. Thus, in the above example, we would not be able to place the staff_id in
our layouts, since staff_id is an attribute of the staff_member element, rather than an element in
its own right.
In order to morph your XML data into the format required by your InDesign publications, you
can use two tools: scripting and XSLT.
XSLT is an official recommendation of the Worldwide Web Consortium and stands for
Extensible Stylesheet Language for Transformations. It's role is to allow developers to take an
XML file and turn it into something else—often completely different from the original. The
XSL file is a kind of blueprint which specifies how the XML file is to be transformed. The
software that actually performs the transformation is called an XSLT processor. InDesign has a
built-in XSLT processor—albeit one which is only meant to work within the confines of the
InDesign environment and which only recognizes a subset of XSLT commands.
XSLT is used in conjunction with another of the Worldwide Web Consortium's XML
specifications—XPath: a sophisticated query language which allows you to precisely target
the various nodes of an XML document. Each transformation detailed within an XSLT file is
designed to be applied to a specific element or group of elements, using XPath to specify the
target nodes.
Linking an XML document to a stylesheet
It is possible to associate an XSLT stylesheet with an XML file by including a processing
instruction in the prolog of the XML file. This takes the following format:
<?xsl:stylesheet href="example.xsl" type="text/xsl"?>
As with HTML, the href attribute specifies the location of the stylesheet file, relative to the
XML document. The type attribute specifies the MIME type of the XSLT file: this is normally
set to "text/xsl" for browser compatibility, although the W3C recommendation specifies
"text/xml". (InDesign will accept either.)
InDesign does not require that the XML document contain a declaration linking it to the
stylesheet being used to transform it. In fact, even if an XML document contains a processing
instructions specifying that a particular stylesheet should be used, InDesign allows you to
override this instruction and use a completely different stylesheet or to simply ignore the
stylesheet altogether.
In order to specify, how stylesheets are handled when importing XML into InDesign, you must

350
ensure that the Show XML Import Options checkbox is activated.

Figure 16-1: The Apply XSLT dropdown in the XML Import Options dialog allows you to use or
override a linked XSLT stylesheet

When the XML Import Options dialog box appears, you can do any of the following:
• To use a stylesheet to transform the imported XML, activate the option Apply XSLT.
• To use the stylesheet specified in the XML file—if there is one—choose Use
Stylesheet from XML from the dropdown menu. (This option is always available—even
if there is no stylesheet declaration in the XML file.)
• To use a different stylesheet for the transformation, choose Browse from the dropdown
menu then locate the XSL file.
• To suppress the linked stylesheet and import the XML with no transformation, simply
deactivate the Apply XSLT checkbox.
The structure of an XSLT document
So, what is XSLT? What does it look like? Well, XSLT is not a programming or scripting
language: in fact, an XSLT file is really just an XML document which—like any other XML
document—needs to obey the basic rules of XML syntax and must use a fixed vocabulary of
element and attribute names in order to be well-formed and valid.
Each stylesheet begins with an XML declaration, confirming that it is an XML file. Then, like
every XML document, it has a root element that contains all other elements—the stylesheet
element.
Within the stylesheet element are a series of template elements which must specify two key
facts:
Which part of the original XML document is being targeted.
The transformation which needs to be performed.
The following is an example of a simple XSL document.
Listing 16-1: A simple XSL file
1 <?xml version="1.0" encoding="UTF-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

351
3 <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
4 <xsl:template match="/">
5 <xsl:element name="courses">
6 <xsl:for-each select="courses/course">
7 <xsl:element name="course">
8 <xsl:for-each select="@*">
9 <xsl:element name="{name(.)}"><xsl:value-of select="."/></xsl:element>
10 </xsl:for-each>
11 </xsl:element>
12 </xsl:for-each>
13 </xsl:element>
14 </xsl:template>
15 </xsl:stylesheet>

Since the XSL document is a XML file, it starts with the standard XML declaration. Next
comes the stylesheet element which contains a namespace declaration specifying that all
elements will use the prefix xsl. Next comes the stylesheet element: it is the root element and
all other elements are contained within it.
Within the stylesheet element is a template element (line 4) which gives details of the
elements that need to be transformed and the nature of the transformation that will be
performed.
Let's take a look at the XML file that this XSL document is designed to work with.
Listing 16-2: The XML file to which the XSLT will be applied
1 <?xml version="1.0" encoding="UTF-8"?>
2 <?xml-stylesheet href="attributes-to-elements.xsl" type="text/xsl"?>
3 <!-- Converting attributes to elements -->
4 <courses>
5 <course name="Excel level 1" level="intermediate" cost="300" duration="2"/>
6 <course name="Excel level 2" level="advanced" cost="150" duration="1"/>
7 <course name="PowerPoint level 1" level="intermediate" cost="300" duration="2"/>
8 <course name="PowerPoint level 2" level="advanced" cost="150" duration="1"/>
9 <course name="Word level 1" level="intermediate" cost="300" duration="2"/>
10 <course name="Word level 2" level="advanced" cost="150" duration="1"/>
11 <course name="Access level 1" level="intermediate" cost="300" duration="2"/>
12 <course name="Access level 2" level="advanced" cost="150" duration="1"/>
13 <course name="Visio level 1" level="intermediate" cost="300" duration="2"/>
14 <course name="Visio level 2" level="advanced" cost="150" duration="1"/>
15 <course name="Project level 1" level="intermediate" cost="300" duration="2"/>
16 <course name="Project level 2" level="advanced" cost="150" duration="1"/>
17 </courses>

You will notice that the file contains no data, as such: all of the data is stored as attribute
values of the course element. This means that the course information could not be used to
populate an InDesign layout.
Now let's look at a fragment of the XML file which results after the XSLT has been applied to
it.
Listing 16-3: The XML file resulting from applying the XSLT

352
18 <?xml version="1.0"?>
19 <courses>
20 <course>
21 <name>Excel level 1</name>
22 <level>intermediate</level>
23 <cost>300</cost>
24 <duration>2</duration>
25 </course>
26 <course>
27 <name>Excel level 2</name>
28 <level>advanced</level>
29 <cost>150</cost>
30 <duration>1</duration>
31 </course>
32 ...
Here, we can see that the course element has no attributes: instead it has four child elements—
one corresponding to each of the attributes in the original file.
To see this transformation take place in InDesign, carry out the following steps.
Create a new document.
Choose View > Structure > Show Structure to make the Structure
pane visible.
Choose Window > Utilities > Tags to make the Tags panel
visible.
In the Tags panel, double-click on the Root tag and rename it
courses.

In the structure pane, right-click on the courses element and


choose Import XML from the context menu.
Navigate to the “chapter16” folder and double-click on the file “02-
simple-xml-file.xml”.

353
In the XML Import dialog, activate Clone Repeating Text Elements
and deactivate all of the other options then click OK.
Explore the resulting data in the Structure pane. You will notice
that all of the imported elements contain no text—just attribute
values.

Choose Edit > Undo then right-click on the courses element and
choose Import XML from the context menu.
In the XML Import dialog, activate Apply XSLT then choose the
file “02-simple-xml-file.xsl” as the stylesheet.
Activate Clone Repeating Text Elements and deactivate all of the
other options then click OK.
Look at the data in the Structure pane: this time, the imported
elements contain text and there are no attributes.

354
Let's now export the data. Right-click on the root element
—courses—and choose Export XML from the menu.
Export the file as “02-simple-xml-file2.xml” in the “chapter16”
folder then open it in Dreamweaver or your preferred XML editor.
Return to InDesign and choose Edit > Undo or just right-click on
courses and choose Delete from the context menu. Since it is the
root element it cannot be deleted; but all of its child elements will
disappear.
Finally, let's rename the root element. In the Tags panel, double-
click on the courses tag and rename it “branches”. This is the name
of the root element of the file we will be using for the rest of this
chapter, “branches.xml”.
As we move on to discuss XSLT in more detail, you will find each of the XSL files discussed
in the “chapter16” folder. Use the above steps to apply each stylesheet to the file called
“branches.xml”—the example file we will be using for the rest of the chapter—and then
expand the elements in the Structure pane to see the result. If you want to look at the file in
Dreamweaver or another editor, just right-click on the root element and choose Export XML,
as described above.
The stylesheet element
In the same way that the <xhtml> element is the topmost element in the HTML hierarchy, the
<xsl:stylesheet> element acts as the container for all other elements in an XSLT stylesheet.

355
You may occasionally encounter <xsl:transform> instead of <xsl:stylesheet>: this is an
alternative name for the same element which is permitted by the W3C recommendation.
The basic format for using the <xsl:stylesheet> element is as follows:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
...
</xsl:stylesheet>

The xmlns attribute of the stylesheet element defines a namespace which uses the prefix xsl.
Namespaces are a mechanism used in XML documents to distinguish between duplicate
element and attribute names—such as might occur if the XML document being processed by the
XSLT stylesheet contained an element name identical to one used in the XSLT vocabulary,
such as <template>. The declaration basically implies that every XSLT element must start
with the prefix “xsl” to mark it out as being part of the XSLT vocabulary.
The template element
The template element lies at the heart of XSLT stylesheets: it specifies exactly which part of
the original XML file needs to be transformed and the precise nature of the transformation. The
general syntax for a template is as follows:
<xsl:template match="An XPath Expression">
...
</xsl:template>

The match attribute normally takes as its value an XPath expression specifying the nodes to be
processed. A template will then typically contain a bunch of other elements that detail the
nature of the transformation to be performed.
Using XPath expressions
XSLT uses a subset of the XPath language to create references to specific parts of an XML
document. XPath expressions are used with several other elements—not just <xsl:template>
and, at a very basic level, can resemble file paths. XPath sees an XML document as a tree
consisting of seven different types of node:
• Root—Each XML file contains exactly one root node which, in turn contains the
document root element and may also contain processing instructions and comments. It
lives one level above the XML data.
• Element—The XML tree contains a node corresponding to every element within the
XML file. Each element node may contain text, attribute or comment nodes, as well as
other elements. Every valid XML file contains at least one element, usually referred to as
the (document) root element.
• Attributes—Attribute nodes are children of element nodes and cann ot contain any
other nodes.
• Comments—A node corresponding to an XML comment.
• Processing instructions—A node corresponding to a processing instruction.

356
• Text—Text nodes contain parsed character data—the stuff that ends up being placed in
InDesign layouts. Text nodes are children of element nodes. The text inside comment,
processing instruction and attribute nodes is treated as part of the node itself.
• Namespace—Namespace nodes are generated for each element that contains a
namespace declaration.
The diagram in figure 16-2 shows the nodes in the XML file we looked at earlier in listing 16-
2. It contains six of the seven node types: root, element, attribute, comment, processing
instruction and text.

Figure 16-2: The XML node tree of the document shown in listing 16-2

Examples of XPath expressions


To examine some examples of XPath expressions, we will use a slightly meatier XML file
called “branches.xml”. You can find this file in the “chapter16” folder of the “indesigncs5js1”
folder. The structure of the XML data in the file is shown in figure 16-3, below.

357
Figure 16-3: The XML node tree of branches.xml

The XPath expressions used in XSLT stylesheets are referred to as location paths. An XPath
expression consists of three components: an axis, a statement and an optional predicate.
• Axis—The axis specifies the relationship between the current node and the node being
targeted by the XPath statement. You can think of it as the direction in which you need to
travel in order to arrive at the target.
• Node test—The node test specifies the node or nodes to retrieve having arrived at the
target.
• Predicate—The predicate is an optional XPath statement which can evaluate to true or
false. Those nodes for which the predicate statement is true are included in the output
produced by the XSLT document.
The generic format of an XPath statement is thus as follows:
axis::nodetest[predicate]
The name of the axis is followed by two colons, the predicate is enclosed in square brackets,
and the nodetest is sandwiched in the middle.
Axes
• ancestor::—Parents, grandparents, great-grandparents (etc.) of the current node.
ancestor-or-self::—The current node itself as well as its parents, grandparents, great-
grandparents etc.
• attribute::—The attributes of the current node.

358
• child::—Children of the current node, but not grandchildren, great-grandchildren, etc.
• descendant::—Children of the current node including grandchildren, great-
grandchildren, etc.
• descendant-or-self::—The current node itself as well as its children, grandchildren,
great-grandchildren, etc.
• following::—Nodes which follow the current node, but not including their descendants.
• following-sibling::—Nodes which follow the current node on the same level, but not
including their descendants.
• namespace::—Namespaces associated with the current node.
• parent::—Parent of the current node.
• preceding::—Nodes which precede the current node, but not including their
descendants.
• preceding-sibling::—Nodes which precede the current node on the same level, but not
including their descendants.
• self::—The current node itself.
Abbreviations
Using axis names can lead to fairly verbose statements; so the most frequently used axes also
have abbreviations.
• child::—can be viewed as the default axis and thus may be omitted.

/child::branches/child::branch
or
/branches/branch

• attribute:: may be abbreviated to @.

/child::branches/child::branch/attribute::name
or
/branches/branch/@name
• parent:: may be abbreviated to two full-stops (".."). Thus, if the current location is
branch_manager, to target the branch, we could use:

parent::branch
or
../branch
• descendant-or-self:: may be abbreviated to //.

descendant-or-self::branch_manager
or
//branch_manager
• self:: may be abbreviated to a dot (".").

359
self::
or
.

Absolute location paths


XPath location paths may be either absolute or relative. Absolute statements begin with a
forward slash.
A forward slash on its own is used to target the root node—one level above the document root.
/
To target the document root element of branches.xml, we would say:
/branches
To target the branch_manager element:
/branches/branch/branch_manager
To target the profile element of the staff_member element:
branches/branch/departments/department/staff/staff_member/profile

Relative location paths


Relative location paths offer more flexibility in how different parts of the XML structure can
be targeted. When a relative expression is used as the value of the match attribute of the
template element, the location path expression can start anywhere within the structure.
Thus, to target the branch_manager element, you simply need to say:
branch_manager
Location paths within elements inside the template element start from the current location.
Thus, if the current location has been set as /branches/branch, to navigate to the department
element using relative syntax, you would use:
departments/department
(This may resemble an absolute location path; but remember that all absolute expressions start
with forward slash.)
Targeting attributes
To target an attribute, simply place an @ symbol in front of the name. Thus, for example, to
target the branch name attribute, you would use:
/branches/branch/@name

Using predicates
Predicates are the part of an XPath expression which provide its precision. There is an
obvious similarity between XPath statements and SQL; and predicates certainly resemble the
WHERE statement found in SQL.
Predicates are written inside square brackets at the end of the expression and may include the
following components:

360
• Node tests—Using element and attribute names.
• Operators—Comparison, logical and arithemitic.
• Literals—Text, numbers and dates. Literals must be placed in single quotes.
• Functions—Both XPath and XSLT have several built-in functions.
Examples of predicates
Using operators
To target only the Birmingham branch data within branches.xml (using a relative location
path), we would say:
//branch[@name='Birmingham']
To target all branches with a female branch manager:
//branch[branch_manager/gender='Female']
To target only the Sales and Operations departments:
//department[@name='Sales' or @name='Operations']
To target all staff members with a staff ID between 25000000 and 26000000, we use the
greater than or equal to and less than or equal to operators—&gt;= (an XML-compliant version
of ">=") and &lt;= (an XML-compliant version of "<="):
//staff_member[staff_id &gt;= 25000000 and staff_id &lt;= 26000000]
Using functions
To target branches with more than 500 staff members, we can use the count() function:
/branches/branch[count(departments/department/staff/staff_member) > 500]
To find all heads of department whose profile contains the word “voluptatur” (the profiles
have been randomly derived from lorem ipsum text), we use the contains() function which
takes two arguments: the string in which to search and the string to search for:
//head[contains(profile, 'voluptatur')]
To find the first staff_member element in each department, we would use the postion()
function:
/staff_member[position()=1]

Using <xsl:apply-templates>
So far, we have looked at two XSLT elements: the root element <xsl:stylesheet> and
<xsl:template>. A typical XSLT stylesheet contains several template elements, each targeting a
different part of the XML document. The use of templates give stylesheets a modular structure
similar to the modularity provided by the use of functions in programming languages where, for
example, a main function calls several subordinate functions.
The key element that makes this modular structure possible is the <xsl:apply-templates>
element. This element (as well as the other elements used in XSLT to generate the content of
the output document) is referred to as a processing instruction.
The <xsl:apply-templates> processing instruction has a similar role to a function call in
programming. The basic syntax is as follows:
<xsl:apply-templates select="an XPath expression"/>

361
or simply:
<xsl:apply-templates/>
If the select attribute is used, <xsl:apply-templates> looks for the closest matching template
for each node in the node set specified by the XPath expression. If the select attribute is
omitted, the node set specified by the match attribute of the parent template is used, as well as
all child elements.
In the example shown in listing 16-4, below, since there is no select parameter, the output will
be all elements and their descendants. So we end up with all the text in the original document
being copied unceremoniously in the output document.
Listing 16-4: Using <xsl:apply-templates> without a select parameter
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4 <xsl:template match="/">
5 <xsl:apply-templates/>
6 </xsl:template>
7 </xsl:stylesheet>
Output file
<?xml version="1.0" encoding="utf-8"?>
10590544
Mills
Graheme
P
06/03/2007
Male
Od quas mossi bea il ent qui non ne sit hic to et et rescipit. Vel ides ma voluptatust. Te mos eniatius udignatem dolorep
edissunt iur. Optatium quam.
22291727
Ware
Haley
H
30/03/2009
Female
Quam autecto recaeriorro beat laceari aut quo conse explat. Bore voluptatur. Tecus veliqui buscium et odit ut aligent.
16950898
Sheldon
Jayde
L
14/07/2003
Female
Quam autecto recaeriorro beat laceari aut quo conse explat. Vendit quias doluptatis quo optatur. Sendit acearibusdam
dolorum. Volorionse
...

(If you apply this stylesheet to “branches.xml” in InDesign, you will get an error, since the
resulting file is essentially a text file and cannot be imported as XML.)
When used without any attributes, the apply-templates element does not copy element or
attribute tags. By contrast, in listing 16-5, the select attribute is used to specifically target the

362
branch_manager element and since a template exists, it is used to process the targeted node.
The branch_manager template simply contains an instruction to copy the original element into
the output document.
Listing 16-5: Using <xsl:apply-templates> with a select parameter
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="/">
6 <branch_managers>
7 <xsl:apply-templates select="//branch_manager"/>
8 </branch_managers>
9 </xsl:template>
10
11 <xsl:template match="branch_manager">
12 <xsl:copy-of select="."/>
13 </xsl:template>
14
15 </xsl:stylesheet>
Output file
1 <?xml version="1.0" encoding="utf-8"?>
2 <branch_managers>
3 <branch_manager>
4 <staff_id>10590544</staff_id>
5 <last_name>Mills</last_name>
6 <first_name>Graheme</first_name>
7 <middle_initial>P</middle_initial>
8 <start_date>06/03/2007</start_date>
9 <gender>Male</gender>
10 <profile>Od quas mossi bea il ent qui non ne sit hic to et et rescipit. Vel ides ma voluptatust. Te mos eniatius udignatem
dolorep edissunt iur. Optatium quam.</profile>
11 <photo href="file://images/10590544.jpg"/>
12 </branch_manager>
...
13
14 </branch_managers>

This time, the stylesheet contains a main template (lines 5 to 9) and a subordinate one (lines 11
to 13). The main template does two things: it creates a new document root element called
branch_managers and it actions the subordinate template using <xsl:apply-templates> (line
7). The subordinate template then uses the <xsl-copy-of> processing instruction to reproduce
the branch_manager element (line12).
Using <xsl:copy>
The <xsl:copy> processing instruction is used to create a skeletal copy of an element with
nothing inside it—child elements are not copied and neither are any of the attributes of the
original item. This means that it needs to be used in conjunction with other elements which can
add some meat to the skeleton produced by <xsl:copy>. Generally speaking, unless you want

363
to output an empty element, you will need to place other processing instructions inside the
<xsl:copy> element to specify what the output elements will contain.
Thus, for example, the file branches.xml contains five branches. The stylesheet shown in
listing 16-6 will simply copy all of these branch elements but ignore their contents—as can be
seen in the output file shown below listing 16-6.
Listing 16-6: Using <xsl:copy>
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="branch">
6 <xsl:copy>
7 <!-- We now need other statements here to flesh out the output document -->
8 </xsl:copy>
9 </xsl:template>
10
11 <xsl:template match="/branches">
12 <xsl:copy>
13 <xsl:apply-templates select="branch"/>
14 </xsl:copy>
15 </xsl:template>
16
17 </xsl:stylesheet>

Output file
1 <?xml version="1.0"?>
2 <branches>
3 <branch />
4 <branch />
5 <branch />
6 <branch />
7 <branch />
8 </branches>

In this example, <xsl:copy> is used twice: once with items inside it and once without any
contents. On lines 12 to 14, <xsl:copy> is used to copy the root element into the output tree and
the <xsl:apply-templates> statement inside it is used to action the <xsl:copy> statement on
lines 6-8. However, since this statement has no other statements inside it, the resulting
elements are empty.
The simplest instruction you can place inside an <xsl:copy> element is <xsl:apply-
templates>. However, since this element on its own will only ever output text, it is fairly
limited in this context.
In listing 16-7, we have added an <xsl:apply-templates> statement inside the <xsl:copy> with
an XPath statement targeting the branch name attribute (line 7). In the resulting file, the name of
each branch is inserted inside the branch element.

364
Listing 16-7: Using <xsl:apply-templates> inside <xsl:copy>
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="branch">
6 <xsl:copy>
7 <xsl:apply-templates select="@name"/>
8 </xsl:copy>
9 </xsl:template>
10
11 <xsl:template match="/branches">
12 <xsl:copy>
13 <xsl:apply-templates select="branch"/>
14 </xsl:copy>
15 </xsl:template>
16
17 </xsl:stylesheet>

Output file
1 <branches>
2 <branch>Birmingham</branch>
3 <branch>Cardiff</branch>
4 <branch>Glasgow</branch>
5 <branch>Leeds</branch>
6 <branch>London</branch>
7 </branches>

The <xsl:value-of> element


Retrieving text from the original XML file in this way is not really the role of the <xsl:apply-
templates> element. This function is normally performed by the <xsl:value-of> processing
instruction. Thus, in line 7 of listing 16-7, we would normally say:
<xsl:value-of select="@name"/>
On lines 11 to 13 of listing 16-8 we use <xsl:value-of> to build the name of the branch
manager, with the <xsl:text> element inserting a space between the first and last name.
Listing 16-8: Using <xsl:copy>
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="branch">
6 <xsl:copy>
7 <name>
8 <xsl:value-of select="@name"/>
9 </name>
10 <manager>
11 <xsl:value-of select="branch_manager/first_name"/>
12 <xsl:text> </xsl:text>
13 <xsl:value-of select="branch_manager/last_name"/>

365
14 </manager>
15 </xsl:copy>
16 </xsl:template>
17
18 <xsl:template match="/branches">
19 <xsl:copy>
20 <xsl:apply-templates select="branch"/>
21 </xsl:copy>
22 </xsl:template>
23
24 </xsl:stylesheet>

Output file
1 <?xml version="1.0"?>
2 <branches>
3 <branch>
4 <name>Birmingham</name>
5 <manager>Graheme Mills</manager>
6 </branch>
7 <branch>
8 <name>Cardiff</name>
9 <manager>Colleen Sinclair</manager>
10 </branch>
11 <branch>
12 <name>Glasgow</name>
13 <manager>Tamara Reilly</manager>
14 </branch>
15 <branch>
16 <name>Leeds</name>
17 <manager>Glen Hope</manager>
18 </branch>
19 <branch>
20 <name>London</name>
21 <manager>Dillon Whittle</manager>
22 </branch>
23 </branches>

This example also shows us a technique for converting an attribute into an element. On lines 7
to 9, we create a new element by inserting literal tags. Inside the element, we use <xsl:value-
of> to insert the branch name.
The <xsl:copy-of> element
To copy complete elements—including all element tags, children and attributes, XSLT
provides the <xsl:copy-of> processing instruction. This creates a copy of the specified
segment of the input tree and everything below it in the XML hierarchy. It tends to be used to
copy elements fairly low down in the XML structure; while <xsl:copy> is used to recreate
elements which are higher up.
For example, on lines 6-7 of listing 16-9, we have added two <xsl:copy-of > statements to
copy all attributes of the branch element as well as the branch_manager element (including

366
all its child elements).
Listing 16-9: Using <xsl:copy-of>
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4 <xsl:template match="branch">
5 <xsl:copy>
6 <xsl:copy-of select="@*"/>
7 <xsl:copy-of select="branch_manager"/>
8 </xsl:copy>
9 </xsl:template>
10 <xsl:template match="/branches">
11 <xsl:copy>
12 <xsl:apply-templates select="branch"/>
13 </xsl:copy>
14 </xsl:template>
15 </xsl:stylesheet>
The resulting file
1 <?xml version="1.0"?>
2 <branches>
3 <branch name="Birmingham" country="UK">
4 <branch_manager>
5 <staff_id>10590544</staff_id>
6 <last_name>Mills</last_name>
7 <first_name>Graheme</first_name>
8 <middle_initial>P</middle_initial>
9 <start_date>06/03/2007</start_date>
10 <gender>Male</gender>
11 <profile>Od quas mossi bea il ent qui non ne sit hic to et et rescipit. Vel ides ma voluptatust. Te mos eniatius udignatem
dolorep edissunt iur. Optatium quam.</profile>
12 <photo href="file://images/10590544.jpg" />
13 </branch_manager>
14 </branch>
...
63 </branches>

Using <xsl:element> and <xsl:attribute>


We have seen a couple of examples of creating a new element by inserting literal tags. The
<xsl:element> element provides the other method of creating elements in the output tree that
do not exist in the input tree. It allows you to create a new element and specify its name either
as a literal string or—more usfully, as an XPath statement. The <xsl:attribute> element
functions in much the same way.
In listing 16-10, we use <xsl:attribute> to create a new attribute named branch_manager and
place inside it the value of the first_name and last_name fields, separated by a space. We
then use <xsl:element> to create a new element called name and place inside it the value of
the name attribute of the branch element.
Listing 16-10: using <xsl:element> and <xsl:attribute>

367
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="branch">
6 <xsl:copy>
7 <xsl:attribute name="branch_manager">
8 <xsl:value-of select="branch_manager/first_name"/>
9 <xsl:text> </xsl:text>
10 <xsl:value-of select="branch_manager/last_name"/>
11 </xsl:attribute>

368
12 <xsl:element name="name">
13 <xsl:value-of select="@name"/>
14 </xsl:element>
15 </xsl:copy>
16 </xsl:template>
17
18 <xsl:template match="/branches">
19 <xsl:copy>
20 <xsl:apply-templates select="branch"/>
21 </xsl:copy>
22 </xsl:template>
23
24 </xsl:stylesheet>
The resulting file
1 <?xml version="1.0"?>
2 <branches>
3 <branch branch_manager="Graheme Mills">
4 <name>Birmingham</name>
5 </branch>
6 <branch branch_manager="Colleen Sinclair">
7 <name>Cardiff</name>
8 </branch>
9 <branch branch_manager="Tamara Reilly">
10 <name>Glasgow</name>
11 </branch>
12 <branch branch_manager="Glen Hope">
13 <name>Leeds</name>
14 </branch>
15 <branch branch_manager="Dillon Whittle">
16 <name>London</name>
17 </branch>
18 </branches>

You can see from the resulting file, how easy it is to convert an attribute into an element and
vice versa.

Try it for yourself!


TUTORIAL: Working with XSLT using Dreamweaver and
InDesign
If you use InDesign, there is a fair chance that you use Adobe Dreamweaver as well. Since
Dreamweaver has some nifty features for working with XSLT stylesheets, let's end this chapter
by getting some practice on using Dreamweaver and InDesign to work with XSLT documents.
If you do not have Dreamweaver, then any code editor will do.
1. Creating an XSL stylesheet in Dreamweaver
Adobe Dreamweaver CS5 allows you to create most of the documents relating to web

369
development—and that includes XSL stylesheets. The documents it creates are web-orientated;
but it's easy enough to modify that. Let's begin by defining a new site pointing to the
“indesigncs5js1” folder.
Defining a new site

Choose Site > New Site.


Enter " InDesign XSLT" in the Site Name box.
Click on the Browse for folder icon, open the “indesigncs5js1”
folder and click Select (Windows) or Choose (Mac).

Click the Save button.


Creating the XSLT file

Choose New from the File menu.


On the left of the New Document dialog box, choose Blank Page.
In the Page type column, choose XSLT fragment.

370
Click the Create button.
Dreamweaver now displays the Locate XML source dialog box
which offers you the chance to associate an XML source file with
the XSL stylesheet.

Click on the Browse button then, in the chapter 16 folder, double-


click branches.xml to select it and click OK.
If necessary, click on the Code button in the top left of the
document window to switch to Code view.
You will notice that Dreamweaver automatically displays the
Bindings panel which shows the structure of the selected XML file.

371
Choose File > Save and save the file as 11-finance_heads.xsl in
the chapter 16 folder.
Dreamweaver's default output method for XSL files is HTML. We are interested in plain XML
output.
On line 15, change the text: output method="html" to: output
method="xml".
Dreamweaver's default XSLT file also contains an internal DTD with some entity declarations
which are useful when producing web output but which we will not need.
Delete the DTD on lines 2-13.
Creating the main (outer) template

Now let's create our main (or outer) template where we will define
the document root element, inside which we will place an
<xsl:apply-templates> element targeting the head element of each
finance department.
To target all heads of department, we can use the XPath statement //heads then, looking at the
structure in the Bindings tab, we can see that we need to go up two levels to get back to the
department level, where we can test for the name "finance". Thus, our predicate will be ../
twice, followed by @name='Finance'.
Modify the <xsl:template> code so that it reads as follows:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

372
3 <xsl:output method="xml" encoding="UTF-8"/>
4
5 <xsl:template match="/">
6 <finance_heads>
7 <xsl:apply-templates select="//head[../../@name='Finance']"/>
8 </finance_heads>
9 </xsl:template>
10
11 </xsl:stylesheet>
Here, we create a new root element called <finance_heads> using literal text and the apply-
templates processing instruction goes inside that. Indenting lines is not important; it simply
makes the code easier to read: just press the Tab key as necessary.
Creating the inner template
The inner template now needs to have its match attribute set to "head" and to create a copy of
each head element, complete with its child elements—a function performed by the <xsl:copy-
of> element. We will also need to add in the branch to identify where each finance head is
based. The branch is four levels above the head element in the stucture; so we will need four
../'s followed by @name to target the branch name.
Enter the following code to create the inner template:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="UTF-8"/>
4
5 <xsl:template match="/">
6 <finance_heads>
7 <xsl:apply-templates select="//head[../../@name='Finance']"/>
8 </finance_heads>
9 </xsl:template>
10
11 <xsl:template match="head">
12 <xsl:copy>
13 <xsl:attribute name="branch">
14 <xsl:value-of select="../../../../@name"/>
15 </xsl:attribute>
16 <xsl:copy-of select="*"/>
17 </xsl:copy>
18 </xsl:template>
19
20 </xsl:stylesheet>

Here, we use <xsl:copy> to recreate the <head> tags, allowing ourselves the freedom to vary
the contents: if we used <xsl:copy-of>, we would simply reproduce the head element. We then
create a new attribute inside the <head> element and set the branch name as its value. Finally,
we use <xsl:copy-of value = "*"/> to copy all of the children of the <head> element but not
the element itself—"*" targets children of the current element; "." targets the element itself.

373
2. Applying the XSLT stylesheet in InDesign
Let's now use InDesign to look at the XML output produced by our stylesheet.
Save your changes then switch over to InDesign.
Create a new blank document.
Choose File > Import XML.
Locate and double-click “branches.xml” inside the “chapter16”
folder of the “indesigncs5js1” folder.
Click OK, making sure that Show XML Import options is
checked.
In the XML Import options dialog, activate Apply XSLT and
choose Browse from the drop-down menu.
Locate and double-click the file you have just created, 11-
finance_heads.xsl.

Click OK, ignoring all the other options.


All we want to do here is to view the XML file produced by our XSLT stylesheet: the other
options won't make any difference to what we see in the structure pane.
Explore the resulting file in the structure pane. You should see five
head elements—one for each branch. Each one should contain a
branch attribute as well as the original child elements.

374
The overview provided in this chapter was designed to give you an overview of how XSL
stylesheets are put together. If you need to create XSL stylesheets from scratch, you will
probably need to delve a little deeper into this complex subject. A good book for beginners is
XSLT Quickly by Bob DuCharme, ISBN: 978-1930110113.
CHAPTER 17: XSLT Processing-Control Elements
In the last chapter, we relied mainly on the <xsl:apply-templates> element to control the
manner in which the input tree was manipulated to produce the output tree. In this chapter, we
will examine a group of XSLT elements which allow you to control the processing of nodes in
a manner similar to the control statements found in programming and scripting languages.
<xsl:if>
Like the if conditional statement found in all programming languages, <xsl:if> allows you to
make a template perform a given series of actions only if a given condition is true. The basic
syntax is as follows:
<xsl:if test = "logical expression">
The test attribute cannot be omitted and uses an XPath expression as its value, much like the
conditional statements we used in location path predicates in the last chapter.
<xsl:choose>, <xsl:when> and <xsl:otherwise>
The <xsl:choose>, <xsl:when> and <xsl:otherwise> elements combine to offer structures

375
similar to the if ... elseif ... else statements used in programming. The <xsl:choose> is the
outer element and takes no attributes. Inside it can be placed one or more <xsl:when> elements
—each one equivalent to a single <xsl:if> and requiring the same test attribute. The
<xsl:otherwise> element comes after all the <xsl:when> elements and acts as the “catch-all”
equivalent of an else statement: it has no attributes.
The basic structure is as follows:
<xsl:choose>
<xsl:when test="conditional statement">
<!-- actions to perform if condition is true -->
</xsl:when>
<xsl:when test="conditional statement">
<!-- actions to perform if condition is true -->
</xsl:when>
<!-- etc. -->
<xsl:otherwise>
<!-- actions to perform if none of the when conditions are true -->
</xsl:otherwise>
</xsl:choose>
It's important to remember that, as with if ... elseif ... else statements, the <xsl:when> and
<xsl:otherwise> sections are mutually exclusive—as soon as the XSLT processor finds one of
them to be true, it executes the statements inside that element only, disregarding the remaining
elements nested inside the <xsl:choose> element. Thus, the <xsl:otherwise> part of this
structure is only ever executed if every single <xsl:when> test proves false.
The <xsl:for-each> element
The <xsl:for-each> element offers a functionality similar to that provided by for and while
loops in programming languages. The statements inside the <xsl:for-each> element are
performed once for each element in the nodeset retrieved by the select statement. The basic
syntax is as follows:
<xsl:for-each select="XPath statement">
<!-- statements -->
</xsl:for-each>

The <xsl:for-each> element basically performs the same function as <xsl:apply-templates>.


However, with <xsl:apply-templates>, the processing of matching elements takes place in a
separate template; while, with <xsl:for-each>, the processing takes place locally—inside the
element itself.
The <xsl:sort> element
The <xsl:sort> element is used inside either the <xsl:apply-templates> or the <xsl:for-each>
element to sort the elements being processed.
The syntax is either:
<xsl:for-each select="XPath statement">
<xsl:sort select="XPath statement"/>

376
<!-- statements -->
</xsl:for-each>

or:
<xsl:apply-templates select="XPath statement">
<xsl:sort select="XPath statement"/>
</xsl:apply-templates>

You will notice that, whereas <xsl:apply-templates> is normally written as an empty element,
here it is written with an opening and closing tag to enable the <xsl:sort> to be nested inside.
<xsl:sort> can accept five attributes—all of which are optional:
Select—As we have seen with <xsl:apply-templates> the nodeset to be sorted. However, if
this is the same nodeset already targeted by the containing <xsl:apply-templates> or <xsl:for-
each> element, the select attribute can simply be omitted.
Order—The sort order, ascending being the default and descending the alternative.
Case-order—Specifies which comes first: upper or lower case letters. The permitted values
are upper-first (the default) and lower-first.
Lang—Specifies the language which will determine the rules used for sorting.
Data-type—Specifies whether the data should be treated as text or numerical. Permitted
values are text (the default), number and a Qname (a user defined data type).

Try it for yourself!


TUTORIAL: Using XSLT control elements
In this tutorial, we will once more be working with the branches.xml file and using Adobe
Dreamweaver in conjunction with InDesign—though you should feel free to use an alternative
editor.
What we are looking to do is to produce a booklet listing all of the people who have joined the
company recently—let's say since the start of 2010. For each person, we will produce an entry
consisting of the following:
• Their name—in the format: last_name, first_name, middle_initial
• Their photo
• A statement saying which branch and department they have joined
• Their profile
• Data sorted by last_name
However, we have three types of person who may need to be listed: staff_member,
(department) head and branch_manager; and, since they are located at different points within
the XML structure, they will need to be processed differently. In addition, in light of their
different positions within the company, we will probably want to word the statement about

377
them joining the company slightly differently for each group.
For this example, we will dispense with the <xsl:apply-templates> element and use <xsl:for-
each> instead.
1. Creating the stylesheet in Dreamweaver
If necessary, choose InDesign XSLT from the pop-up menu at the
top of Dreamweaver's Files panel to return to the site you created
on page 374.
Choose New from the File menu.
On the left of the New Document dialog box, choose Blank Page.
In the Page type column, choose XSLT fragment.
Click the Create button.
Dreamweaver now displays the Locate XML source dialog box
which offers you the chance to associate an XML source file with
the XSLT stylesheet.
Click on the Browse button then, in the chapter17 folder, double-
click branches.xml to select it and click OK.
If necessary, click on the Code button in the top left of the
document window to switch to Code view.
Choose File > Save and save the file as new_guys.xsl in the
chapter17 folder.
On line 15, change the text: output method="html" to: output
method="xml".
Delete the internal DTD on lines 2 to 13.
Creating the output document root element
This time, we will only be creating a single template; so Dreamwever's default template
element will do just fine. The first thing we need inside this template is the root element of the
output file—let's call this element new_guys.
Modify the <xsl:template> code so that it reads as follows:
1 <?xml version="1.0" encoding="UTF-8"?>

378
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="UTF-8"/>
4
5 <xsl:template match="/">
6 <new_guys>
7
8 </new_guys>
9 </xsl:template>
10
11 </xsl:stylesheet>

Creating the <xsl:for-each> element


Now we need the xsl:for-each element and it needs to target all branch_manager, head and
staff_member elements. We will then need to add a predicate which will find everyone who
joined the company in 2010. To specify that we want all branch_manager, head and
staff_member elements, we simply say:
//staff_member|//branch_manager|//head
However, since XSLT does not allow us to use &gt; (>) with dates, we will need to use the
substring() function to extract the year so that we can then do a numeric comparison which
extracts one string from another.

SYNTAX substring(string, start, length)


SUMMARY ... XPath function
Extracts a substring from the target string.

String The string containing the desired substring.


Start An integer specifying the position at which to start extracting characters.
Optional. The number of characters to extract. If omitted extraction continues up to the end of the
Length
string.

Thus, our predicate will be:


substring(start_date, 7,4) &gt;= 2010]
In plain English, extract four characters from the start_date, beginning at character 7 (the first
character in the year) and check whether the resulting number is greater than or equal to 2010.
To apply the predicate to each of the elements specified in the nodetest, we simply place them
in parentheses and append the predicate—like so:
(//staff_member|//branch_manager|//head)[substring(start_date, 7,4) &gt;= 2009]

Insert the following code between the opening and closing tags of
the <new_guys> element.

379
5 <xsl:template match="/">
6 <new_guys>
7 <xsl:for-each select="(//staff_member|//branch_manager|//head)[substring(start_date, 7,4) &gt;= 2010]">
8
9 </xsl:for-each>
10 </new_guys>
11 </xsl:template>

Creating the <xsl:sort> element


We want to sort our people first by last_name, then by first name, then by middle initial. This
is achieved by simply using three <xsl:sort> statements, one after the other.
Add the following 3 lines between the opening and closing
<xsl:for-each> tags.
5 <xsl:template match="/">
6 <new_guys>
7 <xsl:for-each select="(//staff_member|//branch_manager|//head)[substring(start_date, 7,4) &gt;= 2010]">
8 <xsl:sort select="last_name"/>
9 <xsl:sort select="first_name"/>
10 <xsl:sort select="middle_initial"/>
11 </xsl:for-each>
12 </new_guys>
13 </xsl:template>

Inside the <xsl:for-each> element, we now need to place the content which will appear in our
InDesign layout. All of this data will be placed inside a <person> element. First we want the
person's name—comprising: last_name, first_name and middle_initial, followed by their
photo. (This part of the layout will be common to everyone.) Next, we want a paragraph saying
which branch and department they have recently joined and it is this paragraph which will be
different for staff members, heads of department and branch managers. Finally, we want the
profile of each individual which will again be the same for everyone.
Let's begin by inserting those elements which are the common to all staff, leaving a comment as
a placeholder for the <xsl:choose> element we will be inserting later to cater for the different
text we will be creating for the various profiles of staff member.
Creating the common elements

Add the code shown in lines 12 to 28 of the following listing.


Listing 17-1: Our template minus the <xsl:choose> statement
1 <?xml version="1.0" encoding="utf-8"?><!-- DWXMLSource="branches.xml" -->
2 <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8"/>
4
5 <xsl:template match="/">
6 <new_guys>
7 <xsl:for-each select="(//staff_member|//branch_manager|//head)[substring(start_date, 7,4) &gt;= 2010]">
8 <xsl:sort select="last_name"/>

380
9 <xsl:sort select="first_name"/>
10 <xsl:sort select="middle_initial"/>
11
12 <person>
13 <xsl:copy-of select="photo"/>
14
15 <xsl:element name="full_name">
16 <xsl:value-of select="last_name"/>
17 <xsl:text> </xsl:text>
18 <xsl:value-of select="first_name"/>
19 <xsl:text> </xsl:text>
20 <xsl:value-of select="middle_initial"/>
21 </xsl:element>
22
23 <!-- xsl:choose -->
24
25 <bodytext>
26 <xsl:value-of select="profile"/>
27 </bodytext>
28 </person>
29
30 </xsl:for-each>
31 </new_guys>
32 </xsl:template>

On line 6, we created the document root element <new guys> using literal text.
On line 12, inside the <xsl:for-each> statement, we use literal text once more, to create the
<person> element which will act as the container for each employee's details.
On line 13, we recreate the photo element by using <xsl:copy-of>.
On lines 15 to 21, we create a new element called <full_name> and, inside it, we place the
last_name, first_name and middle_initial of each person, using <xsl:value-of> statements.
On lines 17 and 19, we use <xsl:text> to insert spaces between the various parts of the name.
Finally, on lines 25 to 27, after the paragraph of text which we will be creating using
<xsl:choose>, we create an element called <bodytext> and insert the value of the profile
element inside it.
Adding the <xsl:choose> statement
Inside the <xsl:choose> element, we need to cater for three possibilities; so we will need to
include three <xsl:when> statements. Let's begin by creating the skeleton and then we can flesh
it out one step at a time.
Delete the comment shown on line 23 of listing 17-1 and replace it
with the following code:
23 <xsl:choose>
24 <xsl:when test="name() = 'branch_manager'">
25

381
26 </xsl:when>
27 <xsl:when test="name() = 'head'">
28
29 </xsl:when>
30 <xsl:when test="name() = 'staff_member'">
31
32 </xsl:when>
33 <xsl:otherwise>
34
35 </xsl:otherwise>
36 </xsl:choose>

For branch managers, let's create a phrase like: "Jen Giles is the new manager of our Hull
branch."; for department heads: "Peter Hanson is the new head of the Finance department at
our Slough branch."; and, for staff members: "Luke Leigh has recently joined the Operations
team of our Basingstoke branch.". We will place all of this content in a new element called
<leader>.
Since we are targeting three specific elements, the statements we place inside the
<xsl:otherwise> block should never be encountered. So, here, we will simply create the text
"ERROR".
Position the cursor between the <xsl:when> tags relating to the
branch_manager element and enter the following code.
24 <xsl:when test="name() = 'branch_manager'">
25 <leader>
26 <xsl:value-of select="first_name"/>
27 <xsl:text> </xsl:text>
28 <xsl:value-of select="last_name"/>
29 <xsl:text> is the new manager at our </xsl:text>
30 <xsl:value-of select="../@name"/>
31 <xsl:text>branch.</xsl:text>
32 </leader>
33 </xsl:when>

Enter the following code in the name() = head section.


34 <xsl:when test="name() = 'head'">
35 <leader>
36 <xsl:value-of select="first_name"/>
37 <xsl:text> </xsl:text>
38 <xsl:value-of select="last_name"/>
39 <xsl:text> is the new head of the </xsl:text>
40 <xsl:value-of select="../../@name"/>
41 <xsl:text> department at our </xsl:text>
42 <xsl:value-of select="../../../../@name"/>
43 <xsl:text> branch.</xsl:text>
44 </leader>

382
45 </xsl:when>

Enter the following code in the staff_member section.


46 <xsl:when test="name() = 'staff_member'">
47 <leader>
48 <xsl:value-of select="first_name"/>
49 <xsl:text> </xsl:text>
50 <xsl:value-of select="last_name"/>
51 <xsl:text> has recently joined the </xsl:text>
52 <xsl:value-of select="../../@name"/>
53 <xsl:text> department at our </xsl:text>
54 <xsl:value-of select="../../../../@name"/>
55 <xsl:text> branch.</xsl:text>
56 </leader>
57 </xsl:when>
Finally, in the <xsl:otherwise> section, enter:
58 <xsl:otherwise>
59 <leader>
60 <xsl:text>ERROR</xsl:text>
61 </leader>
62 </xsl:otherwise>
The completed template is shown in listing 17-2, below.
Listing 17-2: The completed new_guys XSL stylesheet
1 <?xml version="1.0" encoding="utf-8"?><!-- DWXMLSource="branches.xml" -->
2 <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8" indent="yes"/>
4
5 <xsl:template match="/">
6 <new_guys>
7 <xsl:for-each select="(//staff_member|//branch_manager|//head)[staff_id &gt; '20000000' and substring(start_date, 7,4)
&gt;= 2009]">
8 <xsl:sort select="last_name"/>
9 <xsl:sort select="first_name"/>
10 <xsl:sort select="middle_initial"/>
11
12 <person>
13 <xsl:copy-of select="photo"/>
14
15 <xsl:element name="full_name">
16 <xsl:value-of select="last_name"/>
17 <xsl:text> </xsl:text>
18 <xsl:value-of select="first_name"/>
19 <xsl:text> </xsl:text>
20 <xsl:value-of select="middle_initial"/>
21 </xsl:element>
22
23 <xsl:choose>
24 <xsl:when test="name() = 'branch_manager'">
25 <leader>
26 <xsl:value-of select="first_name"/>

383
27 <xsl:text> </xsl:text>
28 <xsl:value-of select="last_name"/>
29 <xsl:text> is the new manager at our </xsl:text>
30 <xsl:value-of select="../@name"/>
31 <xsl:text>branch.</xsl:text>
32 </leader>
33 </xsl:when>
34 <xsl:when test="name() = 'head'">
35 <leader>
36 <xsl:value-of select="first_name"/>
37 <xsl:text> </xsl:text>
38 <xsl:value-of select="last_name"/>
39 <xsl:text> is the new head of the </xsl:text>
40 <xsl:value-of select="../../@name"/>
41 <xsl:text> department at our </xsl:text>
42 <xsl:value-of select="../../../../@name"/>
43 <xsl:text> branch.</xsl:text>
44 </leader>
45 </xsl:when>
46 <xsl:when test="name() = 'staff_member'">
47 <leader>
48 <xsl:value-of select="first_name"/>
49 <xsl:text> </xsl:text>
50 <xsl:value-of select="last_name"/>
51 <xsl:text> has recently joined the </xsl:text>
52 <xsl:value-of select="../../@name"/>
53 <xsl:text> department at our </xsl:text>
54 <xsl:value-of select="../../../../@name"/>
55 <xsl:text> branch.</xsl:text>
56 </leader>
57 </xsl:when>
58 <xsl:otherwise>
59 <leader>
60 <xsl:text>ERROR</xsl:text>
61 </leader>
62 </xsl:otherwise>
63 </xsl:choose>
64 <bodytext>
65 <xsl:value-of select="profile"/>
66 </bodytext>
67 </person>
68 </xsl:for-each>
69 </new_guys>
70 </xsl:template>
71 </xsl:stylesheet>

2. Creating a layout with placeholders


Back in InDesign, let's create the publication into which the transformed XML file will be
imported.
Create a new file using the settings shown below. (Master Text

384
Frame is probably the only important one.)

Choose File > Save and save the file as “01-new-staff.indd”, in the
“chapter17” folder.
The tags that we need to need to use in our layout cannot be imported, since they will only be
created when the stylesheet is applied to the XML file. So, let's import the XML file (applying
the stylesheet) as our first step: this will also import the tags.
In the Tags window (Window > Utilities > Tags), double click on
the solitary Root tag and change the name to new_guys—the root
element of the file we will be importing.

Choose File > Import XML.


Locate the file “branches.xml” in the “chapter17” folder.
In the Import XML dialog, activate the option Apply XSLT and
click Browse (Windows) or Choose (Mac) in the drop-down
menu.
385
Locate the file “new_guys.xsl” in the “chapter17” folder.
Activate the following options:

• Apply XSLT (and browse for “new_guys.xsl” in the “chapter17” folder)


• Clone repeating text elements
• Do not import contents of whitespace-only elements.
Click OK to import the file.
Creating paragraph styles
Now we need to create a paragraph style to match the two elements which we will be placing
on the page: full_name and profile. Making the style names the same as the tag names is always
a good idea.
Choose New Paragraph Style from the menu in the top right of
the Paragraph Styles panel (Window > Styles > Paragraph
Styles).
Enter the style name full_name.
Since this will effectively be the heading text, choose some
attributes which will make it look like a heading. Here, we have
chosen Trajan 24 point type and set the space after and before to 12
points. (We have also set the text colour to white and added a thick
black paragraph rule.)

386
Create a second style called leader and format it to look like large
body text—in our example, we use Rockwell, 14 point type with 9
points of space after the paragraph.
Create a third style called bodytext and format it to look like body
text—in our example, we use Rockwell, 12 point.
Creating text and graphic placeholders
Now let's prepare a layout with a placeholder for each of the three elements that will end up
on the page: the photo, the full name and the profile.
Hold down Control-Shift (Windows) or Command-Shift (Mac) and
click anywhere inside the margins on page 1 to unlock the master
text frame.
Double-click to position the flashing cursor inside the frame.
Enter the text “Full name”.
In the Paragraph Styles panel, click on full_name to apply the style
you have just created.

387
Press Return then type “Leader”.
Apply the leader style.
Press Return and type “Body text”.
Apply the bodytext style.

Press Return once more to insert a blank paragraph at the end of


the layout. This is required for the placeholders to repeat correctly.
Move the cursor to the very start of the text box, in front of all the
text you have just typed.
Choose Object > Anchored Object > Insert.
In the Object Options sections of the Insert Anchored Object
dialog, choose Graphic from the Content drop-down menu.
Set the height to 30 mm and the width to 36 mm.

Choose Custom from the Position drop-down menu and set the
reference point to the top left.

388
In the Anchored Position section, leave the Reference Point set to
the left, X Relative To set to Text Frame, and the X Offset set to 0
mm. Finally, set the Y Offset to 7.5 mm.

Click OK.
Select the anchored graphic frame.
Choose Window > Text Wrap and click on the second icon (wrap
around bounding box).
Set the Top and Right Offset measurement to 6 mm and the bottom
to 3 mm.

389
With the anchored graphic frame still selected, choose Fitting >
Frame Fitting Options.
Choose Fill Frame Proportionally from the Fitting dropdown
menu.
Set Align From to centre and the Crop Amount to zero all round.

Save your changes.


3. Tagging placeholders
Before we can import XML into the four placeholder items we have just created, we need to
associate them with the appropriate elements within the XML file. Let's begin by removing the
XML we imported earlier.
Choose View > Structure > Show Structure, if necessary.
In the Structure pane, select all of the <person> elements by
390
clicking on the first, holding down the Shift key and clicking on
the last.
Right-click on the selection and choose Delete from the context
menu.

Choose Window > Utilities > Tags, if necessary.


Highlight the text frame with the selection tool.
Click on person in the Tags panel.
Highlight the anchored graphic frame and click on photo in the
Tags panel.

Look in the Structure pane and you should see a person item with
a photo item nested inside it.
When tagging text, it is often a good idea to work in story mode: this enables you to see exactly
what is and isn't tagged.
Select the text frame with the Selection tool or position the cursor
in it with the Text tool.

391
Choose Edit > Edit in Story Editor.
Highlight the text “Full name”, taking care not to include the
anchored graphic in your selection, and click on the full_name tag.
Highlight “Leader” and click on the leader tag.
Highlight “Body text” and click on the bodytext tag.

Double-check that you have inserted a blank line below the body
text line. (The repetition of elements messes up without it.)
Close the Edit Story window to return to the layout.
In the Structure pane, the person element should now contain four
items.

4. Importing XML into placeholders


All that remains for us to do is to import the XML and place it in the layout.
Choose File > Import XML.

392
Locate the file “branches.xml” in the “chapter17” folder.
In the Import XML dialog, activate the option Apply XSLT and
choose Browse from the drop-down menu.
Locate the file “new_guys.xsl” in the “chapter17” folder.
Activate the following options:
• Clone repeating text elements
• Do not import contents of whitespace-only elements.

Click OK to import the file.


The contents of the first person element should automatically
populate the layout.

To place the remaining data, drag the new_guys element from the
structure pane onto the text frame.
You may find that, when an item in the listing starts close to the bottom of the page, the
anchored graphic gets pushed out of position, as shown below.

393
This is easily fixed by using InDesign's Keep Options.
In the Paragraph Styles panel, double-click on the bodytext style
to edit it.
Click on the Keep Options category on the left of the dialog.
Activate the options Keep Lines Together and All Lines in
Paragraph.

Click OK, then double-click on the leader style to edit it.


Go to the Keep Options category again and enter 5 in the Keep with
Next box.
Do the same for the full_name style.
If InDesign's Smart Text Reflow feature is not active and your
document still only has one page with overset text, add a new page,
click on the overset text icon in the bottom right of the text frame
on page one; then, holding down the Shift key, click in the top left
394
of page two to place all the XML data.

CHAPTER 18: Working with InDesign XML Objects


When creating InDesign scripts, there are two ways of working with XML. Firstly, you can
manipulate the InDesign scripting objects which represent the XML related items one
manipulates in the Structure pane and Tags panel. When using this approach, your code will
create items which will be visible in the InDesign interface. For example, if your script
imports XML into a document and maps tags to styles, anyone opening the Structure pane will
see the imported XML and the import mapping will be visible in the Tags panel. This
technique is therefore particularly useful in workflows containing a mixture of manual and
automated processes.
The second approach is to use the JavaScript XML object and its various functions and
properties. With this approach, the XML data is manipulated behind the scenes and there need
be no evidence that XML has been imported into a document. This approach offers the greatest
degree of flexibility in creating custom XML solutions, since you are not restricted to working
in a preset fashion.
This chapter deals with the first approach and chapter 19 deals with the JavaScript XML
object.
The InDesign XMLElement Object
The most important object in the InDesign XML suite is the XMLElement object which
represents any element within an XML structure. Every new InDesign document contains a
single XMLElement object which represents the root element of the XML structure displayed
in the Structure pane. This element is tagged Root by default. To target this element in code,
you would use a statement like the following:
myDocument.xmlElements.item(0)
or
myDocument.xmlElements[0]
Renaming an element
The names of the elements displayed in the Structure pane are controlled by the tags displayed
in the Tags panel. There are two ways to rename an element. Firstly, you can retag it (right-
click on the element and choose Tag Element). To do this in scripting, you would use the
markupTag property of the XMLElement object—for example:
myXMLElement.markupTag = myDocument.xmlTags.itemByName("para")
Tags are represented in scripting by the xmlTag object and the markupTag property of the
XMLElement object returns an xmlTag object.
The second method of changing the name of an element is to rename the tag with which it is
currently associated (double-click the tag in the Tags panel and enter a new name). To do this
with scripting, you would use a statement like the following:

395
myXMLElement.markupTag.name = "para"
which sets the name property of the xmlTag object returned by myXMLElement.markupTag.
The xmlTag object
Creating tags
To create a tag with code, use the add() method of the xmlTags collection:
var tag_x = doc_example.xmlTags.add("chapter");
In the above example, the name of the tag is the argument of the add() method. Although this
argument is optional, there would be little point in creating a series of tags and leaving them
with default names like “Tag1”, “Tag2”, etc.
Loading tags
In code, the loadXMLTags() method of the document object is used to load tags from an XML
file; for example:
doc_example.loadXMLTags(File("/c/examples/example.xml"));
Another method of loading tags into an InDesign document is to load a DTD file. To do this,
you use the importDtd() method of the document object; for example:
doc_example.importDtd(File("/Users/admin/Desktop/CS5/example.xml"));
Mapping tags to styles
The process of mapping tags to styles allows you to associate XML elements with InDesign
paragraph and character styles. When scripting, this operation requires two steps: firstly, you
create an xmlImportMap object for each tag you wish to link to a style. For example, the
following code
doc_example.xmlImportMaps.add("chapter", "chapter")
will map the tag named “chapter” to a style of the same name. The add() method accepts both
strings and object references as arguments. Strings are quick and convenient; but to avoid
discrepancies, it is often better to use an object reference—especially for the style.
var tagToMap = doc_example.xmlTags.item("chapter");
var styleToMap = doc_example.paragraphStyles.item("chapter");
doc_example.xmlImportMaps.add(tagToMap, styleToMap);
After adding all the xmlImport maps you need, the mapXMLTagsToStyles() method of the
document object must then be used to complete the operation. This method takes no arguments
and assumes that all the necessary xmlImportMaps have been created.

SYNTAX doc_example. xmlImportMaps.add(markupTag, mappedStyle)


SUMMARY Method of xmlImportMaps collection of document object
... Maps the specified tag to the specified style.

MarkupTag The tag to be mapped, expressed as an xmlTag object or as a string.


The style to which the tag is to be mapped, expressed as a style object (paragraphStyle,
mappedStyle
characterStyle, cellStyle or tableStyle) or as a string.
doc_example.mapXMLTagsToStyles()

396
Method of document object
Activates tag to style mapping in a document.

Importing XML
In chapter 14, we looked at how to import XML into an InDesign document and how to set
import options. Let's look now at performing these operations via scripting.
The XMLElement object has an importXML() method which will import data into the
specified element. When importing XML manually, the XML Import Options dialog allows you
to customize the way in which data is imported. When scripting, these options are set via the
XMLImportPreferences object.
Listing 18-1 shows an example of importing an XML document into the default root element of
a document. The file is “properties.xml” and is located in the “chapter18” folder.
Listing 18-1: Importing XML and setting import options
1 var doc = app.activeDocument;
2 var xmlRoot = doc.xmlElements.item(0);
3 xmlRoot.markupTag.name = "properties";
4 with (doc.xmlImportPreferences){
5 importToSelected = false;
6 importStyle = XMLImportStyles.mergeImport;
7 createLinkToXML = false;
8 allowTransform = false;
9 repeatTextElements = true;
10 ignoreUnmatchedIncoming = false;
11 importTextIntoTables = false;
12 ignoreWhitespace = true;
13 removeUnmatchedExisting = false;
14 importCALSTables = true;
15 }
16 xmlRoot.importXML( File(app.activeScript.parent + "/properties.xml"));
On line 3, we change the name of the root element to match that of the root element in the XML
file we are going to import.
On lines 4 to 15, the xmlImportPreferences object allows us to set all of the options which
appear in the XML Import Options dialog.
On line 16, we import the XML into the root element of the InDesign document. This is the
scripting equivalent of right-clicking the element in the Structure pane and choosing Import
XML from the context menu.

SYNTAX myxmlElement.importXML(file)
SUMMARY ... Method of xmlElement object
Imports XML content into the specified xmlElement object

File The file to be imported.

Placing XML data in the layout

397
The simplest way of adding XML data to a page—the equivalent of dragging XML content
from the Structure pane onto the page—is to use the placeXML() method of the XMLElement
object. To place the entire XML structure, use the placeXML() method of the root element.

SYNTAX myXMLElement.placeXML(using)
SUMMARY ... Method of xmlElement object
Places XML content into the object specified.

Using The object in which to place the XML, typically a text frame or story.

Try it for yourself!


TUTORIAL: Basic XML import
In this Try it for yourself! tutorial, we will create a basic script for importing XML and
placing it on the page. (The completed script is located in the “chapter18” folder under the
name “02-basic-xml-import_completed.jsx”.) The document we will be creating is shown
below.

Our script will do the folowing:


Create a new document
Create paragraph styles to match the XML content to be placed
Match tags to styles
Import the XML data
Place the XML in the layout.
1. Create the new document
Let's begin by creating the main script containing our variable declarations and function calls.

398
Create a new file in the ESTK and save it in the “chapter18” folder
under the name “02-basic-xml-import.jsx”.
Enter the following code.
1 var g = {};
2
3 main();
4 function main(){createDocument(); createStyles(); mapTags(); importXML(); placeXML();}
5
6 g = null;

On line 1, we create a global object variable called g. Although we will only be placing the
document object in this variable, prefixing it with g will still help to remind us that it is a
global.
In the createDocument() function, we'll create the new document, set some basic preferences
and add a header to each of the master pages.
Begin by adding the following skeleton to your code.
7
8 function createDocument(){
9 g.doc = app.documents.add();
10
11 // Preferences
12
13 // Masters
14
15 }
On line 9, we create our new document and place a reference to it in the global object variable
using the property name g.doc.
Now add the following code under the comment "// Preferences".
11 // Preferences
12 g.doc.viewPreferences.rulerOrigin = RulerOrigin.pageOrigin;
13 g.doc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.millimeters;
14 g.doc.viewPreferences.verticalMeasurementUnits = MeasurementUnits.millimeters;
15 g.doc.documentPreferences.pageSize = "A5";
16
17 // Masters
18
19 }
Here, we set the ruler origin to pageOrigin which will make it simpler to specify
measurements on the master spread. Then we set the measurement system to millimetres: if you
are allergic to millimetres, feel free to use another measurement system.

399
Insert the following lines below the comment "// Masters".
17 // Masters
18 for(var i = 0; i < g.doc.masterSpreads.item(0).pages.length; i ++){
19 with (g.doc.masterSpreads.item(0).pages.item(i).marginPreferences){
20 top = 30;
21 bottom = 20;
22 left = 15;
23 right = 15;
24 }
25 var txt = g.doc.masterSpreads.item(0).pages.item(i).textFrames.add({geometricBounds:[10, 15, 15, 133]});
26 txt.contents ="Your Good Health\t";
27 txt.parentStory.insertionPoints.item(-1).contents = SpecialCharacters.autoPageNumber;
28 txt.paragraphs.item(0).appliedFont = "Arial Black";
29 txt.paragraphs.item(0).pointSize = 10;
30 txt.paragraphs.item(0).justification = Justification.rightAlign;
31 }
32 }

Save your changes.


As we loop through the two pages in the default master spread, we first set the margins then, on
line 25, we create a new text frame into which we add a heading followed by a tab ("Your
Good Health\t") and an automatic page number.
If you wish to test your code, temporarily comment out the last four
function calls on line 4, as shown below.
4 function main(){createDocument();} // createStyles(); mapTags(); importXML(); placeXML();}
The code should produce a document with a heading at the top of its single page.

Choose Edit > Undo or manually remove the comment from line 4.
2. Create paragraph styles
An extract from the XML file which will supply the data for our document is shown below.
1 <?xml version="1.0" encoding="utf-8"?>
2 <?xml-stylesheet href="health-articles.xsl" type="text/xsl"?>
3 <articles>
4 <article>
5 <author>J.Barnes</author>
6 <head>The Positive Weight Loss Approach</head>
7 <para>Once you have made up your mind to lose weight, you should make that commitment and go into it with a positive
attitude. We all know that losing weight can be quite a challenge. In fact, for some, it can be downright tough. It takes time,

400
practice and support to change lifetime habits. But it?s a process you must learn in order to succeed. You and you alone are
the one who has the power to lose unwanted pounds.</para>
8 ...
9 </article>
10 ...
11 </articles>
The elements that contain data and for which we want to create paragraph styles are <author>,
<head> and <para>.
Add the following code to your script.
33
34 function createStyles(){
35 var stlHead = g.doc.paragraphStyles.add();
36 stlHead.name = "head";
37 try{stlHead.appliedFont = "Rockwell";}catch(e){}
38 stlHead.pointSize = "14 pt";
39 stlHead.startParagraph = StartParagraph.nextPage;
40
41 var stlAuthor = g.doc.paragraphStyles.add();
42 stlAuthor.name = "author";
43 try{stlAuthor.appliedFont = "Rockwell"}catch(e){}
44 stlAuthor.pointSize = "12 pt";
45 stlAuthor.spaceAfter = "9 pt";
46
47 var stlPara = g.doc.paragraphStyles.add();
48 stlPara.name = "para";
49 try{stlPara.appliedFont = "Garamond"}catch(e){};
50 stlPara.pointSize = "12 pt";
51 stlPara.spaceAfter = "6 pt";
52 }

Save your changes.


Each attempt to set the appliedFont property is wrapped inside a try ... catch statement; so that,
if the font is not present on the user's system, no error will be generated and the default font
will be used instead.
3. Map tags to styles
Now that we have our styles, we need to import the tags from the XML file and then map tags
to styles to set up the document formatting.
Append the following code to the end of your script.
53
54 function mapTags(){
55 g.doc.loadXMLTags(File(app.activeScript.parent + "/health-articles-paras.xml"));
56 g.doc.xmlTags.item("Root").name = "properties";
57 g.doc.xmlImportMaps.add("head", "head");

401
58 g.doc.xmlImportMaps.add("author", "author");
59 g.doc.xmlImportMaps.add("para", "para");
60 g.doc.mapXMLTagsToStyles ();
61 }

Save your changes.


On line 55, we use the loadXMLTags() method of the document object to import the tags from
a file called “health-articles.xml” which is in the same folder as the active script. On line 56,
we change the name of the default Root tag to properties to match the name of the root element
of the XML document.
On lines 57 to 59, we do our style mapping. Notice the way it works: first you create an
xmlImportMap for each pairing; then you use the mapXMLTagsToStyles () method to
activate the mapping. The xmlImportMaps.add() method takes two arguments: the tag to be
mapped and the style to which it is to be mapped. Both of these can be expressed as strings
and, in our case, the names are the same.
4. Importing the XML
Next we need to import the XML. It contains a link to an XSL stylesheet, so we will need to
set the appropriate properties of the xmlImportPreferences object.
Add the following code to the end of your script.
62
63 function importXML(){
64 with (g.doc.xmlImportPreferences)
65 {
66 importStyle = XMLImportStyles.mergeImport;
67 allowTransform = true;
68 transformFilename = XMLTransformFile.stylesheetInXML;
69 repeatTextElements = true;
70 ignoreWhitespace = true;
71 createLinkToXML = false;
72 ignoreUnmatchedIncoming = false;
73 importCALSTables = false;
74 importTextIntoTables = false;
75 importToSelected = false;
76 removeUnmatchedExisting = false;
77 }
78 g.doc.importXML(File(app.activeScript.parent + "/health-articles.xml"));
79 }

Save your changes.


When importing XML via a script, you first set the XMLimportPreferences (lines 64 to 77).
Line 67 (allowTransform = true;) is equivalent to activating the Apply XSLT checkbox in the
XML Import Options dialog. On line 68, the transformFileName property is set to use the

402
XSL stylesheet specified in the XML document.
68 transformFilename = XMLTransformFile.stylesheetInXML;
If the XSL file is not specified in the XML file, the alternative would be to specify the path to a
specific XSL file—for example:
68 transformFilename = File("~/desktop/indesigncs5js1/chapter18/health-articles.xsl");
On line 78, the XML file is imported using app.activeScript.parent to specify the file called
“health-articles-paras.xml” in the same folder as our script.
5. Placing the XML in the document
Our final step is to create a text frame and place the contents of the root element—and, hence,
all of the XML data—inside it.
Add the following function to the end of your script to complete it.
80
81 function placeXML(){
82 var txf = g.doc.pages.item(0).textFrames.add({geometricBounds:[30,15,190,133]});
83 g.doc.xmlElements.item(0).placeXML(txf);
84 while (txf.overflows)
85 {
86 var txf_previous = g.doc.pages[g.doc.pages.length -1].textFrames.item(0);
87 var pg_next = g.doc.pages.add();
88 txf = pg_next.textFrames.add({geometricBounds:[30,15,190,133]});
89 txf_previous.nextTextFrame = txf;
90 }
91 }

Save your changes.


On line 82, we create a text frame and place it in a variable called txf; then, on line 83, we
place the XML file into the frame. We then use a while loop to test whether we have overset
text—while txf.overflows—and, as long as we do, we create a new page, add a text frame to
it and link it to the text frame on the previous page (lines 84-90).
To test your code, highlight the script in the InDesign Scripts panel
and choose Run Script from the panel menu. (Since we have used
app.activeScript.parent, we need to ensure that the active script is
running from InDesign and not the ESTK.)

The xmlAttribute object


InDesign XML suite includes objects which represent all of the different types of node which
can be found in an XML document. The xmlAttribute object represents an attribute of an XML

403
element and is accessed as a member of the xmlElement object containing the attribute.
Looping through attributes
To loop through all of the attributes of an element, use the xmlAttributes collection, as shown
in listing 18-3, below.
Listing 18-3: Looping through all of the attributes in the root element
1 var doc = app.activeDocument;
2 var xmlRoot = doc.xmlElements.item(0);
3 var strRoot = prompt("Please enter the name of the root element", "");
4 xmlRoot.markupTag.name = strRoot;
5 doc.xmlImportPreferences.importStyle = XMLImportStyles.mergeImport;
6 doc.xmlImportPreferences.repeatTextElements = true;
7 xmlFile = File.openDialog("Please locate the XML file");
8 if(xmlFile != null){
9 xmlRoot.importXML(xmlFile);
10 for(var i = 0; i < xmlRoot.xmlAttributes.length; i ++){
11 alert(xmlRoot.xmlAttributes[i].name + ": " + xmlRoot.xmlAttributes[i].value);
12 }
13 }
On line 3, we use a JavaScript prompt to allow the user to enter the name of the root element
of the XML file being imported. We then change the name of the root element of the InDesign
document to this same name. This means that when the file is imported, the root element of the
imported file will become the root element of the InDesign document. (If the names are
different, the root element of imported document will become a child of the root element of the
InDesign document.)
On line 7, we display a dialog allowing the user to specify the XML file to be imported. If the
user chooses a file, we then import it (line 9) and loop through the attributes of the root
element displaying the name and value of each attribute (lines 10 to 12).
To test this code, create a new blank InDesign document then highlight the script “03-xml-
attributes.jsx” in the “chapter 18” folder in the InDesign Scripts panel and choose Run Script
from the panel menu. (Alternatively, in the ESTK, open the file “03-xml-attributes.jsx” in the
chapter 18 folder. Run the file, setting InDesign as the target application.)
When the prompt appears, enter the name “properties”.

When the openDialog() window appears, navigate to the “chapter 18” folder and double-click
on the file called “properties_sample.xml”.
The structure of this XML file is shown in listing 18-3a. The root element contains two
attributes: ListID and ListType. You should therefore see two alerts, bearing the messages:
ListID = "274931"

404
ListType = "all"

Listing 18-3a: Extract from the file "properties_sample.xml"


1 <?xml version="1.0" encoding="UTF-8"?>
2 <properties ListID = "274931" ListType = "all">
3 <property>
4 <Ref>BR1235</Ref>
5 <Branch>Burelmerth</Branch>
6 <Location>Swatonter Avenue, Burelmerth</Location>
7 <Type>House</Type>
8 <Bedrooms>4</Bedrooms>
9 <Price>370000</Price>
10 <Image href = "file:///images/br1235.jpg" />
11 <Description>An imposing terraced house with four large bedrooms gardens delibeat often gardens delibeat sleep gardens
delibeat gardens aboratu gardens delibeat matter comni gardens delibeat radio gardens delibeat raised gardens delibeat
everything dias gardens delibeat sharp gardens delibeat part gardens delibeat read gardens delibeat problem gardens delibeat
kitchen hit gardens delibeat refurbished utesseq gardens delibeat remain gardens delibeat oil gardens delibeat especially nis
gardens.</Description>
12 <Details>Road mossit board iliqui road mossit car eatatem road mossit central heating aliam road mossit play road mossit
meet road mossit entered autem road mossit modernisation que road mossit ring road mossit communal rem.
13 Central heating aliam metal central heating aliam supply central heating aliam some central heating aliam sit central heating
aliam cotton voluptas central heating aliam shouted central heating aliam space.
14 Stainless steel ipidit tools stainless steel ipidit rule stainless steel ipidit list dolest stainless steel ipidit sell ex stainless steel ipidit
branches velicim stainless steel ipidit storage simaion.
15 Modernisation que sum modernisation que sell derit modernisation que boy tem modernisation que shape modernisation que
add ossitat modernisation que do dolorescimet modernisation que entrance simoluptas.</Details>
16 </property>
...
55 </properties>

Changing attributes to elements


As we have seen, InDesign does not permit the placing of attribute values within a document:
only element content may be placed. This sometimes necessitates the conversion of an attribute
into an element either using XSL or scripting. To change an attribute into an element via
scripting, use the convertToElement() method of the xmlAttribute object.
In listing 18-4, we have amended our previous script to include the conversion of the ListID
and ListType attributes to elements. You will find the script in the “chapter18” folder under
the name “04-elements-to-attributes.jsx”.
Listing 18-4: Converting XML attributes to elements
1 var doc = app.activeDocument;
2 var xmlRoot = doc.xmlElements.item(0);
3 var strRoot = prompt("Please enter the name of the root element", "");
4 xmlRoot.markupTag.name = strRoot;
5 doc.xmlImportPreferences.importStyle = XMLImportStyles.mergeImport;
6 doc.xmlImportPreferences.repeatTextElements = true;
7 xmlFile = File.openDialog("Please locate the XML file");
8 if(xmlFile != null){
9 xmlRoot.importXML(xmlFile);
10 for(var i = xmlRoot.xmlAttributes.length - 1; i >=0; i --){
11 xmlRoot.xmlAttributes[i].name += "_converted";

405
12 xmlRoot.xmlAttributes[i].value += " converted";
13 xmlRoot.xmlAttributes[i].convertToElement();
14 }
15 }
The code is essentially the same as listing 18-3. However, in the for loop on line 10, since we
are effectively deleting elements from the xmlAttributes collection (by changing them to
elements), we have to loop in reverse—from highest to lowest.
Just to show that it can be done, on lines 11 and 12, we change the name and value of each
attribute, by adding the word "converted".
On line 13, we use the convertToElement() method to perform the conversion.
Test the script again with a blank InDesign document. When you have finished, the Structure
pane should resemble the one shown in figure 18-1, below.

Figure 18-1: The Structure pane before and after converting attributes to elements

To switch between the before-and-after shown in figure 18-1, choose Edit > Undo twice and
Edit > Redo twice.
Note that, by default, the new element is placed at the start of its parent element and the name
of the attribute becomes the name of the element.
myxmlAttribute.convertToElement(([location, markup tag])
SYNTAX
Method of xmlAttribute object
SUMMARY ...

406
Converts an
attribute into an
element.
Optional. The position of the converted element within its parent.
Location XMLElementLocation.elementStart (default)
XMLElementLocation.elementEnd
Optional. The markup tag from which the element should take its name. If omitted, the name of
Markup
the attribute is used and a tag is automatically created (if a tag with that name does not already
tag
exist.)
Properties of the xmlAttribute object
Name The name of the attribute. (r/w)
Value The value associated with the attribute. (r/w)

Using XSLT stylesheets


As we have seen, InDesign allows you to transform XML as it is imported by activating the
option Apply XSLT Stylesheet in the XML Import Options dialog.

Figure 18-2: Telling InDesign to use an XSLT stylesheet when importing XML

Specifying which stylesheet to use


You are given the option of using the stylesheet specified in the XML markup—Use stylesheet
from XML. This assumes that the XML file you are importing contains a processing instruction
associating it with an XSLT file—for example:
<?xml-stylesheet type="text/xsl" href="properties.xsl"?>
You can also use the Browse option to locate the stylesheet, overriding any processing
instructions the XML file may contain.
If you wish to use a stylesheet when importing XML via a script, you first set the
XMLimportPreferences of the document.
myDocument.XMLimportPreferences.allowTransform = true;
This is equivalent to activating the Apply XSLT checkbox in the XML Import Options dialog.
The equivalent of choosing Use stylesheet from XML is to set the transformFileName
property of the XMLimportPreferences to StylesheetInXML.
myDocument.XMLimportPreferences.transformFilename = XMLTransformFile.StylesheetInXML;
If the stylesheet is not declared in the XML file, the alternative would be to specify the path to
a specific file—for example:
myDocument.XMLimportPreferences.transformFilename = File("/c/examples/example.xsl");

Supplying values to stylesheet parameters

407
One of the bonuses of applying a stylesheet using scripting is that you are able to pass values to
any parameters which the stylesheet may contain.
Variables and parameters are used in XSLT in much the same way as constants are used in
programming languages. They can be assigned a value and given a name which can be used in
the markup from then on to represent this value. For example, if we were working with
accounting data and were going to refer to the rate of value added tax multiple times in our
stylesheet, we could declare a parameter and assign it a value using code like the following:
<xsl:param name="vatRate">20</xsl:param>
then, whenever we wanted to refer to the rate of VAT in our markup, we would use $vatRate
instead of hard coding the number 20. If the government lowers the VAT rate to 17.5, then we
only have one change to make in our stylesheet—the value of the vatRate parameter.
In XSLT, unlike variables, the value of parameters can be reset at runtime and the good news
is that InDesign allows you to reset the value of these parameters while an XSL file is being
imported. This is achieved using the transformParameters property of the
xmlImportPreferences object, which takes as its value an array of nested arrays each
containing two strings: the name given to the parameter within the XSL file and the value to
which you would like it set. Thus, for example, let's say we are working with an XSL file that
contains the following parameters.
<xsl:param name="clientName">ABC plc</xsl:param>
<xsl:param name="branch">London</xsl:param>
To pass new values to these parameters from inside an InDesign script, we would use code
like the following.
myDocument.XMLimportPreferences.transformParameters = [["clientName", " XYZ plc"], ["branch", " Leeds"]];
Note that the values are always supplied as strings, even when they represent numeric
information.

SYNTAX
XMLImportPreferences properties relating to XSL
SUMMARY
...
Setting this value to true is the equivalent of activating Apply XSLT in the XML
allowTransform
Import Options dialog
Either use the enumerator XMLTransformFile.StylesheetInXML or supply a
transformFilename
reference to an XSL file.
Mechanism for supplying values to any parameters declared in the XSL file. An array
transformParameters of two-item nested arrays using the format [["Param1", "Value1"], ["Param2",
"Value2"], ...]

Try it for yourself!


TUTORIAL: Using parameters to filter XML import
In this Try it for yourself! tutorial, we will examine the use of XSL parameters to enable the
filtering of XML data as it is imported into InDesign. We will create a script which imports

408
data from an XML file containing details of over 500 residential properties. However, the user
will be able to dictate the criteria for data import using the following dialog.

Figure 18-3: The choices made in this dialog will determine the XSL parameter values used when
importing the XML file.

The aim of the script is to create a personalized PDF file for the specified client containing
details of properties which match the criteria they have specified. The XML file we shall be
using is called “properties.xml” and is located in the “chapter18” folder. We have encountered
this file before. It has the following structure.

Figure 18-4: The structure of the XML file used in this tutorial

1. Creating the XSL file


Our XSL file needs to fulfill two main criteria. Firstly, it needs to simplify the structure of the
XML: instead of being subdivided according to branch, we just want all <property> elements
listed directly inside the root <properties> element, a shown below.

409
Also, the only attribute we will need in the output XML tree is the href of the <Image>
element; so, we will also exclude ListID and ListType from the output.
The second objective is to provide a filtering mechanism via the use of parameters. The
technique we will use is an if statement which, instead of comparing element contents to fixed
values—for example, Price &gt;= 200000—will compare them to parameters—for example,
Price &gt;= $Price. Each of the parameters will then be indirectly set from within InDesign,
by the user via the dialog.
Open the ESTK or your favourite XML editor.
Create a new file and save it in the “chapter18” folder under the
name “properties.xsl”.
Since all XSL files are also XML documents, begin by entering the
following obligatory XML declaration.
1 <?xml version="1.0" encoding="utf-8"?>
All XML files must have a root element. In the case of XSL files, this is the <stylesheet>
element in which all other elements must be enclosed.
Add the following skeleton <stylesheet> element to your code.
1 <?xml version="1.0" encoding="utf-8"?>
2
3 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
4 <xsl:output method="xml" encoding="utf-8"/>
5
6 </xsl:stylesheet>
On line 3, the start tag of the <stylesheet> element contains a namespace declaration. A
namespace contains a collection of associated element names and is normally identified by a
web URL. The namespace declaration specifies the prefix which will be used to indicate
which elements fall within the namespace—in this case, the prefix is xsl.
On line 4, we use the <xsl:output> element to specify that the output produced by our
stylesheet will be XML (as opposed to HTML, plain text, etc.).
Now let's declare our parameters.

410
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8"/>
4
5 <xsl:param name="b1"></xsl:param>
6 <xsl:param name="b2"></xsl:param>
7 <xsl:param name="b3"></xsl:param>
8 <xsl:param name="b4"></xsl:param>
9 <xsl:param name="b5"></xsl:param>
10 <xsl:param name="b6"></xsl:param>
11 <xsl:param name="b7"></xsl:param>
12 <xsl:param name="b8"></xsl:param>
13 <xsl:param name="beds"></xsl:param>
14 <xsl:param name="min"></xsl:param>
15 <xsl:param name="max"></xsl:param>
16 <xsl:param name="flats"></xsl:param>
17 <xsl:param name="houses"></xsl:param>
18
19 </xsl:stylesheet>
The parameters named b1 through to b8 will correspond to the eight checkboxes at the bottom
of the dialog which allow the user to choose which branches to include.

Then we have parameters to set the number of bedrooms, the minimum and maximum prices
and whether to include flats and/or houses.

We will see shortly how these parameters are used to filter the output.
The final section of our stylesheet is the <template> element,
which is where we will specify the output we require. Begin by
entering the opening and closing tags just above the closing
</xsl:stylesheet> tag.
18
19 <xsl:template match="properties">
20
21 </xsl:template>
22

411
23 </xsl:stylesheet>
The match attribute allows us to specify the node of the XML document at which we want to
start our processing. Here, we specify the root element <properties>. The first thing we want
to include in our output is the <properties> element itself. However, since we don't want any
of the attributes, let's recreate it rather than copying it.
Add the following code on line 20, between the opening and
closing tags of the <template> element.
19 <xsl:template match="properties">
20 <xsl:element name ="properties">
21
22 </xsl:element>
23 </xsl:template>
24 </xsl:stylesheet>
Using the <xsl:element> element in this way allows us to create a new root element named
properties, like the original, but with no attributes.
Now we need to loop through each of the <branch_sales> elements and, within each
<branch_sales> element, loop through each of the <property> elements in a manner not
dissimilar to the for loops we have used so frequently in our scripts. This is achieved in XSL
by the use of the <xsl:for> element which, like for loops, can be nested.
Insert the following code between the opening and closing tags of
the <xsl:element> element.
20 <xsl:element name ="properties">
21 <xsl:for-each select="branch_sales">
22 <xsl:for-each select="property">
23
24 </xsl:for-each>
25 </xsl:for-each>
26 </xsl:element>
27 </xsl:template>
28 </xsl:stylesheet>
Inside the inner <xsl:for-each select="property">, we need to perform our various tests to
determine which <property> elements will be included in the output XML document: we do
this by using the <xsl:if> element. Between the opening and closing tags of the <xsl:if>
element, we will then use the <xsl:copy-of> element to perform a straight copy of the
<property> element and all of its descendants. Naturally, this copy will only take place when
the test specified by <xsl:if> proves true.
In our test, we need to include the following in our results:
• All properties where the text in the <Branch> element matches the value of any of the
branch parameters (b1 to b8)
• All properties where the number in the <Bedrooms> element is equal to or greater than

412
the value of the beds parameter
• All properties where the number in the <Price> element is equal to or greater than the
value of the min parameter
• All properties where the number in the <Price> element is equal to or less than the
value of the max parameter
• All properties where the text in the <Type> element matches the value of either the
houses or the flats parameter.

Enter the following code between the opening and closing tags of
the inner <xsl:for-each> element.
22 <xsl:for-each select="property">
23 <xsl:if test="(Branch=$b1 or Branch=$b2 or Branch=$b3 or Branch=$b4 or Branch=$b5 or Branch=$b6 or
Branch=$b7 or Branch=$b8) and Bedrooms &gt;= $beds and Price &gt;= $min and Price &lt;= $max and (Type = $flats or
Type =$houses)">
24 <xsl:copy-of select="."/>
25 </xsl:if>
26 </xsl:for-each>
27 </xsl:for-each>

Save your changes.


The key component in the above markup is the test parameter of the <xsl:if> element. Note
how each of the parameters we declared at the top of the script are referenced by prefixing its
name with a dollar sign. Note also the use of brackets to group compound conditions.
The entity references &gt; and &lt; are used to represent > and <, respectively, since these
symbols clash with XML markup. Thus >= (greater than or equal to) becomes &gt;= and <=
(less than or equal to) becomes &lt;=.
The completed XSL document is shown below.
1 <?xml version="1.0" encoding="utf-8"?>
2 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3 <xsl:output method="xml" encoding="utf-8"/>
4
5 <xsl:param name="b1"></xsl:param>
6 <xsl:param name="b2"></xsl:param>
7 <xsl:param name="b3"></xsl:param>
8 <xsl:param name="b4"></xsl:param>
9 <xsl:param name="b5"></xsl:param>
10 <xsl:param name="b6"></xsl:param>
11 <xsl:param name="b7"></xsl:param>
12 <xsl:param name="b8"></xsl:param>
13 <xsl:param name="beds"></xsl:param>
14 <xsl:param name="min"></xsl:param>
15 <xsl:param name="max"></xsl:param>
16 <xsl:param name="flats"></xsl:param>

413
17 <xsl:param name="houses"></xsl:param>
18
19 <xsl:template match="properties">
20 <xsl:element name ="properties">
21 <xsl:for-each select="branch_sales">
22 <xsl:for-each select="property">
23 <xsl:if test="(Branch=$b1 or Branch=$b2 or Branch=$b3 or Branch=$b4 or Branch=$b5 or Branch=$b6 or
Branch=$b7 or Branch=$b8) and Bedrooms &gt;= $beds and Price &gt;= $min and Price &lt;= $max and (Type = $flats or
Type =$houses)">
24 <xsl:copy-of select="."/>
25 </xsl:if>
26 </xsl:for-each>
27 </xsl:for-each>
28 </xsl:element>
29 </xsl:template>
30 </xsl:stylesheet>
Finally in this section, let's insert a processing instruction inside the XML file linking it to the
stylesheet we have just created.
Close the XSL file.
In the “chapter18” folder, open the file called “properties.xml”.
Position the cursor at the end line 2.
Press Return and enter the following processing instruction.
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE properties SYSTEM "properties.dtd">
3 <?xml-stylesheet type="text/xsl" href="properties.xsl"?>
Since the XSL file is in the same folder as the XML, the relative reference used as the value of
the href attribute is simply “properties.xsl”.
Save your changes and then close the XML file.
Having completed our setup of the XML data, let's now turn our attention to scripting.
2. Writing the main script
In the ESTK, choose File > New JavaScript.
Save the new file in the “chapter18” folder under the name “05-
import-xml-xslt.jsx”. (You will also find a file called “05-import-
xml-xslt_completed.jsx” which you can refer to at any time if the
version you are creating becomes problematic.)
As always, let's start by creating the main script which will control
the program flow. Enter the following code.

414
1 var g = {}; main(); g = null;
2 const minResults = 4;
3 function main(){
4 var blnFile = verifyXML();
5 if(blnFile == false){
6 alert("Unable to process XML file.");
7 }
8 else if(blnFile == true){
9 var intResult = createDialog();
10 }
11
12 if(intResult ==2){
13 alert("Operation cancelled");
14 }
15 else if(intResult ==1){
16 var blnResults = importXML();
17 if(blnResults){
18 docPrefs();
19 buildMaster();
20 buildCover();
21 placeXML();
22 createPDF();
23 }
24 }
25 }

Choose File > Save.


On line 1, we declare a global object variable called g which will act as a container for any
information or object which we need to access from within more than one function—including
the dialog elements which will reside in the g.win property, inside the global variable. On the
same line, we also call the main function and then set g to null.
On line 2, we declare a constant called minResults: this represents the minimum number of
results our property search must return before we will go ahead and create a PDF document.
We haven't had to use constants very much in our scripts; but they are very useful. A constant is
basically a variable whose value will remain unchanged and unchangeable for the duration of
the script.
On line 4, we call the function verifyXML(), whose role you can probably guess, and capture
a boolean result from it in the variable blnFile. If the verifyXML() function returns false, on
line 6, we display an error message and the whole thing grinds to a halt.
If verifyXML() returns true, we then call the function createDialog(), which will build and
display the modal dialog shown in figure 18-3 on page 414. The dialog will return the value 1
if the OK button is clicked and 2 if Cancel is clicked. Our script therefore tests for each of
these return values: if the function returns 2, on line 13, we display an error message; if a 1 is
returned, we obtain a boolean result from the function importXML() inside the variable

415
blnResults. If the result is true, we call the five functions which will import and process the
XML file and produce the interactive PDF.
3. Verifying the XML file
Before we create the verifyXML() function, let's create all of the nine functions we will need
in skeleton format so that, when we test the script, we don't get errors due to function calls to
non-existant functions.
Add the following code to your script.
26
27 function verifyXML(){
28
29 }
30
31 function createDialog(){
32
33 }
34
35 function userData(){
36
37 }
38
39 function importXML(){
40
41 }
42
43 function docSetup(){
44
45 }
46
47 function buildMaster(){
48
49 }
50
51 function buildCover(){
52
53 }
54
55 function placeXML(){
56
57 }
58
59 function createPDF(){
60
61 }

Now enter the following code inside the verifyXML() function.


27 function verifyXML(){

416
28 g.xmlFile = File.openDialog("Please locate the XML data.");
29 if(g.xmlFile == null){return false;}
30 else{
31 try{
32 g.xmlFile.open("r");
33 var xmlImport = new XML(g.xmlFile.read());
34 if (xmlImport){return true;}
35 else{return false;}
36 }
37 catch(err){
38 return false;
39 }
40 }
41 }

Save your changes.


On line 28, we use the openDialog() File class method to display a dialog inviting the user to
specify the location of the XML file.
On line 29, if they click the Cancel button, returning the value null into g.xmlFile, we return
false to the blnFile variable used in the function call on line 4.
If they choose a file, we use a try ... catch block to open the file in read mode, read its contents
into a variable called xmlImport and, in the process, convert it to an XML object using the
constructor method new XML() (line 32 to 33).
If we end up with a valid XML object, we assume the XML file is fit for purpose and return
true to the blnFile variable (line 34).
Just to remind you how the value returned by the xmlImport() function affects the program
flow, here again is the code that calls the function and processes the value returned.
4 var blnFile = verifyXML();
5
6 if(blnFile == false){
7 alert("Unable to process XML file.");
8}
9 else if(blnFile == true){
10 var intResult = createDialog();
11 }

Run the code that you have created so far.


When the dialog appears for you to choose a file, click the Cancel
button instead and the following dialog should appear.

417
(If you were to choose a file, nothing would happen, since all our other functions are currently
just empty shells.)
4. Creating the dialog
Now let's complete the createDialog() function, beginning with the dialog window itself.
Insert the following code inside the skeletal createDialog()
function you created earlier.
42
43 function createDialog(){
44 // Window
45 g.win = new Window('dialog', 'Personalised listing');
46 g.win.alignChildren = 'left';
47
48 }
On line 45, when we create the dialog, we set the type to 'dialog'—a modal window. Then, to
keep things tidy, on line 46, we specify that elements contained in the dialog will be aligned
left instead of the default center.
Now add the code which will create the two checkboxes at the top
of the dialog.
46 g.win.alignChildren = 'left';
47 // Property type checkboxes
48 g.win.chkHouses = g.win.add('checkbox', undefined, 'Include Houses');
49 g.win.chkHouses.value = true;
50 g.win.chkFlats = g.win.add('checkbox', undefined, 'Include Flats');
51 g.win.chkFlats.value = true;
52
53 }
Here, we create two checkboxes and set the value of both to true, meaning that they will both
be checked by default when the dialog appears.
Next, we need the four statictext and edittext pairs shown below.

418
Since the layout of these pairs of controls is so similar, we can create them all by writing a
simple function and then calling it four times. In order to position the statictext and edittext
controls next to each other, each pair needs to be in its own group. So the function needs to
create a group and then place the two controls inside it.
Add the following code to the end of the createDialog() function.
51 g.win.chkFlats.value = true;
52 // Text fields
53 buildTextGroup("grpName", "Client Name:", "");
54 buildTextGroup("grpBed", "Min. Bedrooms:", "1");
55 buildTextGroup("grpMin", "Minimum Price:", "100000");
56 buildTextGroup("grpMax", "Maximum Price:", "1000000");
57 function buildTextGroup(groupName, staticLabel, defValue){
58 g.win[groupName] = g.win.add('group');
59 g.win[groupName].stx = g.win[groupName].add('statictext', undefined, staticLabel);
60 g.win[groupName].stx.minimumSize = [100, 20];
61 g.win[groupName].txt = g.win[groupName].add('edittext', undefined, defValue);
62 g.win[groupName].txt.minimumSize = [120, 20];
63 }
64
65 }
The buildTextGroup() function on lines 57 to 63 takes three parameters: the name to be given
to the group control, the text which will be displayed in the statictext control and the default
value to be placed in the edittext control. Each time the function is called, a value is supplied
for each of these parameters (lines 53 to 56).
To assign the group name, on line 58, we use the groupName parameter of the
buildTextGroup() function as an index for the win control. Thus, for example, our first
function call on line 53 supplies the value "grpName" for this parameter. This means that the
name we end up creating, on line 58, will be g.win["grpName"] which is another way of
writing g.win.grpName.
The staticLabel parameter is used as the third parameter of the add() method which creates
the control: this is the text which will be displayed inside it.
In a similar fashion, the defValue parameter is used as the third parameter of the add() method
which creates the edittext control—the default text which will be displayed in the control when
the dialog first loads. Naturally, there is no point in setting a default value for client name, so
on line 53, we pass a zero-length string as the value of this parameter when we call the

419
buildTextGroup() function.
We now need two rows of four checkboxes, so we will need to create two groups and place
four controls in each.

Add the following code at the end of the createDialog() function,


just above the closing brace.
64 // Branch checkboxes
65 var arrBranches1 = ["Burelmerth", "East Nilbury" , "Levelliton" , "Malyernes"];
66 var arrBranches2 = ["Nilbury" , "Pagnadon" , "Shapeldon", "West Nilbury"];
67 g.win.grpBranches1 = g.win.add('group');
68 for(var i = 0; i < arrBranches1.length; i ++){
69 var chk = g.win.grpBranches1.add('checkbox', undefined, arrBranches1[i]);
70 chk.minimumSize = [90, 20];
71 }
72 g.win.grpBranches2 = g.win.add('group');
73 for(var i = 0; i < arrBranches2.length; i ++){
74 var chk = g.win.grpBranches2.add('checkbox', undefined, arrBranches2[i]);
75 chk.minimumSize = [90, 20];
76 }
77
78 }
On lines 65 to 66, we build two arrays (arrBranches1 and arrBranches2) and put the names
of four branches in each one. We then create two group controls and loop through the items in
each array. Inside each loop, we create a checkbox and assign the current value of the
appropriate array as the text that will appear next to the checkbox.
That just leaves the OK and Cancel buttons.
Insert the following code just above the closing brace of the
createDialog() function.
77 // Buttons
78 g.win.grpBut = g.win.add('group');
79 g.win.grpBut.alignChildren = 'center';
80 g.win.grpBut.cancel = g.win.grpBut.add('button', undefined, 'Cancel');
81 g.win.grpBut.ok = g.win.grpBut.add('button', undefined, 'OK');
82 g.win.grpBut.ok.onClick = userData;
83 return g.win.show();
84
85 }

420
Save your changes.
On line 80, when we create the Cancel button, simply by setting the third parameter of the
add() method (the name of the control) to “Cancel”, we create a button which will return 2
when clicked. This will then be picked up by our main script, an error message will be
displayed and no further processing will take place.
13 if(intResult ==2){
14 alert("Operation cancelled");
15 }
On line 82, since we need to perform quite a few steps when the OK button is clicked, we
assign the userData() function as the onClick callback.
Finally, on line 83, we show the dialog.
Let's end this section by creating the userData() callback function which needs to capture the
data entered by the user inside the global g variable where it can be accessed later. (To stop
the script becoming too long, we'll omit data validation. Basically, if inappropriate values are
entered, no results will be obtained.)
Add the following code inside the skeleton userData() function
you created earlier.
86
87 function userData(){
88 (g.win.chkHouses.value == true)? g.strHouses = "House": g.strHouses = "null";
89 (g.win.chkFlats.value == true)? g.strFlats = "Flat": g.strFlats = "null";
90 g.strClientName = g.win.grpName.txt.text;
91 g.strBeds = g.win.grpBed.txt.text;
92 g.strMin = g.win.grpMin.txt.text;
93 g.strMax = g.win.grpMax.txt.text;
94 g.arrBranches = ["null", "null", "null", "null", "null", "null", "null", "null"];
95 for(var i = 0; i < g.win.grpBranches1.children.length; i ++){
96 if(g.win.grpBranches1.children[i].value == true){
97 g.arrBranches[i] = g.win.grpBranches1.children[i].text;
98 }
99 }
100 for(var i = 0; i < g.win.grpBranches2.children.length; i ++){
101 if(g.win.grpBranches2.children[i].value == true){
102 g.arrBranches[i+4] = g.win.grpBranches2.children[i].text;
103 }
104 }
105 g.win.close(1);
106 }

Save your changes.


On lines 88 and 89, we use the ternary operator (the shorthand if statement) to add the words
"House" and "Flat" to g.strHouses and g.strFlats, respectively if the user has activated the
relevant checkbox. These variables will be used later as parameter values for the XSL file.

421
On lines 90 to 93, we add the values in the four text boxes to the global variable using the
property names g.strClientName, g.strBeds, g.strMin and g.strMax.
On line 94, we initialize an array inside g.arrBranches with eight neutral string values then,
on lines 95 to 99 and 100 to 104, we loop through the two groups of Branch checkboxes and,
whenever we find one whose value is true, we replace the value of the current slot of
g.arrBranches with the checkbox text (lines 97 and 102).
We now have the values entered by the user saved as properties of our global variable; so we
can turn our attention to importing the XML and using these values as XSLT parameter values.
5. Importing the XML
In this section, we need to do three things: create a new document, set XML import preferences
and import the XML.
Enter the following code inside the empty importXML() function.

107
108 function importXML(){
109 g.doc = app.documents.add();
110 with (g.doc.xmlImportPreferences){
111 importStyle = XMLImportStyles.mergeImport;
112 repeatTextElements = true;
113 ignoreWhitespace = true;
114 allowTransform = true;
115 transformFilename = XMLTransformFile.stylesheetInXML;
116 transformParameters =[
117 ["b1", g.strBranch[0]], ["b2", g.strBranch[1]], ["b3", g.strBranch[2]],
118 ["b4", g.strBranch[3]], ["b5", g.strBranch[4]], ["b6", g.strBranch[5]],
119 ["b7", g.strBranch[6]], ["b8", g.strBranch[7]], ["beds", g.strBeds],
120 ["min", g.strMin], ["max", g.strMax], ["houses", g.strHouses], ["flats", g.strFlats]
121 ];
122 }
123 g.xmlRoot = g.doc.xmlElements.item(0);
124 g.xmlRoot.markupTag.name = "properties";
125 g.xmlRoot.importXML(g.xmlFile);
126 if(g.xmlRoot.xmlElements.length < minResults){
127 alert("Sorry, your search returned too few results. (" + g.xmlRoot.xmlElements.length + ")");
128 g.doc.close(SaveOptions.NO);
129 return false;
130 }
131 else{
132 return true;
133 }
134 }

Save your changes.


On line 109, we create a new InDesign document and place a reference to it inside the global
variable in the property g.doc.

422
To set XML import preferences, we assign values to the various properties of the
xmlImportPreferences object (110 to 122). It is only necessary to set those properties which
are relevant to the import operation we are performing.
On line 115, we ask InDesign to use the stylesheet specified in the XML file and then we set
the transform parameters. The value required by the transformParameters property is an
array of nested arrays. Each nested array contains two values: the name of the parameter being
targeted, without a dollar sign (these are only required inside the XSL file) and the value we
wish to pass to that parameter, as specified by the user via the dialog.
On line 124, we change the name of the tag associated with the root element, from the InDesign
default Root, to properties; so that it matches the name of the root element of the XML file
being imported.
On line 125, we import the XML file into the root element of the InDesign document using the
file path we obtained from the user in the verifyXML() function, on line 28.
28 g.xmlFile = File.openDialog("Please locate the XML data");
Finally, on lines 126 to 133, we test whether the number of elements imported into the
document root is less than the acceptable minimum—g.xmlRoot.xmlElements.length <
minResults. If it is, we display an error message, close the document and return the value false
into blnResults—the variable used in the function call back in the main() function.
17 var blnResults = importXML();
18 if(blnResults){ // == true can be omitted
19 docPrefs();
20 buildMaster();
21 buildCover();
22 placeXML();
23 createPDF();
24 }
If the number of <property> elements imported is equal to or greater than minResults, we
return the value true (line 132) and the five function calls specified in the main() function will
be executed.
Run the script from within InDesign or the ESTK.
When you are asked to specify the location of the XML file,
double-click on “properties.xml” in the “chapter18” folder.
Try clicking OK with all of the branch checkboxes switched off.
The dialog shown below should appear.

423
Run the script again, but this time enter 3 in the Min bedrooms box
and activate the checkbox for the West Nilbury branch.

Open the structure pane (View > Structure > Show Structure).
Your search should have returned six <property> elements each
containing "West Nilbury" in the Branch element. (If the contents
of the Branch element are not visible in the Structure pane, choose
Show Text Snippets from the menu in the top right of the
Structure pane.)

424
Close the document created by the script without saving.
6. Setting up the InDesign document
In this section of the tutorial, we will create the three functions relating to the document setup:
docPrefs(), buildMaster() and buildCover(). In the docPrefs() function, we will set some of
the properties of the three main preference objects involved in document setup:
viewPreferences, documentPreferences and marginPreferences.
Insert the following code inside the empty docPrefs() function you
created earlier in this tutorial.
135
136 function docPrefs(){
137 // View Prefs
138 g.doc.viewPreferences.horizontalMeasurementUnits=MeasurementUnits.pixels;
139 g.doc.viewPreferences.verticalMeasurementUnits=MeasurementUnits.pixels;
140 // Doc Prefs
141 g.doc.documentPreferences.facingPages = false;
142 g.doc.masterSpreads[0].pages[1].remove();
143 g.doc.documentPreferences.pageWidth = 800;
144 g.doc.documentPreferences.pageHeight = 600;
145 g.doc.documentPreferences.pageOrientation = PageOrientation.landscape;
146 // Margin prefs
147 g.master = g.doc.masterSpreads[0].pages[0];
148 g.master.marginPreferences.left = 36;

425
149 g.master.marginPreferences.top = 100;
150 g.master.marginPreferences.right = 36;
151 g.master.marginPreferences.bottom = 60;
152 }

Save your changes to the script.


Note that, since this will be an interactive PDF designed for on-screen consumption, on lines
138 and 139, we set the measurement units to pixels.
The names of the various properties of the three preference objects make them fairly self-
explanatory; so let's move swiftly on to the buildMaster() function; where we will need to
create two master items: a logo at the top centre of the page and a text box at the bottom centre.

Insert the following code inside the skeleton buildMaster()


function you created earlier.
153
154 function buildMaster(){
155 g.fleLogo= File(app.activeScript.parent + "/images/logo.jpg");
156 g.master.place(g.fleLogo);
157 g.master.allGraphics[0].geometricBounds = [0, 300, 50, 500];
158 g.master.allGraphics[0].fit(FitOptions.frameToContent);
159
160 txtFooter = g.master.textFrames.add();
161 txtFooter.geometricBounds = [600-30, 800-36, 600-10, 36];
162 txtFooter.contents = "Property selection specially prepared for " + g.strClientName;
163 txtFooter.paragraphs[0].justification = Justification.centerAlign;
164 try{txtFooter.paragraphs[0].appliedFont = app.fonts.item("Franklin Gothic Heavy");} catch(e){}
165 }

426
Save your changes.
On line 155, we build the location of the logo by specifying a path relative to the folder in
which our script is saved—app.activeScript.parent + "/images/logo.jpg". We then import
the logo onto the master page. Since the graphic is the most recent addition to the allGraphics
collection of the master page, InDesign will assign it the lowest index. Hence we can refer to it
as g.master.allGraphics[0].
On line 157, we position the graphic by setting the geometricBounds property, which requires
an array of coordinates in the order [y1, x1, y2, x2]. Since the graphic is 200 pixels in width
by 50 pixels in height and the page is 800 pixels wide, we would position it in the top centre
by calculating the coordinates as follows.
y1 = top of page -> 0
x1 = centre of page (400) - width of graphic/2 (100) -> 300
y2 = y1 (0) + height of graphic (50) -> 50
x2 = x1 (300) + width of graphic (200) -> 500
This gives us g.master.allGraphics[0].geometricBounds = [0, 300, 50, 500].
On line 160 we create a text frame but this time, when positioning it, the geometricBounds
must be entered in the order [top, right, bottom, left]. Thus, we align the text frame 30 pixels
below the bottom margin (which is 60 pixels) using the following calculation.
top = height of page (600) - 30 -> 600 - 30
right = width of page (800) - right margin (36) -> 800 - 36
bottom = height of page (600) - 30 + height of box (20) -> 600 - 10
left = left margin (36) -> 36
So we end up with txtFooter.geometricBounds = [600-30, 800-36, 600-10, 36].
On line 162, we add a text message to the box which incorporates the client name entered by
the user in the dialog and, finally, on lines 163 and 164, we set the alignment to center and the
font to "Franklin Gothic Heavy". If you do not have "Franklin Gothic Heavy" installed, change
this to any bold font which is on your system.
On the cover of the document (see below), we will have a full page graphic, the company logo
in the top left and a title containing the same text message we added as a footer on the master—
but in larger letters.

427
Insert the following code inside the empty buildCover() function
you created earlier.
166
167 function buildCover(){
168 var pgCover = g.doc.pages[0];
169 pgCover.appliedMaster = NothingEnum.Nothing;
170
171 var fleCover = File(app.activeScript.parent + "/images/cover.jpg");
172 pgCover.place(fleCover);
173 pgCover.allGraphics[0].geometricBounds = [0, 0, 600, 800];
174 pgCover.allGraphics[0].fit(FitOptions.frameToContent);
175
176 pgCover.place(g.fleLogo);
177 pgCover.allGraphics[0].geometricBounds = [36, 36, 36+50, 36+200];
178 pgCover.allGraphics[0].fit(FitOptions.frameToContent);
179
180 var txtCover = pgCover.textFrames.add();
181 txtCover.geometricBounds = [300, 800-36, 600-60, 250];
182 txtCover.contents = "Property selection\rspecially prepared\rfor\r" + g.strClientName;
183 txtCover.texts[0].justification = Justification.centerAlign;
184 try{txtCover.texts[0].appliedFont = app.fonts.item("Franklin Gothic Heavy");}catch(e){}
185 txtCover.texts[0].fillColor = g.doc.colors.itemByName("Paper");
186 txtCover.texts[0].strokeColor = g.doc.colors.itemByName("Black");
187 txtCover.texts[0].pointSize = "36pt";
188 txtCover.paragraphs[0].pointSize = "48pt";
189 }

Save your changes.


The single page which the new document contains is based on the default master. Since we

428
want it to become a cover page, on line 169, we set the master to None
—NothingEnum.Nothing. We then import and position the full page graphic and the logo,
using the same techniques we employed in the buildMaster() function.
Next, we create a text frame inside a variable called txtCover (line 180) and add four
paragraphs of text, using "\r" within the text string to insert paragraph breaks (line 182). We
then set several attributes of all four paragraphs by targetting txtCover.texts[0]. Finally, on
line 188, we change the first paragraph only to a larger font size than the rest of the text
—txtCover.paragraphs[0].pointSize = "48pt".
7. Placing XML content on document pages
Our placeXML() function will loop through all of the imported <property> elements, creating
a new page for each one and placing the relevant data and image. The imported XML contains
all of the child elements of the <property> element. We only need the data from five of these
elements: <Location>, <Price>, <Image>, <Description> and <Details>.
The Location and Price will combine to form the page heading. The Description will constitute
paragraph two and the Details (which may contain several paragraphs) will come next. The
Image will be placed as an independent graphic in the bottom left, with the text wrapping
around it.

Insert the following shell inside the placeXML() function.


190
191 function placeXML(){
192 app.scriptPreferences.enableRedraw = false;
193 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
194
195 for(var i=0; i < g.xmlRoot.xmlElements.length; i ++){

429
196
197 }
198
199 app.scriptPreferences.enableRedraw = true;
200 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
201 }
At the start of the function, we disable screen redraw and user interaction. This will freeze the
screen and prevent InDesign from displaying any alerts. Next we have the for loop in which
we will shortly specify each of the steps that needs to be performed. After the loop is
completed, we reinstate screen redraw and user interaction (line 200).
The first thing we need to do inside our for loop is create a new
page based on the master page.
195 for(var i=0; i < g.xmlRoot.xmlElements.length; i ++){
196 // Create new page
197 var pgCurrent = g.doc.pages.add();
198 pgCurrent.appliedMaster = g.doc.masterSpreads[0];
199
200 }
201
202 app.scriptPreferences.enableRedraw = true;
203 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
204 }

Next, let's move the five bits of XML data that we need into
variables.
198 pgCurrent.appliedMaster = g.doc.masterSpreads[0];
199 // Retrieve XML data we need on page
200 var xmlProperty = g.xmlRoot.xmlElements[i];
201 var xmlRef = xmlProperty.xmlElements.item(0);
202 var xmlLocation = xmlProperty.xmlElements.item(2);
203 var xmlPrice = xmlProperty.xmlElements.item(5);
204 var xmlDescription = xmlProperty.xmlElements.item(7);
205 var xmlDetails = xmlProperty.xmlElements.item(8);
206 }
207
208 app.scriptPreferences.enableRedraw = true;
209 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
210 }
The elements we need are all children of the <property> element and are targeted by a zero-
based index which reflects the order in which they occur in the XML markup. Thus, for
example, <Ref> is the first element within <property>, so it has an index of zero.
The next step is to create a text frame and place the contents of
these elements inside it.
430
205 var xmlDetails = xmlProperty.xmlElements.item(8);
206 // Add text to page
207 var txtDetails = pgCurrent.textFrames.add();
208 txtDetails.geometricBounds = [100, 800-36, 600-60, 36];
209 txtDetails.contents = xmlLocation.contents + " - ";
210 var strFormatPrice = "";
211 for (j = -3; j >= xmlPrice.contents.length*-1; j -=3){
212 if(Math.abs(j) < xmlPrice.contents.length){
213 strFormatPrice = "," + xmlPrice.contents.substr(j, 3) + strFormatPrice;
214 }
215 else{
216 strFormatPrice = xmlPrice.contents.substr(j, 3) + strFormatPrice;
217 }
218 }
219 strFormatPrice = "£" + xmlPrice.contents.substr(0, xmlPrice.contents.length % 3) + strFormatPrice;
220 txtDetails.contents += strFormatPrice + "\r";
221 txtDetails.contents += xmlDescription.contents + "\r";
222 txtDetails.contents += xmlDetails.contents;
223 }
224
225 app.scriptPreferences.enableRedraw = true;
226 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
227 }
Having added the text of the <Location> element to our text frame (line 209), we use the
substr() function to insert commas as thousands separators in the value held inside the <Price>
element. (JavaScript does not have a built-in formatNumber function.)
In the for loop on lines 211 to 218, we start our counter variable (j) at -3 and loop in jumps of
-3 until we reach the number of characters contained in the <Price> element, multiplied by -1.
Each time we go through the loop, we use the counter variable as an index of the substr()
function. The substr() function allows you to extract a range of characters within a string by
specifying the position of the start character and the number of characters to be extracted.
Using a negative number as the first argument of substr() makes the function extract characters
starting from the end of the string. So, basically, we are extracting groups of three characters
and placing them into the variable strFormatPrice, preceded by a comma.
The test on line 212 (Math.abs(j) < xmlPrice.contents.length) is used to identify when we
have extracted all of the characters from a number whose length is exactly divisible by 3. If we
have just extracted the last three digits from such a number (starting from the right), we do not
need to insert a comma at the start. (Math.abs(j) gives us the absolute value of j—ignoring the
minus sign.)
After the loop, on line 219, we add any remaining characters to the start of strFormatPrice,
preceded by a pound sign. The statement xmlPrice.contents.length % 3 (mod 3) gives the
remainder after the dividing the number of characters in xmlPrice.contents by 3. This number
is used to determine the number of characters to extract (starting from the beginning of
xmlPrice.contents) and add to the start of strFormatPrice after the loop has finished. Let's

431
take as an example the number 300000.

Value retrieved by
Value of Contents of
xmlPrice.contents.substr(j, Math.abs(j) < xmlPrice.contents.length
j StrFormatPrice
3)
-3 000 false ,000
-6 300 true 300,000
xmlPrice.contents.substr(0, xmlPrice.contents.length %
xmlPrice.contents.length % 3
3)
0 "" (Zero characters) 300,000

And if we take the number 2500000 (two and a half million).

Value retrieved by
Value of Contents of
xmlPrice.contents.substr(j, Math.abs(j) < xmlPrice.contents.length
j StrFormatPrice
3)
-3 000 false ,000
-6 500 false (1 character remains) ,500,000
xmlPrice.contents.substr(0, xmlPrice.contents.length %
xmlPrice.contents.length % 3
3)
1 2 2,500,000

On lines 220 to 222, we add the contents of the <Price> element to the text frame followed by
a return, followed by <Description>, followed by a return, followed by <Details>.
220 txtDetails.contents += strFormatPrice + "\r";
221 txtDetails.contents += xmlDescription.contents + "\r";
222 txtDetails.contents += xmlDetails.contents;

Now let's add a bit of formatting.


222 txtDetails.contents += xmlDetails.contents;
223 // Format text
224 txtDetails.paragraphs[0].pointSize = "24pt";
225 txtDetails.paragraphs[0].spaceAfter = "18pt";
226 try{txtDetails.paragraphs[0].appliedFont = app.fonts.item("Franklin Gothic Heavy"); }catch(e){}
227 txtDetails.paragraphs[1].pointSize = "14pt";
228 txtDetails.paragraphs[1].spaceAfter = "18pt";
229 try{txtDetails.paragraphs[1].appliedFont = app.fonts.item("Franklin Gothic Heavy"); }catch(e){}
230 for (var j = 2; j < txtDetails.paragraphs.length; j ++){
231 txtDetails.paragraphs[j].pointSize ="14pt";
232 txtDetails.paragraphs[j].spaceAfter = "6pt";
233 }
234 }
235
236 app.scriptPreferences.enableRedraw = true;
237 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;

432
238 }
Having set the format for the first and second paragraphs, since the <Details> element contains
an unknown number of paragraphs, we loop from paragraph three to the last paragraph
applying the appropriate formats.
Now we need to import a photo of each property and wrap the text
around it.
232 txtDetails.paragraphs[j].spaceAfter = "6pt";
233 }
234 // Add image
235 var fleHouse = File(app.activeScript.parent + "/images/" + xmlRef.contents + ".jpg");
236 pgCurrent.place(fleHouse);
237 pgCurrent.allGraphics[0].geometricBounds = [240, 36, 540, 436];
238 pgCurrent.allGraphics[0].fit(FitOptions.frameToContent);
239 with(pgCurrent.allGraphics[0].parent.textWrapPreferences){
240 textWrapMode = TextWrapModes.boundingBoxTextWrap;
241 textWrapOffset = [12, 0, 0, 30];
242 }
243 }
244
245 app.scriptPreferences.enableRedraw = true;
246 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
247 }
On lines 239 to 242, we use a with statement to set two properties of the textWrapPreferences
object— textWrapMode and textWrapOffset. Note that, on line 239, the text wrap is applied to
pgCurrent.allGraphics[0].parent—the frame containing the graphic.
Since we will be producing an interactive PDF, the final attribute
we will apply to each page is a transition. Complete the
placeXML() function by inserting the following code as shown
below.
241 textWrapOffset = [12, 0, 0, 30];
242 }
243 // Add page transition
244 pgCurrent.parent.pageTransitionType = PageTransitionTypeOptions.fadeTransition;
245 pgCurrent.parent.pageTransitionDuration = PageTransitionDurationOptions.slow;
246 }
247
248 app.scriptPreferences.enableRedraw = true;
249 app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
250 }

Save your changes.


Note that the pageTransition object is a member of the spread object, not the page object. We

433
therefore apply the transitions to pgCurrent.parent.
Since we have used app.activeScript.parent to retrieve a reference
to the folder containing our script, you will need to test the script
by right-clicking on it in the Scripts panel in InDesign and
choosing Run Script.
Check the inside pages of the resulting document and make sure
that the formatting and text wrap are working as they should.
8. Producing an interactive PDF
PDFs are produced in scripting using the exportFile() method of the document object. The
properties of the pdfExportPreferences object determine the nature of the PDF produced. We
will be producing a PDF which opens in full screen and automatically flips the page every 10
seconds.
Insert the following code inside the blank createPDF() function
you created earlier.
251
252 function createPDF(){
253 var indFile = null;
254 while (indFile == null){
255 indFile = File.saveDialog("Specify name of output document, including file extension.");
256 }
257 var strExt = indFile.fullName.toLowerCase().substr(indFile.fullName.lastIndexOf("."));
258 if(strExt != ".indd"){
259 indFile = (File(indFile.toString() + ".indd"));
260 }
261 g.doc.save(indFile);
262
263 app.pdfExportPreferences.viewPDF = true;
264 app.interactivePDFExportPreferences.openInFullScreen = true;
265 app.interactivePDFExportPreferences.flipPages = true;
266 app.interactivePDFExportPreferences.flipPagesSpeed = 10;
267 var pdfFile = File(indFile.fullName.replace(".indd", ".pdf"));
268 var pdfPreset = app.pdfExportPresets.itemByName("[Smallest File Size]");
269 g.doc.exportFile(ExportFormat.interactivePDF, pdfFile, false, pdfPreset);
270 }

Save your changes.


On lines 254 to 256, we use a while loop to force the user to specify the location at which they
would like to save the InDesign document. Then, on line 257, we extract the file extension
from the file path. If the extension is not ".indd", we add this extension to indFile (line 259);

434
we then save the file (line 261). Finally, we set our pdfExportPreferences.
Since we want to save the PDF file in the same folder as the InDesign file, on line 267, we use
the replace function to populate a variable called pdfFile with the file path specified the user
on line 255—but with “.indd” replaced with “.pdf”. We then use pdfFile as the third argument
of the exportFile() method: the location where the file should be saved (line 269).
Test the script by right-clicking on it in the Scripts panel in
InDesign and choosing Run Script.
Have a look through the data in “properties.xml”, which should
now contain the line <?xml-stylesheet type="text/xsl"
href="properties.xsl"?>
(as specified on page 420), then enter a few random parameters in
the dialog.
After the document has been created, the PDF file should be
produced and should open in full screen view, provided you have
Acrobat or Acrobat Reader installed on your machine.
Check that the results match the criteria you searched for, either in
the document itself or in the Structure pane.

CHAPTER 19. The JavaScript XML Object


The ExtendScript XML object allows you to deal with XML data in a more abstract fashion
than when manipulating InDesign's XML objects. Whereas objects like XMLElement and
XMLAttribue have a physical representation in the Structure pane, ExtendScript's XML object
exists only in memory. It views any valid XML data as a collection of data nodes arranged in a
hierarchical, tree-like structure. It provides properties and functions which allow you to target
specific parts of the structure and retrieve specific types of information.
Creating an XML object
The XML object is always a logical representation of an XML element. To target an entire
document, you create an XML object representing the root element. However, you could also
create an object which targets a particular element further down the structure. The construction
syntax is shown below.

SYNTAX myXMLObject = [new] XML(source)

435
SUMMARY ... myXMLObject = [new] XML(source)
XML object constructor
Creates an XML object in the variable myXMLObject.

The text from which the XML object will be created. May be a string literal or, more typically, a
Source
text file.

Most of the time, you will be reading in XML from a file and creating an XML object which
represents the data it contains. The code for doing this is shown in listing 19-1.
Listing 19-1: Creating an XML object based on the contents of a file
1 var xmlFile = File.openDialog("Select XML file.");
2 if(xmlFile == null){alert("No file chosen!");}
3 else{
4 try{
5 xmlFile.open("r");
6 var strXML = xmlFile.read();
7 xmlFile.close();
8 XML.ignoreWhitespace = true;
9 XML.ignoreComments = true;
10 XML.ignoreProcessingInstructions = true;
11 var xmlTree = new XML(strXML);
12 alert(xmlTree.toString());
13 }
14 catch(e){
15 alert("No XML created.");
16 }
17 }
On line 6, we read the contents of the file selected by the user into a variable called strXML.
On line 11, we use the constructor function to create an XML document out of the data read in
from the file. Try running this script (“01-create-xml.jsx” in the (“chapter 19 ” folder) and
specifying “01-basic.xml ” as the XML file.
Node types
XML documents may contain several different types of node and the nodeKind() function of
the XML object can be used to confirm the type of each node.

myXMLElement.nodeKind()
SYNTAX XML object function
SUMMARY ... Returns the type of XML content represented by myXMLElement.
Possible values are: element, attribute, text, comment or processing-instruction.

Of the five types of node, the most important from an InDesign point of view are element, text
and attribute. Elements are the key containers in XML documents: they may contain other
elements, text and attributes. It's also important to remember that the text inside an element is
treated as a separate node.

436
You will notice that nodeKind() is a function when, in most environments, it would be a
property. This is generally the case with the XML object: all the useful stuff is done with
functions, rather than properties.
Accessing nodes
To get an idea of the syntax used to navigate through an XML structure, we will use the
“properties.xml” file we encountered in the last chapter.

If we run the code shown in listing 19-1, the variable xmlTree will contain the root element
<properties>. Let's look at how we would refer to other parts of the structure.
Accessing specific nodes
To access the <branch_sales> element we would say:
xmlTree.branchSales
This gives us a collection of nodes. To access just the first node, we would use:
xmlTree.branchSales[0]
To refer to the ListID attribute, we would say:
xmlTree.@ListID

Accessing nodes by relationship


Statements like those above are useful when processing a known XML tree. In addition, there
are a number of functions which allow you to target nodes by their relationship to other nodes,
rather than by their specific attributes.
To target the nodes directly below xmlTree in the hierarchy, we would use:
xmlTree.children()
This would return all of the branch_sales elements.
The children() function does not accept any arguments and returns element and other nodes
inside the specified container element—but not their children. In addition, we have the child()
function: this is used to target child nodes with a given element name.

SYNTAX xmlObject.child (element name)


SUMMARY ... XML object function
Returns an array of all child elements of the type specified by the element name parameter.

437
Element name The XML element to be matched, as a string.

The child() function returns an array of elements; thus, if we wanted to count the number of
property nodes within branch_sales, we would say:
xmlTree.child("branch_sales").child("property").length()
(Note that length() is also a function of the XML object, not a property.) In a similar fashion,
the attributes() function retrieves all the attributes within the specified node.
To move in the opposite direction, we have the parent() function, which returns the element
containing the specified node.
There is no function which targets siblings of an element—i.e. elements on the same level.
However, this can be achieved by using parent().children(). Thus, to find the number of
siblings of the first <branch_sales> element, we would say:
xmlTree.child("branch_sales")[0].parent().children().length() -1
There is also a childIndex() function which returns the position of a given element within its
parent node.
To target all nodes within a given container, use the descendants() function.

SYNTAX xmlObject.descendants ([element name])


SUMMARY XML object function
... Returns all descendants of the specified element type (if optional element name parameter is supplied) or
simply all descendants (if it is omitted).

Element name The XML element to be matched, expressed as a string.

The descendants() function takes an optional argument which, if used, causes the function to
return only nodes of the specified type—regardless of their position within the structure. Thus
if we wanted to target all Bedrooms elements within xmlTree, we would say:
xmlTree.descendants("Bedrooms")
If we simply said:
xmlTree.descendants()
we would basically be targeting every node contained directly or indirectly by the XML root.

Try it for yourself!


TUTORIAL: XML browser utility
In the following tutorial, we will practice manipulating the XML object by creating an XML

438
browser script which presents the user with a hierarchical representation of their chosen XML
file, in a treeview control. They can select any node and click a button to add all of the text in
that node to the end of the document, with or without carriage returns. They can also choose to
apply a paragraph style to the text being inserted.

1. The main() function


In the ESTK, choose File > New JavaScript.
Save the new file as “05-xml-object-explorer.jsx” in the
“chapter19” folder.
Enter the following code.
1 #targetengine "session"
2 var g = {};
3 main();
4 function main(){
5 g.xmlFile = File.openDialog ("Please select the XML file");
6 if(g.xmlFile == null){
7 alert("No file selected.");
8 return;
9 }
10 else{
11 var blnXML = readXML();
12 if(blnXML){createDialog();}
13 }
14 }

Save your changes.


Since this script will require a non-modal dialog, on line 1, we set the target engine to a

439
persistent engine, which we call "session".
On line 5, we invite the user to specify the XML file and if she does, on line 11, we call the
function readXML().
If readXML() returns the value true, indicating that the file was successfully converted into an
XML object, we call the function createDialog() which does the rest of the work.
2. Creating the XML object
To create an XML object, we need to read the file specified by the user and then use a new
XML constructor statement to convert it into XML.
Add the following code at the end of your script.
15
16 function readXML(){
17 try{
18 // Read XML
19 g.xmlFile.open("r");
20 var strXML = g.xmlFile.read();
21 g.xmlFile.close();
22 XML.ignoreComments = true;
23 XML.ignoreProcessingInstructions = true;
24 g.xmlTree = new XML(strXML);
25 if(g.xmlTree.descendants().length() == 0){
26 throw new Error("No XML content in file.");
27 }
28 // Create document
29 g.doc = app.documents.add();
30 g.doc.viewPreferences.rulerOrigin = RulerOrigin.pageOrigin;
31 g.mPrefs = g.doc.pages[0].marginPreferences;
32 g.dPrefs = g.doc.documentPreferences;
33 g.txf = g.doc.pages[0].textFrames.add();
34 g.txf.geometricBounds=[g.mPrefs.top, g.mPrefs.left,
35 g.dPrefs.pageHeight-g.mPrefs.bottom,
36 g.dPrefs.pageWidth-g.mPrefs.right ];
37 return true;
38 }
39 catch(e){
40 alert("No XML created. " + e.message);
41 return false;
42 }
43 }

Save your changes.


On lines 19 to 21, inside a try ... catch statement, we open the file specified by the user, read
its content into the variable strXML and then close the file.
On lines 22 to 23, we specify that we do not wish to include comment and processing
instruction nodes in the imported XML.

440
On lines 24 to 27, we use the constructor to attempt to create an XML object from the file just
read; then, we check the number of descendants of the resulting object. If the file did not
contain valid XML, g.xmlTree will have zero descendants; in which case we throw an error
which gets picked up inside catch statement, where an error message is displayed and the
function returns false (lines 40 to 41).
If we do end up with a valid XML object, on lines 29 to 37, we create a new document and
add a text frame to its default page, coincident with the page margins. The function then returns
true to the variable blnXML used in the function call on line 11.
Run the function either from InDesign or the ESTK.
When the openDialog window is displayed, double-click on the
file called “05-textfile.txt” in the “chapter19” folder.
The following error message will be displayed and the script will
terminate.

3. Creating the dialog


Because we will be using a non-modal dialog, the createDialog() function needs to be the last
function in our script. Otherwise, the dialog will disappear as soon as it appears, when line
following g.win.show() is executed. The remaining functionality of the script will therefore be
attached to the callback functions of the dialog buttons.
Add the following function skeleton at the end of your script.
44
45 function createDialog(){
46 // Create window
47 g.win = new Window('palette', 'XML Explorer');
48 g.win.onClose = function(){g = null;}
49 g.win.add('statictext', undefined, 'Tip! You can create paragraph styles without closing this dialog, then click the Refresh
button');
50
51 // Tree view
52
53 // Fonts dropdown
54

441
55 // Buttons
56
57 // Progress bar
58
59 // Show window
60
61 }

Save your changes.


On line 47, we create a non modal dialog by setting the window type to palette; then, on line
48, we create an inline onClose callback function which simply sets the value of the global
variable to null.
Since the dialog will be non modal, the user will be able to create paragraph styles without
having to close it—hence the message inside the statictext control on line 49.
Now let's create the treeview control. Add the following code
below the comment “// Tree view".
51 // Tree view
52 g.win.trv = g.win.add('treeview');
53 g.win.trv.size = [600, 250];
54
55 // Fonts dropdown

Next, we need the dropdownlist which will display the available


paragraph styles. Since this will be arranged in the same row as the
buttons for inserting text from the XML file and closing the dialog,
we need to place all of them inside the same group control. Add the
following below the “// Fonts dropdown" comment.
55 // Fonts dropdown
56 var grp = g.win.add('group');
57 var arrStyles = g.doc.paragraphStyles.everyItem().name;
58 g.win.ddlStyles = grp.add('dropdownlist', undefined, arrStyles);
59 g.win.ddlStyles.selection = 0;
60 var fleRefresh = File(app.activeScript.parent + "/images/refresh.png");
61 g.win.icoRefresh = grp.add('iconbutton', undefined, fleRefresh);
62 g.win.icoRefresh.onClick = function(){
63 var arrStyles = g.doc.paragraphStyles.everyItem().name;
64 g.win.ddlStyles.removeAll();
65 for(var i = 0; i < arrStyles.length; i ++){
66 g.win.ddlStyles.add('item', arrStyles[i]);
67 }
68 g.win.ddlStyles.selection = 0;

442
69 }
70
71 // Buttons

On line 57, we create an array populated with the names of all the paragraph styles in the
document. Since the document has only just been created, this will initially consist of
InDesign's built-in “[No Paragraph Style]” and “[Basic Paragraph]”, plus any default fonts set
up on the user's machine.
On lines 60-61, we create an iconbutton—a control which functions like a button but displays
a graphic instead of text. The path to the image displayed in the button is specified on line 60,
using the folder containing our script as a reference point. (The “images” folder inside the
“chapter19” folder contains an image called “refresh.png”.)
On lines 62 to 69, we specify what happens when the user clicks the refresh button, namely:
line 63, update the array of font names; line 64, clear out the existing items from the dropdown;
lines 65 to 67, add the items in arrStyles to the dropdown; and, line 68, set the selection to the
first item— the neutral “[No Paragraph Style]”.

SYNTAX
SUMMARY myContainer.add(type, [bounds, image file])
... Syntax for adding an iconbutton control to a group of panel

Type The type of control to create—'iconbutton'.


Bounds Optional. Postion and dimensions of object.
Image Optional. A file object pointing to the appropriate file path. Iconbutton images should be saved in
file PNG format.

Next, we need the buttons. Add the code shown below underneath
the “//Buttons” comment.
71 // Buttons
72 g.win.btnAddAsIs = grp.add('button', undefined, 'Add Item Contents');
73 g.win.btnAddAsIs.onClick = addXMLToDoc;
74 g.win.btnAddReturns = grp.add('button', undefined, 'Add With Returns');
75 g.win.btnAddReturns.onClick = addXMLToDoc;
76 g.win.btnClose = grp.add('button', undefined, 'Close');
77 g.win.btnClose.onClick = function(){g.win.close();}
78
79 // Progress bar
The only difference between the “Add Item Contents” and “Add with Returns” buttons is that
the latter will add a Return after each item it adds. It therefore makes sense to assign the same

443
callback function to both—addXMLToDoc().
If the user chooses an XML file with a lot of data, it may take some
time to process all of the nodes; we should therefore display a
progress bar while the processing takes place.
Add the following code below the “// Progress bar” comment.
79 // Progress bar
80 g.win.prg = g.win.add('progressBar');
81 g.win.prg.maxvalue = g.xmlTree.descendants().length();
82 g.win.prg.value = 0;
83 g.win.prg.size = [600, 10];
84
85 // Show window
On line 81, we set the maxValue property of the progress bar control to match the number of
descendants in the XML object g.xmlTree. When applied to the root node of an XML tree, the
descendants() function basically returns every single node in the structure. Each time we
process a node, we will add 1 to the progress bar causing it to, hmm, progress.
Finally, add the following code to show the dialog.
85 // Show window
86 g.win.show();
87 updateTreeView();
88 }
The updateTreeView() function call comes after we have made the window visible. One of
the steps in this function will be to update the progress bar. The user will also be able to see
the first few nodes of the treeview control being constructed, so they will know that progress
is being made.
Save your changes and then test your script from the InDesign
Scripts panel, as opposed to the ESTK (since we used
app.activeScript.parent in specifying the location of the
iconbutton image on line 60, on page 447).
When the openDialog() window appears, double-click on the file
called “properties.xml” in the “chapter19” folder.
After the nodes have been processed and the progress bar
disappears, create a new paragraph style. Don't bother naming it or
assigning it any attributes.

444
Click on the refresh icon button and then have a look at the
dropdown. The new paragraph style you have just created should
appear in the list.

Click on the Close button to dismiss the dialog.


4. The updateTreeView() function
Because of the hierarchical nature of XML documents, in order to navigate their structure, we
will need to repeatedly call a recursive function, like we did in the object explorer script in
chapter 11, when discussing page items.
Insert the updateTreeView() at the end of your script.
89
90 function updateTreeView(){
91 for (var h = 0; h < g.xmlTree.children().length(); h++){
92 processNode(g.xmlTree.children()[h], g.win.trv);
93 }
94 g.win.prg.visible = false;
95 }
On lines 91 to 93, we loop through the child nodes of the XML object inside g.xmlTree. With
each iteration, we call the recursive function processNode(), passing it the two arguments it
will need: the nodes to be processed and the node within the tree view to which we will add
an item corresponding to each XML node. In this case, because we are processing the top level
children only, the target is the tree view control itself, rather than one of its node. The
descendants of these children will be processed by recursive function calls from within
processNode().
Add the processNode() function at the end of your script.
96
97 function processNode(xmlNode, treeViewNode){
98 if(xmlNode.children().length() > 0){

445
99 // Process container nodes
100 treeViewNode = treeViewNode.add('node', xmlNode.name());
101 treeViewNode.xml = xmlNode;
102 for(var i = 0; i < xmlNode.children().length(); i ++){
103 processNode(xmlNode.children()[i], treeViewNode)
104 }
105 }
106 else{
107 // Process text nodes
108 var treeViewItem = treeViewNode.add('item', xmlNode.toString());
109 treeViewItem.xml = xmlNode;
110 }
111 // Update progress bar
112 g.win.prg.value ++;
113 }

Save your changes.


On line 97, in the function definition, we specify the two parameters that the function needs:
xmlNode (the node being processed) and treeViewNode (the tree view node to which the
XML node being processed will be attached).
We then carry out a test to see whether we are dealing with an element that contains other
elements or with a text node. If the node being processed has child nodes, we add a node
object to the tree view (line 100); whereas, if it does not, we add an item object (line 108).
After adding the new node or item, we then add a custom property to it called xml and assign,
as its value, the XML node it represents (lines 101 and 109). This means that, when the user
selects an item in the tree view and clicks one of the Add buttons, we will be able to process
the node stored in the selected tree view item.
On line 112, we add 1 to the progress bar to keep it in tandem with the nodes being processed.
On lines 102 to 104, we loop through the children of the node being processed, making a
recursive function call to processNode() for each of them.
5. The addXMLToDoc() function
When the user selects a node or item in the tree view control and clicks one of the Add buttons,
the addXMLTodoc() callback function needs to do two things. Firstly, it needs to place all of
the text in the selected node and its descendants into the document; and, secondly, it needs to
flow the text, creating new pages and text frames as necessary.
Begin by adding the following skeleton function to the end of your
script.
114
115 function addXMLToDoc(){
116 var xmlSel = g.win.trv.selection.xml.descendants();
117 g.win.prg.maxvalue = xmlSel.length();

446
118 g.win.prg.value = 0;
119 g.win.prg.visible = true;
120
121 // Insert text
122
123 // Flow story
124
125 app.select(g.txf);
126 }
If you remember, when we constructed the treeview, we added a custom property called xml
to each of the nodes and items we created and populated it with a reference to the XML node it
represents. On line 116, we read the value of the xml property of the element selected by the
user into the variable xmlSel. This will be the node that we will examine when we come to
insert text into the document.
On lines 117 to 119, we bring our progress bar back to life, setting its maxvalue property to
match the number of descendants inside xmlSel, resetting its value to zero, then making it
visible.
Position the cursor on line 122, below the comment “// Insert text”
and add the following code.
121 // Insert text
122 if(g.win.ddlStyles.selection.text != "[No Paragraph Style]"){
123 g.txf.parentStory.insertionPoints[-1].appliedParagraphStyle = g.doc.paragraphStyles.item(g.win.ddlStyles.selection.text);
124 }
125 if(g.win.trv.selection.type == "item"){
126 g.txf.parentStory.insertionPoints[-1].contents = g.win.trv.selection.xml.toString();
127 if(this == g.win.btnAddReturns){
128 g.txf.parentStory.insertionPoints[-1].contents = "\r";
129 }
130 g.win.prg.value ++;
131 }
132 else{
133 for(var i = 0; i < xmlSel.length(); i ++){
134 g.win.prg.value ++;
135 if(xmlSel[i].nodeKind() == "text"){
136 g.txf.parentStory.insertionPoints[-1].contents = xmlSel[i].toString();
137 if(this == g.win.btnAddReturns){
138 g.txf.parentStory.insertionPoints[-1].contents = "\r";
139 }
140 }
141 }
142 g.win.prg.visible = false;
143 }
144
145 // Flow story

Before placing the text, we start by setting the paragraph style of the last insertion point of the
story to match the style selected by the user, provided it is not the neutral “[No Paragraph

447
Style]” (lines 122 to 124).
On lines 125 to 131, if the selected element on the tree view is of the item type—indicating
that its xml property contains a reference to a text node—we add the node referenced by the
treeview item at the insertion point, followed by a Return if the value of g.win.btnAddReturns
is true.
On lines 132 to 143, we deal with the other type of tree view element, the node type, which
will always hold a reference to the hierarchical group of XML elements returned by the
descendants() function used to populate xmlSel, back on line 116. Here, we loop through the
descendants and, whenever the nodeKind() function returns “text”, we add the item to the end
of the story, followed by the optional Return character.
Each time we process one of the items inside xmlSel, we add 1 to the value of the progress bar
(lines 130 and 134). On line 142, after all items have been processed, we hide the control.
Position the cursor on line 146, below the comment “// Flow story”
and add the following code.
145 // Flow story
146 while (g.txf.overflows)
147 {
148 var txf_previous = g.doc.pages[g.doc.pages.length -1].textFrames.item(0);
149 var pg_next = g.doc.pages.add();
150 g.txf = pg_next.textFrames.add();
151 g.txf.geometricBounds=[g.mPrefs.top, g.mPrefs.left,
152 g.dPrefs.pageHeight-g.mPrefs.bottom,
153 g.dPrefs.pageWidth-g.mPrefs.right ];
154 txf_previous.nextTextFrame = g.txf;
155 }
156
157 app.select(g.txf);
158 }

Here we use a while loop to continually create a new page, add a text frame to it and link it to
the text frame on the previous page; until the statement g.txf.overflows ceases to be true.
Save your changes and then test your script from the InDesign
Scripts panel.
When the openDialog() window appears, double-click on the file
called “properties.xml” in the “chapter19” folder.
Create a new paragraph style and name it “Heading1”.
Click on the refresh icon button to update the dropdown.
Choose Heading1 from the styles dropdown menu.
448
Expand the first branch_sales node.
Expand the first property node.
Expand the Branch node then select the text item “Burelmerth”.
Click the Add with Returns button to insert the text and apply the
Heading1 style.

Close the expanded nodes.


Highlight the first branch_sales node.
Choose [Basic Paragraph] from the dropdown.
Click Add with Returns.
All items in the first branch_sales node should be placed and pages and text frames
automatically created, as required.

449
Try the browser utility on some of your own files. In theory, you
should be able to use it with any XML file.

CHAPTER 20. Exporting XML


Given the increasing choice of media via which documents can be distributed, as long as a
document exists only in InDesign, it is of limited use to its owners. Exporting a document in
PDF format increases the number of things you can do with it. However, PDF is not meant to
be a flexible format from which you can repurpose documents. Exporting a document in XML
format widens its scope.
The nature and structure of an InDesign document determines how easily it can be exported as
XML. The ideal structure for XML export is as follows:
• All text is formatted using paragraph and character styles
• All text is contained within a series of linked text frames rather than in lots of separate
ones
• All images are anchored within the text.
If these features do not exist within a document, it may be worth spending some time massaging
it a little, either manually or via scripts.
Once a document has been licked into shape, the key steps to exporting are as follows:
• Create a tag to match each paragraph and character style
• Use the Match Styles to Tags feature so that text in a particular style will be exported
within the appropriate tags
• Specify the tag which will be used as the root tag—or stay with InDesign's default tag

450
named “Root”
• Specify the tag to be used as the container tag for the various stories within the
document—or stay with InDesign's default tag named “Story”
• Specify the tag to be used for images—or stay with InDesign's default tag named
“Image”
• Specify the tags to be used for tables and table cells—or stay with InDesign's default
tags named “Table” and “Cells”, respectively
• Restructure the XML as necessary
• Export the XML, specifying the appropriate export options.
Documents which are hard to export as XML
The documents which are most difficult to export as XML—using the workflow outlined above
—are those which consist of many unlinked text frames and where all images are independent
of the text. For example, figure 20-1 shows the product catalogue of a fictitious company. Each
product page contains details of several products: product name, price, description and photo.
However, each of these elements is in a separate frame. Clearly, there is no prospect of
converting this layout to a story-based, XML-friendly structure. So, here, we would rely on
scripting to perform the XML export.

Figure 20-1: a typical product catalogue layout

Instead of relying on the mapping of tags to styles, we need to focus on recognizing frames and
their contents. For automation to be possible, there has to be some degree of consistency in the
manner in which the document is built. Fortunately, this is usually the case—if for no other
reason than the fact that everyone knows how to copy and paste!
In many cases, your script can recognize elements by the style of the frame that contains them.
At other times, you may have to rely on position, proximity to other elements or the content of
the frame... Let's examine our catalogue example in more detail.

451
Try it for yourself!
TUTORIAL: Exporting XML from a catalogue
Open the file called “catalogue.indd” in the “chapter20” folder and have a quick flick through
the pages.
After the contents and introduction, the product catalogue is divided into sections: “F Series”,
“G Series”, and so forth.
The section relating to each series starts with a header and some descriptive text. The products
in each series are further divided into ranges, with each range starting with a header and
introduction.
The listing for each product consists of two groups of frames. The first group relates to the
product details and consists of three separate frames, containing: product name and price,
some header text, followed by product description.

The second group relates to the product image and consists of two frames: caption and product
photo.

Let's say that, for the purposes of this exercise, we are interested in exporting just the product
information. The structure of the XML document we are looking to create is illustrated in
figure 20-2.

452
• The root element will be called <catalogue>.
• Inside this, we will create a number of <series> elements.
• Inside each <series> element, we will create a <title>, <description> and <ranges>.
• Inside <ranges>, we have a number of <range> elements
• Inside each <range> element, we have <title>, <description> and <products>
• Inside <products>, we have the individual <product> elements
• Inside each <product> element, we have <name>, <price>, <summary>, <details> and
<image>
• Finally, each <image> element has an href attribute which specifies the location of the
linked file.

Figure 20-2: the XML structure to be exported from our fictitious product catalogue

Let's look at creating a script which will produce the necessary XML structure.
In the ESTK, create a new document and save it in the chapter20 folder under the name “01-
export-xml.jsx”.
1. Creating the main function
We will divide the operation into three sections:
• Deleting any existing XML structure and renaming the root element
• Building the XML structure
• Exporting the XML.

453
We will create a function for each of these sections and, as usual, call the three functions from
our main function.
Add the following code to your script
.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 createRoot();
7 buildStructure();
8 exportXML();
9}

2. Defining the root element


Now let's write the createRoot() function which will delete any existing XML and rename the
root element “catalogue”.
10
11 function createRoot(){
12 if(app.documents.length > 0){g.doc = app.activeDocument;}else{return;}
13 // Delete any existing XML structure below the root
14 var highestIndex = (g.doc.xmlElements[0].xmlElements.length) -1;
15 for (var i = highestIndex; i >= 0; i --){
16 currentElement = g.doc.xmlElements[0].xmlElements[i];
17 currentElement.remove();
18 }
19 // Rename root element
20 g.doc.xmlElements[0].markupTag = "catalogue";
21 }
The statement doc.xmlElements[0] (used on lines 14 and 20) refers to the undeletable root tag.
Our for loop goes through all of the XML elements inside it and removes them. As always
when deleting elements, we loop in reverse—starting with the highest index and working back
to the lowest. If we went from lowest to highest, we would get error after passing the halfway
mark. (As a matter of interest, you could replace lines 14 to 18 with the single line:
g.doc.xmlElements[0].remove();
Since the root element cannot be deleted, attempting to delete it simply causes everything
inside it to be deleted. However, since it makes your code look as though you are attempting to
delete something your are not, it may be clearer to delete the children.
On line 20, we rename the root tag.
3. Looping through the document pages
We now need to loop through all of the document pages and, for each page—since we are
interested in both text frames and images—we need to loop through the pageItems and analyze
their contents. When we find a page item which is a text frame, we need to loop through each

454
of the paragraphs it contains and test whether any of them have content that needs to be
exported inside our XML structure.
Because of the labyrinthine nature of this process, we will make use of nested functions. This
makes it possible to create the logic of the script first and fill in the details later. Since there
will be some information which is needed in several different nested functions, we will define
a namespace object variable called h, inside the
buildStructure() function. Any information required in more than one of its nested function
will be added as a property of h. In addition, we will prefix all of the nested function names
with “h_” to make them easy to recognize.
Add the buildStructure() function to the end of your script.
22
23 function buildStructure(){
24 var h = {};
25 // Loop through pages
26 var numPages = g.doc.pages.length;
27 for (var i = 0; i < numPages; i++){
28 var currentPage = g.doc.pages[i];
29 var arrPageItems = currentPage.allPageItems;
30 h_sortByPosition();
31 // Loop through page items
32 var numPageItems = arrPageItems.length;
33 for (var j =0; j < numPageItems; j ++){
34 var currentPageItem = arrPageItems[j];
35 if(currentPageItem.constructor.name == "TextFrame"){
36 // Loop through paragraphs
37 var numParas = currentPageItem.paragraphs.length;
38 for (var k = 0; k < numParas; k ++){
39 var currentPara = currentPageItem.paragraphs[k];
40 var currentStyle = currentPara.appliedParagraphStyle.name;
41 h_createTextElements();
42 } // end for k
43 }
44 else if(currentPageItem.constructor.name == "Image" && h.currentProductName != undefined){
45 h_createImageElement();
46 }
47 } // end for j
48 } // end for i
49
50 }
On line 24, we define the object variable h, which will have a similar namespacing role to the
g variable we have been using throughout these tutorials to hold global information. The
difference is that data added to the h variable will only be available to functions nested inside
the buildStructure() function, whereas information in g is available from inside any function
in the script.
The function features three levels of looping: for i loops through the pages of the document
(line 27); for j loops through the page items on each page (line 33); and, whenever a textFrame
page item is encountered, for k loops through the paragraphs inside it (line 38). Comments

455
have also been inserted at the end of lines 42, 47 and 48 to emphasize where each loop ends.
Note that, on line 29, when we read the pageItems into arrPageItems, we use the allPageItems
collection—as opposed to simply pageItems. The pageItems collection contains fairly
anonymous pageItem objects; whereas allPageItems returns a motley crew of elements of
different types. The two types of page item in which we have an interest are textFrames and
images.
Before looping through the page items on each page, it is important to have them in the right
order. Because the information is all in separate frames, we need to rely on the position of the
text frames to determine the export order. Therefore, on line 30, we make a function call to
h_sortByPosition(), which we will write shortly, and which will sort the array of page items
captured inside arrPageItems on line 29 according to their position from the top of the page.
On line 35, we test whether the page item currently being processed is a textFrame. Since we
are dealing with the allPageItems collection, constructor.name will return textFrame—as
opposed to simply pageItem, which would be the case with the pageItems collection.
If the item is a textFrame, on lines 38 to 42, we loop through the paragraphs. On line 37, we
capture the number of paragraphs into a variable called numParas; then, on line 38, we use
this variable as the upper limit in our for loop. We then grab the paragraph style in a variable
called currentStyle and then, on line 41, make a call to h_createTextElements()—a function
which will determine which XML element (if any) the paragraph should be exported inside,
based on such criteria as its content and paragraph style.
On line 44, we test whether the page item is an image and, if it is, on line 45, we make our
third nested function call, this time to h_createImageElement(), which will do exactly what it
says on the tin.
Let's write the first of the three nested functions: h_sortByPosition().
4. Sorting page items by distance from top of page
Before we loop through the page items, we need to consider the order in which they will be
encountered. In general, InDesign orders pageItems on the LIFO (last-in-first-out) principle. If
you add three text boxes to a page, the last one you create will end up with an index of zero,
the second with an index of 1 and the first with an index of 2.
We want to export the product details in the order in which they appear on the page—in other
words, we will want to order page items based on their distance from the top of the page. If
we place the items in an array, we can put them in the right order by sorting the array using a
custom function—a useful option offered by the sort() method of JavaScript's Array object.
The syntax is as follows:

SYNTAX myArray.sort (function)


SUMMARY Method of Array Object
... Sorts items in an array according to the values returned by the function.

456
A custom function defining which of each pair of elements passed to the function should precede
Function
the other.

The custom function accepts two arguments—the two array elements being compared.
The statements inside the function create a return value which should fall into one of three
categories:
• Less than zero—in which case, the first element will precede the second
• Zero—in which case, the items will remain in their current relative positions
• More than zero—in which case the second element will come before the first in the final
sort order.
The code that we need to sort our page items into the required order is very simple.
Position the cursor on line 49—just above the closing brace of the
buildStructure() function. Press Return to leave a blank line then
add the following code.
49
50 function h_sortByPosition(){
51 arrPageItems.sort(byPosition);
52 function byPosition(item1, item2){
53 return item1.geometricBounds[0] - item2.geometricBounds[0];
54 }
55 }
56
57 }
On line 51, we sort the arrPageItems array using a function called byPosition(). This function
simply returns the difference between the Y coordinates of the two objects —line 53:
return item1.geometricBounds[0] - item2.geometricBounds[0];
The geometricBounds property specifies the position of a pageItem as an array of four
coordinates: [y1, x1, y2, x2]. Hence geometricBounds[0] will give us the distance of each item
from the top of the page. If the byPosition() function returns a negative number, this means that
item1 has a lower Y value than item2 and is therefore closer to the top of the page. Item1 will
therefore be placed before item 2 in the sort order. If the function returns a positive value, this
means that item1 has a greater Y value than item2, is further down the page and so will appear
after item2 in the sorted array.
5. Looping through all paragraphs in the text frame
Once we have established that a given pageItem is a textFrame, we can loop through all the
paragraphs it contains and ascertain where each one belongs within our XML output structure.
This is done in the third level of looping (using counter k) inside our if statement.
Let's now write the h_createTextElements() function which will

457
carry out the detection. Position the cursor on line 56—just above
the closing brace of the buildStructure() function. Press Return to
create a new line then add the following code.
56
57 function h_createTextElements(){
58 if(currentStyle == "Heading1" && currentPara.contents.indexOf("Series") > -1){
59 h_seriesElement();
60 }
61 else if(currentStyle == "Heading2" && currentPara.contents.indexOf("Range") > -1){
62 h_rangeElement();
63 }
64 else if(currentPara.fillColor.name == "Paper" && currentPara.contents.indexOf("£") > -1){
65 h_productNamePrice();
66 }
67 else if(currentPara.fillColor == "Black" && currentPageItem.fillTint < 50){
68 h_summaryElement();
69 }
70 else if(currentStyle == "BodyText"){
71 h_detailsElement();
72 }
73 }
74
75 }
Since the catalogue will necessarily start with text frames containing information about a
series and then a range, we will need to begin our processing by testing for these items and
then work our way down the proposed XML hierarchy. The function uses a series of
conditional statements to identify each type of data and call the function which will process
that type of content.
We have already created our root element (<catalogue>). There is nothing in the document
which corresponds to the next element we need (<series>). However, on line 58, we test
whether the paragraph is a signal for the creation of series element. The trigger for us to create
the <series> element is the occurrence of a heading containing the name of a series. The
heading itself will be placed in the <title> element; but, whenever we encounter the title, we
first need to add a <series> element to the <catalogue> root and then add the <title> to the
new <series> element. So spotting the <title> is the key to detecting the start of each series.

458
Basically, we are looking inside currentPageItem and then inside currentPara for a
paragraph style called “Heading1” and the word “Series”.
The fact that we have a style name to search for makes life a little easier; but even if styles
were not in use, as long as the document was formatted in a consistent manner, we could still
detect the nature of each paragraph quite easily. Instead of using the code we have used on line
58, we would have to perform a series of tests—for example:
if(currentPara.appliedFont.name.indexOf("Rockwell") > - 1){
if(currentPara.pointSize == 24){
if(currentPara.fillColor.name == "CatalogueBlue"){
...
On line 61, detecting the range title and creating the necessary elements is very similar to what
we have just done with the <series> element. The range title uses a style called "Heading2"
and contains the word "Range".
61 else if(currentStyle == "Heading2" && currentPara.contents.indexOf("Range") > -1){
The <ranges> element will be created each time we encounter a new series—as a child of the
<series> element. When we encounter a range title, we need to add a <range> element to the
current <ranges> element then add <title>, <description> and <products> elements to
<range>—as shown below.

459
On line 64, we test the paragraph to see if it contains product information:
64 else if(currentPara.fillColor.name == "Paper" && currentPara.contents.indexOf("£") > -1){
There are four components to the product data—each in a separate pageItem object:

• The product and price are in one textFrame—white text on a blue background.
• The product summary is in the second textFrame—with a light blue background.
• The product details are in a third textFrame—plain and using the bodyText style.
• The product image is an independent graphic.
(Each product image has a caption bearing the product name: since we already have an
element containing the product name, we will ignore this.)
The test on line 64 simply checks that the fillColor of the text is “Paper” and the“£” sign is
present which implies that it is the frame which contains the product reference and price.
Whenever we encounter a product/price textFrame as we loop through the pageItems on each
page of the document, we need to add a <product> element to the current <products> element
and add <name> and <price> elements to <product>, since these are the two pieces of data
this type of page item contains.

On line 67, we check for the paragraph that contains the summary data by testing for black text
on a background which has a 50% tint.
67 else if(currentPara.fillColor == "Black" && currentPageItem.fillTint < 50){
Whenever this statement is true, we know that we have the product summary. The <product>
element will already have been created, so we just need to add the <summary> element to it—
as shown below.

460
Our final check is for the product details and, here, we just check that the paragraph style is
“BodyText”.
70 else if(currentStyle == "BodyText"){
This test, in itself, is insufficient since the “BodyText” style is used for paragraphs other than
the product details; so we will carry out a further test on the width of the text frame when we
define the h_detailsElement() function, which is called on line 71.
Whenever the else if statement on line 70 is true, we just need to add a <details> element to
the current <product> element—as shown below.

The h_createTextElements() function has described the logical tests which will determine
how text is processed, now we need to actuallly specify the details of how the XML elements
will be created, by writing the four functions called from inside h_createTextElements().
Let's begin with h_seriesElement().
Creating the <series> element and its children
Once we identify that currentPara is a series title, we can create the <series> element and
add the <title>, <description> and <ranges> elements to it.

Position the cursor on line 74—above the closing brace of the


buildStructure() function. Leave a blank line then add the
461
following code.
74
75 function h_seriesElement(){
76 // Create series element and add title, description and ranges elements
77 var currentSeries = g.doc.xmlElements[0].xmlElements.add("series");
78 var currentSeriesTitle = currentSeries.xmlElements.add("title");
79 var currentSeriesDesc = currentSeries.xmlElements.add("description");
80 h.currentRanges = currentSeries.xmlElements.add("ranges");
81 // Add content to the range title element
82 currentSeriesTitle.contents = currentPara.contents;
83 // Detect how many of following paragraphs are range description
84 var descParas = k + 1;
85 currentPara = currentPageItem.paragraphs[descParas];
86 while (currentPara.appliedParagraphStyle.name == "BodyText"){
87 currentSeriesDesc.contents += currentPara.contents;
88 descParas ++;
89 currentPara = currentPageItem.paragraphs[descParas];
90 }
91 }
92
93 }
The code for adding an element as a child of an existing element is straightforward—for
example, line 77:
77 var currentSeries = g.doc.xmlElements[0].xmlElements.add("series");
By placing each newly created element inside a variable in this way, we can easily refer to it
later when we need to add child elements to it (lines 78 to 80).
The actual text inside the paragraph currently held in our currentPara variable belongs inside
our <title> element. On line 82, we add text to the element by setting its contents property to
the contents property of currentPara.
The description elements needs to contain the paragraph or paragraphs following the Series
title and preceding the range title.

The paragraph style used for the description is “BodyText”; so, on lines 86 to 90, we create a
while loop which adds each of the paragraphs following the series title to the <description>

462
element, providing its style is “BodyText”.
On line 84, we set a variable called descParas to the current value of our for loop counter
variable k, plus one. This is because we want to access the paragraph following the one
currently being processed inside the for loop. Thus, on line 85, we use descParas as an index
to enable our currentPara variable to point to the paragraph following our title.
Inside the while loop, we add the current paragraph to our <description> element via the
variable currentSeriesDesc (line 87). We then add one to descParas and reset currentPara,
again using descPara as the index. The while loop continues to run until we find a paragraph
whose style is not “BodyText”—i.e., when we hit the first range title in that series.
Creating the range element and its children
The h_rangeElement() function needs to add a <range> element to the current <ranges>
element then add <title>, <description> and <products> elements to it.

Position the cursor on line 92—above the closing brace of the


buildStructure() function. Leave a blank line then add the
following code.
92
93 function h_rangeElement(){
94 // Create range element and add title, description and products elements
95 var currentRange = h.currentRanges.xmlElements.add("range");
96 var currentRangeTitle = currentRange.xmlElements.add("title");
97 var currentRangeDesc = currentRange.xmlElements.add("description");
98 h.currentProducts = currentRange.xmlElements.add("products");
99 // Add content to the range title element
100 currentRangeTitle.contents = currentPara.contents;
101 // All BodyText paragraphs following range title (up to end of text frame) are range description
102 for (var kFinish = k + 1; kFinish < numParas; kFinish ++){
103 currentPara = currentPageItem.paragraphs[kFinish];
104 currentRangeDesc.contents = currentPara.contents;

463
105 }
106 }
107
108 }
On line 95, we create the range element as a child of the current <ranges> element—stored as
a property of the h variable called h.currentRanges. We then create a <title>, <description>
and <products> element inside <range> (lines 96 to 98).
The content of the <title> element needs to be the text inside the paragraph we have just tested
for—the range title; so, on line 100, we simply set the contents of the <title> element stored
inside our currentRangeTitle variable to the contents of the current paragraph.
The paragraph(s) immediately following the title constitute the range description and need to
become the content of the <description> element we have just created. These paragraphs will
always be the last items in the current textFrame—as shown below.

We therefore "fast forward" the current loop (for (var k = 0; k < numParas; k ++)) and add
all remaining paragraphs in the text frame as the content of the <description> element.
On lines 102 to 105, we construct another for loop using the counter variable kFinish which
starts at k + 1—enabling us to start with the paragraph following the range title—and continues
until we run out of paragraphs.
Inside the kFinish loop, we reset the currentPara variable using kFinish as the index and then
set the contents of the <description> element (via the currentRangeDesc variable) to the
contents of currentPara (lines 103 and 104).
6. Testing the code we have created so far
Let's test the code we have created so far. It should get as far as creating the XML output tree
with the exception of the product details. Each branch in our output structure will end in an
empty <products> element.
Before we can test our code, we need to do comment out all of the function calls to functions
that we haven't yet written. Insert two slashes at the start of the following lines to temporarily
turn them into comments or highlight each line and choose Edit > Comment or Uncomment
Selection.
• exportXML() (line 8)
• h_productNamePrice() (line 65)
• h_summaryElement() (line 68)
• h_detailsElement() (line 71)

464
Here is the code we have writtten so far.
1 var g = {};
2 main();
3 g = null;
4
5 function main(){
6 createRoot();
7 buildStructure();
8 exportXML();
9}
10
11 function createRoot(){
12 if(app.documents.length > 0){g.doc = app.activeDocument;}else{return;}
13 // Delete any existing XML structure below the root
14 var highestIndex = (g.doc.xmlElements[0].xmlElements.length) -1;
15 for (var i = highestIndex; i >= 0; i --){
16 currentElement = g.doc.xmlElements[0].xmlElements[i];
17 currentElement.remove();
18 }
19 // Rename root element
20 g.doc.xmlElements[0].markupTag = "catalogue";
21 }
22
23 function buildStructure(){
24 var h = {};
25 // Loop through pages
26 var numPages = g.doc.pages.length;
27 for (var i = 0; i < numPages; i++){
28 var currentPage = g.doc.pages[i];
29 var arrPageItems = currentPage.allPageItems;
30 h_sortByPosition();
31 // Loop through page items
32 var numPageItems = arrPageItems.length;
33 for (var j =0; j < numPageItems; j ++){
34 var currentPageItem = arrPageItems[j];
35 if(currentPageItem.constructor.name == "TextFrame"){
36 // Loop through paragraphs
37 var numParas = currentPageItem.paragraphs.length;
38 for (var k = 0; k < numParas; k ++){

465
39 var currentPara = currentPageItem.paragraphs[k];
40 var currentStyle = currentPara.appliedParagraphStyle.name;
41 h_createTextElements();
42 } // end for k
43 }
44 else if(currentPageItem.constructor.name == "Image" && h.currentProductName != undefined){
45 h_createImageElement();
46 }
47 } // end for j
48 } // end for i
49
50 function h_sortByPosition(){
51 arrPageItems.sort(byPosition);
52 function byPosition(x,y){
53 return x.geometricBounds[0] - y.geometricBounds[0];
54 }
55 }
56
57 function h_createTextElements(){
58 if(currentStyle == "Heading1" && currentPara.contents.indexOf("Series") > -1){
59 h_seriesElement();
60 }
61 else if(currentStyle == "Heading2" && currentPara.contents.indexOf("Range") > -1){
62 h_rangeElement();
63 }
64 else if(currentPara.fillColor.name == "Paper" && currentPara.contents.indexOf("£") > -1){
65 //h_productNamePrice();
66 }
67 else if(currentPara.fillColor == "Black" && currentPageItem.fillTint < 50){
68 //h_summaryElement();
69 }
70 else if(currentStyle == "BodyText"){
71 //h_detailsElement();
72 }
73 }
74
75 function h_seriesElement(){
76 // Create series element and add title, description and ranges elements
77 var currentSeries = g.doc.xmlElements[0].xmlElements.add("series");
78 var currentSeriesTitle = currentSeries.xmlElements.add("title");
79 var currentSeriesDesc = currentSeries.xmlElements.add("description");
80 h.currentRanges = currentSeries.xmlElements.add("ranges");
81 // Add content to the range title element
82 currentSeriesTitle.contents = currentPara.contents;
83 // Detect how many of following paragraphs are range description
84 var descParas = k + 1;
85 currentPara = currentPageItem.paragraphs[descParas];
86 while (currentPara.appliedParagraphStyle.name == "BodyText"){
87 currentSeriesDesc.contents += currentPara.contents;
88 descParas ++;
89 currentPara = currentPageItem.paragraphs[descParas];
90 }
91 }
92

466
93 function h_rangeElement(){
94 // Create range element and add title, description and products elements
95 var currentRange = h.currentRanges.xmlElements.add("range");
96 var currentRangeTitle = currentRange.xmlElements.add("title");
97 var currentRangeDesc = currentRange.xmlElements.add("description");
98 h.currentProducts = currentRange.xmlElements.add("products");
99 // Add content to the range title element
100 currentRangeTitle.contents = currentPara.contents;
101 // All BodyText paragraphs following range title (up to end of text frame) are range description
102 for (var kFinish = k + 1; kFinish < numParas; kFinish ++){
103 currentPara = currentPageItem.paragraphs[kFinish];
104 currentRangeDesc.contents = currentPara.contents;
105 }
106 }
107
108 }
Ensure that catalogue.indd is the active document and that the Structure pane is open (View >
Structure > Show Structure). Run the script from the Scripts panel in InDesign or switch
over to the ESTK and run it from there, setting InDesign as the target application.
As the script runs, you should see the XML elements appearing in the Structure pane. When the
script finishes, the Structure should contain the following elements.

Uncomment lines 8, 65, 68 and 71.


Creating the <product>, <name> and <price> elements
Now let's add the h_productNamePrice() function which will create the <product>, <name>
and <price> elements.

467
Insert the following code on line 107, above the closing brace of
the buildStructure() function.
107
108 function h_productNamePrice(){
109 // Create product element and add title, description and products elements
110 h.currentProduct = h.currentProducts.xmlElements.add("product");
111 h.currentProductName = h.currentProduct.xmlElements.add("name");
112 var currentProductPrice = h.currentProduct.xmlElements.add("price");
113 // Split current paragraph on tab character to obtain array containing product name and price
114 var arrProductPrice = currentPara.contents.split("\t");
115 h.currentProductName.contents = arrProductPrice[0];
116 arrProductPrice[1] = arrProductPrice[1].replace("£", ""); // Lose currency symbol
117 currentProductPrice.contents = arrProductPrice[1];
118 }
119
120 }
On line 110, we add the <product> element to the most recently created <products> element;
then, on lines 111 and 112, respectively, we add the <name> and <price> elements to
<product>.
The product name is separated from the price by a Tab character. Therefore, on line 114, we
are able to use the split() method of the JavaScript String object to create an array called
arrProductPrice, whose first item is the product name and whose second item is the price.
On line 115, we set the contents of the newly created <name> element to the first item of
arrProductPrice.
Since we do not need the currency symbol in the price field of our XML data, on line 116, we
suppress the pound sign using the replace() method of the JavaScript String object.
Finally, on line 117, we set the contents of the <price> element to the second item of
arrProductPrice.
7. Creating the product <summary> element
Next up, we need the h_summaryElement() function which simply needs to add the

468
<summary> element to <product> and put the necessary data inside it.

Insert the following code on line 119, above the closing brace of
the buildStructure() function.
119
120 function h_summaryElement(){
121 // Add product summary to current product element - set content to current paragraph
122 var currentProductSummary = h.currentProduct.xmlElements.add("summary");
123 currentProductSummary.contents = currentPara.contents;
124 }
125
126 }
On line 122, we add a <summary> element to the most recently created <product> element.
Then, on line 123, we set the contents of the <summary> element to the contents of the current
paragraph.
8. Creating the product <details> element
The text frame containing the product details uses the BodyText style. However, since the same
style is used elsewhere, we need a second method of identifying it. One unique attribute is the
with of the text frame. The other text frames in which BodyText is used are full width,
extending from left to right margin; whereas the product details text frames have a width of 100
mm.
If we cannot guarantee that the units of measurement in use will be millimetres, we should
change them to millimetres before testing the width of the current pageItem, then change them
back to whatever the previous setting happened to be.
Insert the following code on line 125, above the closing brace of
the buildStructure() function.
125
126 function h_detailsElement(){
127 var userMeasurements = g.doc.viewPreferences.horizontalMeasurementUnits;
128 g.doc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.MILLIMETERS;
129 var currentFrameWidth = currentPageItem.geometricBounds[3] - currentPageItem.geometricBounds[1] ;
130 g.doc.viewPreferences.horizontalMeasurementUnits = userMeasurements;
131 // Detect whether paragraph is product details
132 if(currentFrameWidth < 110){
133 // Add product details to current product element - set content

469
134 var currentProductDetails = h.currentProduct.xmlElements.add("details");
135 currentProductDetails.contents = currentPageItem.contents;
136 // Move details element before image
137 currentProductDetails.move(LocationOptions.BEFORE, h.currentProductImage);
138 }
139 }
140
141 }
First we capture the current horizontal measurement units in a variable called
userMeasurements (line 127). On line 128, we change the horizontal measurement units to
millimetres; so that, on line 129, we can capture the width of the textFrame in millimetres in a
variable called currentFrameWidth. (The geometricBounds lists the coordinates of the
textFrame in the order: y1, x1, y2, x2; so geometricBounds[3] is x2 and geometricBounds[1] is
x1.)
On line 130, we restore the horizontal measurement units to their original settings.
Having captured the width of the textFrame in the desired units, on line 132, we test to see
whether this figure is less than 110—probably a safer strategy than saying:
if(currentFrameWidth ==100).
We then attach a new <details> item to the most recently created <product> element and set
its contents to the entire contents of the textFrame—not just the current paragraph (lines 134 to
135).
Since we have sorted the pageItems according to distance from the top of the page, the image
will be encountered before the product details because its Y measurement is lower.

We want the image to be the last child of the <product> element. For this reason, on line 137,
we use the move() method of the xmlElement object to ensure that the <details> element will
precede the <image> element in the XML output. (We have yet to write the code that creates
the <image> element; but when we do, we will use a variable called currentProductImage to
hold each <image> element that we create.)
9. Creating the product <image> element
We have now completed all the code that needs to execute if the current pageItem is a
textFrame object. We can now add the h_createImageElement() function to say what we
want to happen when the current page item is an image. Since the only images we will
encounter are product images, we can go straight ahead and add an <image> element as a child
of the current <product> element.

470
The <image> element needs an href attribute; so we will then need to grab the file path of the
image (using the itemLink property) and make it the content of the href attribute. The image's
itemLink property will return a platform specific file path; so we will also need to modify it a
little to make it InDesign-friendly and generic.
Insert the following code on line 140, above the closing brace of
the buildStructure() function.
140
141 function h_createImageElement(){
142 var userMeasurements = g.doc.viewPreferences.horizontalMeasurementUnits;
143 g.doc.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.MILLIMET ERS;
144 var currentFrameWidth = currentPageItem.geometricBounds[3] - currentPageItem.geometricBounds[1] ;
145 g.doc.viewPreferences.horizontalMeasurementUnits = userMeasurements;
146 if(currentFrameWidth < 60){
147 var strLink = currentPageItem.itemLink.filePath;
148 var blnNameMatch = strLink.toLowerCase().indexOf(h.currentProductName.contents.toLowerCase()) > -1;
149 if(blnNameMatch){
150 h.currentProductImage = h.currentProduct.xmlElements.add("image");
151 // Grab image URL and set it as content of href attribute
152 strLink = strLink.replace(/\\/g, "/");
153 strLink = strLink.replace(":", "/");
154 strLink = strLink.replace("//", "/");
155 strLink = "file:///" + strLink;
156 var currentImageURL = h.currentProductImage.xmlAttributes.add("href", strLink);
157 }
158 }
159 }
160 }
The product images are about 50 millimetres in width; so we employ the same technique that
we used previously to calculate the width of the box and, on line 146, place a condition that
the width must be less than 60 mm.
On line 147, we place the file path of the image in a variable called strLink.
The name of each image matches the name of the corresponding product; so, on line 148, we
test whether strLink contains the name of the most recently created product.
If the test proves true, we add an <image> element to the current <product> element (line
150).
Before create the href attribute, we carry out three replace operations to get the file path of the
image into the right format. Line 152 replaces backslash with forward slash ( Windows file

471
paths). Line 153 replaces colon with forward slash (Mac file paths).
Since Windows filepaths normally contain a colon after the drive letter, we may end up with
"//" in the resulting string. Line 154 therefore replaces "//" with "/".
We then add the prefix "file:///" in front of the resulting string to give us our InDesign-friendly
image file path.
Finally, on line 156, we create the href attribute and assign the file path contained in the
strLink variable as its contents.
10. Exporting the XML
Having created our XML structure and assigned the necessary content to each element, we are
ready to export the XML file. This only requires a few lines of code.
Position the cursor after the closing brace of the buildStructure()
function, press Return a couple of times then enter the following
code.
161
162 function exportXML(){
163 var fleOutput = null;
164 while (fleOutput == null){
165 fleOutput = File.saveDialog("Please specify the name and location of XML file");
166 }
167 var strExt = fleOutput.fullName.toLowerCase().substr(fleOutput.fullName.lastIndexOf("."));
168 if(strExt != ".xml"){
169 fleOutput = (File(fleOutput.toString() + ".xml"));
170 }
171 try{
172 g.doc.exportFile(ExportFormat.xml, fleOutput);
173 }
174 catch(err){
175 alert("No XML file created.\rPlease choose File > Export XML to export your XML data.");
176 }
177 }
On line 165, we use the saveDialog() method of the File object to allow the user to specify the
file name and location of the XML document. On lines 167 to 170, we test the file extension of
the file path supplied by the user. If it does not end in ".xml", we add the extension for them
(line 169).
If all is well, inside a try ... catch statement, we export the file to the specified location (line
172); if there is an error, we display an alert inviting the user to export the file at some point in
the future (line 175).
And that completes our code. Before testing the final script, don't
forget to uncomment any lines you still have commented out.
Check the Structure pane after the script has run and look at the

472
structure of the resulting XML.

Open the XML file that was created in Dreamweaver, the ESTK, or
any other editor.
The file that is produced has no indentation and is difficult to read.
However, if you open it in Dreamweaver, you can automatically
indent elements by choosing Commands > Apply Source
Formatting.

473
CHAPTER 21. Conclusion
Deploying your solutions
App.doScript
When you start developing scripts to do real work for you, your scripts become longer—too
long to be easily edited in a single file. The doScript() method of the application object allows
you to modularize a script: to split it up into separate building blocks, each of which could
potentially be used in other projects.

SYNTAX app.doScript (script[, language])


SUMMARY ... Method of Application Object
Inserts the code specified by the script parameter.

Script The file containing the script to be included or a string literal containing legitimate code.
The language in which the code being included is written. Requires one of the following
enumerations:
Language ScriptLanguage.unknown
ScriptLanguage.javascript
ScriptLanguage.applescriptLanguage

Using this function is equivalent to inserting whatever code the file being included may contain
on the line containing the doScript() statement. So whether it all works depends on the
suitability of the contents of the script file for inclusion in that position. In a typical scenario,
you would have a main script which the user launches and which then uses doScript() to
include each subsequent module.

474
Export as binary
When distributing your scripts to other people, the ESTK offers a useful facility for converting
code into binary format, using the file extension “.jsxbin”. This creates a script that can be run
in the normal way but whose code cannot be altered. If someone is using a script that you have
written, this means that you can then be sure they have not modified it and that any problems
they report are probably genuine, rather than due to them experimenting with the original code.
Naturally, you should always retain a version of the original ".jsx" file, since you will not be
able to edit the “.jsxbin” file either. To export a file in “.jsxbin” format:
• With the script file open, choose File > Export as Binary, enter a name and click the
Save button.

Try it for yourself!


TUTORIAL: Creating a modular solution
1. Creating the start file
In this tutorial, you will create a modular version of the script you created in chapter 19.
In the ESTK, create a new file and save it as “0100-start.jsx” in the
folder called “solution”, in “chapter21” folder.
Enter the code shown below.
1 #targetengine "session"
2 app.doScript(File(app.activeScript.parent + "/0200-main.jsxbin"));
3 app.doScript(File(app.activeScript.parent + "/0300-readXML.jsxbin"));
4 app.doScript(File(app.activeScript.parent + "/0400-createDialog.jsxbin"));
We use the doScript() method three times to include each of the modules into which we will
be splitting the script. The numeric prefix is used simply to cause the files to be sorted by the
order in which they are inserted whenever they are listed. Using big numbers makes it easier to
insert new modules in between the existing ones.
You will notice that we saved “0100-start.jsx" as a regular “.jsx” file. This is because the
#targetengine statement cannot be used in files saved in binary format.
2. Creating the include files
We will now take the file called “xml-object-explorer.jsx” which we created in chapter 19
and split it into three different files, each containing a section of the original script.
Save your changes to “0100-start.jsx” then, still in the ESTK, copy
the first file name (“0200-main.jsxbin” on line 2), without the
quotation marks. (This is simply to save yourself the trouble of

475
having to type it later.)
Open the file “xml-object-explorer.jsx”, which you will find in the
“chapter21” folder.
Let's now create our first sub-file—“0200-main.jsxbin”. First, we need to remove all of the
code that we don't need in the file we are about to create.
Begin by removing the first line (#targetengine "session"), for the
reasons just stated above.
Next, delete all of the code from line 16—the start of the
readXML() function—to the end of the script.
Choose File > Export As Binary and make sure you are still in
the “solution” folder.
Select the file name and replace it with the one you copied earlier,
by pasting over it—or just enter the name “0200-main.jsxbin”.

The Export As Binary command creates the file in the specified location but does not open it,
so “xml-object-explorer.jsx” should still be your active file.
Close “xml-object-explorer.jsx” without saving your changes then
reopen it from the File > Recent Files submenu.
This time, delete all of the code except for the readXML()
476
function.
Choose File > Export As Binary once more, enter the name
“0300-readXML.jsxbin” and again save the file in the “solution”
folder.
Close “xml-object-explorer.jsx” without saving your changes then
reopen it from the File > Recent Files submenu.
This time, delete lines 1 to 44—everything above the
createDialog() function.
Choose File > Export As Binary and enter the name “0400-
createDialog.jsxbin”.
Close “xml-object-explorer.jsx” without saving your changes.
Switch over to InDesign and, in the Scripts panel, navigate to User
> indesigncs5js1 > chapter21 > 0100-start.jsx.
Double-click on the script 0100-start.jsx to run it. Thanks to
app.doScript(), everything should run just as it did when all of the
code was all contained in a single “.jsx” file.

Further reading
Adobe documentation
Most of the documentation offered by Adobe on InDesign scripting can be found at the
following URL:
www.adobe.com/products/indesign/scripting/
The three key items you should download, and probably already have, are as follows:
The Adobe Indesign CS5 Scripting Guide: Javascript
www.adobe.com/products/indesign/scripting/pdfs/InDesignCS5_ScriptingGuide_JS.pdf
The InDesign Scripting Guide scripts
www.adobe.com/products/indesign/scripting/downloads/indesign_cs5_sample_scripts.zip
Adobe Creative Suite JavaScript Tools Guide
www.adobe.com/products/indesign/scripting/pdfs/JavaScriptToolsGuide_CS5.pdf
Books on InDesign automation

477
Scripting InDesign CS3/4 with JavaScript by Peter Kahrel
ISBN: 978-0-596-55960-1
A Designer's Guide to Adobe InDesign and XML by James J. Maivald & Cathy Palmer
ISBN: 978-0321503558
Video tutorials on InDesign automation
InDesign CS5: Dynamic Publishing Workflows in XML: James Maivald
www.lynda.com
Books on JavaScript
JavaScript: The Definitive Guide by David Flanagan
ISBN: 978-0596101992
Object-Oriented JavaScript by Stoyan Stefanov
ISBN: 978-1847194145
Books on XML
Learning XML by Erik T. Ray
ISBN: 978-0596004200

XML in a Nutshell by Elliotte Rusty Harold and W. Scott Means


ISBN: 978-0596100506
Books on XSLT
XSLT Quickly by Bob DuCharme
ISBN: 978-1930110113
XSLT, 2nd Edition by Doug Tidwell
ISBN: 978-0596527211
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers by Sal Mangano
ISBN: 978-0596009748

Online resources
Adobe resources
Adobe Forums: InDesign scripting user forum
http://forums.adobe.com/community/indesign/indesign_scripting
InDesign Exchange
www.adobe.com/cfusion/exchange/index.cfm?event=productHome&exc=19
Other people's websites
Marc Autret
www.indiscripts.com
Peter Kahrel
www.kahrel.plus.com/indesignscripts.html
Teus de Jong
www.teusdejong.nl
Dave Saunders
www.jsid.blogspot.com

478
Steve Wareham
www.stevewareham.com

479
Table of Contents
CHAPTER 1. Introduction
What are you letting yourself in for?
The Scripts panel
The ExtendScript Toolkit
Workspaces
Line numbers
Syntax highlighting
Code Collapse
Auto completion
Organizing your scripts
Favorites
TUTORIAL: Getting started
1. Installing the work files
2. Setting up the ESTK
3. Creating a basic script
About the tutorials in this book
CHAPTER 2. Scripting essentials
Comments
Writing scripts
Variables
Naming variables
Variable data types
Declaring and initializing variables
Expressions and operators
Arithmetical operators
Comparison operators
Logical operators
The InDesign object model
Properties and methods
The Object Model viewer
Essential object syntax
Working with object properties
Property values
Using enumerations
Working with object methods
Using properties records
Creating new elements
Creating a new document, book or library
Adding a page to a document

480
Creating default and document colours
Creating default and document paragraph styles
Referencing objects
Using a numeric index
Using a named index
Using an ID
Targeting a range of objects
Targeting every item in a collection
Targeting currently active objects
Active document
Active window
Counting objects
JavaScript dialog windows
The alert function
The confirm function
The prompt function
The String object
The length property
The indexOf() method
The charAt() method
The toUpperCase() and toLowerCase() methods
The replace() method
The substring() and slice() methods
InDesign text objects and strings
The Array object
Creating arrays
InDesign objects and arrays
Array properties and methods
The JavaScript Object object
CHAPTER 3. Conditional statements, loops and functions
If else statements
Using else if
Switch statements
Using break statements
For loops
Using a for loop to test whether a document is open
Looping in reverse
Break and continue
While loops
Functions
Defining and calling a function
Returning a value from a function

481
Passing parameters to a function
Local and global variables
Using namespaces with variables
ExtendScript engines and variables
CHAPTER 4. Creating dialogs
Dialog controls
Layout controls
Text controls
Self-validating text controls
Controls which offer the user a choice
Self-validating combobox controls
Button controls
Referring to controls
Displaying a dialog
Creating static labels and text boxes
Placing dialog objects inside variables
The canCancel property
The minWidth property
Dropdown controls
The stringList property
The selectedIndex property
Creating radiobutton and checkbox controls
Radiobutton controls
TUTORIAL: Using self-validating controls
1. The main function
2. Building the dialog
2a. The createDialog() function shell
2b. The watermark text editTextbox
2c. Angle comboBox
2d. Fonts dropdown
2e. Validating user input
3. Adding the watermark text to the master pages
3a. Creating the “watermark” layer
3c. Creating the watermark text frame
3d. Formatting the watermark text
CHAPTER 5. ScriptUI Dialogs
The Window object
Creating a window
Container objects and the add() method
Tabbed panels
Panels
Groups

482
Coding styles for ScriptUI
Placing containers inside containers
Text Controls
StaticText
StaticText creation properties
Using the multiline and scrolling properties
EditText
EditText creation properties
List Controls
Drop down list
Displaying images
List box
Treeview
Buttons
Creating Cancel and OK buttons
Events
Button events
CHAPTER 6. Working with Files and Documents
About files and documents
Creating a new document
Opening a document
Letting the user choose a file
Using filter expressions (Windows only)
Using Filter functions (Mac only)
Creating a cross-platform mask
Allowing multiple selections
Testing whether a document is already open
Testing whether an array of documents is already open
Saving a document
Testing whether a document has been saved
Saving the changes to a document
Using the saveDialog method
Closing a document
Reading from a text file
Testing whether a file exists
Writing to a text file
Creating a new file
Working with folders
Creating folders
Reading files in a folder
TUTORIAL: Navigating folders and files recursively
1. Creating the main function

483
2. The getFolder() function
3. The inputDialog() function
4. The OKButtonClick() callback function
5. The outputDialog() function
6. The recursive addItem() function
7. The archiveItems() function
CHAPTER 7. Document Layout
Document and default Preferences
View Preferences
Document Preferences
Page attributes
Bleed and slug
MarginPreferences
Margin settings
Column settings
Implementing document setup
Placing text and images
Placing via the document object
Placing items onto a page or spread
Placing items into a frame
Placing items inside a text object
TUTORIAL: Automating document setup
1. Creating the main function
2. The getFiles() function
3. The createDialog() function
4. The setupEvents() function
5. The buildDocument() function
CHAPTER 8. Working with text
Understanding text objects
Overwriting text objects
Inserting text
Inserting text before an object
Working with Fonts
Font names
TUTORIAL: Creating a font and style selection dialog
1. Creating an array of font names
2. Creating separate arrays for font and style names
3. Creating the dialog
4. onChange callback for the fonts dropdown
5. onClick callback for the Close button
6. Displaying the dialog
Finding and replacing text

484
Clearing Find/Change preferences
TUTORIAL: Creating a clean-up text script
1. Creating the skeleton of the script
2. The createDialog() function
2a. The scope dropdownlist
2b. The checkboxes for choosing clean-up operations
2c. The Cancel and Clean up buttons
3. The cleanUpText() function
3a. The function skeleton
3b. Checking the scope of the Change/Find operations
3c. Carrying out the selected clean-up operations
3d. Creating the doFindChange() function
Tables
Creating tables
Adding text to table cells
Writing a value into every cell
Writing an array to a row
TUTORIAL: Updating table data
1. Variable declaration and function calls
2. The importDates() function
2a. Reading the data file
2b. Converting the data to an array
2c. Formatting the data to resemble the publication
3. Updating the tables in the publication
CHAPTER 9. Working with Images
InDesign image objects
The image and its container
Targeting all graphic within a document
Accessing graphics via the pageItems collection
TUTORIAL: Relinking images
1. Creating the dialog
2. Callback functions
3. The relinkGraphics() function
Working with links
Ascertaining the link type
Identifying poster images
Excluding both media and posters
FilePath versus name
Embedding and unembedding linked graphics
TUTORIAL: Unembedding unlinked images
1. Creating the main script
2. The findEmbedded() function

485
3. The createDialog() function
4. The exportImage() function
The image object
Independent and anchored graphics
TUTORIAL: Finding stretched images
1. Creating the main() function
2. The findStretched() function
3. The createDialog() function
4. The fixImages() function
CHAPTER 10. Page Items and Layers
TUTORIAL: Navigating all page items in a document
The hierarchical structure of the allPageItems collection
1. The main() function
2. Creating the window
3. Constructing the treeview control
Ascertaining pageItem type
Using constructor.name
Using instanceof
Identifying textFrames, groups and buttons
Identifying picture frames
Identifying movies and sound
Identifying type on a path
Identifying regular graphic objects
PageItems and layers
TUTORIAL: Creating a layer manager utility
1. Creating the main() function
2. The loadPageItems() function
3. Creating the dialog box
3a. Creating the window itself
3b. Creating the object type checkboxes
3c. Creating the multi-column listbox
3d. Creating the selectItems dropdown and button
3e. Creating the move layer dropdown and button
3f. Creating the Delete and Close buttons
4. Creating the updateListbox() function
5. Creating callback functions
5a. Page items listbox onDoubleClick
5b. Select by Type button onClick
5c. Move to Layer button onClick
5d. Delete and Close buttons onClick
6. Testing the script
CHAPTER 11. Error handling and debugging

486
Creating scripts for different InDesign versions
Detecting the InDesign version
Creating conditional code
Running old scripts with InDesign CS5
Detecting the platform
Basic error handling techniques
Using try ... catch
The error object
Throwing your own errors
Eliminating simple errors
Creating scripts for other people
Avoiding references to specific locations
Using app.activeScript
Using Folder.current
Debugging in break mode
Stepping through a script
Examining variables, objects and statements
Using breakpoints
TUTORIAL: Working in break mode
1. Stepping through a script
2. Setting breakpoints
Using alerts for debugging
Writing values to the JavaScript console
CHAPTER 12. Interactive Documents
Overview
Setting preferences
View preferences
Document preferences
Transparency preferences
Adding page transitions
Automatic layout adjustment
Shortening a document
Creating buttons
Button states
Behaviors
TUTORIAL: Creating an interactive presentation from images
1. The main() function
2. Retrieving the image files
3. Creating the dialog
4. Document setup
5. Setting up the title page
6. Adding navigation buttons

487
7. Importing the images
8. Exporting an interactive PDF
CHAPTER 13: XML Essentials
What is XML?
Structure of an XML document
Elements
Attributes
Entity references
CDATA sections
Comments
Processing instructions
XML validation
Well-formedness
Schema validation
DTDs versus XML schemas
Creating XML
Microsoft Access
Microsoft Excel
FileMaker Pro
CHAPTER 14: InDesign XML Essentials
XML elements, tags and styles
Creating tags
Mapping tags to styles
Importing XML
TUTORIAL: Basic XML workflow
1. Renaming the Root tag
2. Loading tags from an XML file
3. Creating paragraph styles
4. Mapping tags to styles
5. Importing the XML file
6. Placing the XML content on the page
CHAPTER 15: Working with DTDs
Using an internal DTD
Using an external DTD
Declaring elements
Declaring elements that contain other elements
Specifying the occurrence of child elements
Limiting occurrences to a choice
Declaring elements that contain only data
Declaring elements with mixed content
Declaring empty elements
Declaring attributes

488
Attribute data types
TUTORIAL: Creating a DTD and using it for validation
1. The XML file
2. Creating the DTD
CHAPTER 16: XSLT Essentials
Linking an XML document to a stylesheet
The structure of an XSLT document
The stylesheet element
The template element
Using XPath expressions
Examples of XPath expressions
Axes
Abbreviations
Absolute location paths
Relative location paths
Targeting attributes
Using predicates
Examples of predicates
Using <xsl:apply-templates>
Using <xsl:copy>
The <xsl:value-of> element
The <xsl:copy-of> element
Using <xsl:element> and <xsl:attribute>
TUTORIAL: Working with XSLT using Dreamweaver and InDesign
1. Creating an XSL stylesheet in Dreamweaver
Defining a new site
Creating the XSLT file
Creating the main (outer) template
Creating the inner template
2. Applying the XSLT stylesheet in InDesign
CHAPTER 17: XSLT Processing-Control Elements
<xsl:if>
<xsl:choose>, <xsl:when> and <xsl:otherwise>
The <xsl:for-each> element
The <xsl:sort> element
TUTORIAL: Using XSLT control elements
1. Creating the stylesheet in Dreamweaver
Creating the output document root element
Creating the <xsl:for-each> element
Creating the <xsl:sort> element
Creating the common elements
Adding the <xsl:choose> statement

489
2. Creating a layout with placeholders
Creating paragraph styles
Creating text and graphic placeholders
3. Tagging placeholders
4. Importing XML into placeholders
CHAPTER 18: Working with InDesign XML Objects
The InDesign XMLElement Object
Renaming an element
The xmlTag object
Creating tags
Loading tags
Mapping tags to styles
Importing XML
Placing XML data in the layout
TUTORIAL: Basic XML import
1. Create the new document
2. Create paragraph styles
3. Map tags to styles
4. Importing the XML
5. Placing the XML in the document
The xmlAttribute object
Looping through attributes
Changing attributes to elements
Using XSLT stylesheets
Specifying which stylesheet to use
Supplying values to stylesheet parameters
TUTORIAL: Using parameters to filter XML import
1. Creating the XSL file
2. Writing the main script
3. Verifying the XML file
4. Creating the dialog
5. Importing the XML
6. Setting up the InDesign document
7. Placing XML content on document pages
8. Producing an interactive PDF
CHAPTER 19. The JavaScript XML Object
Creating an XML object
Node types
Accessing nodes
Accessing specific nodes
Accessing nodes by relationship
TUTORIAL: XML browser utility

490
1. The main() function
2. Creating the XML object
3. Creating the dialog
4. The updateTreeView() function
5. The addXMLToDoc() function
CHAPTER 20. Exporting XML
Documents which are hard to export as XML
TUTORIAL: Exporting XML from a catalogue
1. Creating the main function
2. Defining the root element
3. Looping through the document pages
4. Sorting page items by distance from top of page
5. Looping through all paragraphs in the text frame
Creating the <series> element and its children
Creating the range element and its children
6. Testing the code we have created so far
7. Creating the product <summary> element
8. Creating the product <details> element
9. Creating the product <image> element
10. Exporting the XML
CHAPTER 21. Conclusion
Deploying your solutions
App.doScript
Export as binary
TUTORIAL: Creating a modular solution
1. Creating the start file
2. Creating the include files
Further reading
Adobe documentation
Books on InDesign automation
Video tutorials on InDesign automation
Books on JavaScript
Books on XML
Books on XSLT
Online resources
Adobe resources
Other people's websites

491

You might also like