This page describes how to program a Web Browser Object, including how to send JavaScript to the object, and how JavaScript within the web page can trigger Panorama code.

WKWebView vs. WebView

At the time this was written (Dec 2022), macOS includes two different classes for displaying web content: the original WebView class, and the more modern WKWebView class. Beginning with Panorama X 10.2 b31, Panorama supports both of these classes. By default, Panorama will use WKWebView if the computer is running macOS 10.13 or later, while using the older WebView class if running 10.12 or earlier. You can, however, change the default using the Preferences>General panel, and you can also select the class for an individual Web Browser Object.

Unless indicated otherwise, the programming features described below are only available when the WkWebView class is being used.

Web Browser Object Identifier

Many of the features described below are accessed by using the objectaction statement. This statement performs actions on a specified object. Usually the object is specified by it’s name, which means that the object must have a name. To learn how to assign a name to an object, see Object Names.

For example, suppose the current form as a Web Browser object that you’ve given the name My Browser. You could tell the browser to go back to the previous page with this code:

objectaction "My Browser","goback"

But remember, this will only work if you’ve set the object name to My Browser.

Web Page Navigation

A Web Browser object can display web pages directly from the internet, or it can display HTML content generated by Panorama itself.

If the Formula panel contains a URL, the web content will be accessed directly from the internet.

You can also specify the URL in code, like this:

changeobject "My Browser",
    "$WebBrowserFormulaMode","Literal Text",
    "formula","http://apple.com"

The URL can also be specified with a formula:

let appleProduct = ""
changeobject "My Browser",
    "$WebBrowserFormulaMode","Formula",
    "formula",{"http://apple.com"+appleProduct}

With this formula, the web browser can be sent to different pages simply by changing the appleProduct variable, for example:

appleProduct = "iphone"
showvariables appleProduct

or:

appleProduct = "watch"
showvariables appleProduct

Another way navigation can happen is by the user clicking on links within a web page. When the link is clicked, the web page changes, even though the formula associated with the web page hasn’t changed. If your code needs to find out what page is actually being displayed, it can use the geturl action. This code will find out what web page is currently being displayed, even if the user has clicked on a link to switch to a different page.

local myCurrentURL
objectaction "My Browser","geturl",myCurrentURL
message myCurrentURL

There is also a gettitle action that retrieves the page title of the currently displayed page.

You can also set up a subroutine in the Web Browser object that will be triggered immediately whenever the web page changes. This allows you to change elements of your Panorama form automatically when a link is clicked – for example to update the window browser or select a Text List item (the Panorama Help databases performs both of these actions when a link is clicked). If you want the code to be triggered immediately when the link is clicked, the subroutine label must be newPage: (this must be exact, including upper and lower case). Here is a simple example that changes the title of the form window to the suffix of the URL each time a link is clicked.

Here is the code:

newPage:

local url
objectaction info("clickedobjectid"),"geturl",url
windowname arraylast(url[1,-2],"/")
return

As links are clicked, the window title will update.

It would probably be nicer to display the web page title in the window title. There is a gettitle action that will retrieve this, but it won’t work in the newPage: subroutine. The problem is that when this subroutine is triggered, the web page hasn’t actually loaded yet, so title isn’t known yet (the title is part of the HTML that hasn’t been downloaded from the web server yet). To trigger code when the web page has finished loaded, make a subroutin with the label pageLoaded:. Here is a revised version of the code that will update the window title with the web page title.

pageLoaded:

local pagetitle
objectaction info("clickedobjectid"),"gettitle",pagetitle
windowname pagetitle
rtn

As links are clicked, the window title will update.

You’ll notice that this code uses the info(“clickedobjectid”) function to identify the web browser containing the links. You could also use the object name, but using this function allows the code to be used with different web browser objects without modification.

It’s possible to include both the newPage: and pageLoaded: subroutines in the Web Browser procedure code. In this example, when a link is clicked the Panorama window title will first immediately change to the URL suffix of the new page. Then a short time later (after the web page finishes loading), it will change to the title of the web page.

