Configure drag & drop in the structure view

The 7.4.0 release of FontoXML introduces experimental drag and drop support in the structure view (also known as "Outline"). To enable this experiment, ensure that the enable-experiment/drag-and-drop-in-structure-view-sidebar configuration value is set to true.

Drag and drop works by computing the state of one of a number of operations for positions where an item can be dropped. Such operations can be added by using the addDragAndDropOperation function, and are considered in order of decreasing priority, as specified at registration. By default, FontoXML includes one such operation to support drag and drop in single documents, provided that the dropped item can be placed under the node represented by the parent item. The fontoxml-dita add-on provides another operation for moving topicrefs in and between DITA maps.

Creating a drag & drop operation

A drag & drop operation is passed information in three parts: the item that is moving (passed as an array movingItems), the item that is to be the new parent (passed as newParentNode) and the position where the item is being dropped (passed as dropPosition, as a combination of a parentNode and referenceNode). To allow maximum flexibility in creating drag & drop operations to, for instance, outdent items, the new parent may not be the same parent under which the item was dropped, but may also be an ancestor. 

The current implementation of drag & drop does not support dragging more than a single item (not counting descendants). Therefore, the movingItems array will, for now, always hold a single entry. A future release may extend this to allow multiple items to be dragged and dropped.

Each of these is based on the configured structure view items, and are therefore composed of:

  • hierarchyNodeId, representing the HierarchyNodeId of the closest hierarchy node
  • contextNodeId, representing the NodeId of the target node
  • sourceNodeId, representing (for items in the hierarchy) the NodeId of the source node of the document reference for the hierarchy node corresponding to the item, or null otherwise

The root level of the structure view is represented by the parent items being set to null.

Due to the asynchronous nature of the UI, drag and drop operations may get their state queried in situations where the nodes referenced by these properties no longer appear in their documents. This could cause functions such as unsafeMoveNodes to fail. To detect and avoid such cases, make sure to check whether blueprint.lookup actually returns a node, and use blueprintQuery's isInDocument method to check whether relevant nodes are still part of their document.


The following is the source of the built-in custom mutation to support drag & drop in a single document. This can be a good starting point for creating your own drag & drop operation:

import CustomMutationResult from 'fontoxml-base-flow/src/CustomMutationResult.js';
import blueprintMutations from 'fontoxml-blueprints/src/blueprintMutations.js';
import blueprintQuery from 'fontoxml-blueprints/src/blueprintQuery.js';
import domInfo from 'fontoxml-dom-utils/src/domInfo.js';
import evaluateXPathToNumber from 'fontoxml-selectors/src/evaluateXPathToNumber.js';

function getContextNode(blueprint, ids) {
	return (ids && blueprint.lookup(ids.contextNodeId)) || null;

export default function moveHierarchyNodeInDom(stepData, blueprint, _format, _selection) {
	// Moving multiple nodes is not yet supported by the UI nor allowed by this custom mutation
	if (stepData.movingNodes.length !== 1) {
		return CustomMutationResult.notAllowed();

	var movingNode = getContextNode(blueprint, stepData.movingNodes[0]);
	var newParentNode = getContextNode(blueprint, stepData.newParentNode);
	var dropParentNode = getContextNode(blueprint, stepData.dropPosition.parentNode);
	var dropReferenceNode = getContextNode(blueprint, stepData.dropPosition.referenceNode);

	if (
		!newParentNode ||
		!dropParentNode ||
		!movingNode ||
		!blueprintQuery.isInDocument(blueprint, movingNode) ||
		domInfo.isDocument(newParentNode) ||
		movingNode.ownerDocument !== newParentNode.ownerDocument
	) {
		// This operation does not support drag & drop to the root level (which could
		// possibly create a new document?), or between different documents.
		// Additionally, ignore any operation state computation for nodes that are no
		// longer in their document (see warning above).
		return CustomMutationResult.notAllowed();

	if (blueprintQuery.contains(blueprint, movingNode, newParentNode)) {
		// A node should not be inserted as its own descendant
		return CustomMutationResult.notAllowed();

	if (
		dropReferenceNode !== null &&
		blueprint.getParentNode(dropReferenceNode) !== dropParentNode
	) {
		// The drop position reference node and parent node are not directly related, try to
		// find an ancestor of the reference node that is a child of the parent node.
		dropReferenceNode = blueprintQuery.findClosestAncestor(
			function(ancestor) {
				return blueprint.getParentNode(ancestor) === dropParentNode;
		if (dropReferenceNode === null) {
			// No descendant relation at all, block the operation
			return CustomMutationResult.notAllowed();

	// Check if we can traverse from the drop position to the new parent without skipping over
	// following elements or text, and determine the corresponding insertion position for the
	// moved node.
	var referenceNode = dropReferenceNode;
	for (
		var ancestor = dropParentNode;
		ancestor !== newParentNode;
		referenceNode = blueprint.getNextSibling(ancestor),
			ancestor = blueprint.getParentNode(ancestor)
	) {
		if (referenceNode === movingNode) {
			referenceNode = blueprint.getNextSibling(referenceNode);
		if (referenceNode === null) {
		if (
			domInfo.isElement(referenceNode) ||
			domInfo.isTextNode(referenceNode) ||
				'count(following-sibling::*) + count(following-sibling::text())',
			) > 0
		) {
			return CustomMutationResult.notAllowed();

	// Dropping a node before itself has no effect
	if (movingNode !== referenceNode) {
		// We can now just move the moving node to the new position

	// If we reached this point, the drop was successful. Fonto will validate the result to
	// ensure it is valid according to the schema, and disable the operation otherwise.
	return CustomMutationResult.ok();

Disabling drag and drop for a specific item

Preventing a user from drag and dropping a specific item in the structure view can be done through basic configuration. This is done by using the property isDraggable of configureAsStructureViewItem.


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

export default function configureSxModule() {
	configureAsStructureViewItem(sxModule, 'self::p', {
		isDraggable: false,
		icon: 'arrow-circle-down'