Your first editor

This guide will teach you how to build a simple Substance editor with basic editing functionalities. We call it SimpleWriter.

## The model

Let's start with defining a model that our editor can operate on. We want to support the following node types:

- `Body` - holds references to the actual content elements
- `Paragraph` a text paragraph that can be annotated
- `Heading` Headings of 3 different levels
- `Strong` Annotation that marks text as strong
- `Emphasis` Annotation that marks text as emphasized
- `Link` Link annotation
- `Comment` Comments in the text

Substance has a number of preimplemented node types, so we only need define the Body and Comment nodes.

The body node inherits from Container.

    class Body extends Container {}

    Body.define({
      type: 'body'
    })

The Comment node is a PropertyAnnotation and defines a content property to hold the comment text.

    class Comment extends PropertyAnnotation {}

    Comment.define({
      type: 'comment',
      content: { type: 'string', default: '' }
    })

## Node Converters

We choose HTML as a serialization format for our article. Hence, we need to define a converter for our body node, which converts an HTML body element to a Substance body node referencing a sequence of content nodes (paragraphs, headings).

    export default {
      type: 'body',
      tagName: 'body',
      import: function(el, node, converter) {
        node.id = 'body'
        node.nodes = el.children.map(function(child) {
          var childNode = converter.convertElement(child)
          return childNode.id
        })
      },
      export: function(node, el, converter) {
        el.append(converter.convertNodes(node.nodes))
      }
    }

And here is the converter for the comment node.

    export default {

      type: 'comment',
      tagName: 'span',

      matchElement: function(el) {
        return el.is('span[data-type="comment"]')
      },

      import: function(el, node) {
        node.content = el.attr('data-comment')
      },

      export: function(node, el) {
        el.attr({
          'data-type': 'comment',
          'data-comment': node.content
        }.append(node.content))
      }
    }

## HTML Importer

    class SimpleHTMLImporter extends HTMLImporter {
      convertDocument(htmlEl) {
        var bodyEl = htmlEl.find('body')
        this.convertElement(bodyEl)
      }
    }

## Node Components

For each node, we need to implement a Component in order to display it. The body element for us will be where editing starts. Containers can be made editable by just wrapping them in ContainerEditor, which we do here.

    class BodyComponent extends Component {
      render($$) {
        let node = this.props.node;
        let el = $$('div')
          .addClass('sc-body')
          .attr('data-id', this.props.node.id);

        el.append(
          $$(ContainerEditor, {
            disabled: this.props.disabled,
            node: node,
            commands: this.props.commands,
            textTypes: this.props.textTypes
          }).ref('body')
        )
        return el;
      }
    }

## Commands

In order to create comments in the user interface, we need to define a Command for it.

    class CommentCommand extends AnnotationCommand {
      canFuse()   { return false }
      canDelete() { return false }
    }

## Tool Components

In order to edit comments we provide a tool component for it.

    class EditCommentTool extends Tool {

      render($$) {
        let Input = this.getComponent('input')
        let Button = this.getComponent('button')
        let el = $$('div').addClass('sc-edit-comment-tool')

        el.append(
          $$(Input, {
            type: 'text',
            path: [this.props.node.id, 'content'],
            placeholder: 'Please enter comment here'
          }),
          $$(Button, {
            icon: 'delete',
            style: this.props.style
          }).on('click', this.onDelete)
        )
        return el
      }
      ...
    }

## Nodes as packages

Substance uses packages where possible, to be able to extend editors with new functionalities. Each plugin/extension comes with it's own package definition.

Here's the `BodyPackage`.

    export default {
      name: 'body',
      configure: function(config) {
        config.addNode(Body)
        config.addComponent(Body.type, BodyComponent)
        config.addConverter('html', BodyConverter)
      }
    }

And the `CommentPackage`.

    export default {
      name: 'link',
      configure: function(config, options) {
        config.addNode(Comment)
        config.addConverter('html', CommentConverter)

        // Tool to insert a new comment
        config.addCommand('comment', CommentCommand, {nodeType: 'comment'})
        config.addTool('comment', AnnotationTool, {target: options.toolTarget || 'annotations'})
        // Tool to edit an existing comment, should be displayed as an overlay
        config.addCommand('edit-comment', EditAnnotationCommand, {nodeType: 'comment'})
        config.addTool('edit-comment', EditCommentTool, { target: 'overlay' })

        // Icons and labels
        config.addIcon('comment', { 'fontawesome': 'fa-comment'})
        config.addLabel('comment', 'Comment')
      }
    }

