Configure contextual operations

In this guide we'll create and configure contextual operations for a few elements in a dummy editor.

What you'll need

  • A basic instance of the Fonto editor
  • Some familiarity with operations

Step 1:  Create an operation

In this example we'll start with an operation that sets the severity attribute of the <allergy-warning> element in the Recipe training editor. Creating a contextual operation is the same as creating any other operation, except that the operation will always be provided with a contextNodeId for the element on which the contextual menu is shown.

  • Create a new file /packages/recipe-sx-module/src/operations.json.

  • Add the following code to this file to add two operations for setting severity to "harmless" and "lethal"

operations.json
{
    "set-severity[harmless]": {
        "label": "Harmless",
        "description": "The allergies described here are not severe.",
        "steps": {
            "type": "operation/set-attributes",
            "data": {
                "attributes": {
                    "severity": "harmless"
                }
            }
        }
    },
    "set-severity[lethal]": {
        "label": "Lethal",
        "description": "The allergies described here are quite severe.",
        "steps": {
            "type": "operation/set-attributes",
            "data": {
                "attributes": {
                    "severity": "lethal"
                }
            }
        }
    }
}

The set-attributes operation that is used in our custom operations requires only the contextNodeId and attributes operation data. Because contextNodeId is already determined when the user clicks one element or another, no further changes to the above are needed.

Step 2: Find the configuration of your element

In this guide we're using the <allergy-warning> element as an example. In the Configure elements guide, the configuration for this element was already determined. In the following example we've already added the contextualOperations property where we'll add in the contextual operations in the next step.

configureSxModule.js
// <allergy-warning>
configureAsFrame(sxModule, 'self::allergy-warning', 'allergy warning', {
    blockHeaderLeft: [
        createMarkupLabelWidget()
    ],
    backgroundColor: 'amber',
    defaultTextContainer: 'p',
    contextualOperations: [
        /* Add contextual operations here */
    ]
});

Alternatively, you could use a whole new function call specifically for adding contextual operations:

configureSxModule.js
configureContextualOperations(sxModule, 'self::allergy-warning', [
    /* Add contextual operations here */
]);

Step 3: Refer to contextual operation

A reference to one operation is always an object with the name property and optionally hideIn. The name property refers to the name we've given our operations; in this guide, "set-severity[harmless]" and "set-severity[lethal]". The hideIn property is an array that may contain any number of the following strings, which are the places where this contextual operation is then not shown: "context-menu", "element-menu", "breadcrumbs-menu", "structure-view".

{
    name: 'set-severity[harmless]',
    hideIn: ['breadcrumbs-menu']
}

The configuration code for <allergy-warning> may become:

configureSxModule.js
// <allergy-warning>
configureAsFrame(sxModule, 'self::allergy-warning', 'allergy warning', {
    blockHeaderLeft: [
        createMarkupLabelWidget()
    ],
    backgroundColor: 'amber',
    defaultTextContainer: 'p',
    contextualOperations: [
        { name: 'set-severity[harmless]', hideIn: ['breadcrumbs-menu'] },
        { name: 'set-severity[lethal]', hideIn: ['breadcrumbs-menu'] },
        { name: 'delete-node' }
    ]
});

Step 4: Organise the menu

The contextual menus allow you to group items together, or create a submenu. This is recommended for operations that are similar. In place of a normal contextual operation reference, you could also use a different object.

For example, to create a submenu:

{
    subMenuLabel: 'Severity',
    subMenuIcon: 'warning',
    contents: [
		/* More contextual operation references, groups or submenus here */
	]
}

For example, to group operations together by separating them from the rest with a horizontal line:

{
    contents: [
		/* More contextual operation references or submenus here */
	]
}

Or, if you want to label that group:

{
    menuGroupHeading: 'Severity',
    contents: [
		/* More contextual operation references or submenus here */
	]
}

The configuration for <allergy-warning> where two contextual operations for changing the severity attribute are placed in a submenu could look like this:

