Operations

Interaction with documents usually occurs through executing operations, such as inserting an item or wrapping the selected text. Packages can define their own operations by combining operations, transforms and actions defined by the platform or the instance its own packages.

New and existing operations

Operations are commonly defined in operations.json files, which is an object mapping operation names to their definitions. Any operations.json or operations-*.json file in the src/ directory of your package will be automatically included.

Alternatively, operations can be registered in JavaScript using the AsyncSchemaLocationToSchemaExperienceResolver API.

Fonto Editor's SDK includes many operations out of the box, and in most cases custom operations will reuse SDK operations. See also the list of operations in our API documentation.

Operation properties

Each operation is defined as an object with mostly optional properties.

The exhaustive list of operation properties is documented on our API documentation.

The most commonly used properties are:

Property

Type

Description

label

string

(Optional) Used when the operation is used in a UI element such as a button or menu item.

When configuring a shortcut for an operation, also consider adding a label to the operation. This will include the operation in the shortcuts overview.

description

string

(Optional) Used as tooltip text by UI elements using the operation.

icon

string

(Optional) An icon displayed by UI elements using the operation. This should be a Font Awesome icon.

alternatives

string[]

(Optional) Used to define multiple, alternative, operations if the previous operation failed.

steps

object | object[]

Steps executed when the operation is invoked. Steps form a pipeline, passing step data from each step to the next.

Labels, footnotes and icons can be overridden for an operation by using the "label", "description" or "icon" props on buttons or menu items.

Alternative operations

An alternative form of the operation definition uses the alternatives property instead of steps, providing an array of operation names. Such an operation will adopt the behavior of the first of these operations which is enabled.

JSON

{
	"section-button": {
		"label": "Section",
		"alternatives": [
			"convert-selected-block-to-section",
			"insert-new-section",
			"horizontally-insert-new-section"
		]
	}
}

An example operation using alternatives

Operation steps

Steps or state steps can either consist out of one step or out of an array of steps. Each step has a type and optional data. Each step can modify step data internally, and the data template for each step can define new properties, possibly reusing existing data using the "expression.to.property" syntax.

JSON

{
	"convert-paragraph-to-codeblock": {
		"label": "Code block",
		"steps": [
			{
				"type": "transform/setContextNodeIdToSelectionAncestor",
				"data": {
					"selectionAncestorNodeSpec": "self::p or self::pre"
				}
			},
			{
				"type": "operation/convert-element",
				"data": {
					"contextNodeId": "{{contextNodeId}}",
					"nodeNames": [
						"codeblock"
					]
				}
			}
		]
	}
}

An operation with two steps that both use step data

There are different types of steps, which are prefixed in the type property of the step. The steps can already exist in the platform or else can be created:

Step type

Description

See also

transform

Is used to manipulate the step data. This should ideally not have any side-effects.

List of existing transforms, addTransform

action

Is used to change the state of the application other than the document content. This includes, for example, controlling UI components or communicating with the CMS. An action can also abort the running operation by returning the addAction.CANCEL_OPERATION value from its run callback.

List of existing actions, addAction

operation

Not all operations should be directly used in a button, these operations can be used as sub-operation. Sub-operations are passed the current step data from the operation that invokes them, which can be used to control their behavior.

It is also possible to omit the operation name part of a step with the type operation. In this case, the name of the operation will be read from the operationName property of the stepData. This allows for composing fully dynamic operations. Because the state of these operations may not be automatically invalidated, it is recommended to consider alternatives instead.

List of existing operations, Creating an operation

custom-mutation

Is used to manipulate the XML. Only use this if none of the existing operations can be used.

addCustomMutation

modal

Is used to open a modal. Modals can be submitted to provide step data for the next step in the operation, or canceled to abort the operation.

Create a modal

Operation state

Each operation has an associated state, consisting of two booleans: enabled and active. The enabled flag indicates whether the operation is allowed to run given the current context. The active flag symbolizes whether the effect of the operation already applies, and is usually visualized by showing the UI component as active/pressed/selected (see also the isSelected prop on components such as Button).

