Skip to main content

InDesign Scripting with ScriptUI: How to Move Cursor and Select Text


Based on the code pattern in my previous article, here I’d like to show the methods to move the cursor and to select text with button click on ScriptUI.

Using buttons to move cursor and to select text is useful when working with a script, which has a UI. You don’t need to move your mouse running all over the whole screen very often for doing your tasks. Most of the time, you can concentrate your mouse movement just around the UI of your script.

The code is divided into 4 parts:

Part 1, from line 10 to line 28:
This is the coding pattern for the modeless dialog.

Part 2, from line 37 to line 57:
The main UI is created in this part.

Part 3, three groups of buttons are created here, together with the functions, which are attached to the button click event.
  • Group 1:
  1. from line 60 to line 67, the code setups the buttons for moving cursor.
  2. from line 69 to line 118, the event listener "MoveCursor" is defined.
  • Group 2:
  1. from line 121 to line 128, the code setups the buttons for expanding the selection.
  2. from line 130 to line 179, the event listener "ExpandSelectionChar" is defined.
  • Group 3:
  1. from line 182 to line 193, the code setups the buttons for reducing the selection.
  2. from line 198 to line 230, the event listener "ReduceSelectionChar" is defined.

Part 4, from line 233 to line 240:
There is the button to close the UI.

The code is listed at the end of this article, and it has been tested on InDesign CC 2017 and 2018.

Explanation 1:
Let's look at the three event listening functions, there are the same blocks:
var myDocument;
try {
 myDocument = app.activeDocument;
}
catch (xError) {
 return;
}

According to line 3 the script will be applied to the active document. if there is no active document, the script just returns and does nothing.

Explanation 2:
There is also a line in three listeners:
if (myDocument.selection.length > 0)
This line will prevent the situation that, you just open a document, but not click on any text of it. If you have clicked on the text, the cursor is there, then "myDocument.selection.length" will be greater than "0".

Explanation 3:
Before going to get the position of the cursor, we use the following line of code to see, if the current selection is a insert point:
if (myDocument.selection[0] instanceof InsertionPoint)
In the same way, we must decide, if the "selection" is a block of selected characters. We use the following code:
if ((myDocument.selection[0] instanceof Text) ||
  (myDocument.selection[0] instanceof Word) ||
  (myDocument.selection[0] instanceof Character) ||
  (myDocument.selection[0] instanceof TextStyleRange)) 
Because, the selected characters can be a character, a word, a piece of text or a text style range.
(I forgot what a text style range is. I will update this article, when I find the answer.)

Explanation 4: To set the insert point
To set the insert point programmatically, we use the following code:
var insertPoint = theParent.insertionPoints.item(index);
myDocument.select(insertPoint);
app.activate();
  • The variable $index$ is the index number of the insert point. It begins with "0".
  • Please refer to the the Fig 1. below for the possible value of the variable $index$.
  • It is $-(n+1) \le index \le n$, where $n$ is the length of the story.
  • If $index < -(n+1)$ or $n < index$, an exception will be thrown. You should use the try-catch block to catch the exception.
  • The line "app.activeate();" sends the control to the app, i.e. InDesign. So the script UI will lose the focus. This line is a work around. Without this line, the clicked button will not pop up.
  • For the variable "theParent", please read the complete code below.

Fig 1. index of the insert point and the values of start and end of the selection

Explanation 5: To select a piece of text
To select a piece of text programmatically, we use the following code:
var range = theParent.characters.itemByRange(start, end);
myDocument.select(range);
app.activate();
  • The variables $start$ is the ordinal number of the first character and $end$ is the ordinal number of last character of the selection in the story. The $value$ of $start$ and $end$ must fall into the range: $-n \le value < n$, where $n$ is the length of the story.
  • If $value < -n$ or $n \le value$, an exception will be thrown. You should use the try-catch block to catch the exception.
  • Please refer to Fig 1. above for the possible value of $start$ and $end$.
  • The value of $start$ and $end$ can be any number in the above range. It is possible for $start < end$, $start = end$ or $start > end$.
  • So, if $start = end$, then there is only one character selected.
  • About the line "app.activeate();", please refer to "Explanation 4".
  • For the variable "theParent", please read the complete code below.
Explanation 6: Boundary checking
  • In the complete code below, I use the nonnegative value of the variables $index$, $start$ and $end$ to check the boundary. This simplifies the code.
  • The code between line 93 to 101 of the function "MoveCursor" checks the variables together with the direction of cursor movement to prevent the insert point goes outside of the story.
  • The code between line 155 to 163 of the function "ExpandSelectionChar" checks the variables with the direction of expansion to prevent the selection goes outside of the story.
  • There is no need of boundary checking for the function "ReduceSelectionChar". Because the selection shrinks to a insert point. When it becomes to a insert point, the function returns (line 224).