configureSxModule.js
// <allergy-warning>
configureAsFrame(sxModule, 'self::allergy-warning', 'allergy warning', {
    blockHeaderLeft: [
        createMarkupLabelWidget()
    ],
    backgroundColor: 'amber',
    defaultTextContainer: 'p',
    contextualOperations: [
        { subMenuLabel: 'Severity', subMenuIcon: 'warning', hideIn: ['breadcrumbs-menu'], contents: [
            { name: 'set-severity[harmless]' },
            { name: 'set-severity[lethal]' }
		]},
        { name: 'delete-node' }
    ]
});

Bonus: Implement the default context menu for tables

The contextual menus are so flexible that you can create very elaborate grouped and nested contextual menus with them. You can even recreate the default table context menu that is provided by the platform for platform supported table implementations (DITA/CALS, DITA/simpletable, XHTML, TEI).

Below is an example configureSxModule.js and operations.json that you can use in an application package. The example below implements the same menu as the platform provides for a CALS table.

configureSxModule.js
import configurationManager from 'fontoxml-configuration/src/configurationManager.js';

import configureProperties from 'fontoxml-families/src/configureProperties.js';

import t from 'fontoxml-localization/src/t.js';

import configureAsCalsTableElements from 'fontoxml-table-flow-cals/src/configureAsCalsTableElements.js';