The state of an operation is derived from the loaded documents and other application state, including the current cursor position or selection, but also the state of UI elements. Normally, the steps used to determine the state of an application are derived from the steps used to run it, although an operation can also specify the getStateSteps explicitly. Be careful that the behavior of these steps still corresponds to the actual behavior of the operation, so an operation is not enabled when it cannot be executed correctly.

When determining the state of the operation, each of the step types functions as follows:

Step type

Behavior when getting operation state

transform

It is assumed that a transform glues other operation steps together by modifying step data without side-effects, so in most cases the transform is executed as normal.

If a separate get state callback was passed to addTransform, that is executed instead. This can be used if the transform is not entirely without side-effects, or can be simplified when getting operation state.

action

If a get state callback was passed to addAction, that function may return the state of the operation at this point, or return null to continue processing. Otherwise, processing continues with the next operation step (the action is ignored).

operation

The state of the operation being referenced is computed and used. Built-in operations that modify the DOM use a process similar to custom mutations to determine their state.

custom-mutation

The custom mutation is executed on a Blueprint without affecting the actual DOM. If the custom mutation returns CustomMutationResult.ok() and the resulting DOM is valid, the operation is enabled.

If the custom mutation returns CustomMutationResult.notAllowed() or the result is not valid according to the schema or other validation concerns (e.g., tables or confgureAsInvalid rules), the operation is disabled.

Custom mutations can set the active flag using the setActive() method on the CustomMutationResult created by either method.

modal

Modals are obviously only opened when the operation is executed. However, the data returned by a modal could be required to correctly compute the state of subsequent operation steps.

Therefore, if the modal's React component specifies a default value for the data prop (using the standard React defaultProps property), the properties in this object are assigned to the step data for the operation.

This way, modals can define stub data as a stand-in during state computation for data that would otherwise be provided by the user. For more complex scenarios, we recommend adding a transform step after the modal that may perform additional processing on the step data as required.

If any step determines the operation state to be disabled, processing stops.

Invalidating operation states

Actions and transforms are commonly used to pull some external state or data into the operations pipeline. If you have a custom action or transform that relies on external data you must also signal Fonto Editor when that data has changed, by calling OperationsManager#invalidateOperationStatesByStepType. Fonto Editor will then, behind the scenes, re-compute the operation state when necessary.

For example, an action that uses the selection may invalidate corresponding operation states when the selection changes by subscribing to the selection change notifier. This is usually done in the same install.js that registers the action:

JavaScript

addAction(
    'sendCurrentParagraphToCms',
    async function sendParagraph(stepData) {
        await someManager.sendDataToCms(stepData.paragraph);
        return;
    }
);

// In this case the operation state will be invalidated every time the notifier is triggered.
selectionManager.selectionChangeNotifier.addCallback(() => {
    operationsManager.invalidateOperationStatesByStepType(
        'action',
        'sendCurrentParagraphToCms'
    );
});

Contextual operations

Contextual operations are normal operations, but intended to work based on a specific context (node). 
Contextual operations are therefore always provided a contextNodeId step data by the menu, which references the context to which the menu applies.

Fonto Editor provides four common UI components that provide such a context:

A contextual operation can be configured as a menu option in a component that is opened by the user by clicking accordingly, or as part of a group in that menu, or as part of a sub-menu. Please refer to the Configure contextual operations guide for more information.

Grouping menu options or placing them in a sub-menu is recommended to make similar operations, or operations with similar concerns, more easily findable.

Contextual menu

One portion of the contextual menu is determined by the contextual operations that have been configured for a given element. The contextual menu will show the operations for that element, and also for all ancestor elements that have contextual operations. After a certain point the operations for ancestor elements are placed inside a sub-menu for that element.

Other parts of the contextual menu are sometimes used by add-on functionality such as Fonto Content Quality or the spell-checker. These parts are not driven by contextual operation configuration.

A contextual operation can be hidden from this component by including "context-menu" as part of the hideIn property for that operation.

Element menu

The "element-menu" refers to the menu that you see when you click on the createElementMenuButtonWidget widget, as shown here:

This widget is usually configured in the blockOutsideAfter widget area of an element that has contextual operations configured.

