4.6 Interactive Content in ExternalPageFrames

The CBA ItemBuilder can be extended with custom HTML5/JavaScript content using ExternalPageFrames. This approach enables the implementation of any interaction format that goes beyond what the built-in CBA ItemBuilder components provide—from custom visualizations and simulations to specialized input methods and third-party library integrations. As described in section 3.14, HTML5/JavaScript content is embedded using components of type ExternalPageFrame (rendered as iframes at runtime).

ExternalPageFrames require programming in HTML5/JavaScript. The embedded content must support the CBA ItemBuilder API for data transfer and must work in all target browsers. Long-term archiving independent of the rendering technology is not possible for content implemented directly in HTML5/JavaScript (see section 3.14 for a discussion of pros and cons).

4.6.1 Workflow: Creating and Integrating Custom Content

The following steps describe the complete workflow for creating custom interactive HTML5/JavaScript content and integrating it into CBA ItemBuilder items:

  1. Create an HTML file with the custom interactive content (HTML, CSS, JavaScript). Use the template in section 4.6.1.1 as a starting point.
  2. Add the HTML file (and any resources such as libraries, images, audio files) to the CBA ItemBuilder project via the Embedded HTML Explorer (see section 3.14.2).
  3. Place an ExternalPageFrame component on a page and set its Page Address to the local HTML file (see section 3.14.1).
  4. Use the JavaScript API (section 4.6.3) to communicate between the custom content and the CBA ItemBuilder runtime—send log data, set variables, trigger events, persist state.
  5. Define scoring based on FSM variables set from JavaScript, or use the result_text() operator to capture responses (see chapter 5).

The key technical aspects to consider when creating content for ExternalPageFrames are:

  • Size: Items have a fixed pixel size (see section 3.2.2). The ExternalPageFrame is embedded as an iframe with a fixed size. Use CSS to ensure content fits without scrollbars. If proportional scaling is active in the deployment, all components including ExternalPageFrames are scaled via CSS transformation.

  • Embedding: The default approach is to embed all files (HTML, CSS, JavaScript, libraries) into the CBA ItemBuilder Project File via the Embedded HTML Explorer (see section 3.14.2). Embedded content is stored in the external-resources folder of the project’s ZIP archive and works for both online and offline delivery. An entry point file (e.g., index.html) is required.

  • Communication: The JavaScript API (see section 4.6.3) provides bidirectional communication between the embedded content and the CBA ItemBuilder runtime via postMessage. This includes logging, setting variables, triggering events, and calling JavaScript from the FSM.

  • State Persistence: ExternalPageFrames lose their state when the page is unloaded. Implement getState() and setState() to save and restore state across page changes (see section 4.6.6).

4.6.1.1 HTML Template for ExternalPageFrames

The following minimal template provides the boilerplate code for an ExternalPageFrame that communicates with the CBA ItemBuilder runtime. It can serve as a starting point for any custom interactive content:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <style>
        /* Size must match the ExternalPageFrame component dimensions */
        html, body { margin: 0; padding: 0; overflow: hidden; }
    </style>
</head>
<body>
    <!-- Custom interactive content goes here -->
    <div id="content"></div>

    <script>
    // === CBA ItemBuilder Communication API ===

    // Internal state for postMessage
    var indexPath = "";
    var userDefIdPath = "";
    var traceCount = 0;

    // Initialize: receive component path from CBA ItemBuilder
    window.addEventListener("message", function(event) {
        try {
            var data = JSON.parse(event.data);
            if (data.indexPath !== undefined) {
                indexPath = data.indexPath;
                userDefIdPath = data.userDefIdPath;
            }
        } catch(e) { }
    });

    // --- Send log entry to CBA ItemBuilder trace data ---
    function postLogEvent(message, type) {
        var msg = {
            indexPath: indexPath,
            userDefIdPath: userDefIdPath,
            traceMessage: message,
            traceType: type || "CustomEvent",
            traceCount: traceCount++
        };
        window.parent.postMessage(JSON.stringify(msg), '*');
    }

    // --- Set an FSM variable value ---
    function setFsmVariable(variableName, newValue) {
        var msg = {
            indexPath: indexPath,
            userDefIdPath: userDefIdPath,
            microfinVariable: {
                variableName: variableName,
                newValue: newValue
            },
            traceCount: traceCount++
        };
        window.parent.postMessage(JSON.stringify(msg), '*');
    }

    // --- Trigger an FSM event ---
    function postFsmEvent(eventName) {
        var msg = {
            indexPath: indexPath,
            userDefIdPath: userDefIdPath,
            microfinEvent: eventName,
            traceCount: traceCount++
        };
        window.parent.postMessage(JSON.stringify(msg), '*');
    }

    // --- State persistence (called automatically by runtime) ---
    function getState() {
        try {
            // Return all state that needs to survive page changes
            return JSON.stringify({ /* your state data */ });
        } catch(e) { return "{}"; }
    }

    function setState(stateString) {
        try {
            var state = JSON.parse(stateString);
            // Restore your component state from the parsed object
        } catch(e) { }
    }

    // === Custom interactive logic goes here ===

    </script>
