Skip to content

Displaying HTML content

Lauri-Matti Parppei edited this page Jun 10, 2024 · 19 revisions

More elaborate panels and views can be created using HTML.

There are two options: HTML panel and HTML window. The panel is a modal, which allows you to display a one-time HTML screen, with buttons for dismissing it. HTML window is a standalone window, which you can update while the user edits the document. Both come with a preloaded CSS, which you can append to taste.

Examples below use HTML code created straight inside the plugin code, but you probably should use Beat.assetAsString() to fetch the HTML content from a plugin asset. See the existing plugins for simple examples.

NOTE: You cannot run Beat plugin code from inside the windows. They are separate instances from the app itself. When using a HTML window, you can use Beat.call(evalString) at any time to execute JavaScript code in the plugin, or use htmlWindow.runJS(evalString) to evaluate code inside the window instance. This is a constant ping-pong of asynchronous data, but we'll get to that.

HTML Panel

Beat.htmlPanel(htmlContent, width, height, callback(result), okButton = true/false)

You can run JavaScript code inside your HTML page, but can't call normal Beat plugin methods. However, there is an object called Beat available inside the HTML scope, which can be used to call the host document and to store data to be passed on after the panel is done.

Callback method is invoked when the user closes the panel. Callback method receives result object, which contains two keys, data and inputData. You can use both. data is custom data set in code, while inputData has HTML form input values.

Optionally, you can set okButton to true, to have OK and Cancel buttons instead of the default Close. Pressing OK submits data and runs callback, while Cancel just closes the panel.

Always call Beat.end() in the callback to terminate the plugin and wipe it from memory. You don't have to do this, if you know what you are doing.

Fetching data from the panel

There are three ways to fetch data from a HTML panel:

  1. Storing an object (note: only an object) into Beat.data inside your HTML. It will be returned in the callback alongside other data.

  2. Using HTML inputs. Just remember to add rel='beat' attribute. The received object will then contain inputData object, which contains every input with their respective name and value (and checked value, too).

  3. Calling Beat.call("javaScriptString") inside the HTML sends code to be evaluated by the main plugin. Note that the JS parameter needs to be a string, ie. Beat.call("Beat.log('Hello world');").

If you want to create your own submit button, use sendBeatData() method in your JavaScript.

HTML code:

<script>
    Beat.data = { customData: "This will be sent to the callback." }
</script>
<!-- Optional -->
<button onclick='sendBeatData()'>Send Data</button>

Plugin code:

Beat.htmlPanel(html, 400, 400, function (htmlData) {
    // We will now receive any data set in the HTML panel
    Beat.alert("This is what we got:", JSON.stringify(htmlData))
})

Bare-bones example

let htmlContent = "<h1>Hello World</h1>\
                   <input type='text' rel='beat' name='textInput'>\
                   <script>Beat.data = { 'hello': 'world' }</script>"

Beat.htmlPanel(
    htmlContent,
    600, 300,
    function (result) {
        /*
        Callback receives:
        result = {
            data: { hello: 'world' },
            inputData: [
                { name: 'textInput', value: '' }
            ]
        }
        */
        Beat.log(JSON.stringify(result))
        Beat.end()
    },
    true   // setting this true shows OK/Cancel buttons instead of plain Close
)

HTML Window

A standalone, floating window. Creating a window will make plugins reside in memory, so be sure to terminate it using Beat.end() in callback, if needed.

let htmlWindow = Beat.htmlWindow(htmlContent, width, height, callback)

The biggest difference to htmlPanel is that you can transfer data in real time between plugin and its HTML window.

htmlWindow.runJS("evaluatedJavaScript") evaluates the given JavaScript string in the window. Inside the HTML window, you can use Beat.call("evaluatedJavaScript") to access Beat. Think of this as a client/server setup.

Always remember to terminate the plugin using Beat.end() in the callback if you don't want to leave it running in the background.

Interacting with windows

