18 Replies Latest reply on Oct 17, 2013 2:31 PM by McShaman

    Linking a JS object to ScriptUI listbox item

    McShaman Level 1

      I am writing a script that that takes elements from a document and for each element makes a custom object with its own properties and methods. This list of elements is then spat out into a ScriptsUI listbox.

       

      How do I link each listbox item to its associated obejct? i.e. So if I double click on a listbox item it will run a particular method within that object.

       

      P.S. I dont want to have to store array ids in a column or something hacky like that.

        • 1. Re: Linking a JS object to ScriptUI listbox item
          Mac_rk Level 2

          var names = ["1", "2"];

          var w = new Window ("dialog", undefined,undefined, {closeButton: false});

          w.alignChildren = "right";

          var main = w.add ("group");

          var list = main.add ("dropdownlist", [0,0,240,20], names);

          list.minimumSize.width = 150;

          var buttons = w.add ("group")

          buttons.add ("button", undefined, "OK", {name: "ok"});

          buttons.add ("button", undefined, "Cancel", {name: "cancel"});

          if (w.show ()==1){

              if  (list.selection==0){

                  alert ("1")

                  }

              else if (list.selection==1){

                   alert ("2")

                  }

              else{

                  alert ("Select Drop list")

                  }

              }

          • 2. Re: Linking a JS object to ScriptUI listbox item
            McShaman Level 1

            This solution is no good for me as the list I will be generating is dynamic (based on elements in a docment). There is no way for me to know how many elements there will be in a users document or what they are so I cant possibly do an if statement for each itme.

            • 4. Re: Linking a JS object to ScriptUI listbox item
              McShaman Level 1

              Can you put id of an element in a column then hide the column from the user?

              • 5. Re: Linking a JS object to ScriptUI listbox item
                TᴀW Adobe Community Professional & MVP

                I can't remember offhand if you can hide a column. But why go to all

                that trouble? I would create a separate array to link the items in the

                list to your collection of objects. If the list changes, you'll just

                have to make sure to update the array.

                 

                When somebody clicks on an item in the list, you can get the index of

                that item, and then use the array to get the object you're after. It's

                difficult to go into more detail, because you haven't told us anything

                about how you're maintaining your list of custom objects. But if, say,

                you've got a separate array containing your home-made objects, and

                you've added a unique ID field to your homemade object, then the linking

                array I suggest would contain, perhaps, a list of those IDs.

                 

                Perhaps you'd need to create a method to get the object by ID.

                 

                So if the user selects the 5th item in the dropdown list, to get the

                object you're after you'd check the linking array: myObjectID =

                myLinkingArray[5] and then run the method to get the object you're after

                myObject = getObjectByID(myObjectID).

                 

                Perhaps a neater solution would be to store your homemade objects in an

                array based on key strings:

                 

                objArray[/String/myHomemadeObject.name] = myHomemadeObject; // this

                presumes that all objects have a unique name property:

                homemadeObject.name = "some unique string"

                 

                Then, in the linking array, you could simply store the list of names --

                corresponding to the order in the dropdown list.

                 

                Then, to get the object you're after, you wouldn't need a separate

                method anymore, you could just access it directly via the objArray:

                 

                theObjectImAfter =

                homeMadeObjectArray[/String/myLinkingArray[dropdownListSelection]];

                 

                This method is based on the fact that arrays can be keyed to strings, e.g.

                 

                myArray["cat"] = object1;

                myArray["dog"] = object2;

                myArray["cow"] = object3;

                 

                myObj = myArray["dog"]; // myObj now contains object2

                 

                Again, you haven't provided much by way of details, so there's an

                element of speculation here.

                 

                Ariel

                 

                PS You could probably use InDesign's ID property which I believe is

                unique for all objects in InDesign. So you're linking array could simply

                convert that to a string and access it directly:

                 

                myHomemadeObjectsArray[String(object1.ID)] = object1 and so on.

                 

                Have fun...

                • 6. Re: Linking a JS object to ScriptUI listbox item
                  TᴀW Adobe Community Professional & MVP

                  Thinking about it, theoretically you could add a custom property to each

                  listItem:

                   

                  w = new Window("dialog");

                  l = w.add("listbox");

                  with (l){

                       i = add("item", "Dog");

                       i.key = "key1";

                       j = add("item", "Cat");

                       j.key = "key2";

                       k = add("item", "Cow");

                       k.key = "key3";

                  }

                   

                  l.onDoubleClick = function(){alert(l.selection.key)}

                  w.show();

                   

                  Double click on an item to show it's key.

                   

                  So now, it's really simple: just store your homemade objects in an array

                  based on a list of string keys (or, as I mentioned, the objects' ID

                  converted to a string). Create your listbox, and for each Item, add a

                  custom property (call it .key, say) equal to the unique ID of the object

                  that the listbox item represents. Then, to get hold of the object you're

                  after, just use:

                   

                  myObject = arrayOfHomemadeObjects[listBox.selection.key];

                   

                  I think that's as direct as it can be.

                   

                  Ariel

                  • 7. Re: Linking a JS object to ScriptUI listbox item
                    McShaman Level 1

                    Yeah there is good reason why I have kept it quite broad...

                     

                    1. If I give too many details I tend to find people focus on other issues

                    2. This is more a concept that I keep encountering that I would like to fix more elegantly

                     

                    As I mentioned in my original post I don't really want to have to have a column with IDs in it (well not visible to the user anyway). Also the info in the list items may not be unique so I wont be able to use that info to identify the item.

                     

                    Your right... I could create an array of objects that reflects the items in the list and use the index to link them together... However that means every time the list changes it will have to be reflected exactly in the array.

                     

                    I would love to extend the ListItem prototype to add this property... However I have always had problems doing this with native Adobe objects

                    • 8. Re: Linking a JS object to ScriptUI listbox item
                      McShaman Level 1

                      Got it!

                       

                      From what I can tell the issue with extending native Adobe objects is not a problem with ScriptUI objects.

                       

                      So the following works:

                       

                      ListItem.prototype.targetObject = undefined;

                       

                      var myDialog = new Window( 'dialog' );

                      var list = myDialog.add( 'listbox' );

                       

                      var paraStyles = app.activeDocument.allParagraphStyles;

                      var listItems = [];

                      for( var i = 0; i < paraStyles.length; i++ ) {

                           var listItem = list.add( 'item', paraStyles[i].name );

                           listItem.targetObject = paraStyles[i];

                           listItems.push( listItem );

                      }

                       

                      myDialog.show();

                      • 9. Re: Linking a JS object to ScriptUI listbox item
                        TᴀW Adobe Community Professional & MVP

                        Yes, I've also heard that it's risky to extend the prototype with

                        ExtendScript built-in objects.

                         

                        I would intuitively guess that it's safer to do it without actually

                        creating a new property using prototype.

                         

                        So try doing it like I showed in my sample script (ie no prototype) ...

                        Just, if you like, instead of adding a .key property, add a targetObject

                        property that actually contains the paragraph style, but do this after

                        the listItem has already been added to the list. I would guess it's more

                        robust that way.

                         

                        Ariel

                        • 10. Re: Linking a JS object to ScriptUI listbox item
                          Marc Autret Level 4

                          Hi McShaman,

                           

                          Ok, but what is the advantage of your method?

                           

                          Technically, a ListItem is nothing but a structure for something-present-in-a-list-widget. It's a volatile object, strictly UI-oriented, and I don't think you gain much in storing any persistent DOM reference here. Note that the lifetime of a ListItem is at best the one of the parent ListBox (which manages the .items collection property).

                           

                          In your snippet, you have:

                          • an array of DOM specifiers (paraStyles);

                          • whose each element (i.e. reference) is duplicated into the custom targetObject property of each (volatile) ListItem;

                          • and, in addition, you maintain an array, listItems, of all these (volatile) ListItem references.

                           

                          What is the point of all these cross-references? Most will become instantly invalid as soon as you'll invoke, say, listbox.removeAll()!

                           

                          IMHO, you shouldn't interlace DOM level and UI level. Usually, DOM references are much more persistent than UI stuff, so in your case paraStyles is probably the right place to manage the data you need to render and/or update. Give more ability to that object, refine it so that it can tell the UI what to display.

                           

                          An interesting fact, for instance, is that any Array is still an Object. You can safely inject additional keys in a regular array of strings. What is cool is that the ListBox only sees the Array side when it is in initializing the list items, while your object may embed other useful data keeping on hand various correspondances such as index-to-ID, ID-to-name, etc.

                           

                          Hence, my suggestion is to implement an "array refiner" providing two main properties:

                          1. Seen as a sequential Array (from ScriptUI), it will simply output the pretty names of the objects.

                          2. Seen as an associative Object, it will maintain an index-to-specifier relationship so that one can resolve the underlying DOM object whenever needed—including in the UI scope, of course!

                           

                          Here is a raw demonstration of that approach:

                           

                          // Array refiner
                          // ---
                          function refineDomArray(/*DOM[]&*/a)
                          {
                              var i = a.length, t;
                              while( i-- )(a[i]=(t=a[i]).name),(a['_'+i]=t.toSpecifier());
                              return (t=null),a;
                          }
                          
                          // Your stuff
                          // ---
                          var pStyles = refineDomArray(app.activeDocument.allParagraphStyles),
                              myDialog = new Window('dialog'),
                              lb = myDialog.add('listbox', undefined, pStyles);
                          
                          
                          // Example: display the para style ID on dbl click
                          // ---
                          lb.onDoubleClick = function()
                              {
                              var ps = resolve(pStyles['_'+this.selection.index]);
                              alert( "Style ID: " + ps.id );
                              };
                          
                          myDialog.show();
                          

                           

                          Hope that may help.

                           

                          @+

                          Marc

                          • 11. Re: Linking a JS object to ScriptUI listbox item
                            TᴀW Adobe Community Professional & MVP

                            Kind of a mini-MVVM pattern...

                             

                            Ariel

                            • 12. Re: Linking a JS object to ScriptUI listbox item
                              McShaman Level 1

                              Hey Marc, (and Ariel as well)

                              Ok, but what is the advantage of your method?

                              Well the main advantage I suppose was that it was simple. By linking a DOM object to a ListItem I can provide DOM targeted actions items without having to use a translating middle man. Doing things like sorting and deleting ListItems is no longer a problem because the index of the ListBox does not have to match a secondary array.

                              It's a volatile object, strictly UI-oriented, and I don't think you gain much in storing any persistent DOM reference here. Note that the lifetime of a ListItem is at best the one of the parent ListBox (which manages the .items collection property).

                               

                              What is the point of all these cross-references? Most will become instantly invalid as soon as you'll invoke, say, listbox.removeAll()!

                               

                              I understand that the UI element is volatile... However I don't see how storing a reference to a DOM object here is any different to creating a secondary array for the purposes of mapping a DOM object to a UI element. Isn't this array considered just as volatile?

                              An interesting fact, for instance, is that any Array is still an Object. You can safely inject additional keys in a regular array of strings. What is cool is that the ListBox only sees the Array side when it is in initializing the list items, while your object may embed other useful data keeping on hand various correspondances such as index-to-ID, ID-to-name, etc.

                              This is interesting, have never seen a mixed associative/numeric array used in this way... However I am not sure how this will effect sorts and deleting items form the middle of the list one at a time. i.e. if item[4] is delete item[5] now becomes the new item[4] etc. so now I would have to have some functionality to also delete item['_4'] update item['_5'] (and above) to one increment less.

                               

                              I am considering it... But it managing this second array just seems awkward.

                              • 13. Re: Linking a JS object to ScriptUI listbox item
                                TᴀW Adobe Community Professional & MVP

                                It's worth googling MVVM (model, view, view-model). It's a standard

                                programming pattern, the idea behind which is to separate the UI from

                                the programming logic.

                                Possibly overkill for a little InDesign script though...

                                Ariel

                                • 14. Re: Linking a JS object to ScriptUI listbox item
                                  McShaman Level 1

                                  I'm always keen to follow best practices.

                                   

                                  Interesting read... Thanks for that Ariel.

                                   

                                  Quick question...

                                   

                                  The philosophy of MVVM seems to be content (model) - translation (view-model) - display (view). The philosophy of OOP is to keep your code modular.

                                   

                                  The content component is the easy part, I can build an object with properties and methods to set and get this data. But the translation and display components are reliant on the existence of content component to work.

                                   

                                  Normally I would bundle these concepts up into a single object... But from what I have read it seems the MVVM suggests separating these concepts. Is that right?

                                   

                                  e.g.

                                   

                                  var DataClass = function() {

                                       // Properties and set/get methods

                                  }

                                   

                                  var UIClass = function() {

                                       // Properties and methods to display user interface

                                  }

                                   

                                  var TranslatorClass = function() {

                                       // Properties and methods to communicate between DataClass and UIClass

                                  }

                                  • 15. Re: Linking a JS object to ScriptUI listbox item
                                    TᴀW Adobe Community Professional & MVP

                                    That sounds about right.

                                     

                                    I'm not sure if there's a huge advantage in a small script to working on

                                    separating these things too much. For instance, one of the advantages of

                                    this model is that you can easily switch UI's -- from desktop to web to

                                    console -- without rewriting the program each time since the main code

                                    is UI agnostic.

                                     

                                    However, in InDesign scripting, your option for a UI is pretty limited:

                                    ScriptUI. So that advantage wouldn't appear to be too relevant.

                                     

                                    Still, it does mean that implementing some sort of array to keep track

                                    of your listbox is in the "spirit" of good programming models, I suppose.

                                     

                                    Ariel

                                    • 16. Re: Linking a JS object to ScriptUI listbox item
                                      Marc Autret Level 4

                                      Yep, it seems that the Translator component of the MVVM pattern wouldn't be very useful in a ScriptUI perspective.

                                       

                                      The most asbtract pattern you have to deal with, I think, is the GenericList widget (ListBox, DropDownList, maybe TreeView?) and its relationship with what we may call a DataProvider (following Flex and .NET terminology). But ScriptUI/ExtendScript does not allow a serious level of data binding.

                                       

                                      The whole problem is to emulate something of a dynamic link so that the data provider can notify the List widget of some changing. As a response, the List then would update its items accordingly. A skeleton of this approach could be drawn using custom event notification:

                                       

                                      const EXTERNAL_EVENT_TYPE = 'myEventType';
                                      
                                      var externalProcess = function F()
                                      {
                                          if( F.targetWidget )
                                              {
                                              F.targetWidget.dispatchEvent(new UIEvent(EXTERNAL_EVENT_TYPE));
                                              }
                                      };
                                      
                                      // =====================================
                                      // UI
                                      // =====================================
                                      
                                      var w = new Window('dialog', "Example"),
                                          // register the LB as a target:
                                          lb = externalProcess.targetWidget = w.add('listbox'),
                                          b = w.add('button',undefined,"Test Change");
                                      
                                      lb.addEventListener(
                                          EXTERNAL_EVENT_TYPE,
                                          function(){ alert("Something is happening. I need to update my items!"); }
                                          );
                                      
                                      b.onClick = externalProcess;
                                      w.show();
                                      

                                       

                                      but I'm afraid this code—which, by the way, works!—leads us to an anti-pattern! Indeed, while we have to register and maintain a reference to the List widget in the external object (which is assumed to wrap the data), we still need to tell the list how to access the data provider in order to update the list items. The only benefit of the scheme above is that the external process can notify the list at any moment. Maybe this could make some sense in a non-modal context (palette), but this doesn't solve the original problem.

                                       

                                      So, intuitively, I would tend to take the opposite view: instead of making the external process trigger some event in the UI when its data change, let simply provide a generic resync() method to the list widget. The paradigm, here, is that a data provider should always be an array structure whose elements expose a toString() or whatever feature. If the list widget has a reference to some data provider, then it can load and/or resync the list items using always the same abstract routines. Then we have something purely prototypal:

                                       

                                      ListBox.prototype.load = DropDownList.prototype.load = function(/*obj[]&*/dp)
                                      // =====================================
                                      // A simple load method based on the assumption that
                                      // all dp items offer a toString() ability
                                      {
                                          // Manage a reference to the data provider
                                          // ---
                                          this.properties||(this.properties={});
                                          this.properties.data||(this.properties.data=[]);
                                          dp?(this.properties.data=dp):(dp=this.properties.data);
                                      
                                          // Vars
                                          // ---
                                          var CAN_SEP = 'dropdownlist'==this.type,
                                              n = (dp&&dp.length)||0,
                                              i, s;
                                      
                                          // Add the ListItem elems
                                          // ---
                                          for (
                                              i=0 ;
                                              i < n ;
                                              s=(''+dp[i++]),
                                              this.add(CAN_SEP && '-'==s ? 'separator' : 'item', s)
                                              // . . .
                                              // One could improve the code to support additional
                                              // keys for: multicol, images, checks, etc.
                                              // . . .
                                              );
                                      
                                          return this;
                                      };
                                      
                                      ListBox.prototype.resync = DropDownList.prototype.resync = function(/*obj[]&*/dp)
                                      // =====================================
                                      // Resync, or even reload, the data provider items
                                      {
                                          this.selection = null;
                                          this.removeAll();
                                          return this.load(dp||null);
                                      };
                                      
                                      ListItem.prototype.get = function()
                                      // =====================================
                                      // Return an object instance from the DP (behind this ListItem)
                                      {
                                          return this.parent.properties.data[this.index] || null;
                                      };
                                      

                                       

                                      From that point, what could the client code look like? We basically have two options:

                                       

                                      #1 The process is non-modal and then the external object will invoke List.resync(…) when required; in this case it MUST have a reference to the widget.

                                       

                                      #2 The process is modal, meaning that the 'external' object in fact is entirely interacted from the UI (e.g. via a button click); in that case no external reference to the list widget is actually required since the UI module itself knows exactly when a List.resync() is required, so why should it delegate the job? Here is a basic example in such a context:

                                       

                                      // Let's have some arbitrary 'OOP' stuff available
                                      //--------------------------------------
                                      var MyClass = function(uid,str)
                                      {   // constructor
                                          this.id=uid||0;
                                          this.rename(str||'');
                                      };
                                          // various methods
                                      MyClass.prototype.rename = function(str){ this.name=str||'<unknown>'; };
                                          // . . .
                                          // toString()
                                      MyClass.prototype.toString = function(str){ return this.name; };
                                      
                                      var myDataProvider = [
                                          // some array of instances
                                          new MyClass(3, "Name 3"),
                                          new MyClass(5, "Name 5"),
                                          new MyClass(7),
                                          new MyClass(11, "Name 11, OK?")
                                          ];
                                      
                                      var processChanges = function()
                                      {   // emulate various changes in the data provider
                                          myDataProvider[0].rename("New name (3)");
                                          myDataProvider[2].rename("Finally Born 7");
                                          myDataProvider[myDataProvider.length] = new MyClass(13, "Did you miss 13?");
                                          myDataProvider[myDataProvider.length] = new MyClass(17, "Hello 17");
                                          myDataProvider.splice(1,1);
                                      };
                                      
                                      // Now the User Interface:
                                      //--------------------------------------
                                      var w = new Window('dialog', "Example"),
                                          lb = w.add('listbox').load(myDataProvider),
                                          b = w.add('button', undefined, "Test Changes!");
                                      
                                      lb.onDoubleClick = function()
                                          {
                                          var someInstance = this.selection.get();
                                          alert( "My secret UID is: " + someInstance.id );
                                          };
                                      
                                      b.onClick = function()
                                          {
                                          processChanges();
                                          lb.resync();
                                          };
                                      
                                      w.show();
                                      

                                       

                                      Does it make sense?

                                       

                                      @+

                                      Marc

                                      • 17. Re: Linking a JS object to ScriptUI listbox item
                                        TᴀW Adobe Community Professional & MVP

                                        Marc, this is a great post. As usual, though, it takes work to go

                                        through. So I put it aside for a spare moment to look at carefully, and

                                        then tend to forget about it.

                                         

                                        But thanks for sharing this.

                                         

                                        Ariel

                                        • 18. Re: Linking a JS object to ScriptUI listbox item
                                          McShaman Level 1

                                          Thanks for all that effor Marc, its is going to take me a while for me to understand completly. But I am going over it bit by bit.