Running JavaScript macros in LibreOffice

OpenOffice and LibreOffice have the ability to run macros. These macros can be written in a number of programming languages. The most common one is called OpenOffice Basic or LibreOffice Basic. For this language there is a built-in editor. It is also possible to record macros in Basic and edit them afterwards.

OpenOffice Basic or LibreOffice Basic is not standardized in the OpenDocument Format (ODF). ODF does not standardize on any programming language for use in the macros. In fact, in LibreOffice/OpenOffice, one can use Basic, Python, Java and JavaScript.

Macros can be embedded in documents and triggered from certain actions in the document. Many of the drawing and form elements of and ODF document can register listeners to triggers. In text, one can use <text:a/> or <text:execute-macro/> to trigger execution of a macro.

Here is a small example of a hyperlink in a text, that triggers execution of a macro:

<text:a xlink:type="simple" xlink:href="#">
 <office:event-listeners>
  <script:event-listener xlink:type="simple"
    script:language="ooo:script" script:event-name="dom:click"
    xlink:href="vnd.sun.star.script:Standard.test.Main?language=Basic&location=document"/>
  </office:event-listeners>
  Trigger!
</text:a>

This is the code of the Basic macro:

Sub Main
  MsgBox ("Hello World!")
End Sub

The above code is written in Basic. The equivalent trigger, written in JavaScript and placed in a form element, would look like this:

  <script:event-listener
    script:event-name="form:itemstatechange"
    script:language="ooo:script"
    xlink:href="vnd.sun.star.script:Library1.Macro1.js?language=JavaScript&location=document"
    xlink:type="simple"/>

Note that this trigger is still specific to OpenOffice/LibreOffice, because it uses the value ooo:script to specify the script language. In HTML pages, one would use <script type='text/javascript'/>. The ODF specification says:

Script language names are implementation-dependent. The names identifying script languages should begin with by a namespace prefix, followed by a ":" (U+003A, COLON) separator.

This means that a text/javascript is not an acceptable value. Also, using that value causes scripts to stop working in OpenOffice/LibreOffice; only the value ooo:script is used for all scripting languages. Both the Basic and the JavaScript version use the same value for the script language name.

Can we use this to add support for JavaScript macros in WebODF? Yes, but we would need to use a custom namespace to specify that we use JavaScript and this script would work in other ODF implementations. Even if we could use text/javascript there would be a problem, since there is no common API to access and modify the contents of ODF documents.

UNO API

In LibreOffice/OpenOffice, the API that is used is the UNO API. This programming interface can be accessed from all supported programming languages and is very specific to LibreOffice/OpenOffice.

We have written a small example to set the value of a user variable from a toggle button. I will close this blog with the code for this toggle. Note that all of this is just one macro. Each macro needs its own file and JavaScript macros cannot import other JavaScript files. So if you have an additional toggle, you will need an additional JavaScript file which is 99% identical to the other one.

I conclude this blog with the very complex toggle macro. If you know of a simpler way to toggle a user value with JavaScript, please let me know.

/*global importClass, Packages, UnoRuntime, XTextFieldsSupplier, XModel,
  XMultiServiceFactory, XPropertySet, XDependentTextField, XSCRIPTCONTEXT,
  XUpdatable */
importClass(Packages.com.sun.star.uno.UnoRuntime);
importClass(Packages.com.sun.star.frame.XModel);
importClass(Packages.com.sun.star.text.XTextFieldsSupplier);
importClass(Packages.com.sun.star.text.XDependentTextField);
importClass(Packages.com.sun.star.beans.XPropertySet);
importClass(Packages.com.sun.star.lang.XMultiServiceFactory);
importClass(Packages.com.sun.star.util.XUpdatable);

function toggleDocumentField(name, value1, value2, doc) {
    "use strict";
    var fieldMasters, // All of the document master fields
        fieldMaster,  // Specific document master field
        fieldName,
        field,
        factory,
        oldValue;

    // get the textfield master which contains all the textfields
    fieldMasters  = UnoRuntime.queryInterface(XTextFieldsSupplier, doc).getTextFieldMasters();
    factory = UnoRuntime.queryInterface(XMultiServiceFactory, doc);

    // if the field exists in the document, use it, else create a new field.
    fieldName = "com.sun.star.text.FieldMaster.User." + name;
    if (fieldMasters.hasByName(fieldName)) {
        fieldMaster = UnoRuntime.queryInterface(XPropertySet,
            fieldMasters.getByName(fieldName));
    } else {
        fieldMaster = UnoRuntime.queryInterface(XPropertySet,
            factory.createInstance("com.sun.star.text.FieldMaster.User"));
        fieldMaster.setPropertyValue("Name", name);
    }

    // set the value of the field
    oldValue = fieldMaster.getPropertyValue("Content") + String("");
    if (oldValue === value1) {
        fieldMaster.setPropertyValue("Content", value2);
    } else {
        fieldMaster.setPropertyValue("Content", value1);
    }

    // create your field to insert
    // do not set name or content, the value will come from the master field.
    field = UnoRuntime.queryInterface(XDependentTextField,
        factory.createInstance("com.sun.star.text.TextField.User"));
    field.attachTextFieldMaster(fieldMaster);
    UnoRuntime.queryInterface(XUpdatable, field).update();
}

var doc = UnoRuntime.queryInterface(XModel, XSCRIPTCONTEXT.getInvocationContext());
if (!doc) {
    doc = XSCRIPTCONTEXT.getDocument();
}
toggleDocumentField("visible", "true", "false", doc);