htmlWindow.title — window title, can be changed at any time
htmlWindow.setHTML(htmlString) — set window content (Note: This will wipe the existing JS scope in your window, and it's essentially treated as a newly-loaded page)
htmlWindow.setRawHTML(htmlString) — set window content, overriding any Beat injections (Warning: You won't be able to call any Beat methods after this)
htmlWindow.close() — close the window and run callback
htmlWindow.runJS(javascriptString) — sends JavaScript to be evaluated in the window

htmlWindow.focus() — make this window key window
htmlWindow.toggleFullScreen() — toggle full screen mode
htmlWindow.isFullScreen() — checks if the window is in full screen mode (returns true/false)

htmlWindow.resizable — if the window can be resized by user, true by default
htmlWindow.setFrame(x, y, width, height) — set window position and size
htmlWindow.getFrame() — returns position and size for the window
htmlWindow.screenSize() — returns size for the screen window has appeared on
htmlWindow.screenFrame() — returns the full frame for the screen ({ x: x, y: y, width: w, height: h })
htmlWindow.gangWithDocumentWindow() — attach the window to its document window (makes the window move along with the document window)
htmlWindow.detachFromDocumentWindow() — detach from document window

Communicating with host plugin/document

Read this part carefully to understand how communication between a plugin and an HTML window works.

You can run any regular JavaScript inside HTML panel/window, but NOT your normal Beat plugin code. Any communication with the host plugin happens through evaluation. Think of it as host/client situation: plugin is the host, HTML window is the client. Communication is handled using strings containing JavaScript code.

However, you can format your evaluated code as anonymous functions. Don't let this confuse you — any method sent to your plugin or client window will be considered a string.

Sending JavaScript to a window

let window = Beat.htmlWindow(html, 300,100, () => {
    Beat.end()
})
window.runJS("console.log('This is logged in the window')")

To send data into an open window, use window.runJS(evalString). It evaluates a string containing JavaScript code inside the HTML page.

Sending JavaScript to the plugin

If you want to run plugin code from inside an HTML window, you similarly need to send it to the plugin for evaluation. There are multiple ways to do this. There are also some quirks because of JavaScript scope and asynchronous communication, which is why you need to associate any functions in a custom object. More about this later.

Communicating with the window is a constant and convoluted ping-pong of evaluations and stringified data. Look through existing plugin code or drop by Beat Discord to ask for help.

Beat.call(js, ...parameters) — evaluates given JavaScript string in the plugin
Beat.callAndWait(js, ...parameters) — evaluates given JavaScript string in the plugin and returns a promise (macOS 11+ only)
Beat.callback(js, ...parameters, callback, errorHandler) — evaluates given JavaScript string in the plugin and executes either callback or error handler after the response (macOS 11+ only)

Examples

// Log 'Hello' inside the plugin
Beat.call("Beat.log('Hello')")

// Runs the given code inside the plugin, and handles the return value
Beat.callback("return 'Hello world'",
    (returnValue) => {
        Beat.log("Return value: " + returnValue) // logs 'Hello world'
    },
    (error) => {
        // Something went wrong or the plugin returned NULL value
    }
)

Using strings can be a bit clumsy. That's why you can also send anonymous functions inside for evaluation. NOTE that this code is still converted to a string, so you can't use any window-scope variables, unless you provide them as the second argument.

// Execute call with no response
Beat.call((arg) => {
    // This code is converted to a string and executed inside your plugin
    Beat.log("This code is run inside the plugin with argument: " + arg)
}, "Argument")

// Execute call with no response
Beat.callback(
    // This code is converted to a string and executed inside your plugin
    (arg1, arg2) => {    
        Beat.log("Arguments: " + arg1 + ", " + arg2)
        let line = Beat.lines()[0]
        return line.string
    },
    // Values for arg1 and arg2 
    ["Argument 1", "Argument 2"],
    // Return value handler
    (returnValue) => {
        Beat.log("   ... received: " + returnValue)
    },
    (error) => {
        // returned NULL or error
    }
)

There are some quirks because of JavaScript scope and asynchronous communication. This is why you can't run plain functions through evaluation.

For example, this WILL NOT WORK:

HTML:

<!-- This won't work: -->
<script>Beat.call("hello()");</script>

Plugin:

function hello() {
    Beat.alert("Hello World")
}

Instead, to call a custom function from inside the HTML, you need to register a custom Beat object. This WILL work.

Plugin:

Beat.custom = {
    hello: function () {
        Beat.alert("Hello World!")
    }
}

HTML:

<!-- This WILL work: -->
<script>
    // Plain string 
    Beat.call("Beat.custom.hello()")

    // Anonymous function turned into a string
    Beat.call(() => {
        Beat.custom.hello()
    })
</script>

You can also use promises:

let promise = Beat.callAndWait("Beat.custom.hello()")
promise.then(
    function (result) {
        // Success
        Beat.log(result)
    },
    function (error) {
        // Something went wrong
        Beat.log(error)
    }
)

Examples

HTML window example

This example displays current scene title in a floating HTML window, and logs success whenever that happens.

Beat.openConsole()

let htmlContent = "<b>Current Scene</b>\
                   <p id='sceneTitle'>...</p>\
                   <script>\
                       function setTitle(string) {\
                       	   var element = document.getElementById('sceneTitle');\
                           element.innerHTML = string\
                           Beat.call('Beat.custom.success()')\
                       }\
                   </script>"

let htmlWindow = Beat.htmlWindow(htmlContent, 300, 80, function () {
    Beat.end()
})

const scenes = Beat.outline()

Beat.onSceneIndexUpdate(function (index) {
    let scene = scenes[index]
    let title = scene.string
    htmlWindow.runJS("setTitle('" + title + "')")
})

Beat.custom = {
    success: function() { 
                  Beat.log("Success!")
             }
}

Asset files

HTML files can be saved as assets in your plugin container folder, and then stored into a string variable.

let ui = Beat.assetAsString("ui.html")
Beat.htmlWindow(ui, 600, 500, function () { Beat.end(); })

Saving & recalling window frame

You can store the window position, so that when the plugin is opened next time, it shows at the same position.

htmlWindow = Beat.htmlWindow(html, 800, 600, function () {
    // Save current frame position to plugin user defaults
    let frame = htmlWindow.getFrame()
    Beat.setUserDefault("frame", frame)
    Beat.end()
})

// Fetch plugin position from user defaults AFTER it was already opened.
// If a position was saved, set window frame accordingly.
const frame = Beat.getUserDefault("frame")
if (frame) htmlWindow.setFrame(frame.x, frame.y, frame.width, frame.height);