Substance
Substance is a JavaScript library for manipulating documents based on data. It enables you to build completely custom web-based editors. See Substance in action:
- Substance HTML Editor - A minimal HTML editor component based on Substance
- Lens Writer - A full-fledged scientific editor
- Archivist Writer - A modern interface for tagging entities and subjects in digital archives
Motivation
Building a web editor is a hard task. Native browser support for text editing is limited and not reliable and 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 transactions
- Define a custom HTML structure and attach a
Substance Surface
on it to make it editable - Implement custom tools for any possible task like toggling annotations, inserting content or replacing text
- Control undo/redo behavior and copy&paste
- and more.
Getting started
A good way to get started with Substance is checking out our fully customizable and highly reliable HTML Editor component. Unlike most web-based editors, Substance operates on a Javascript data model, and uses HTML just as an input and output format. You can inject the editor using React.render
.
var HtmlEditor = require('substance-html-editor');
React.render(
$$(HtmlEditor, {
ref: 'htmlEditor',
content: '<p>Hello <strong>world</strong></p><p>Some <em>emphasized</em> text</p>',
toolbar: Toolbar,
enabledTools: ["text", "strong", "emphasis"],
onContentChanged: function(doc, change) {
console.log('new content', doc.toHtml());
}
}),
document.getElementById('editor_container')
);
Please see the README of HtmlEditor for configuration options.
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]);
A very simple one is the HtmlArticle specification used by our HtmlEditor. Lens Writer defines a scientific article including bib items and figures with captions etc.
Manipulate documents programmatically
Substance documents can be manipulated incrementally using simple operations. Let's grab an existing article implementation and create instances for it.
var doc = new RichTextArticle();
When you want to update a document, you should wrap your changes in a transaction, so you don't end up in 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) {
// Get the body container
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
});
});
Developing editors
In order to build your own editor based on Substance we recommend that you poke into the code of existing editors. HtmlEditor implements an editor for HTML content in under 200 lines of code.
Editor initialization
Editors to setup a bit of Substance infrastructure first, most importantly a Substance Surface, that maps DOM selections to internal document selections. Here's the most important parts from the initialization phase.
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.
2-column editing
We provide a framework, that allows building
Development
Testing
- Running the test-suite headless (using Phantom.js)
$ npm test
- Running the test-suite in a browser for debugging:
$ npm start
Then open http://localhost:4201/test in your browser.
- Running test-suite using Karma to generate a code coverage report.
$ npm run karma
The report will be stored in the coverage
folder.