Webview intergration Part 3

In the first two parts, we looked at integrating JavaScript/jQuery plugins and how to establish hooks between WebView and FileMaker.

In this part, we will take a closer look at these hooks using a more complex example.

I chose the sortable/portlets plugin (https://jqueryui.com/sortable/#portlets) because it fascinated me from the first moment. It not only displays information but also allows free arrangement, providing a very flexible way of presenting information. Many of my solutions are based on this great plugin. See my Kanban (http://iconiccoding.com/2020/12/22/kanban/) or the Week/Month Planner (http://iconiccoding.com/2022/12/29/week-month-planner).

I have already written an entire article about this plugin (http://iconiccoding.com/2021/03/13/jquery-portlets) and will use the solution described there as a basis here.

For more information on what this plugin is and how to integrate it, please refer to this article, as I want to focus here on how to find and connect the hooks between plugins and FileMaker.

OK, let’s dive in by looking at the documentation of the jQuery plugin.
https://jqueryui.com/sortable/#portlets

OPTIONS are properties that can be added to the Portlets.
METHODS are functions that can be used within the Portlets.
EVENTS are used to capture events triggered by the Portlets.

When I see a plugin that I want to integrate, I always first ask myself which hooks (i.e., calls from the WebView to FileMaker) I need. For this plugin, Portlets are sorted within and between columns via drag and drop, and the details can be shown or hidden by collapsing/expanding.

Additionally, there should be a “+ Create Card” element in the columns that creates a new Portlet in the column when clicked.

Furthermore, a dialog for editing Portlet fields in FileMaker should open when clicked.

So, we need hooks for a click event and at the end of a drop event that will call a script in FileMaker.

To follow the next steps, please refer to the HTML from this article: http://iconiccoding.com/2021/03/13/jquery-portlets

Let’s start with the simplest one, collapsing and expanding, as this does not pass any calls to FileMaker.

To collapse and expand the Portlets, there is already a routine, so there is nothing further for us to do here:

$( ".portlet-toggle" ).on( "click", function() {
var icon = $( this );
icon.toggleClass( "ui-icon-minusthick ui-icon-plusthick" );
icon.closest( ".portlet" ).find( ".portlet-content" ).toggle();
});

But it’s easy to see how simple it is in jQuery to create an event handler for a mouse click on objects:
Since we want to use the function for all Portlets here, not just for a specific one, we use a class of Portlets:

$( ".classname ).on( "click", function() { // handler code here });

For detailed information on how these handlers work, please refer to the jQuery documentation: https://api.jquery.com/on/

For now, let’s just understand that all our Portlets trigger an event when clicking on the graphic element with the class “portlet-toggle.” This event toggles the graphic element:

icon.toggleClass( "ui-icon-minusthick ui-icon-plusthick" );

and toggles the content of the Portlet in or out:

icon.closest( ".portlet" ).find( ".portlet-content" ).toggle();

The first hook we want to create ourselves is the call of a card window for the details in FileMaker when clicking on a Portlet. So, we need a very similar handler to the one described above, but of course, we want to target a different class within the Portlet. Since the user expects a detail window when clicking on the body of the Portlet, let’s take a closer look at the DIV of the Portlet:

<div class="portlet" id=12345>
<div class="portlet-header">Feeds</div>
<div class="portlet-content">Lorem ipsum dolor sit amet, consectetuer adipiscing elit</div>
</div>

We see that in addition to the class “portlet-header” for the header with the title line, there is a class “portlet-content” for the detail text. (If not, you can always assign a class to an object yourself to be able to address it). Now let’s create a small event handler that returns a call to a FileMaker script:

$( ".portlet-content" ).on( "click", function( e ) {
var id = $(this).closest('.portlet').attr('id');
$( self.location = '__script&param=selectItem||||' + id );
});

Let’s go through this handler line by line:

$( ".portlet-content" ).on( "click", function( e ) {

This calls this handler for all objects with the class ‘portlet-content’ when a Click Event occurs.
The (e) in function( e ) stands for the event passed to this handler, which is the Click event in this case.
The line

var id = $(this).closest('.portlet').attr('id');

is significant. Here, the variable ‘id’ is assigned the id of the object that was clicked.
$this refers to the object that was clicked. But since we need the id of the portlet because we want to address a specific portlet now, we need to ‘walk’ to the next (closest) object of the class ‘portlet’.

.closest('.portlet')

There we then get the attribute ‘id’.

.attr('id')

Then we use the hook

$( self.location = '__script&param=selectItem||||' + id )

to call a FileMaker script.
Alternatively, you could use

FileMaker.PerformScript ( '__script', 'selectItem||||' + id )

to call a script. But as I mentioned before, I’ll explain later why I’m not using it here.
We pass an action and the Portlet id as parameters to the script, separated by 4 pipes. Four pipes are already a very secure separator that normally does not occur in any text, but in general, you can use any string here; it just needs to be defined in the FileMaker script.

With this method, we set more hooks in the code. Here, we always use a class within the Portlet as the trigger because we want to provide this function for all Portlets and then always call the same script in FileMaker with different actions and the respective Portlet Ids.

In the FileMaker script, find the corresponding record for the Portlet and display it in a card window so the user can make changes.

For creating a new Portlet, I simply created a small Portlet that is always displayed at the beginning of the column and consists only of a header. I assigned the classes “function-header disabled” to it. Classes are separated by spaces here.

Very simple as it brings along all properties of a portlet but looks like a button, yet cannot be dragged.
Here, too, I have set up a simple hook:

$( ".function-header" ).on( "click", function( e ) {
var id = $(this).attr('id');
$( self.location = '__script&param=newItem||||' + id );
});

Here, you don’t need to traverse the hierarchy to get the id of the parent object (as we did with the portlets) but can directly query the id of the object:

var id = $(this).attr('id');

Now, let’s focus on the most important function of this jQuery plugin, sorting within and between columns.
To place the hook correctly, one needs to figure out at what point it can collect all data and how to access that data.

When dropping, you can again refer to the documentation to find out which events are executed at the right time, but if I remember correctly, I had to seek help multiple times on Stack Overflow, so don’t give up right away if you don’t find the right event immediately.

I then used the Events/Update and Events/Stop to set the hook for the FileMaker script.
These events are integrated into the portlet as follows:

$( ".column").sortable({
update: function( event, ui ) {
var changedList = this.id;
var order = $(this).sortable('toArray');
var positions = order.join(';');
output = changedList + '[' + positions + ']'
},
stop: function( event, ui ) {
var id = ui.item.attr("id");
$( self.location = 'fmp20://$/Default Portlets?script=Return_Portlet&param=reorder||||' + output + id );
}
});

Here, let’s go through each line one by one, and I must say now that as a FileMaker programmer, I have often consulted Stack Overflow and other sources on the internet until it worked.
And I’m sure that many programmers with greater talent and better knowledge in JavaScript will be bewildered when they see my solution, but it works for me.

In the documentation of the Update event, it states: “This event is triggered when the user stopped sorting and the DOM position has changed.”
So, after the drop is completed, we can retrieve data from the DOM object.
The Stop event, on the other hand, is triggered “when sorting has stopped.”

So, we retrieve data with the first event into variables and call the FileMaker script with the second event because we then have easy access to the id of the dropped portlet.

In the Update event, we define with a function that we write the target column into the changedList variable:

var changedList = this.id

Then, we retrieve the new order of the portlets in the target column as an array, see documentation Methods/toArray.

var order = $(this).sortable('toArray');

and replace the comma in the delivered list with a semicolon.

var positions = order.join(';')

Then, we create a variable that writes this array into square brackets so that we can separate it more easily as a parameter in the FileMaker script.

output = changedList + '[' + positions + ']'

In the Stop event, we can then use this output variable since the Update event always happens before the Stop event.

The Stop event simply retrieves the id of the dropped portlet.

var id = ui.item.attr("id")

and then calls the FileMaker script with the final parameter using a hook.

$( self.location = 'fmp20://$/Default Portlets?script=Return_Portlet&param=reorder||||' + output + id )

The parameter has the following format:

Action||||ColumnTargetId[SortedTargetColumnPortletsIds]DraggedPortletId

for example,

reorder||||2[22;1;17]17

With this, we can then write the data back into variables in the FileMaker script and adjust the portlet FileMaker records.
Since we get a sorted list of portlet ids including the dropped portlet, we can simply go through them and increase the sort for the portlets by 1 with each iteration.

# Return_Portlet in file Default Portlets

Set Variable [ $parameter ; Value: Substitute ( GetValue ( Get ( ScriptParameter ) ; 1 ) ; "||||" ; ¶ ) ]
Set Variable [ $function ; Value: GetValue ( $parameter ; 1 ) ]
Set Variable [ $value1 ; Value: GetValue ( $parameter ; 2 ) ]
Set Error Capture [ On ]
If [ $function = "reorder" ]
#Get target Column Id
Set Variable [ $idColumn ; Value: GetValue ( Substitute ( $value1 ; "[" ; ¶ ) ; 1 ) ]
#Get actual portlet Ids from target column (including dropped portlet)
Set Variable [ $idList ; Value:
Let (
[
Var1 = Substitute ( $value1 ; ["[" ; ¶] ; ["]" ; ¶] ) ;
Var2 = GetValue ( Var1 ; 2 ) ] ; Substitute ( Var2 ; ";" ; ¶ ) )
]
#Get dropped portlet Id
Set Variable [ $idDropped ; Value:
Let (
[
Var1 = Substitute ( $value1 ; ["[" ; ¶] ; ["]" ; ¶] ) ;
Var2 = GetValue ( Var1 ; 3 ) ] ; Var2 )
]
Set Variable [ $m ; Value: ValueCount ( $idList ) ]
If [ $m > 0 ]
Set Variable [ $z ; Value: 1 ]
Set Variable [ $sort ; Value: 1 ]
Loop
Set Variable [ $id ; Value: GetValue ( $idList ; $z ) ]
#Set global field for relation to portlet
Set Field [ Team::g_portlet_Id ; $id ]
Commit Records/Requests [ Skip data entry validation ; With dialog: Off ]
#Set (new) Column
Set Field [ Team~Portlets#G_portlet_Id::idColumn ; $idColumn ]
#Set portlet sort
Set Field [ Team~Portlets#G_portlet_Id::sort ; $sort ]
Set Variable [ $sort ; Value: $sort+1 ]
Set Variable [ $z ; Value: $z+1 ]
Exit Loop If [ $z > $m ]
End Loop
Set Field [ Team::g_portlet_Id ; "" ]
End If
Commit Records/Requests [ With dialog: Off ]
Go to Object [ Object Name: "webview" ]
Else If [ $function = "selectItem" ]
Set Field [ Team::g_portlet_Id ; $value1 ]
Commit Records/Requests [ With dialog: Off ]
New Window [ Style: Card ; Using layout: “Portlet Card” (Portlets) ]
Go to Related Record [ Show only related records ; From table:
“Team~Portlets#G_portlet_Id” ; Using layout: “Portlet Card” (Portlets) ]
Adjust Window [ Resize to Fit ]
Else If [ $function = "newItem" ]
Set Variable [ $max ; Value: ExecuteSQL ( "SELECT MAX(sort) FROM Portlets
WHERE idColumn=?" ; "##" ; ¶ ; $value1 ) ]
New Window [ Style: Card ; Using layout: “Portlet Card” (Portlets) ]
Go to Layout [ “Portlet Card” (Portlets) ; Animation: None ]
New Record/Request
Set Field [ Portlets::idColumn ; $value1 ]
Set Field [ Portlets::sort ; $max +1 ]
Commit Records/Requests [ With dialog: Off ]
Else If [ $function = "deleteItem" ]
Show Custom Dialog [ "Message" ; "Delete ?" ]
If [ Get ( LastMessageChoice ) = 2 ]
Exit Script [ Text Result: ]
End If
Delete Record/Request [ With dialog: Off ]
Close Window [ Current Window ]
Refresh Window []
Else If [ $function = "saveItem" ]
Close Window [ Current Window ]
Refresh Window []
End If

The script above also shows the very simple other actions that are called using hooks, which I will spare explaining here.

At this point, I conclude this part of the series and hope that the explanation is not too complicated.

In summary, I would like to say:
Implementing hooks can be a challenge for a non-Java/jQuery savvy programmer like me.
Don’t be discouraged if it doesn’t work right away. I myself have spent days scouring the internet for some hooks.
Try to read the documentation – yes, I know, it’s not my favorite activity either!
Consult the internet (https://stackoverflow.com, https://www.w3schools.com, etc.)
and now also ChatGPT – Although it always says it can’t program, it actually can 🙂

There will probably be another part of this series in which I present other plugins (not from https://api.jqueryui.com) and how to integrate them.
But I can’t say how quickly I’ll get to it.

One more thing:
Oh, and why do I prefer $( self.location ) over FileMaker.PerformScript ( ) ?
The answer is once again that apparently FileMaker has not fully implemented their call.
Namely, if you embed a call via HTML link in our example in the portlet content, it will not be executed when clicked if you use FileMaker.PerformScript ( ) for the enclosing portlet’s Click Event.
Why this is the case, FileMaker would have to explain, but in any case, it’s very annoying.

As always, the following applies to this article:
You have any questions? Feel free to send me an email, I’ll try to answer as soon as possible.