</body>
</html>

A CBA ItemBuilder project file containing a working template is shown in Figure 4.73. When creating a new CBA ItemBuilder Project File, an HTML file example.html with JavaScript examples is created automatically and can be viewed in the Embedded HTML Explorer (see section 3.14.2).

FIGURE 4.73: Template for content developed for ExternalPageFrames (html|ib).

4.6.1.2 Integration Patterns

The following patterns describe common ways to integrate custom content with the CBA ItemBuilder:

Pattern 1: Capture a response and score it. The custom content captures user input and stores it in an FSM variable. Scoring is then defined using the standard CBA ItemBuilder scoring (see chapter 5):

  1. Define an FSM variable (e.g., V_Response of type STRING)
  2. In JavaScript: setFsmVariable("V_Response", userAnswer)
  3. In scoring: use variable_in(V_Response, "correct", "also_correct") or matches() on the variable

Pattern 2: Custom content with page navigation. After the user completes an interaction, trigger an FSM event that advances to the next page or task:

  1. Define an FSM event (e.g., EV_Done)
  2. In JavaScript: postFsmEvent("EV_Done")
  3. In FSM: define a rule that navigates on EV_Done (e.g., next_task() or page switch)

Pattern 3: Pass configuration from CBA ItemBuilder to JavaScript. Use the callExternalPageFrame() operator to initialize the custom content with parameters:

  1. In the FSM Task Initialization or a rule: callExternalPageFrame(epf_id, "init", V_ItemNumber)
  2. In JavaScript: the parameters are received as arguments to a global function

Pattern 4: Persist state across page changes. Implement getState() and setState() to save and restore the interactive content when users navigate away and return (see section 4.6.6).

Examples: Additional examples of content embedded in ExternalPageFrames are provided in section 6.6, including continuous audio playback, speech recognition, mathematical input, GeoGebra applets, H5P content, QTI items, and SurveyJS questionnaires.

4.6.2 JSON Configuration Editor for ExternalPageFrames

The CBA ItemBuilder provides a visual JSON configuration editor that allows item authors to edit configuration data for ExternalPageFrame content directly within the CBA ItemBuilder, without manually editing JSON files. This is useful when the embedded HTML5/JavaScript content is parameterizable—for example, when the same interactive component can be configured with different stimuli, difficulty levels, or display options.

How to access: Right-click on an ExternalPageFrame component in the Page Editor or the Component Edit view and select the entry Configure using JSON Editor from the context menu.

The JSON Config Editor requires the Chromium preview feature to be enabled (see section A.4 for the -DuseChromiumFeature parameter).

How it works: The JSON Config Editor uses a request-response protocol between the CBA ItemBuilder and the embedded HTML5/JavaScript content:

  1. When the item author selects Configure using JSON Editor, the CBA ItemBuilder opens the Preview and sends a postMessage to the ExternalPageFrame iframe with a callId of the form importJsonData-{componentId}.

  2. The iframe content responds with a postMessage containing two pieces of information:

    • jsonSchema: A JSON Schema that defines the structure, types, and validation rules for the configuration data. The schema determines which form fields are displayed in the editor.
    • configFileName: The name of the JSON file where the configuration should be stored (e.g., config.json).
  3. The CBA ItemBuilder opens a form-based JSON editor (powered by the JSON Editor library) that renders input fields based on the provided JSON Schema. If a configuration file already exists, its values are pre-filled.

  4. After the item author edits the configuration and clicks Save, the JSON data is written to the file external-resources/{configFileName} inside the CBA ItemBuilder Project File.

Implementing JSON Config Editor support in ExternalPageFrame content: To enable the JSON Config Editor for custom HTML5/JavaScript content, the iframe must handle the configuration request. The following JavaScript code shows the required message handler:

// Listen for configuration requests from CBA ItemBuilder
window.addEventListener("message", function(event) {
    try {
        var data = JSON.parse(event.data);

        // Respond to JSON Config Editor request
        if (data.callId && data.callId.startsWith("importJsonData-")) {
            var response = {
                callId: data.callId,
                jsonSchema: {
                    type: "object",
                    title: "Item Configuration",
                    properties: {
                        stimulus: {
                            type: "string",
                            title: "Stimulus Text",
                            default: "Enter the stimulus here"
                        },
                        difficulty: {
                            type: "integer",
                            title: "Difficulty Level",
                            minimum: 1,
                            maximum: 5,
                            default: 3
                        },
                        showFeedback: {
                            type: "boolean",
                            title: "Show Feedback",
                            default: true
                        }
                    }
                },
                configFileName: "config.json"
            };
            window.parent.postMessage(JSON.stringify(response), '*');
        }
    } catch(e) { }
});

The jsonSchema follows the JSON Schema specification. The schema defines the form fields displayed in the editor. Supported types include string, integer, number, boolean, array, and object with nested properties.

Loading the configuration at runtime: The configuration file is stored in external-resources/ and can be loaded by the iframe content at runtime using a relative path:

// Load configuration from the config file
fetch('config.json')
    .then(response => response.json())
    .then(config => {
        // Use config.stimulus, config.difficulty, config.showFeedback, etc.
        document.getElementById('stimulus').textContent = config.stimulus;
    });

This approach separates the interactive component logic (HTML/JavaScript) from the item-specific configuration (JSON), enabling reuse of the same ExternalPageFrame content across different items with different configurations.

4.6.3 Communication API: ExternalPageFrames (JavaScript) and Finite-State Machine

The JavaScript API for embedded content in ExternalPageFrames provides bidirectional communication between JavaScript and the CBA ItemBuilder runtime. The following table summarizes all available channels:

TABLE 4.3: Communication API between ExternalPageFrame (JavaScript) and CBA ItemBuilder runtime.
Direction Method Purpose
JS → Runtime postMessage with traceMessage Write custom log entries to trace data
JS → Runtime postMessage with microfinVariable Set FSM variable value (for scoring)
JS → Runtime postMessage with getVariable Request current FSM variable value
JS → Runtime postMessage with microfinEvent Trigger FSM event (e.g., to call next_task())
Runtime → JS callExternalPageFrame() operator Call JavaScript with parameters from FSM
Runtime → JS setState(stateString) Restore state when page is revisited
JS → Runtime getState() return value Save state before page is unloaded

Each of these communication channels is described in detail below.

4.6.4 Provide Information to the Finite-State Machine from JavaScript

The communication between the embedded JavaScript and the CBA ItemBuilder runtime (and thus the Finite-State Machine as defined by item authors in the CBA ItemBuilder) is done via postMessages.

window.parent.postMessage(JSON.stringify(message), '*');    

Depending on the configuration of the JSON string passed as message, posteMessages can be sent for various purposes. The various use cases are illustrated in the item shown in Figure 4.74.

FIGURE 4.74: Item Illustrating the API for ExternalPageFrames (html|ib).

Create Custom Log-Entries: Log events (i.e., event-based data generated during test-taking) are stored by the delivery software and are available after the assessment. Only for components from the CBA ItemBuilder model are log events automatically created when the components are added to the design of pages (and thus the design of assessment components) via the Page Editor. For content that is included as an ExternalPageFrame, care must be taken in the programming of the HTML5/JavaScript content to ensure that all relevant information is logged (see section 2.8.1 for completeness of log data).

If the payload transmitted as postMessage contains a traceMessage attribute, a log entry is created by the CBA ItemBuilder runtime:

var message = "Random message, e.g., " + new Date().toLocaleString(); 
var type = "CustomTraceType";

var pass_data = {
     indexPath: indexPath,
     userDefIdPath: userDefIdPath, 
     traceMessage: message,
     traceType: type,        
     traceCount : traceCount++
 };
 window.parent.postMessage(JSON.stringify(pass_data), '*'); 
Data passed from an ExternalPageFrame can be found in the CBA ItemBuilder log data with events of type JavaScriptInjected. They can contain log events from the embedded content. However, this interface can also be used to store result data from ExternalPageFrames.

The log entries can use their event types if the traceType attribute is also passed. The transmission of log data from ExternalPageFrames can be used to transmit arbitrary string serializable data and have them stored by the delivery environment.

Set FSM Variables: From a content included as ExternalPageFrame, also values of FSM variables can be set. This option allows, for instance, returning values from embedded content, which can also be used in scoring. In addition, arbitrary calculations at runtime of items are also possible via JavaScript and possible embedded libraries. To set a variable, the payload microfinVariable must be submitted as a JSON structure with the values variableName (name of the variable that will be set) and newValue (new value for the named variable). The type of the variable must be taken into account.

