Skip to main content
Inspiring
May 25, 2023
Answered

Validating Data in ScriptUI

  • May 25, 2023
  • 10 replies
  • 8119 views

I was referred to @Peter Kahrel excellent tutorial on ScriptUI located here https://creativepro.com/files/kahrel/indesign/scriptui.html, but I am having a few issues with my scripts:

  • On page 93, he has an example with this code:

 

w.input = w.group.add ('edittext {characters: 10, active: true, justify: "right"}');​

I don't see where "characters: 10" is explained. I thought it would limit the input to 10 characters, but it does not seem to, either in his example or in my testing.  Is there a way to do this? One of my input fields needs to be 7 characters and one of them needs to be 8 character date in format YYYYMMDD.

 

  • For the field that needs to be 7 characters, I use an Apply instead of an Okay button and I used his red-background code (and modified it to NOT allow commas or decimals). But I don't want the apply button enabled unless the field has seven characters - not 8 or 6. I tried this and it doesn't work:

 

            win.Panel2.txt2.onChanging = function () {
                var valid = /^[\d]+$/.test (win.Panel2.txt2.text);
                this.graphics.backgroundColor = this.graphics.newBrush (this.graphics.BrushType.
                SOLID_COLOR, valid ? [1, 1, 1, 1] : [1, 0.5, 0.5, 1]);
                win.Panel2.ApplyBtn.enabled = valid && win.Panel2.txt2.text.length=7;              
                }​

 

  • Similarly for the date field, I found this: https://www.scaler.com/topics/date-validation-in-javascript/ and I verified Date.parse() generates NaN for an invalid date in the format above and a number for a valid date. I want the apply button to be enabled only if I have a valid date in the field, and I want the red text if the field contains anything besides numbers. I tried this and it doesn't work - the Apply button is never enabled:

 

            win.Panel2.txt3.onChanging = function () {
                var valid = /^[\d]+$/.test (win.Panel2.txt3.text);
                this.graphics.backgroundColor = this.graphics.newBrush (this.graphics.BrushType.
                SOLID_COLOR, valid ? [1, 1, 1, 1] : [1, 0.5, 0.5, 1]);
                var valid2 = Date.parse(win.Panel2.txt3.text);
                win.Panel2.ApplyBtn.enabled = !valid2==NaN;              
                }​

 

 

I'll probably have more questions as I get further into my script, and I'll add them to this thread, if that is okay.

 