Explanation 7: direction
I use the parameter "direction" for the functions "MoveCursor", "ExpandSelectionChar" and "ReduceSelectionChar" to indicate the cursor moving direction. So, besides the code in "Explanation 4 and 5", there are some extra code to calculate the proper values of the variables "index", "start" and "end" according to the value of "direction". For the algorithms in detail, please refer to the complete code below.

Here is the UI:

Here is the code:
#targetengine "session"; // not needed in Illustrator/AfterEffects
// the above line is necessary to create a modeless dialog with
// var win = new Window("palette", ...);
//
// refer to the sample program from Adobe: "SnpCreateDialog.jsx".
// Which is usually located at the folder (Windows):
// C:\Program Files (x86)\Adobe\Adobe ExtendScript Toolkit CC\SDK\Samples\javascript

// Beginning of the coding pattern of the Modeless Dialog----------------------
function DemoOfMoveCursorSelectTextModelessDialog() {
 this.windowRef = null;
}

function setupWindow() {
 var ww = new Window("palette", "Demo of Moving Cursor and Selecting Text");
 addComponents(ww);
 return ww;
}

DemoOfMoveCursorSelectTextModelessDialog.prototype.run = function () {
 var win = setupWindow();
 this.windowRef = win;

 win.show();
 return true;
}

new DemoOfMoveCursorSelectTextModelessDialog().run();
// End of the coding pattern of the Modeless Dialog----------------------------

// =================================================
// Above lines are the programming pattern for a modeless dialog with ScriptUI
//
// The following lines are the function to create the "palette".
// You may modify the following function, splite it to functions, in order to design
// your window UI.
function addComponents(w) {
 w.grpPanel = w.add('group');
 w.grpPanel.orientation = "column";

 // --- PN Move -----
 w.PN01 = w.add('panel', undefined, 'Moving Cursor');
 AddMoveCursorButtons(w, w.PN01);

 // --- PN Expand -----
 w.PN02 = w.add('panel', undefined, 'Expand the Selection');
 AddExpandSelectionButtons(w, w.PN02);

 // --- PN Reduce -----
 w.PN03 = w.add('panel', undefined, 'Reduce the Selection');
 AddReduceSelectionButtons(w, w.PN03);

 // --------------
 AddCloseButton(w);

 return w;
}

// --- PN01 -------------------------------------------------------------------
function AddMoveCursorButtons(w, PN) {
 w.grpTop = PN.add('group');
 w.grpTop.orientation = "row";

 w.btnMoveCursorBackward = w.grpTop.add('button', undefined, '<C');
 w.btnMoveCursorBackward.size = [33, 23];
 w.btnMoveCursorBackward.onClick = function () { MoveCursor(-1); }
 //
 w.btnMoveCursorForward = w.grpTop.add('button', undefined, 'C>');
 w.btnMoveCursorForward.size = [33, 23];
 w.btnMoveCursorForward.onClick = function () { MoveCursor(1); }
}

function MoveCursor(direction) {
 var myDocument;
 try {
  myDocument = app.activeDocument;
 }
 catch (xError) {
  return;
 }

 if (myDocument.selection.length > 0) {
  selectionDirection = 0;
  selectionFirstIndex = -1;

  var theParent = myDocument.selection[0].parent;
  var firstIndex = myDocument.selection[0].index;
  var theText = myDocument.selection[0];
  var storyLength = theText.parentStory.length;
  var insertPoint;

  // if the cursor is at the beginning of the story, it cannot be moved backward.
  if (firstIndex == 0 && direction < 0) {
   app.activate();
   return;
  }
  // if the cursor is at the end of the story, it cannot be moved forward.
  if (firstIndex >= storyLength && direction > 0) {
   app.activate();
   return;
  }

  if (myDocument.selection[0] instanceof InsertionPoint) {
   // direction = 1: move forward; direction = -1: move backward.
   insertPoint = theParent.insertionPoints.item(firstIndex + direction);
  } else if ((myDocument.selection[0] instanceof Text) ||
  (myDocument.selection[0] instanceof Word) ||
  (myDocument.selection[0] instanceof Character) ||
  (myDocument.selection[0] instanceof TextStyleRange)) {
   var strLen = myDocument.selection[0].length;
   if (direction > 0) {
    // move cursor to the end of the selection
    insertPoint = theParent.insertionPoints.item(firstIndex + strLen);
   } else {
    // move cursor to the beginning of the selection
    insertPoint = theParent.insertionPoints.item(firstIndex);
   }
  }
  myDocument.select(insertPoint);
  app.activate();
 }
}