export default function configureSxModule(sxModule) {
    if (configurationManager.get('app/use-default-table-context-menu')) {
        return;
    }

    configureAsCalsTableElements(sxModule, {
        // copied this from our dita-example tbl-decl-mod package, 
        // nothing needs to change here but we still need this configuration to
        // associate the OVERRIDE below with the correct schema elements
        table: {
            localName: 'table'
        },
        entry: {
            defaultTextContainer: 'p'
        },
        // OVERRIDE: added this to disable the default (not so flexible) table context menu
        useDefaultContextMenu: false
    });

    configureProperties(sxModule, 'self::table', {
        // OVERRIDE: added hideIn because we manually include these operations in the context menu for entry below
        contextualOperations: [
            { name: ':cals-table-insert-title', hideIn: ['context-menu'] },
            { name: ':cals-table-insert-desc', hideIn: ['context-menu'] },
            { name: 'cals-table-delete', hideIn: ['context-menu'] }
        ]
    });

    configureProperties(sxModule, 'self::entry', {
        // OVERRIDE: new nested/grouped contextual operations that mimic the structure of the
        // default context menu for tables
        contextualOperations: [
            {
                // This allows us to overwrite the initial heading of the context menu that is usually the markup label of the context node
                // Without the override, we would get the markup label of entry here, which would be Cell.
                heading: 'Table',
                contents: [
                    {
                        contents: [
                            { name: 'contextual-row-insert' },
                            { name: 'contextual-row-after-insert' },
                            { name: 'contextual-column-insert' },
                            { name: 'contextual-column-after-insert' }
                        ]
                    },
                    {
                        contents: [
                            { name: 'contextual-row-delete' },
                            { name: 'contextual-column-delete' },
                            // defined in the operations.json below
                            { name: 'contextual-cals-table-delete' }
                        ]
                    },
                    {
                        contents: [
                            {
                                subMenuLabel: 'Merge cells',
                                contents: [
                                    { name: 'contextual-merge-cell-left' },
                                    { name: 'contextual-merge-cell-right' },
                                    { name: 'contextual-merge-cell-above' },
                                    { name: 'contextual-merge-cell-below' }
                                ]
                            }
                        ]
                    },
                    {
                        contents: [
                            { name: 'contextual-split-cell-into-rows' },
                            { name: 'contextual-split-cell-into-columns' }
                        ]
                    },
                    {
                        contents: [
                            {
                                subMenuLabel: 'Alignment',
                                contents: [
                                    {
                                        contents: [
                                            { name: 'set-cell-horizontal-alignment-left' },
                                            { name: 'set-cell-horizontal-alignment-center' },
                                            { name: 'set-cell-horizontal-alignment-right' },
                                            { name: 'set-cell-horizontal-alignment-justify' }
                                        ]
                                    },
                                    {
                                        contents: [
                                            { name: 'set-cell-vertical-alignment-top' },
                                            { name: 'set-cell-vertical-alignment-middle' },
                                            { name: 'set-cell-vertical-alignment-bottom' }
                                        ]
                                    }
                                ]
                            },
                            {
                                subMenuLabel: t('More'),
                                contents: [
                                    {
                                        menuGroupHeading: t('Table'),
                                        contents: [
                                            // these are all defined in the operations.json below
                                            {
                                                name: 'contextual-cals-table-title-insert'
                                            },
                                            {
                                                name: 'contextual-cals-table-desc-insert'
                                            },
                                            {
                                                name:
                                                    'contextual-cals-open-table-column-sizing-popover'
                                            },
                                            { name: 'contextual-cals-table-delete' }
                                        ]
                                    },
                                    {
                                        subMenuLabel: t('Cell'),
                                        // original contextual operations for entry
                                        contents: [
                                            { name: 'contextual-column-insert' },
                                            { name: 'contextual-column-after-insert' },
                                            { name: 'contextual-column-delete' },
                                            { name: 'contextual-row-insert' },
                                            { name: 'contextual-row-after-insert' },
                                            { name: 'contextual-row-delete' }
                                        ]
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        contents: [
                            { name: 'toggle-table-overflow-expanded' },
                            { name: 'toggle-table-overflow-mode' }
                        ]
                    }
                ]
            }
        ]
    });
}

All operations below are copied and slightly edited from their original counterparts (without the documentation):
"cals-table-delete", "cals-table-desc-insert" and "cals-table-title-insert" are originally defined in "fontoxml-dita",
and "cals-open-table-column-sizing-popover" in "fontoxml-table-flow-cals".
All operations have been modified to set the contextNodeId to the closest ancestor table because we want to use them on the contextual operations menu for a table entry (see the previous configureSxModule.js), but they expect to be used on a table/tgroup element.

operations.json
{
    "contextual-cals-table-delete": {
        "label": "t__Remove table",
        "description": "t__Remove the current table from the document.",
        "icon": "times",
        "steps": [
            {
                "type": "operation/table-contextual-delete",
                "data": {
                    "contextNodeId": "x__$data('contextNode')/ancestor::table[1]"
                }
            }
        ]
    },
    "contextual-cals-table-desc-insert": {
        "label": "t__Add description",
        "description": "t__Add a description to this figure.",
        "steps": [
            {
                "type": "operation/horizontal-insert",
                "data": {
                    "childNodeStructure": [
                        "desc",
                        [{ "bindTo": "selection.start", "empty": true }],
                        ["?fontoxml-text-placeholder", "t__text=\"type the description\""],
                        [{ "bindTo": "selection.end", "empty": true }]
                    ],
                    "contextNodeId": "x__$data('contextNode')/ancestor::table[1]"
                }
            }
        ]
    },
    "contextual-cals-table-title-insert": {
        "label": "t__Add title",
        "description": "t__Add a title to this figure.",
        "steps": [
            {
                "type": "operation/horizontal-insert",
                "data": {
                    "childNodeStructure": [
                        "title",
                        [{ "bindTo": "selection.start", "empty": true }],
                        ["?fontoxml-text-placeholder", "t__text=\"type the title\""],
                        [{ "bindTo": "selection.end", "empty": true }]
                    ],
                    "contextNodeId": "x__$data('contextNode')/ancestor::table[1]"
                }
            }
        ]
    },


    "contextual-cals-open-table-column-sizing-popover": {
        "label": "t__Edit column sizes",
        "description": "t__Set the column sizing for this table.",
        "icon": "column-resizer",
        "steps": [
            {
                "type": "operation/open-table-column-sizing-popover",
                "data": {
                    "contextNodeId": "x__$data('contextNode')/ancestor::table[1]",
                    "popoverAnchorNodeId": "x__$data('contextNode')/ancestor::tgroup[1]"
                }
            }
        ]
    }
}

The benefit of using this approach over simply setting useDefaultContextMenu to true when configuring table elements is that for a little extra upfront work, you get complete flexibility in the structure of the entire menu. With the default context menu, any additional contextual operations are included under the "More" submenu, and automatically grouped and listed for you. With a custom menu you can decide for yourself what the context menu looks like for any element.

If you want to use (and modify) this menu yourself, add both files to a new application package which depends on the sx configuration that you want to override (if anything).
Then make sure you depend on this package in every SX shell package in which your table element is used.


Was this page helpful?