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:
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:
Substance Surface
on it to make it editableA 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.
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.
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
});
});
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.
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.
We provide a framework, that allows building
$ npm test
$ npm start
Then open http://localhost:4201/test in your browser.
$ npm run karma
The report will be stored in the coverage
folder.