Now we provide a configuration on editor level.

    import {
      BasePackage, StrongPackage, EmphasisPackage, LinkPackage, Document,
      ParagraphPackage, HeadingPackage
    } from 'substance'

    export default {
      name: 'simple-writer',
      configure: function (config) {
        config.defineSchema({
          name: 'simple-article',
          ArticleClass: Document,
          defaultTextType: 'paragraph'
        })

        // BasePackage provides core functionaliy, such as undo/redo
        // and the SwitchTextTypeTool. However, you could import those
        // functionalities individually if you need more control
        config.import(BasePackage)

        // core nodes
        config.import(ParagraphPackage)
        config.import(HeadingPackage)
        config.import(StrongPackage, {toolTarget: 'annotations'})
        config.import(EmphasisPackage, {toolTarget: 'annotations'})
        config.import(LinkPackage, {toolTarget: 'annotations'})

        // custom nodes
        config.import(BodyPackage)
        config.import(CommentPackage, {toolTarget: 'annotations'})

        // Override Importer/Exporter
        config.addImporter('html', SimpleHTMLImporter)
      }
    }

## Define a SimpleWriter component.

The SimpleWriter component forms our editor's heart. Some basic Substance infrastructure is set up by AbstractEditor, which we inherit from. We need to implement AbstractEditor#render and AbstractEditor#documentSessionUpdated. Substance uses a Component API similar to React, which be relatively easy to understand.

The following code shows the setup of an editor, rendering a toolbar on top and document's body. We delegate setting up the editor to the Body component, which is defined in a package and sets up the actual editor.

    class SimpleWriter extends AbstractEditor {

      render($$) {
        let el = $$('div').addClass('sc-simple-writer')
        let configurator = this.props.configurator
        let SplitPane = this.componentRegistry.get('split-pane')
        let ScrollPane = this.componentRegistry.get('scroll-pane')
        let commandStates = this.commandManager.getCommandStates()
        let Body = this.componentRegistry.get('body')

        let contentPanel = $$(ScrollPane, {
          scrollbarPosition: 'right',
          overlay: SimpleWriterOverlayTools,
        }).append(
          $$(Body, {
            disabled: this.props.disabled,
            node: this.doc.get('body'),
            commands: configurator.getSurfaceCommandNames(),
            textTypes: configurator.getTextTypes()
          }).ref('body')
        ).ref('contentPanel')

        el.append(
          $$(SplitPane, {splitType: 'horizontal'}).append(
            $$(Toolbar, {
              commandStates: commandStates
            }).ref('toolbar'),
            contentPanel
          )
        )
        return el
      }

      documentSessionUpdated() {
        let toolbar = this.refs.toolbar
        if (toolbar) {
          let commandStates = this.commandManager.getCommandStates()
          toolbar.setProps({
            commandStates: commandStates
          })
        }
      }
    }

## Use your new editor

    let cfg = new Configurator()
    cfg.import(SimpleWriterPackage)

    window.onload = function() {
      // Import article from HTML markup
      let importer = cfg.createImporter('html')
      let doc = importer.importDocument(fixture)
      // This is the data structure manipulated by the editor
      let documentSession = new DocumentSession(doc)
      // Mount SimpleWriter to the DOM and run it.
      SimpleWriter.mount({
        documentSession: documentSession,
        configurator: cfg
      }, document.body)
    }

Find the complete code for SimpleWriter on Github.

## Exercises

- Enable a Substance core node type (e.g. Superscript, Image) for SimpleWriter (very easy)
- Create a simple Highlight node type to emphasize parts of the text with a yellow background. Serialize as `<span data-type="highlight">...</span>` Tip: Look at existing implementations such as Strong. (easy)
- Create a new text type FancyParagraph, that works like a regular paragraph, just with different styles. Serialize as `<p data-type="fancy">...</p>` (easy)
- Create a simple Person node type with properties `firstname`, `lastname`, which are editable via regular input elements. Look at Input Example as a reference implementation. Create a tool that allows insertion of Person nodes into the document (as a block element). Serialize as `<div data-type="person" data-firstname="John" data-lastname="Doe"></div>`. (medium)
- Create new Monster node type that can be inserted inside the text. See InlineNode example as a reference implementation. Render a monster as small image that appears in the text. Bonus points: Allow different monster types and provide UI to change the moster type. Render a different image for each monster type. (medium)