A contextual operation can be hidden from this component by including "element-menu" as part of the hideIn property for that operation.

Breadcrumbs menu

The breadcrumbs menu refers to the "breadcrumb trail" of menu items in the status bar on the /editor route, as shown here:

If the element represented by such a menu item has configured contextual operations, the menu item is turned into a split menu item. Clicking the split part of the menu opens a drop with a menu with contextual operations in it.

A contextual operation can be hidden from this component by including "breadcrumbs-menu" as part of the property for that operation.

Structure view menu

The structure view menu refers to the button shown on a structure view item if the element for that item has configured contextual operations on it, as shown here:

A contextual operation can be hidden from this component by including "structure-view" as part of the hideIn property for that operation.

Localization

Some strings in an operation should be localized, namely everything that is shown in the interface to the user at some point. These always include the label and description, but could also include any step data property you've added on your own. They can be marked for localization by using the t__ prefix. See the documentation on Localize the interface for more information.

JSON

{
	"toggle_bold": {
		"label": "t__Toggle bold",
		"steps": [
			{
				"type": "operation/toggle-inline-formatting",
				"data": {
					"nodeName": "b"
				}
			}
		]
	}
}

An operation with a localized label

Inline XPath/XQuery

In the step data of an operation step, the x__ prefix can be used to signal the rest of the string should be interpreted and be executed as an XQuery script. This script is supplied the following context:

Property

Description

$data

A map() containing the step data. All keys ending with 'nodeId' or 'NodeId' are resolved and placed on the $data object under the same key, without the 'Id' part of their key. The keys are of type xs:string.

A step data containing a contextNodeId creates $data variable with two keys: the plain contextNodeId property of type xs:string and a contextNode property of type node().

context item

The context item is absent.

The result of this XQuery script will be inserted in the step data, under the given key. Values of type xs:string will be kept strings, xs:numeric will be a JavaScript Number, etc, etc. However, Values of type node() will be transformed to their NodeId. This way, it is valid to return a node into a property like contextNodeId.

In the following example inline XPath is used to set both the contextNodeId operation data for set-attributes, as well as determine the value of @clonableAttribute.

JSON

{
	"clone-attribute-to-parent": {
		"steps": [
			{
				"type": "transform/setContextNodeIdToSelectedElement"
			},
			{
				"type": "operation/set-attributes",
				"data": {
					"contextNodeId": "x__$data('contextNode')/..",
					"attributes": {
						"clonableAttribute": "x__$data('contextNode')/@clonableAttribute"
					}
				}
			}
		]
	}
}

Importing modules

Modules can be imported using the import module syntax, like how every other XQuery script works. This allows for better re-use of code.

JSON

{
	"clone-attribute-to-parent": {
		"steps": [
			{
				"type": "transform/setContextNodeIdToSelectedElement"
			},
			{
				"type": "operation/set-attributes",
				"data": {
					"contextNodeId": "x__$data('contextNode')/..",
					"attributes": {
						"clonableAttribute": "import module prefix='http://www.example.com/ns'; prefix:execute-a-function($data('contextNode'))"
					}
				}
			}
		]
	}
}

PleaseWaitMessage

The step data may contain a property called pleaseWaitMessage. This should be a string that should be displayed during the run of the operation. Use this for long running operations (like saving all documents). The modal opens when the step setting this property is ran and will close when the operation ends.

Examples

The following operations are valid operation definitions using standard Fonto Editor functionality.

Inserting an important note

JSON

{
	"note-important-insert": {
		"label": "t__Important note",
		"steps": {
			"type": "operation/vertical-insert",
			"data": {
				"childNodeStructure": [
					"note",
					{ "type": "important" },
					[{ "bindTo": "selection", "empty": true }]
				]
			}
		}
	}
}

Marking a word or selection as a trademark

JSON

{
	"tm-insert": {
		"label": "t__Trademark",
		"description": "t__Identifies a term or phrase that is trademarked.",
		"icon": "trademark",
		"steps": [
			{
				"type": "operation/toggle-inline-frame-element",
				"data": {
					"nodeName": "tm",
					"attributes": { "tmtype": "tm" }
				}
			}
		]
	}
}

Using data from the previous operation step