var varname = "V_OutputInteger";
var value = Math.floor(Math.random() * 100);

var pass_data = {
     indexPath: indexPath,
     userDefIdPath: userDefIdPath,
     microfinVariable: { variableName: varname, newValue: value },            
     traceCount : traceCount++
};

window.parent.postMessage(JSON.stringify(pass_data), '*');  

Request Value of FSM Variables: Besides setting FSM variables, the value of FSM variables can also be retrieved. To do this, the JavaScript sends a postMessage with the payload getVariable, in which the name of the desired variable and an (arbitrary) callId must be contained as variableName:

var callId = "ID" + new Date().toLocaleString();
 
var pass_data = {
      indexPath: indexPath,
      userDefIdPath: userDefIdPath,
      getVariable: {  variableName: variableName,   callId: callId},
      traceCount : traceCount++
};

window.parent.postMessage(JSON.stringify(pass_data), '*');  

The CBA ItemBuilder runtime responds to this postMessage and transmits back the value of the variable, which can then be captured and further processed with an EventListener. In the example in Figure 4.74, the transmitted return JSON is displayed as alert.

Trigger FSM Event: FSM events defined in the Finite-State Machine syntax in the Events: section can be triggered from ExternalPageFrames when a payload with the microfinEvent attribute is submitted via postMessage:

var eventName = "EV_JSTriggeredEvent"; 
var pass_data = {
     indexPath: indexPath,
     userDefIdPath: userDefIdPath, 
     microfinEvent: eventName,
     traceCount : traceCount++
};
window.parent.postMessage(JSON.stringify(pass_data), '*');  

Using this part of the interface, transitions can be triggered in the Finite-State Machine, and operators can be used. If, for example, HTML5/JavaScript content included via an ExternalPageFrame should be able to end a task, then the next_task() operator (see section 4.4.6) can be integrated into the Finite-State Machine and placed into a Rule, which is triggered by an FSM Event. The FSM Event is fired when the JavaScript content sends a postMessage with the payload microfinEvent and the corresponding name of the FSM Event.

4.6.5 Call JavaScript-Function of ExternalPageFrames from Finite-State Machine(s)

Once an HTML5/JavaScript resource included as ExternalPageFrame is loaded, functions can be called using the FSM operator callExternalPageFrame:

callExternalPageFrame(UserDefinedId, ...)

Several ExternalPageFrames can be integrated in one page or in one Task of a CBA ItemBuilder Project Files. The first parameter must be the UserDefinedId of the particular ExternalPageFrame component that should process the JavaScript call. Additional optional parameters are possible. As illustrated in Example 4.75, the optional parameters can be used to pass static strings or values of FSM variables. The values of the arguments are then available in the called JavaScript function.

FIGURE 4.75: Example Item Illustrating JavaScript calls from FSM (html|ib).

As can be seen in the item in Figure 4.75, calls to JavaScript functions from the Finite-State Machine can be used for quite different purposes.

4.6.6 Provide ExternalPageFrames-State for Persistence (getState/setState)

HTML/JavaScript extensions inserted as ExternalPageFrame on pages inside CBA ItemBuilder projects lose their content when the page that contains the ExternalPageFrame is unloaded. Unloading occurs on page changes either within a task or when exiting a task. To allow ExternalPageFrames to save and restore their content, the API includes the getState/setState-functionality.

The following behavior is expected: When the getState() methods is called, then a string (e.g., a JSON serialized object) can be passed from the ExternalPageFrame to the CBA ItemBuilder runtime. The call of getState() is done automatically before the page that contains the ExternalPageFram is unloaded by the CBA ItemBuilder Runtime.

Content from embedded JavaScript/HTML5 will be lost if in a Task the Page is left, on which the ’ExternalPageFramecomponent is embedded. If the content is needed again when the page is revisited, it must be passed to the CBA ItemBuilder runtime viagetState(). Furthermore, the embedded content needs to handle the call tosetState()` and restore its content when requested.
function getState() {
    try { 
      // Specific JavaScript Code Required 
        var json = JSON.stringify(/* your data */);
        return json;
    } 
    catch (e) {
        console.debug(e); 
    }
}

When navigating to a page that contains an ExternalPageFrame that has already been displayed, the CBA ItemBuilder Runtime calls the setState() method and passes the state serialized as a string that was passed the last time getState() was called.

function setState(stateString) {
    try {
        // Specific JavaScript Code Required 
        // ...
    } 
    catch (e) {
        console.debug(e); 
    }
}

The minimal example shown in Figure 4.76 illustrates the expected behavior.

FIGURE 4.76: Example Item Illustrating JavaScript calls for Persistence (html|ib).