Indesign Cs5 Automation Using XML Javascript Compress
Indesign Cs5 Automation Using XML Javascript Compress
Automation
Using XML &
JavaScript
Grant Gamble
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
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
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.
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.
19
in Finder (Mac) or Reveal in Explorer (Windows) from the menu
in the top right of the Scripts panel.
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.
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.
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.
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 “{ }”.
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
35
—as shown below. Naturally, however, square brackets are not required when these
arguments are used in actual scripts.)
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:
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.)
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.
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;
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()
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.
Figure 2-6: The confirm function allows the user to accept or reject the statement displayed
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.
43
email, we could say:
var strEmail = prompt("Please enter an email address", "");
var intDot = strEmail.lastIndexOf(".");
alert("Position of last dot is " + intDot);
SYNTAX containerString.charAt(index)
SUMMARY ... JavaScript String object method
Returns the character at the position specified by index.
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.
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.
replace text Text which takes the place of the search text, specified as a string or regex.
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.
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.
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);
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']
49
Index position The position within the array where the splicing takes place
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.
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}
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.
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();
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();
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.
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.
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).
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
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.
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.
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.
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.
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.
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
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.
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 }
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.
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.
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.
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
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 }
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.
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.
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.
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.
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.
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
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
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.
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();
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.
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.)
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.
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
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.
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.
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.
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
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
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.
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.
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
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).
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"));
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.
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.)
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.
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.
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.
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.
126
files.)
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.
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.
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).
Print dialog Optional. boolean specifying whether or not the print dialog should be displayed.
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.
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.
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".
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 }
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.)
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 }
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 }
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 }
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.
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.
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
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.
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.
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.
SYNTAX myGraphicFrame.fit(FitOptions)
SUMMARY Method of rectangle, oval, polygon, graphic and image
... Eqvuivalent to choosing options from the Object > Fitting submenu.
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.)
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 }
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 }
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 }
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.
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 }
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 }
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"
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 }
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.
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.
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;
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.
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.
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}
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 }
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 }
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.
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 }
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 }
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.
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 }
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 }
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.
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.
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.
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".
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.
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 }
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
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.
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.
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.
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
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.
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
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 }
212
the text fields. You should see the following error message.
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 }
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.
217
If you try the script on any of your own documents, be sure to
choose File > Revert afterwards.
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 }
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 }
220
Figure 9-2: A simple script for embedding and umembedding linked graphics.
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.
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 }
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
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
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 }
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
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 }
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 }
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"].
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 }
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
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.
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 }
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.
242
SYNTAX myPageItem.getElements()[0].constructor.name
SUMMARY ... Syntax for retrieving object type of a pageItem
Returns a string containing the name.
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 }
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 }
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 }
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 }
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 }
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 }
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.
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
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.
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.
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
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
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.
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
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 }
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 }
268
211 }
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 }
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).
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 }
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.
274
main techniques:
• Use conditional statements within the script which execute different lines of code
depending on the version
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
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);
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.
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
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.
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.
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.
(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.
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
$.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;
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
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.
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});
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.
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 }
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 }
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 }
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(...)
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 }
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 }
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.
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:
< <
> >
& &
" "
' '
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>
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:
< <
> >
& &
" "
' '
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.
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.
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.)
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.
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.
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.
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.
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.
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
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.)
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”.
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.
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.
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"?>
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.
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>
...
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 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 >
Here the permitted values are “RGB” and “CMYK” and “CMYK” is the default.
340
the Root tag and rename it properties.
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'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'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>
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.
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.
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+)>
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">
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.
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
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
/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
.
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—>= (an XML-compliant version
of ">=") and <= (an XML-compliant version of "<="):
//staff_member[staff_id >= 25000000 and staff_id <= 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>
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>
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.
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
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.
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.
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>
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).
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>
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) >= 2010]">
8
9 </xsl:for-each>
10 </new_guys>
11 </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
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>
382
45 </xsl:when>
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>
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.
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.
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.
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.
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.
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.
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.
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
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.
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 }
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 }
401
58 g.doc.xmlImportMaps.add("author", "author");
59 g.doc.xmlImportMaps.add("para", "para");
60 g.doc.mapXMLTagsToStyles ();
61 }
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 }
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"
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)
Figure 18-2: Telling InDesign to use an XSLT stylesheet when importing XML
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"], ...]
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
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 >= 200000—will compare them to parameters—for example,
Price >= $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 >= $beds and Price >= $min and Price <= $max and (Type = $flats or
Type =$houses)">
24 <xsl:copy-of select="."/>
25 </xsl:if>
26 </xsl:for-each>
27 </xsl:for-each>
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 >= $beds and Price >= $min and Price <= $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 }
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 }
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 }
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.
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 }
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 }
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 }
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 }
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.
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
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;
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 }
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 }
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.
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
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.
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.
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.
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 }
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.
441
55 // Buttons
56
57 // Progress bar
58
59 // Show window
60
61 }
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
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.
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 }
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.
449
Try the browser utility on some of your own files. In theory, you
should be able to use it with any XML file.
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.
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}
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:
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.
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.
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.
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.
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.
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
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