Metadata sidebar

This recipe covers a very basic metadata form for use in a sidebar. It's mostly meant to serve as an example for an editing roundtrip between an opened XML document and a custom form within a sidebar.

This is by no means an example for a be-all and end-all metadata sidebar. Assumptions have been made, and functionality has been omitted, in favour of brevity. It should be carefully evaluated and refined when building a production worthy metadata sidebar.

Recommended reading

  • The 'Create a sidebar' guide for instructions on creating and setting up a sidebar.

  • The 'Create a form' guide for detailed information about creating forms.

MetadataSidebar.jsx.js

JavaScript

import React, { Component } from 'react';

// More information about each of the following imports can be found in the FontoXML Editor API
// section of the documentation (http://documentation.fontoxml.com/api/).
import {
	Button,
	Form,
	FormRow,
	SidebarHeader,
	SidebarInlay,
	TextInput
} from 'fds/components';

import documentsManager from 'fontoxml-documents/documentsManager';
import evaluateXPathToFirstNode from 'fontoxml-selectors/evaluateXPathToFirstNode';
import evaluateXPathToString from 'fontoxml-selectors/evaluateXPathToString';
import getNodeId from 'fontoxml-dom-identification/getNodeId';
import operationsManager from 'fontoxml-operations/operationsManager';
import readOnlyBlueprint from 'fontoxml-blueprints/readOnlyBlueprint';
import selectionManager from 'fontoxml-selection/selectionManager';
import t from 'fontoxml-localization/t';

// For this example a mandatory, ever-present XML node is assumed.
const XPATH_FOR_RETRIEVING_NODE = 'map/topicmeta/navtitle';

// Retrieve the text content of an XML node when used in conjunction with 'evaluateXPathToString'.
const XPATH_FOR_RETRIEVING_NODE_VALUE = 'string(.)';

export default class MetadataPanel extends Component {
	state = {
		documentId: null,
		metadataNodeId: null,
		metadataNodeValue: ''
	}

	// The selection can change quite often when users are editing documents, so keep performance
	// in mind when acting on these changes.
	setStateDataOnSelectionChange = () => {
		// Query the focused document for form data and update the component state.
		var focusedDocumentId = selectionManager.focusedDocumentId,
			documentNode = documentsManager.getDocumentNode(focusedDocumentId),
			metadataNode = evaluateXPathToFirstNode(XPATH_FOR_RETRIEVING_NODE, documentNode, readOnlyBlueprint) || null,
			metadataNodeId = metadataNode ? getNodeId(metadataNode) : null,
			metadataNodeValue = metadataNode ?
				evaluateXPathToString(XPATH_FOR_RETRIEVING_NODE_VALUE, metadataNode, readOnlyBlueprint) :
				'';

		this.setState({
			documentId: focusedDocumentId,
			metadataNodeId: metadataNodeId,
			metadataNodeValue: metadataNodeValue
		});
	}

	componentDidMount () {
		// Get notified when the users' selection changes, which potentially means another document
		// is now focused or the focused document was modified.
		//
		// 'selectionChangeNotifier.addCallback' returns a deregistration function.
		this.removeSelectionChangedCallback = selectionManager.selectionChangeNotifier.addCallback(
			this.setStateDataOnSelectionChange
		);

		// Manual update to set initial form data when opening the sidebar.
		this.setStateDataOnSelectionChange();
	}

	componentWillUnmount () {
		// Call the deregistration function right before the component is destroyed.
		this.removeSelectionChangedCallback();
	}

	save = () => {
		// Details about the 'replace-node' operation, and potential alternatives, can be found in
		// the FontoXML Editor API section of the documentation. It's recommended to read through
		// them to find out which operation is best suited for your usecase.
		//
		// Tip: make sure to only execute a single operation for an intuitive undo/redo stack.
		//
		// 'operationsManager.executeOperation' returns a Promise. It's recommended to implement
		// 'then' and 'catch' handlers, and present users with some form of feedback when saving.
		operationsManager.executeOperation(
			'replace-node',
			{
				contextNodeId: this.state.metadataNodeId,
				replacementNodeStructure: [
					'navtitle',
					this.state.metadataNodeValue
				]
			});
	}

	// Below is a very basic form with a single field and a save button. The field and save button
	// are disabled when there is no metadata node id set, meaning a node matching
	// 'XPATH_FOR_RETRIEVING_NODE' could not be found within the focused document.
	//
	// Tip: using an explicit 'save' button is just one of many UI patterns for updating a document
	// with the form data. The 'onChange' of either the form or individual fields can be used for
	// (near) instantaneous updates for instance.
	//
	// Tip: use 'operationsManager.getOperationState' to determine the outcome of an operation
	// before actually attempting it. Its result can be reflected in the UI for instance.
	render () {
		return (
			<metadata-panel>
				<SidebarHeader
					subtitle={ t('Example metadata sidebar') }
					title={ t('Metadata') }
					button={
						<Button
							isDisabled={this.state.metadataNodeId === null}
							label={ t('Save') }
							onClick={this.save}
							tooltipContent={ t('Save the metadata.') }
							type="primary"
						/>
					}
				/>

				<SidebarInlay isScrollContainer={true}>
					<Form labelPosition="before">
						<FormRow
							key="text"
							label={ t('Example field') }>
							<TextInput
								isDisabled={this.state.metadataNodeId === null}
								name="text"
								onChange={(metadataNodeValue) => this.setState({ metadataNodeValue })}
								placeholder={ t('type something here') }
								tooltip={ t('This field serves as an example.') }
								value={this.state.metadataNodeValue}
							/>
						</FormRow>
					</Form>
				</SidebarInlay>
			</metadata-panel>
		);
	}
}