JSON

{
	"equation-block-insert": {
		"label": "Add equation",
		"description": "Add an equation to this figure.",
		"icon": "calculator",
		"steps": [
			{
				"type": "operation/open-mathml-editor",
				"data": {
					"mathMlPart": ["mathml"]
				}
			},
			{
				"type": "command/horizontal-insert",
				"data": {
					"childNodeStructure": [
						"equation-block",
						"{{mathMlPart}}"
					]
				}
			}
		]
	}
}

Operations for different use cases

Fonto Editor provides a wide range of operations, both complete ones only requiring some configuration and operations specifically meant to be a step of another operation. For a complete list of operations, refer to the Operations API documentation.

Toggling inline elements

Inline elements can be divided in two groups. The first group contain elements which only apply some kind of formatting without suggesting any meaning. An example is the b element found in DITA. These elements should be configured with the configureAsInlineFormatting family. For this family, the toggle-inline-formatting-element operation should be used. The second group contains elements which describe the semantics of a text phrase. An example is the apiname element found in DITA. These elements should be configured with the configureAsInlineFramefamily. For this family type, the toggle-inline-frame-element operation should be used.

The following example shows the usage of the toggle-inline-formatting-element operation for toggling a DITA b element:

JSON

{
    "b-toggle": {
        "label": "Bold",
        "description": "Applies bold formatting to text.",
        "icon": "bold",
        "keyBinding": "ctrl+b",
        "osxKeyBinding": "cmd+b",
        "steps": [
            {
                "type": "operation/toggle-inline-formatting-element",
                "data": {
                    "nodeName": "b"
                }
            }
        ]
    }
}

The following example shows the usage of the toggle-inline-frame-element operation for toggling a DITA apiname element:

JSON

{
    "apiname-insert": {
        "label": "API name",
        "description": "Provides the name of an application programming interface.",
        "steps": [
            {
                "type": "operation/toggle-inline-frame-element",
                "data": {
                    "nodeName": "apiname"
                }
            }
        ]
    }
}

As shown in the examples, both operations require the nodeName to be set.

Inserting structures

It is often necessary to not only insert just one element, but a whole XML structure. For this, the horizontal-insert and vertical-insert operations can be used. These operations differ in the way they look for the right spot for the structure to be inserted. In short, the horizontal-insert inserts the given structure somewhere under a given node, taking into account the schema. The vertical-insert takes a different approach, it tries to insert the given structure as close to the selection as possible. See the API pages of both operations for more information.

The following example shows how to insert a DITA consequence element using the horizontal-insert operation:

JSON

{
    "consequence-insert": {
        "label": "Add consequence",
        "description": "Specifies the consequence of failing to avoid a hazard.",
        "steps": [
            {
                "type": "operation/horizontal-insert",
                "data": {
                    "childNodeStructure": [
                        "consequence",
                        [
                            {
                                "bindTo": "selection",
                                "empty": true
                            }
                        ]
                    ]
                }
            }
        ]
    }
}

The example for inserting an image doubles as an example for the vertical-insert operation.

Inserting images

Operations for inserting images generally consist of two operations. The first operation, or step, opens the browse modal in which the user can select an image to insert. The second operation inserts an element representing the image and the required surrounding elements.

Note that in this example, data is being passed from the browse modal to the vertical-insert operation. This data is passed to the vertical-insert operation via the model property on the operation data, the {{ and }} indicate that the value should come from the previous operation.

JSON

{
    "image-figure-insert": {
        "label": "Figure with image",
        "description": "A figure that groups an image with its title and description.",
        "steps": [
            {
                "type": "operation/open-image-browser-for-insert"
            },
            {
                "type": "operation/vertical-insert",
                "data": {
                    "model": {
                        "href": "{{permanentId}}"
                    },
                    "childNodeStructure": [
                        "fig",
                        [
                            "title",
                            [
                                {
                                    "bindTo": "selection",
                                    "empty": true
                                }
                            ]
                        ],
                        [
                            "desc"
                        ],
                        [
                            "image",
                            {
                                "href": {
                                    "bindTo": "href"
                                }
                            }
                        ]
                    ]
                }
            }
        ]
    }
}

