substance

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

Substance

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

See Substance in action:

Building a web editor is a hard task. Native browser support for text editing is limited and not reliable. There are many pitfalls such as handling selections, copy&paste or undo/redo. Substance was developed to solve the common problems of web-editing and provides API's for building custom editors.

With Substance you can:

  • Define a custom article schema
  • Manipulate content and annotations using operations and tranformations
  • Define a custom HTML structure for your content and attach a Substance Surface on it to make it editable
  • Implement tools for any possible task like toggling annotations, inserting content or replacing text
  • Control copy&paste behavior by defining custom HTML converters
  • and much more.

Substance provides a ready to use Editor component. It can be integrated easily into an existing web application.

var Editor = require('substance/ui/editor');
var Component = require('substance/ui/component');
var $$ = Component.$$;
var proseEditor = Component.mount($$(Editor, {
  content: '<p>hello world</p>'
}), $('#editor_container'));

Substance Editor takes HTML as an input and lets you access to edited content at any time.

proseEditor.getContent();

Behind the curtains, your document is converted to a Javascript document model, that guarantees you a reliable and sideffect-free editing experience. You have access to this data as well.

proseEditor.getDocument();

You may want to restrict the supported content types and customize the toolbar a bit. Or you decide to define completely new content types. This is all possible by patching your very own editor.

Defining custom article formats.

Substance allows you to define completely custom article formats.

var Paragraph = Substance.Document.Paragraph;
var Emphasis = Substance.Document.Emphasis;
var Strong = Substance.Document.Strong;
 
var Highlight = Document.ContainerAnnotation.extend({
  name: 'highlight',
  properties: {
    created_at: 'date'
  }
});
 
var schema = new Document.Schema("my-article", "1.0.0");
schema.getDefaultTextType = function() {
  return "paragraph";
};
schema.addNodes([Paragraph, Emphasis, Strong, Highlight]);

We provide a reference implementation, the Substance Article. Usually want to come up with your own schema and only borrow common node types such as paragraphs and headings. The Notepad demo implements a nice example for reference.

Substance documents can be manipulated incrementally using simple operations. Let's grab an existing article implementation and create instances for it.

var Article = require('substance/article');
var doc = new Article();

When you want to update a document, you must wrap your changes in a transaction, to avoid inconsistent in-between states. The API is fairly easy. Let's create several paragraph nodes in one transaction.

doc.transaction(function(tx) {
  tx.create({
    id: "p1",
    type: "paragraph",
    content: "Hi I am a Substance paragraph."
  });
 
  tx.create({
    id: "p2",
    type: "paragraph",
    content: "And I am the second pargraph"
  });
});

A Substance document works like an object store, you can create as many nodes as you wish and assign unique id's to them. However in order to show up as content, we need to show them on a container.

doc.transaction(function(tx) {
  var body = tx.get('body');
 
  body.show('p1');
  body.show('p2');
});

Now let's make a strong annotation. In Substance annotations are stored separately from the text. Annotations are just regular nodes in the document. They refer to a certain range (startOffset, endOffset) in a text property (path).

doc.transaction(function(tx) {
  tx.create({
    "id": "s1",
    "type": "strong",
    "path": [
      "p1",
      "content"
    ],
    "startOffset": 10,
    "endOffset": 19
  });
});

Transformations are there to define higher level document operations that editor implementations can use. We implemented a range of useful transformations that editor implementations can use. However, you are encouraged to define your own. Below is a shortened version of a possible searchAndReplace transformation.

function searchAndReplace(txargs) {
  // 1. verify arguments args.searchStr, args.replaceStr, args.container 
  // 2. implement your transformation using low level operations (e.g. tx.create) 
  // ... 
  var searchResult = search(tx, args);
  
  searchResult.matches.forEach(function(match) {
    var replaceArgs = _.extend({}, args, {selection: match, replaceStr: args.replaceStr});
    replaceText(tx, replaceArgs);
  });
  
  // 3. set new selection 
  if (searchResult.matches.length > 0) {
    var lastMatch = _.last(searchResult.matches);
    args.selection = lastMatch;
  }
  
  // 4. return args for the caller or transaction context 
  return args;
}
module.exports = searchAndReplace;

Transformations always take 2 parameters: tx is a document transaction and args are the transformation's arguments. Transformations are combinable, so in a transformation you can call another transformation. You just need to be careful to always set the args properly. Here's how the transformation we just defined can be called in a transaction.

surface.transaction(function(txargs) {
  args.searchStr = "foo";
  args.replaceStr = "bar";
  return searchAndReplace(tx, args);
});

Using the transaction method on a Surface instance passes the current selection to the transformation automatically. So you will use surface transactions whenever some kind of selection is involved in your action. If the selection doesn't matter you can use the same transformation within a document.transaction call. Make sure that your transformations are robust for both scenarios. If you look at the above example under (3) we set the selection to the last matched element after search and replace. If something has been found.

Look at our reference implementation for orientation.

Editors need to setup a bit of Substance infrastructure first, most importantly a Substance Surface, that maps DOM selections to internal document selections.

this.surfaceManager = new Substance.Surface.SurfaceManager(doc);
this.clipboard = new Substance.Surface.Clipboard(this.surfaceManager, doc.getClipboardImporter(), doc.getClipboardExporter());
var editor = new Substance.Surface.ContainerEditor('body');
this.surface = new Surface(this.surfaceManager, doc, editor);

A Surface instance requires a SurfaceManager, which keeps track of multiple Surfaces and dispatches to the currently active one. It also requires an editor. There are two kinds of editors: A ContainerEditor manages a sequence of nodes, including breaking and merging of text nodes. A FormEditor by contrast allows you to define a fixed structure of your editable content. Furthermore we initialized a clipboard instance and tie it to the Surface Manager.

We also setup a registry for components (such as Paragraph) and tools (e.g. EmphasisTool, StrongTrool). Our editor will then be able to dynamically retrieve the right view component for a certain node type.

To learn how to build your own editor check out this tutorial on creating a Notepad editor with Substance.