(The forum gave me an error about invalid HTML that it removed, but I'm not seeing what it changed).

 

Thanks in advance!

This topic has been closed for replies.
Correct answer Marc Autret

@Marc Autret (or others), sorry to keep bothering you, but I have one field in which the Stack Overrun problem is re-occurring and I can't figure out how to work around it.
For this field, there will likely be dashes and numbers, and all letters should be uppercase, except the letter "v" which should always be lowercase. And I want to remove the letter X as it is initially in the field, but is not used. It should never be more than 30 characters, but it could be less.

I'm using this code without the textselection and it works, except it wraps back to the beginning of the field:

    win.Panel2.txt1.onChanging = function(t){
        win.Panel2.ApplyBtn.enabled = false;
        win.Panel2.CancelBtn.enabled = true;
        t = this.text;
        t=t.toUpperCase();
        t=t.replace(/V/g, "v");
        t=t.replace(/X/g, "");
        win.Panel2.txt1.text = t
        t = this.text;
        if(t.length>30){
            t=t.slice(0,30);
            this.text = '';
            this.textselection =t;
        }
        win.Panel2.ApplyBtn.enabled = true;
        }          

This gives me the stack overrun error (I tried a lot of variants with no success, but  this is one of them that fails):

 win.Panel2.txt1.onChanging = function(t){
        win.Panel2.ApplyBtn.enabled = false;
        win.Panel2.CancelBtn.enabled = true;
 t = this.text;
        if(/\w+/.test(t)){      
            t=t.toUpperCase();
            t = t.replace(/V/g, "v");
            t=t.replace(/X/g, "");
            this.text = '';
            this.textselection = t; 
        }
        t = this.text;
        if(t.length>30){
            t=t.slice(0,30);
            this.text = '';
            this.textselection =t;
        }
         var valid1 = /[^-0-9]+/.test (win.Panel2.txt1.text);
        this.graphics.backgroundColor = this.graphics.newBrush (this.graphics.BrushType.
        SOLID_COLOR, valid1 ? [1, 1, 1, 1] : [1, 0.5, 0.5, 1]);
        win.Panel2.ApplyBtn.enabled = valid1;   
        }   

Assistance will be greatly appreciated!!!


Hi @Marshall_Brooks 

 

Sorry I don't understand your code 😕😕 I'm afraid I've already detailed everything I can say about my own approach, so the code below basically just repeats the same logic.

 

Some advice to prevent infinite loops and other errors: Keep your objects encapsulated, only play with text/textselection once (and only if needed!), do not take the risk of reintroducing a changing event while you modify the text, do not resolve again the EditText instance (it is and remains this within the event handler, that's all you need), avoid external object references as much as possible, work with local strings until you have to finally update—once!—the text of the control.

 

Steps: 1. normalize the input (remove junk characters, adjust letter case, size, etc); 2. if the normalized string is not the input string, update this.text through the textselection trick; 3. then, check whether the string is strictly valid and update the button state accordingly.

 

    <yourEditText>.onChanging = function(t,s)
    {
         t = this.text;

         // Normalize.
         s = t && t.slice(0,30).toUpperCase()
         .replace(/[^-0-9A-WY-Z]+/gi,'')
         .replace(/V/gi,'v');

        // Need filtering?
        if( s !== t )
        {
            this.text = '';
            this.textselection = s;
        }

        // Valid?
        t = 30===s.length;

        <yourOkButton>.enabled = t;
        this.graphics.backgroundColor = BH[+t]; // Using the [OK,KO] brush array.
    };

 

There are certainly other ways to approach the question. If my scheme doesn't work for you, I'm sure my colleagues on this forum will provide you with better options.

 

Best,

Marc

10 replies

Peter Kahrel
Community Expert
Community Expert
June 6, 2023

onMouseover is not a radiobutton event. . .

Inspiring
June 6, 2023

That would explain it!!!

I can do it from the click event.

Complicated question:

User clicks Button 1 and makes changes. Apply button is enabled. User does not click Apply. User Clicks Button 2.

else if(win.radioPanel.rad02.value) { 
            if (win.Panel2.ApplyBtn.enabled){
            if (confirm("Do you want apply the changes?") ===false) {
                win.Panel2.ApplyBtn.enabled = false;
            }
            else{
                return;                             
            }
         }

I want to replace the return statement with the same steps as the reply button.

If I can't, it works, but it shows Button 2 selected with the Button 1 info in the input boxes. If there anything simple like Return that would change it back to Button 1 (or the previously selected button?)

Inspiring
June 21, 2023

@Peter Kahrel - I ran into a new issue, and I hope I'm missing something obvious, but I'm somewhat afraid I'm not.

Now that the code is pretty much working, I decided I might try making the window borderless.

I looked at Pages 8 and 9 of the guide.

I can make the window borderless, but I agree, it looks plain with no border on it.

I tried:

w = new Window ("dialog", undefined, undefined, {borderless: true});
w.margins = [0,0,0,0];
myPanel = w.add ("panel");
myPanel.add ("statictext", undefined, "borderless: not quite true");
w.show ();

And it looks like Peter's example. There is a THIN gray border around the window, but not really noticeable compared to no border at all. (It also locks up FrameMaker and when I click the blue square, it says FM did not respond and I have to close and re-open it manually).

I tried:

w = new Window ("dialog", undefined, undefined, {borderless: true});
w.margins = [0,0,0,0];
myPanel = w.add ("panel", undefined,"",{borderStyle:'black'});
myPanel.add ("statictext", undefined, "borderless: not quite true");
w.show ();

And that gives me more what I am looking for (and also locks up FM).

However, I have a bounded window since I wanted my panels side-by-side, so my window looks like this:

var win = new Window("palette", "Update Document Variables:", [$.screens[0].right/2 - 255,$.screens[0].bottom/2 - 143, $.screens[0].right/2 + 255, $.screens[0].bottom/2 + 143],{borderless:true}); // bounds = [left, top, right, bottom] Width - 505 Height 285
    win.margins = [0,0,0,0];
//    myPanel = win.add ("panel", undefined,"",{borderStyle:'black'});    
    myPanel = win.add ("panel",[0,0,200,200],"",{borderStyle:'black'});
    win.radioPanel = win.add("panel", [5, 5, 230, 250], "");
    win.Panel2 = win.add("panel", [235, 5, 505, 250], "");
    win.okBtn = win.add("button", [187,255,277,280], "OK"); //Centered on divider
	win.okBtn.Active="True"
    win.defaultElement=win.okBtn;
	win.okBtn.onClick = function() {
	    win.close();
     }    
    win.show();

The commented line didn't seem to do anything. The new bounded panel looks like what I want, but it is covering over my other panels and controls.

Is there a way to make the new panel transparent?

If not, I think I can get the effect I want by:

  • Changing the panel name to P3.
  • Expanding the bounds of P3 to match the window dimensions.
  • Moving all my controls to be on top of P3 by doing a find and replace on "win." and carefully replacing with "win.P3." (So win.Panel2 becomes win.P3.Panel2.), while being careful to NOT change lines like win.Close(); and win.Show(); to win.P3.Close();

Is there an easier solution?

Peter Kahrel
Community Expert
Community Expert
June 5, 2023

What I would do is this:

- Assume that a user will have the application (FM) always on the same screen.

- The script looks for a config file. When it first runs the config file isn't there, so it places its window at the centre of the first screen (wherever, really, doesn't matter).

- The user drags the script window to some other place.

- When the use clicks OK, before executing whatever needs to be done, the script writes a text file that contains the location of the window. And you can add all the user selections.

- Next time the script runs it reads the config file and places the script window where the user last put it. And adds all the selections the user made earlier.

Inspiring
June 5, 2023

@Peter Kahrel - That is clever. That is very, very clever. Unfotunately, I see quite a few problems with it:

  • I don't think assuming FM will always be on the same screen is a valid assumption. Lots of our users use laptops, sometimes with an external monitor, sometimes not. It might or might not work for me. Screen0 works for me, b/c I have the laptop screen turned off when I use the (dual) external monitors.  But if I had a single large screen montior, I would probably use FM on it, but still have the laptop screen up - possibly with the laptop as Screen0.
  • I don't think most users are going to think to drag the script window to some other place. I think they will be similar to either "Where was the window?" or "Oh, it's on the smaller screen. I thought it would be on the main one. Oh well ..."
  • If the user does drag the window to the larger external monitor and then starts again with the laptop only, I "think" the script will read the info from the text file and display the script window on the large screen that is not connected and thus not visible.

As much as I hate to admit it, I think my best option is to display the window on Screen0 and if someone uses a second display and uses FM on it, they'll have to adjust to the window being on the wrong screen - which is what happens when I have the window auto-center without specifying parameters.

 

I appreciate the thoughts and feedback, though!!!

Inspiring
June 6, 2023

Ran into another snag. I'll try to be brief.

I have a pop-up palette window with radio buttons on the left and input boxes and an apply button on the right. You click a radio button and the script reads values into the input boxes and you click Apply and it writes the input box values to variables. That works fine.

I wanted to add a "MouseOver" function so that if the apply button was enable, but was not clicked, you would get a confirmation dialog to drop the changes BEFORE the button click was applied.

 

The "click" code looks like this:

win.radioPanel.rad01.onClick = win.radioPanel.rad02.onClick = win.radioPanel.rad03.onClick = function () {

		if(win.radioPanel.rad01.value) { 
// Do stuff;
}
}

I tried to add something similar for the first button:

      win.radioPanel.rad01.onMouseover = function(){
          if (win.radioPanel.rad01.value){
            alert("mousedover!");
            }
}

But it doesn't seem to be doing anything - I never get the alert - unless I made a typo.

I tried onmouseover and onMouseOver and without the if statement and no luck - but no real error messages either.

Peter Kahrel
Community Expert
Community Expert
June 5, 2023

> if I am writing for other users, I have no way to know what resolution they are using

 

Well, there's your answer. You can't tell which screen FM is on, so you can't decide where to place the script window.

Inspiring
June 5, 2023

I'm afraid you are correct.

It seems pretty basic to want to be able to display a window on the same screen as the application is running on.

I found three things, not sure any of them are useful:

  • app.ScreenHeight and app.Screen Width refer to the FM window, not necessarily the FM monitor - i.e. if my monitor is 1920x1080, but I have FM scaled down to 800x900, the app.ScreenWidth is 800.
  • There is an app.DisplayName, but I don't know how it works. alert(app.DisplayName); showed a blank message.
  • There is an app.IsOnScreen - returns 1 if the app is visible - but I don't know that that means you can use something like If (Screen0.app.IsOnScreen){}
Peter Kahrel
Community Expert
Community Expert
June 5, 2023

The trouble is that (as far as I know) you won't be able to figure out whether FM is on the second monitor. Once you figured that out, then yes, you can place a window on any screen.

Inspiring
June 5, 2023

Does what I posted in the other thread help? I can tell the resolution of the monitor that FM is on or that the document is on.  If monitor 2 was a different resolution than monitor 1, I could test for that.

But for me, both monitors are the same resolution and if I am writing for other users, I have no way to know what resolution they are using ...

(But I could guess ... i.e. is screen0.bottom == App.ScreenHeight, display on screen 0, else if screen1.bottom == App.ScreenHeight, display on screen1.)

But again, fails if screen0 and screen1 have the same resolution.

Peter Kahrel
Community Expert
Community Expert
June 5, 2023


> Can I call a universal function - i.e.

> w.input.onChanging = DoSomething();

> function DoSomething(){
> }

Yes.

> does ScriptUI (or more likely ESTK) support optional variables for functions

Yes. How it works exactly you can look up or determione by experiments.

Inspiring
June 5, 2023

Thanks!  Does anyone know how to put the ScriptUI pop-up window on the second monitor if FM is on the second monitor?

Peter Kahrel
Community Expert
Community Expert
May 31, 2023

Unfortunately, it moves the cursor back to the beginning after it does the replacement

 

Yes, that's annoying. Not much you can do about it I don't think. Unless @Marc Autret has an idea.

Marc Autret
Legend
June 3, 2023

Playing with text then textselection, in that order, may solve the cursor issue in EditText widgets. That's a bit of black magic so I do not guarantee the universality of the following code:

 

   // . . .
   // Your UI components (w, w.input, etc)
   // . . .

   // Predeclare brushes
   var gx = w.input.graphics;
   var BH =
   [
   gx.newBrush(gx.BrushType.SOLID_COLOR, [1.0, 0.5, 0.5, 1]), // KO
   gx.newBrush(gx.BrushType.SOLID_COLOR, [1.0, 1.0, 1.0, 1])  // OK
   ];

   // Text changing handler
   w.input.onChanging = function(  t)
   {
      t = this.text;
      if( !/^\d{0,7}$/.test(t) )
      {
         t = t.replace(/\D+/g,'').slice(0,7);
         this.text = '';
         this.textselection = t;
      }

      t = 7===t.length;
      w.ok.enabled = t;
      this.graphics.backgroundColor = BH[+t];
   }
   
   // . . .
   // Other stuff, w.show()
   // . . .

 

@Marshall_Brooks 

 

Note 1. — Dispatching new KeyboardEvent instances does not work in ScriptUI in the way you expect it. We tried this strategy for a very long time to work around some bugs, but it only produces logical events without any impact in the UI. Put more briefly, we have never succeeded in emulating keystrokes in a SUI control (although we can positively receive and analyze KB user events through a "keydown" listener).

 

Note 2. — To my knowledge, the property EditText.graphics.backgroundColor is no longer effective in CC (?) but it may depend on the OS (?) [Still works in CS3-CS6 though.]

 

Hope that helps.

 

Best,

Marc

Inspiring
June 5, 2023

@Marc Autret - That works amazingly - exactly what I was asking for. I don't really understand it, but I can make it work. Thank you SO much!!!

Playing with text then textselection, in that order, may solve the cursor issue in EditText widgets. That's a bit of black magic so I do not guarantee the universality of the following code:

Works fine on my system - as long as it works on Windows in FM15 and up, I'm fine. (Actually, as long as it allows input on those systems, I'm okay with it. (And ESTK seems a bit too good at ignoring errors - i.e. many times my code runs (improperly), if I just didn't define something properly. VBA would have thrown an error - but that has plusses also.

Note 2. — To my knowledge, the property EditText.graphics.backgroundColor is no longer effective in CC (?) but it may depend on the OS (?) [Still works in CS3-CS6 though.]

 Pity - it is working for me and I really like the effect.  I assume CC is Creative Cloud and CS is Creative Suite.  How can I know what I am running. It is working for me in Win10 and I see from time to time that Creative Cloud has been updated, so either I have CC but am using something else, or it is working again.

Peter Kahrel
Community Expert
Community Expert
May 25, 2023

Here's a sample of how to enable a button only when a text field's content has a certain length:

 

 

w = new Window ('dialog');
  w.field = w.add ('edittext {characters: 10, active: true}');
  w.button = w.add ('button {text: "Apply"}');

  w.field.onChanging = function () {
    w.button.enabled = w.field.text.length === 7;
  }

w.show();

 

 

You'd probably need to set the burron's state when the script is started, but I didn't add that here because the field might be populated from a history file.

 

(But I now see that you already fixed it. Mustard after the meal. Sorry.)

 

P.

Inspiring
May 25, 2023

Actually, stupid question time ...

I've done much more in Visual Basic, thus =7 seemed logical to me. I changed it to ==7 and that worked. You said ===7.

What are the differences and when would I use either one?

Peter Kahrel
Community Expert
Community Expert
May 26, 2023

= is used to assign a value to a variable.

== and === test equality. === is used for strict equality, which means that the two items you compare must be the same type. == is for looser comparisons. == is slower because it tries to coerce the two items into type-equality.

 

Example:

 

var a = 5;

var b = '5';

a == b; // => true

a === b // => false

 

a is a number, b is a string. Strict comparison returns false because the variables are not the same type. Loose comparison returns true, it successfully coerces the variables into the same type (string or number, I don't know which).

 

Because strict comparison evaluates considerably quicker you should use it where you can. Especially in InDesign's DOM, when you test some enumeration repeatedly. For example

 

if (myPage.side === PageSideOptions.LEFT_HAND) {

 

is considerably quicker than

 

if (myPage.side == PageSideOptions.LEFT_HAND) {

 

If you don't know whether the two items to be compared are type-compatible, use ==

Peter Kahrel
Community Expert
Community Expert
May 25, 2023

> I don't see where "characters: 10" is explained.

 

Well, the PDF has an index, which has an entry for 'characters' and guides you to p. 11, where it says:

 

> The characters property is used to set the control’s width

 

The control's width is not the same as the number of characters that you can enter in the control, but I'll agree with you that there's room for confusion here.
You can monitor what's entered into a control ('field'), there are some examples of that in the PDF.

 

> But I don't want the apply button enabled unless the field has seven characters

 

Take a look at this script: https://creativepro.com/files/kahrel/indesign/grep_query_manager.html
There's an option to save a configuration as a preset. The interface has two buttons: Save and Cancel. While you're typing a preset name, if what you type matches an existing preset, the Save button's text changes to Replace. It's in the function getValidName

 

> I'll probably have more questions as I get further into my script, and I'll add them to this thread, if that is okay.

 

Absolutely!

 

Inspiring
May 25, 2023

Ran into a hiccup with the date. Date.parse() will not work for me. 20230523 is valid and what I want, but 05232023 is also valid, but NOT compatible for my purposes. There is code further up the page that should work, but it doesn't seem to work with ScriptUI/ESTK.

Inspiring
May 25, 2023

I figured out the date validation code and posted it in the other thread related to this:

https://community.adobe.com/t5/framemaker-discussions/extendscript-and-date-formats/td-p/13798426/page/2

Inspiring
May 25, 2023

Figured out the first issue:

win.Panel2.ApplyBtn.enabled = valid && win.Panel2.txt2.text.length=7;

needs to be:

win.Panel2.ApplyBtn.enabled = valid && win.Panel2.txt2.text.length==7;

 Double == before the 7