You can even run JavaScript code automatically either before or/and after each page loads, allowing you to automatically modify web pages on the fly. This is sometimes called “JavaScript Injection”. See Working with JavaScript below to learn how this is done – you simply need to include one or more objectaction statements in the newPage and/or pageLoaded code.

Forward and Back Navigation

Browsers like Safari and Chrome have Back and Forward buttons. Your Panorama forms can have these buttons also, simply by using the goback and goforward actions. It only takes one line of code to implement a Back button (in this case, the button itself was created with the Font Awesome Icons wizard).

It’s not always possible to go back or forward. For example, if you just opened a new browser, there’s nowhere to go back or forward to. You can use the cangoback and cangoforward actions to check whether or not it’s possible to to forward or back. A button with this code will go back to the previous page if there is one, or beep if there isn’t.

local greenLight
objectaction "My Browser","cangoback",greenLight
if greenLight
    objectaction "My Browser","goback"
else
    beep
endif

Working with JavaScript

Panorama has a programming language, and Web Browsers have their own programming language - JavaScript. It’s possible for Panorama code to run JavaScript code in the web browser. It’s also possible for JavaScript code to trigger Panorama code.

JavaScript is a powerful and rich language. The examples below contain very simple, basic code to illustrate how the link between Panorama code and JavaScript code works. Don’t be fooled, however, the power of JavaScript goes far beyond the trivial examples shown here. It’s far out of the scope of this documentation to teach JavaScript, but if you want to take maximum advantage of this capability, you’ll need to study some of the many books, courses and/or web pages available for JavaScript education. Here are some of the free resources that we’ve found helpful.

Invoking JavaScript Code from Panorama

To invoke JavaScript code from Panorama, use the script (or javascript) action. This action requires two additional parameters - the JavaScript code to run, and a field or variable where the result returned from JavaScript should be placed.

objectaction objectID,"script","... javascript code ...",result

The result parameter is optional. You should leave this parameter off if the JavaScript code doesn’t return a value. For example, this example tells the web browser to scroll to the <a href="#pricing"> tag in the currently displayed web page (of course this will only work if there is such a tag).

objectaction "My Browser","script",{location.href = "#pricing";}

The location.href code doesn’t generate a value, so the result parameter has been omitted.

Passing Values between Panorama and JavaScript

In many situations you’ll want the JavaScript to calculate a value and return that value back to your Panorama code. The value could be a number, text, or a compound value like an array or an object (JavaScript objects are equivalent to Panorama Data Dictionaries). Let’s start with simple non-compound values. This example calculates the sum of 2+2 using JavaScript and puts it into the Panorama somenumber variable.

local somenumber
objectaction "My Browser","script",{2+2;},somenumber

Of course Panorama can easily perform addition on its own, so you would never write code like this in the real world. But we have to start somewhere. Here’s another example that uses JavaScript to generate a random number between 1 and 10.

local somenumber
let mycode = {Math.floor(Math.random() * (10 - 1)) + 1;}
objectaction "My Browser","script",mycode,somenumber