// --- PN02 -------------------------------------------------------------------
function AddExpandSelectionButtons(w, PN) {
 w.grpMid = PN.add('group');
 w.grpMid.orientation = "row";

 w.btnExpSeletionCharBackward = w.grpMid.add('button', undefined, '<S');
 w.btnExpSeletionCharBackward.size = [33, 23];
 w.btnExpSeletionCharBackward.onClick = function () { ExpandSelectionChar(-1); }
 //
 w.btnExpSeletionCharForward = w.grpMid.add('button', undefined, 'S>');
 w.btnExpSeletionCharForward.size = [33, 23];
 w.btnExpSeletionCharForward.onClick = function () { ExpandSelectionChar(1); }
}

function ExpandSelectionChar(direction) {
 var myDocument;
 try {
  myDocument = app.activeDocument;
 }
 catch (xError) {
  return;
 }

 if (myDocument.selection.length > 0) {
  var theParent = myDocument.selection[0].parent;
  var firstIndex = myDocument.selection[0].index;
  var theText = myDocument.selection[0];
  var storyLength = theText.parentStory.length;
  var strLen = myDocument.selection[0].length;

  // if the cursor at the beginning of the story, if cannot be moved backward.
  if (firstIndex == 0 && direction < 0) {
   app.activate();
   return;
  }
  // if the cursor at the end of the story, it cannot be moved forward.
  if (firstIndex + strLen >= storyLength && direction > 0) {
   app.activate();
   return;
  }

  if (myDocument.selection[0] instanceof InsertionPoint) {
   // there is no selection. so select the text from the cursor in the direction,
   // which the variable "direction" points.
   // direction = 1: move forward; direction = -1: move backward.
   if (direction < 0) {
    firstIndex -= 1;
   }
   var newRange = theParent.characters.itemByRange(firstIndex, firstIndex);
   myDocument.select(newRange);
   app.activate();
  } else if ((myDocument.selection[0] instanceof Text) ||
  (myDocument.selection[0] instanceof Word) ||
  (myDocument.selection[0] instanceof Character) ||
  (myDocument.selection[0] instanceof TextStyleRange)) {
   if (direction < 0) {
    firstIndex -= 1;
   }
   var newRange = theParent.characters.itemByRange(firstIndex, firstIndex + strLen);
   myDocument.select(newRange);
   app.activate();
  }
 }
}

// --- PN03 -------------------------------------------------------------------
function AddReduceSelectionButtons(w, PN) {
 w.grpBtm = PN.add('group');
 w.grpBtm.orientation = "row";

 w.btnExpSeletionCharBackward = w.grpBtm.add('button', undefined, '>S');
 w.btnExpSeletionCharBackward.size = [33, 23];
 w.btnExpSeletionCharBackward.onClick = function () { ReduceSelectionChar(1); }
 //
 w.btnExpSeletionCharForward = w.grpBtm.add('button', undefined, 'S<');
 w.btnExpSeletionCharForward.size = [33, 23];
 w.btnExpSeletionCharForward.onClick = function () { ReduceSelectionChar(-1); }
}

// note:
// direction = 1: the right side of the selection doesn't move, the left side of the selection move to right
// direction = -1: the left side of the selection doesn't move, the right side of the selection move to left
function ReduceSelectionChar(direction) {
 var myDocument;
 try {
  myDocument = app.activeDocument;
 }
 catch (xError) {
  return;
 }

 if (myDocument.selection.length > 0) {
  var theParent = myDocument.selection[0].parent;
  var firstIndex = myDocument.selection[0].index;
  var theText = myDocument.selection[0];
  var strLen = myDocument.selection[0].length;

  if (myDocument.selection[0] instanceof InsertionPoint) {
   // Do nothing, when it is a insertPoint
   app.activate();
   return;
  } else if ((myDocument.selection[0] instanceof Text) ||
  (myDocument.selection[0] instanceof Word) ||
  (myDocument.selection[0] instanceof Character) ||
  (myDocument.selection[0] instanceof TextStyleRange)) {
   if (direction > 0) {
    firstIndex += 1;
   }
   strLen -= 1;
   var newRange = theParent.characters.itemByRange(firstIndex, firstIndex + strLen - 1);
   myDocument.select(newRange);
   app.activate();
  }
 }
}

// --- Last Part of UI (No Panel) ---------------------------------------------
function AddCloseButton(w) {
 w.grpButtons = w.add('group');
 w.btnClose = w.grpButtons.add('button {text: "Close"}');

 w.btnClose.onClick = function () {
  w.close();
 }
}

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

Comments

Unknown said…
Hi.
Is there a way to choose word by word?
basillamacak said…
This comment has been removed by a blog administrator.
Cheng-I Chien said…
About the selection of a word.

Try to find the word in "story.words".