Removing structures

Most inline elements can be removed by using the backspace key when the cursor is positioned after these elements. Other elements can be deleted by selecting them via the breadcrumbs and pressing backspace or delete. Alternatively, a delete operation can be offered via the element's contextual menu. For this, the platform offers the delete-node operation.

The operation in this example will be triggered via a context menu. This means that the contextNodeId is being set implicitly.

JSON

{
    "syntaxdiagram-delete": {
        "label": "Remove syntax diagram",
        "description": "Remove this syntax diagram.",
        "icon": "times",
        "steps": [
            {
                "type": "operation/delete-node"
            }
        ]
    }
}

When the node that should be removed is not the one containing the selection directly, a transform can be added to set the contextNodeId to a different node.

JSON

{
    "plentry-delete-as-row": {
        "label": "Remove row",
        "description": "Remove this row from the parameter table.",
        "icon": "times",
        "steps": [
            {
                "contextNodeId": "x__fonto:selection-common-ancestor()/ancestor-or-self::*[fonto:dita-class(., 'pr-d/plentry')][1]",
                "type": "operation/delete-node"
            }
        ]
    }
}

Table operations

The platform offers a range of operations for working with tables. These are obviously only relevant when configuring an application which supports at least one table standard.

Inserting table

Inserting a table can be done with the table-insert operation. This operation expects the childNodeStructure to be set. It needs this structure to determine which table structure to use. In this example, the table-insert operation is used to insert a CALS table.

The CALS standard defines the table element as being some sort of figure-like element containing a tgroup element, the actual table itself. If this were to be a xhtml-table-insert operation, the value of the childNodeStructure would just have been "table". See the table-insert API page for more information.

JSON

{
    "cals-table-insert": {
        "label": "CALS table",
        "description": "A generic CALS table",
        "icon": "table",
        "initialData": {
            "childNodeStructure": [
                "table",
                [
                    "tgroup"
                ]
            ],
            "rows": 1,
            "columns": 1
        },
        "steps": [
            {
                "type": "operation/table-insert"
            }
        ]
    }
}

Adding and removing rows/columns

Adding and removing the rows or columns of tables can be done with the following operations:

These operations can be used directly and do not need any configuration. All these operations are available in their contextual variant:

These operations assume the contextNodeId to be set, which is implicitly done when used in a contextual menu.

Merging cells

Merging cells in any direction can be done with the following operations (if the used table standard supports merged cells):

These operations can be used directly and do not need any configuration.

These operations assume the contextNodeId to be set, which is implicitly done when used in a contextual menu.

Table styling

Removing tables

Removing tables can be done by using the table-delete operation.

Custom mutations

Use a custom mutation if none of the existing operations meet the requirements. See addCustomMutation for more information on how to make a custom mutation.

Triggering operations

There are multiple ways to trigger the execution of an operation. The most obvious way is to click a button in the masthead. Another way is to click a button in the context menu. An operation can also be triggered by keyboard shortcuts when it is configured to do so.

See the "Creating a masthead" guide for more information about creating a masthead and binding operations to the buttons. See configureContextualOperations for more information about adding a contextual operation to an element.

Masthead

The most obvious way of triggering an operation is by clicking a button in the masthead of the application. Most of the insert-like operations should be located in the masthead, grouped by use. These operations are executed with an empty stepData.

Contextual menu

Another way of triggering an operation is by clicking a button in a context menu. These menus can be triggered by right-clicking elements which are configured to have a context menu, or by clicking the elementMenuButtonWidget. Operations which are triggered using a contextual menu are executed with a contextNodeId property on the stepData. The contextNodeId property is a NodeId which can be used in inline XPath expressions in an operation and resolved using the getNodeById method on DocumentsManager.

Contextual operations may be configured using configureProperties, using the contextualOperations property.

Keyboard shortcut

An operation can also be triggered by keyboard shortcuts when it is configured to do so. Generally, operations used for inline formatting are triggered with keyboard shortcuts. These operations are executed with an empty stepData.

When configuring a shortcut for an operation, also consider adding a label to the operation. This will include the operation in the shortcuts overview.