When this code is run the somenumber variable will be assigned a value between 1 and 10. First this uses the JavaScript Math.random() function to calculate a number from 0 to 1, then multiplies by 9 and adds 1, then uses the JavaScript Math.floor function to make sure the number is an integer and not a value like 4.398418. The final random integer will be put into the Panorama somenumber variable. (Once again this is a somewhat silly example for illustration purposes – if you really need to do this you should use Panorama’s randominteger( function.)

Instead of hard coding that this function returns integers from 1 to 10, you might like to use Panorama variables to specify the minimum and maximum values. To pass a Panorama field, variable or even an entire expression into the JavaScript code, surround the expression with and »$. This evaluates the expression and places it into the JavaScript as a constant.

local somenumber
let min = 1
let max = 10
let mycode = {Math.floor(Math.random() * ($«max»$ - $«min»$)) + $«min»$;}
objectaction "My Browser","script",mycode,somenumber

Important: When a field or variable is embedded into JavaScript code, only the value is embedded. You can not modify the value of the field or variable from within the JavaScript. In other words, as far as the JavaScript is concerned, the field or variable is a constant value.

So far the examples have dealt only with numbers, but text can also be passed into JavaScript code, and returned from JavaScript code. Here is an example that takes the text in the variable blurb and reverses it, so that the characters are backwards.

let jsCode = |||
    
function reverseString(str) {
    let newString = "";
    for (let i = str.length - 1; i >= 0; i--) {
        newString += str[i];
    }
    return newString;
}
reverseString($«blurb»$);|||

let blurb = "Now is the time"
objectaction "My Browser","script",jsCode,blurb

Once again this is not a practical application, Panorama has a stringreverse statement that will do the same job faster and easier. The goal here is simply to illustrate how to pass text strings from Panorama to JavaScript and back.

The examples shown so far have used and »$ to embed a variable, but any Panorama formula can be embedded. The example above could be modified to both reverse AND upper case the text passed to it:

reverseString($«upper(blurb)»$);|||

Or you could reverse only the lower case vowels:

reverseString($«stripchar(blurb,"aaeeiioouu")»$);|||

To embed a Panorama formula with no quotes or translation, use and »^. For example, this code will create two JavaScript variables in the current web page, greeting and price.

let variableName = "greeting"
let variableValue = "Bonjour"
let jsCode = {var ^«variableName»^ = $«variableValue»$;}
objectaction "My Browser","script",jscode
variableName = price
variableValue = 12.95
objectaction "My Browser","script",jscode

Passing Compound Values

Passing compound values like arrays and objects is a bit more complicated. If you try to pass a compound value directly, you’ll get an error because Panorama doesn’t understand JavaScript compound objects. But Panorama does understand JSON, so you can pass these values back to Panorama by using the JSON.stringify( function. This example creates an array in JavaScript, converts it to JSON, then passes it back to Panorama.

let scriptResult
let jsCode = {var countdown = [3, 2, 1];
JSON.stringify(countdown);}
objectaction "My Browser","script",jscode,scriptResult
let pCountDown = exportdataarray(jsonimport(scriptResult),",")

Panorama then takes the JSON, converts it to a Data Array (using jsonimport(), then converts it to a text array (using exportdataarray(). The final value in the pCountDown variable will be 3,2,1.

This example creates an object in JavaScript, converts it to JSON, then passes it back to Panorama.

let scriptResult
let jsCode = |||var person = {First:"Bob", Last:"Smith", Age:36};
JSON.stringify(person);}
objectaction "My Browser","script",jscode,scriptResult
let pPerson = jsonimport(scriptResult)
let first = getdictionaryvalue(pPerson,"First")
let last = getdictionaryvalue(pPerson,"Last")
let age = getdictionaryvalue(pPerson,"Age")

Panorama then takes the JSON, uses jsonimport( to convert it to a dictionary (see Data Dictionaries), then uses getdictionaryvalue( to extract individual items and put them in separate variables.

To pass a compound value from Panorama to JavaScript, you first have to use jsonexport( or jsonexportline( to convert it into JSON, then use and »^ to embed it into the JavaScript code. This example takes the current person in the database, converts it to JSON, then creates a JavaScript variable label that contains that person’s name and address in the format of a mailing label.

let jsCode = |||var person = ^«jsonexportline()»^;
var label = person.Name.concat("\n",person.Address,"\n",person.City,", ",person.State);|||
objectaction "My Browser","script",jscode

Document Object Model

Usually the reason for using JavaScript within Panorama is to interact with the currently displayed web page – either to programmatically examine the contents web page, or to modify the contents of the web page (or both). To do that you’ll use Document Object Model, usually abbreviated as DOM. The DOM is a structure that contains every element of the web page. There are many books and courses available about the DOM, but these two free resources are an excellent place to start.

The following two sections below will provide a basic introduction to basic techniques for using the DOM to examine and modify the currently displayed web page. Here at ProVUE Development we are definitely not experts in JavaScript or the Document Object Model, so this is definitely not a comprehensive curriculum on this topic. If you want to learn more so that you can take full advantage of the DOM, the Mozilla documentation linked to above is highly recommended. In fact, we constructed the examples below primarily by studying this Mozilla documentation.

Using the DOM to Examine the Displayed Web Page

The DOM allows your Panorama code to identify and examine any component of the displayed web page. (Note: In the DOM, components are actually called elements.) Here’s an actual example that uses the DOM to find out the what’s in the head section of the current web page (from the <head> to </head> tag). The result is placed in the local variable headHTML.

local headHTML
objectaction "My Browser","script",{document.head.outerHTML;},headHTML

The DOM represents a document with a logical tree. Each branch of the tree ends in a node, and each node contains objects. DOM methods allow programmatic access to the tree. With them, you can access and change the document’s structure, style, or content. The main branch always starts with document. In the example above we start with document, then take the head branch (there is also a body branch). The head branch has subtrees for the title, styles, links, etc. The outerHTML branch returns the actual HTML of of the specified branch of the tree. So

document.head.outerHTML;

will retrieve the HTML in the header portion of the current web page. Voila!

You can perform much more complicated queries of the web page. The example below builds a list of all of the URLs linked to from the current page. It starts by building a JavaScript array of all of the <a href="url">...</a> tags in the page. It then extracts the link from each of these tags, and converts the list of links into a JSON array. Panorama converts the JSON array into a Text Array, then de-duplicates the list.

local scriptResult
let jsCode = |||
    var atags = document.getElementsByTagName('a');
    var hrefs = [];
    for (let i = 0; i < atags.length; i++){
      hrefs[i] = atags[i].href;
    }
    JSON.stringify(hrefs);
|||
objectaction "My Browser","script",jsCode,scriptResult
let links = exportdataarray(jsonimport(scriptResult),cr())
arraydeduplicate links,links,cr()

I’m not going to claim that code like this is easy or simple. Writing code like this requires a lot of study of both the DOM and JavaScript documentation. But once you’ve learned them, the full power of these tools is available form within Panorama.

Using the DOM to Modify the Displayed Web Page

The DOM can also be used to modify the displayed web page. The modification happens right in the browser and is immediately displayed (the original copy of the web page on the web server is not modified, so this modification is only temporary). Here is a very simple example that changes the background color of the currently displayed web page to lime green.

objectaction "My Browser","script",
    {document.body.style.backgroundColor = "#77FF77";}

Note: If you try this and you don’t see the background color change, it means that the page you are viewing doesn’t have any background area visible – it’s all covered with actual content. This is pretty common on many modern web pages. You can see this effect if you try it on a page from the Panorama documentation – here I’ve changed the normal blue background to lime green. (Again, remember, this only happens in the currently displayed copy of this web page – we’re not changing this documentation on the ProVUE web server! If you reload the page, you’ll see it will change back to the normal blue background.)

JavaScript can even add entirely new content to the web page. This example creates a new h4 heading and inserts it at the top of the web page.

const heading = document.createElement("h4");
const headingText = document.createTextNode("Modified on ^«datepattern(today(),"Month ddnth, YYYY")»^");
heading.appendChild(headingText);
heading.style.textAlign = "center";
heading.style.color = "#770077";
document.body.prepend(heading);

Here’s what happens if you run this JavaScript code on a Panorama help page.

The top of this Panorama documentation page contains this HTML to display the page title.

<div id="_page_Header" class="pageheader">Web Browser Programming</div>

For Panorama documentation pages the page header always has an id of _page_Header. We can take advantage of that to write JavaScript that inserts new text below the page heading.

const pageHeading = document.getElementById("_page_Header");
const heading = document.createElement("h6");
const headingText = document.createTextNode("Modified on ^«datepattern(today(),"Month ddnth, YYYY")»^");
heading.appendChild(headingText);
heading.style.textAlign = "center";
heading.style.color = "#770077";
pageHeading.append(heading);

The first line of this code gets the _page_Header element into a variable. Then the last line of the code adds the next text after the page header element. The result looks like this:

Note that unlike the previous example which would work on on any web page, this example (and the following examples) will only work on Panorama documentation pages (or at least only pages that have a _page_Header element).

Instead of adding new content, JavaScript can also modify existing content. This example centers the page heading and changes the color to red.

const pageHeading = document.getElementById("_page_Header");
pageHeading.style.textAlign = "center";
pageHeading.style.color = "#AA0000";

Here’s the result:

Here’s another example that modifies the text of the page header.

const pageHeading = document.getElementById("_page_Header");
const originalHeading = pageHeading.innerHTML;
pageHeading.innerHTML = originalHeading.toUpperCase();

This code converts the page header to all upper case.

Finally, JavaScript can even be used to remove content from the web page.

const pageHeading = document.getElementById("_page_Header");
pageHeading.remove();

After running this code, the page header has completely disappeared.

Of course this technique will only work with a web page that is constructed with the proper _page_Header element. Usually techniques like this are used on web pages that are specifically constructed to work with your JavaScript code. For example, the Panorama documentation web pages are designed to work with JavaScript built into the Help database. This system works because the Help database never displays any other web pages other than the ones constructed for this purpose.

It is possible to build more complicated JavaScript code that will work with a wide variety of web pages. However, that topic is beyond the scope of this help topic.

Triggering Panorama Code from a Web Page

It’s possible to build a special web page that has buttons or links in it that will trigger Panorama code. For example, you could create a link that opens some other Panorama form or database. Of course buttons or links constructed this way will only work if the web page is displayed in a Panorama Web Browser Object. If you display the web page in a regular web browser (Safari, Chrome, etc.) these buttons and links won’t work.

To illustrate this we’ll create a web page with three links that let you choose a color. When one of these links is clicked, a Panorama alert opens, like this.

The HTML code for these special links looks like this:

<hr>
Choose a color:
<a href="#" onClick="window.webkit.messageHandlers.panoramaAction.postMessage('Gold');">Gold</a>
<a href="#" onClick="window.webkit.messageHandlers.panoramaAction.postMessage('Silver');">Silver</a>
<a href="#" onClick="window.webkit.messageHandlers.panoramaAction.postMessage('Bronze');">Bronze</a>
<hr>

To make these special links work, the Web Browser Object must have a javaScript mini subroutine in the procedure code, like this:

This code will be triggered when the web browser runs this JavaScript code. Note that this code must be written exactly as shown here, including upper and lower case.

window.webkit.messageHandlers.panoramaAction.postMessage(...)

The JavaScript code sends a value to the Panorama code – in our example this value is Gold, Silver or Bronze. The Panorama code can find out what value was sent by using the info(“javascriptvalue”) function. In this example the value is being directly used in the alertsheet statement, but more commonly you would place it in a variable for further processing.

If you’re planning on using many links back to Panorama code, you can simplify your HTML by creating a JavaScript function to send the message. In this case I’ve named the function panorama( but any name can be used, as long as you are consistent.

<body>
<script>function panorama(message) {
    window.webkit.messageHandlers.panoramaAction.postMessage(message);
}
</script>
<hr>
Choose a color:
<a href="#" onClick="panorama('Gold');">Gold</a>
<a href="#" onClick="panorama('Silver');">Silver</a>
<a href="#" onClick="panorama('Bronze');">Bronze</a>
<hr>

By using the `JSON.stringify( function the JavaScript can pass multiple values back to the Panorama code:

<body>
<script>function raceevent(medal,country,name) {
var message = JSON.stringify({"medal":medal, "name":name, "country":country});
window.webkit.messageHandlers.panoramaAction.postMessage(message);
}
</script>
<hr>
Choose a medal:
<a href="#" onClick="raceevent('Gold','Sweden','Lars Sverdlan');">Gold</a>
<a href="#" onClick="raceevent('Silver','France','Thyss Phillipe');">Silver</a>
<a href="#" onClick="raceevent('Bronze','Canada','Mark Tavens');">Bronze</a>
<hr>

Clicking on one of these links probably doesn’t produce the desired result:

Here is the revised Panorama code that can handle this compound input.

This produces a nice looking alert.

Of course displaying an alert is a rather simplistic application. More useful would be performing actions like opening a form, opening another database, opening a Finder window, performing a database selection, or even adding new data to the database.

You may wonder if Panorama code can pass a value back to the JavaScript code. This isn’t directly possible, but you can use the objectaction statement as described in the previous sections to perform actions on the web page.


See Also


History

VersionStatusNotes
10.2NewNew in this version.