├── .eslintrc.js
├── .gitignore
├── README.md
├── app
├── app.css
├── app.js
├── fixture.js
└── index.html
├── index.es.js
├── lib
├── body
│ ├── Body.js
│ ├── BodyComponent.js
│ ├── BodyConverter.js
│ └── BodyPackage.js
├── comment
│ ├── Comment.js
│ ├── CommentCommand.js
│ ├── CommentConverter.js
│ ├── CommentPackage.js
│ ├── EditCommentTool.js
│ └── _comment.css
└── simple-writer
│ ├── SimpleHTMLImporter.js
│ ├── SimpleWriter.js
│ ├── SimpleWriterPackage.js
│ └── _simple-writer.css
├── make.js
├── package.json
└── simple-writer.css
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "node": true
6 | },
7 | "parserOptions": {
8 | "sourceType": "module",
9 | "ecmaVersion": 6
10 | },
11 | "extends": "eslint:recommended",
12 | "globals": {
13 | "Promise": true,
14 | "Map": true
15 | },
16 | "rules": {
17 | // 0 - off, 1 - warning, 2 - error
18 | "indent": ["error", 2, { "SwitchCase": 1 }],
19 | "semi": [0, "never"],
20 | "comma-dangle": [2, "only-multiline"],
21 | "no-cond-assign": 2,
22 | "no-console": [2, { allow: ["warn", "info", "error", "assert"] }],
23 | "no-constant-condition": 2,
24 | "no-control-regex": 2,
25 | "no-debugger": 2,
26 | "no-dupe-args": 2,
27 | "no-dupe-keys": 2,
28 | "no-duplicate-case": 2,
29 | "no-empty": 1,
30 | "no-empty-character-class": 2,
31 | "no-ex-assign": 2,
32 | "no-extra-boolean-cast": 2,
33 | "no-extra-parens": 0,
34 | "no-extra-semi": 2,
35 | "no-func-assign": 2,
36 | "no-inner-declarations": 2,
37 | "no-invalid-regexp": 2,
38 | "no-irregular-whitespace": 2,
39 | "no-negated-in-lhs": 2,
40 | "no-obj-calls": 2,
41 | // turned of as we want to be able to use this.hasOwnProperty() for instance
42 | "no-prototype-builtins": 0,
43 | "no-regex-spaces": 2,
44 | "no-sparse-arrays": 0,
45 | "no-unexpected-multiline": 2,
46 | "no-unreachable": 2,
47 | "no-unsafe-finally": 2,
48 | "use-isnan": 2,
49 | "valid-jsdoc": 0,
50 | "valid-typeof": 2,
51 | "strict": 0, // [2, "safe"],
52 |
53 | // Best practices
54 | "accessor-pairs": 0,
55 | "array-callback-return": 2,
56 | "block-scoped-var": 2,
57 | "complexity": [0, 10],
58 | "consistent-return": 0,
59 | "curly": [2, "multi-line"],
60 | "default-case": 2,
61 | "dot-location": [2, 'property'],
62 | "dot-notation": 0,
63 | "eqeqeq": 2,
64 | "guard-for-in": 2,
65 | "no-alert": 2,
66 | "no-caller": 2,
67 | "no-case-declarations": 2,
68 | "no-div-regex": 2,
69 | "no-else-return": 0,
70 | "no-empty-function": 0,
71 | // if you want to check for undefined or null use lodash/isNil
72 | "no-eq-null": 2,
73 | "no-eval": 2,
74 | "no-extend-native": 2,
75 | "no-extra-bind": 2,
76 | "no-extra-label": 2,
77 | "no-fallthrough": 2,
78 | "no-floating-decimal": 2,
79 | "no-implicit-coercion": 2,
80 | "no-implicit-globals": 2,
81 | "no-implied-eval": 2,
82 | "no-invalid-this": 2,
83 | "no-iterator": 2,
84 | "no-labels": 2,
85 | "no-lone-blocks": 0,
86 | "no-loop-func": 2,
87 | "no-magic-numbers": 0,
88 | "no-multi-spaces": 2,
89 | "no-multi-str": 0,
90 | "no-native-reassign": 2,
91 | "no-new": 0,
92 | "no-new-func": 0,
93 | "no-new-wrappers": 2,
94 | "no-octal": 2,
95 | "no-octal-escape": 2,
96 | "no-param-reassign": 0,
97 | "no-proto": 2,
98 | "no-redeclare": 2,
99 | "no-return-assign": 2,
100 | "no-script-url": 2,
101 | "no-self-assign": 2,
102 | "no-self-compare": 2,
103 | "no-sequences": 2,
104 | "no-throw-literal": 2,
105 | "no-unmodified-loop-condition": 2,
106 | "no-unused-expressions": 2,
107 | "no-unused-labels": 2,
108 | "no-useless-call": 2,
109 | "no-useless-concat": 2,
110 | "no-useless-escape": 2,
111 | "no-void": 2,
112 | "no-warning-comments": 0,
113 | "no-with": 2,
114 | "radix": 2,
115 | "vars-on-top": 0,
116 | "wrap-iife": 2,
117 | "yoda": 0,
118 | // variables
119 | "init-declarations": 0,
120 | "no-catch-shadow": 2,
121 | "no-delete-var": 2,
122 | "no-label-var": 2,
123 | "no-restricted-globals": 2,
124 | "no-shadow": 0,
125 | "no-shadow-restricted-names": 2,
126 | "no-undef": 2,
127 | "no-undef-init": 2,
128 | "no-undefined": 0,
129 | "no-unused-vars": 2,
130 | "no-use-before-define": [2, { "functions": false, "classes": false }]
131 | }
132 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | dist
4 | node_modules
5 | .lock-wscript
6 | server.cjs*
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SimpleWriter
2 |
3 | SimpleWriter is the official Substance starter example. It sets up a minimal environment for Substance editor development. Fork this code and create your own editor.
4 |
5 | Read the [tutorial](http://substance.io/docs/beta5/your-first-editor.html).
6 |
7 | ## Install
8 |
9 | ```bash
10 | $ git clone https://github.com/substance/simple-writer.git
11 | ```
12 |
13 | Now install dependencies and start the dev environment.
14 |
15 | ```bash
16 | $ npm install
17 | $ npm start
18 | ```
19 |
20 | And navigate to `http://localhost:5555`.
21 |
--------------------------------------------------------------------------------
/app/app.css:
--------------------------------------------------------------------------------
1 | /* Substance Component styles */
2 | @import '../node_modules/substance/substance.css';
3 | /* You may want to use your own reset and pagestyle */
4 | @import '../node_modules/substance/substance-reset.css';
5 | @import '../node_modules/substance/substance-pagestyle.css';
6 |
7 | /* Using url here, so font-awesome does not get bundled. */
8 | @import url('./font-awesome/css/font-awesome.min.css');
9 |
10 | /* comment */
11 | @import '../lib/comment/_comment.css';
12 | /* simple-writer*/
13 | @import '../lib/simple-writer/_simple-writer.css';
14 |
15 | body { overflow: hidden; }
16 |
--------------------------------------------------------------------------------
/app/app.js:
--------------------------------------------------------------------------------
1 | import { Configurator, EditorSession } from 'substance'
2 | import SimpleWriter from '../lib/simple-writer/SimpleWriter'
3 | import SimpleWriterPackage from '../lib/simple-writer/SimpleWriterPackage'
4 | import fixture from './fixture'
5 |
6 | let cfg = new Configurator()
7 | cfg.import(SimpleWriterPackage)
8 |
9 | window.onload = function() {
10 | // Import article from HTML markup
11 | let importer = cfg.createImporter('html')
12 | let doc = importer.importDocument(fixture)
13 | // This is the data structure manipulated by the editor
14 | let editorSession = new EditorSession(doc, {
15 | configurator: cfg
16 | })
17 | // Mount SimpleWriter to the DOM and run it.
18 | SimpleWriter.mount({
19 | editorSession: editorSession
20 | }, document.body)
21 | }
22 |
--------------------------------------------------------------------------------
/app/fixture.js:
--------------------------------------------------------------------------------
1 | export default `
2 | SimpleWriter
3 | This is the official Substance editor boilerplate example. Fork it, and create your own editor.
4 | You can find the source code on Github.
5 | `
6 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example Editor
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/index.es.js:
--------------------------------------------------------------------------------
1 | /*
2 | Expose SimpleWriter as an ES6 module
3 | */
4 |
5 | export { default as SimpleWriter } from './lib/simple-writer/SimpleWriter'
6 | export { default as SimpleWriterPackage } from './lib/simple-writer/SimpleWriterPackage'
7 |
--------------------------------------------------------------------------------
/lib/body/Body.js:
--------------------------------------------------------------------------------
1 | import {Container} from 'substance'
2 |
3 | class Body extends Container {}
4 |
5 | Body.define({
6 | type: 'body'
7 | })
8 |
9 | export default Body
--------------------------------------------------------------------------------
/lib/body/BodyComponent.js:
--------------------------------------------------------------------------------
1 | import { Component, ContainerEditor } from 'substance'
2 |
3 | class BodyComponent extends Component {
4 | render($$) {
5 | let node = this.props.node;
6 | let el = $$('div')
7 | .addClass('sc-body')
8 | .attr('data-id', this.props.node.id);
9 |
10 | el.append(
11 | $$(ContainerEditor, {
12 | disabled: this.props.disabled,
13 | node: node,
14 | commands: this.props.commands,
15 | textTypes: this.props.textTypes
16 | }).ref('body')
17 | );
18 | return el;
19 | }
20 | }
21 |
22 | export default BodyComponent
--------------------------------------------------------------------------------
/lib/body/BodyConverter.js:
--------------------------------------------------------------------------------
1 | export default {
2 | type: 'body',
3 | tagName: 'body',
4 |
5 | import: function(el, node, converter) {
6 | node.id = 'body'
7 | node.nodes = el.getChildren().map(function(child) {
8 | var childNode = converter.convertElement(child)
9 | return childNode.id
10 | })
11 | },
12 |
13 | export: function(node, el, converter) {
14 | el.append(converter.convertNodes(node.nodes))
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/body/BodyPackage.js:
--------------------------------------------------------------------------------
1 | import Body from './Body'
2 | import BodyConverter from './BodyConverter'
3 | import BodyComponent from './BodyComponent'
4 |
5 | export default {
6 | name: 'body',
7 | configure: function(config) {
8 | config.addNode(Body)
9 | config.addComponent(Body.type, BodyComponent)
10 | config.addConverter('html', BodyConverter)
11 | }
12 | }
--------------------------------------------------------------------------------
/lib/comment/Comment.js:
--------------------------------------------------------------------------------
1 | import { PropertyAnnotation, Fragmenter } from 'substance'
2 |
3 | /**
4 | Comment node type, based on PropertyAnnotation. Defines
5 | comment property which holds the comment content as a string.
6 | */
7 | class Comment extends PropertyAnnotation {}
8 |
9 | Comment.define({
10 | type: 'comment',
11 | content: { type: 'string', default: '' }
12 | })
13 |
14 | // in presence of overlapping annotations will try to render this as one element
15 | Comment.fragmentation = Fragmenter.SHOULD_NOT_SPLIT
16 |
17 | export default Comment
--------------------------------------------------------------------------------
/lib/comment/CommentCommand.js:
--------------------------------------------------------------------------------
1 | import { AnnotationCommand } from 'substance'
2 |
3 | /**
4 | Command implementation used for creating, expanding and
5 | truncating comments.
6 |
7 | Fusion and deletion are disabled as these are handled by EditCommentTool.
8 | */
9 | class CommentCommand extends AnnotationCommand {
10 | canFuse() { return false }
11 | canDelete() { return false }
12 | }
13 |
14 | export default CommentCommand
15 |
--------------------------------------------------------------------------------
/lib/comment/CommentConverter.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | type: 'comment',
4 | tagName: 'span',
5 |
6 | /**
7 | Custom matcher, needed as matching by tagName is not sufficient
8 | */
9 | matchElement: function(el) {
10 | return el.is('span[data-type="comment"]')
11 | },
12 |
13 | /**
14 | Extract comment string from the data-comment attribute
15 | */
16 | import: function(el, node) {
17 | node.content = el.attr('data-comment')
18 | },
19 |
20 | /**
21 | Serialize comment node to span with data-type and data-comment
22 | attributes.
23 | */
24 | export: function(node, el) {
25 | el.attr({
26 | 'data-type': 'comment',
27 | 'data-comment': node.content
28 | }.append(node.content))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/comment/CommentPackage.js:
--------------------------------------------------------------------------------
1 | import { AnnotationTool, EditAnnotationCommand } from 'substance'
2 | import Comment from './Comment'
3 | import CommentCommand from './CommentCommand'
4 | import EditCommentTool from './EditCommentTool'
5 | import CommentConverter from './CommentConverter'
6 |
7 | /**
8 | Comment package that can be imported by SimpleWriter
9 |
10 | Provides a Comment node definition, a converter for HTML conversion,
11 | commands and tools for creation, and editing of comments.
12 | */
13 | export default {
14 | name: 'link',
15 | configure: function(config, options) {
16 | config.addNode(Comment)
17 | config.addConverter('html', CommentConverter)
18 |
19 | // Tool to insert a new comment
20 | config.addCommand('comment', CommentCommand, {nodeType: 'comment'})
21 | config.addTool('comment', AnnotationTool, {toolGroup: options.toolGroup || 'annotations'})
22 | // Tool to edit an existing comment, should be displayed as an overlay
23 | config.addCommand('edit-comment', EditAnnotationCommand, {nodeType: 'comment'})
24 | config.addTool('edit-comment', EditCommentTool, { toolGroup: 'overlay' })
25 |
26 | // Icons and labels
27 | config.addIcon('comment', { 'fontawesome': 'fa-comment'})
28 | config.addLabel('comment', 'Comment')
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/comment/EditCommentTool.js:
--------------------------------------------------------------------------------
1 | import { Tool } from 'substance'
2 |
3 | /**
4 | Simple comment editor, based on a regular
5 | input field. Changes are saved when the input
6 | field is blurred.
7 | */
8 | class EditCommentTool extends Tool {
9 |
10 | render($$) {
11 | let Input = this.getComponent('input')
12 | let Button = this.getComponent('button')
13 | let el = $$('div').addClass('sc-edit-comment-tool')
14 |
15 | el.append(
16 | $$(Input, {
17 | type: 'text',
18 | path: [this.props.node.id, 'content'],
19 | placeholder: 'Please enter comment here'
20 | }),
21 | $$(Button, {
22 | icon: 'delete',
23 | style: this.props.style
24 | }).on('click', this.onDelete)
25 | )
26 | return el
27 | }
28 |
29 | onDelete(e) {
30 | e.preventDefault();
31 | let node = this.props.node
32 | let session = this.context.editorSession
33 | session.transaction(function(tx, args) {
34 | tx.delete(node.id)
35 | return args
36 | })
37 | }
38 | }
39 |
40 | export default EditCommentTool
41 |
--------------------------------------------------------------------------------
/lib/comment/_comment.css:
--------------------------------------------------------------------------------
1 | .sc-comment {
2 | background: rgba(234, 231, 35, 0.5);
3 | }
4 |
5 | .sc-edit-comment-tool > * {
6 | display: inline-block;
7 | margin-right: 5px;
8 | }
9 |
--------------------------------------------------------------------------------
/lib/simple-writer/SimpleHTMLImporter.js:
--------------------------------------------------------------------------------
1 | import { HTMLImporter } from 'substance'
2 |
3 | /**
4 | HTML importer for the SimpleArticle. We delegate the work to
5 | BodyConverter.
6 | */
7 | export default class SimpleHTMLImporter extends HTMLImporter {
8 | convertDocument(htmlEl) {
9 | var bodyEl = htmlEl.find('body')
10 | this.convertElement(bodyEl)
11 | }
12 | }
--------------------------------------------------------------------------------
/lib/simple-writer/SimpleWriter.js:
--------------------------------------------------------------------------------
1 | import { AbstractEditor, Toolbar} from 'substance'
2 |
3 | /**
4 | We extend from AbstractEditor which provides an abstract implementation
5 | that should be feasible for most editors.
6 | */
7 | class SimpleWriter extends AbstractEditor {
8 |
9 | /*
10 | We render a toolbar, an editor for the body content
11 | */
12 | render($$) {
13 | let SplitPane = this.componentRegistry.get('split-pane')
14 | let el = $$('div').addClass('sc-simple-writer')
15 | let ScrollPane = this.componentRegistry.get('scroll-pane')
16 | let Overlay = this.componentRegistry.get('overlay')
17 | let ContextMenu = this.componentRegistry.get('context-menu')
18 | let Dropzones = this.componentRegistry.get('dropzones')
19 | let commandStates = this.commandManager.getCommandStates()
20 | let configurator = this.props.editorSession.getConfigurator()
21 | let Body = this.componentRegistry.get('body')
22 | let contentPanel = $$(ScrollPane, {
23 | scrollbarPosition: 'right'
24 | }).append(
25 | $$(Body, {
26 | disabled: this.props.disabled,
27 | node: this.doc.get('body'),
28 | commands: configurator.getSurfaceCommandNames(),
29 | textTypes: configurator.getTextTypes()
30 | }).ref('body'),
31 | $$(Overlay),
32 | $$(ContextMenu),
33 | $$(Dropzones)
34 | ).ref('contentPanel')
35 |
36 | el.append(
37 | $$(SplitPane, {splitType: 'horizontal'}).append(
38 | $$('div').addClass('se-toolbar-wrapper').append(
39 | $$(Toolbar, {
40 | commandStates: commandStates
41 | }).ref('toolbar')
42 | ),
43 | contentPanel
44 | )
45 | )
46 | return el
47 | }
48 | }
49 |
50 | export default SimpleWriter
51 |
--------------------------------------------------------------------------------
/lib/simple-writer/SimpleWriterPackage.js:
--------------------------------------------------------------------------------
1 | import {
2 | BasePackage, StrongPackage, EmphasisPackage, LinkPackage, Document,
3 | ParagraphPackage, HeadingPackage, CodeblockPackage, SwitchTextTypePackage
4 | } from 'substance'
5 |
6 | import BodyPackage from '../body/BodyPackage'
7 | import CommentPackage from '../comment/CommentPackage'
8 | import SimpleHTMLImporter from './SimpleHTMLImporter'
9 |
10 | /**
11 | Standard configuration for SimpleWriter
12 |
13 | We define a schema (simple-article) import some core packages
14 | from Substance, as well as custom node types.
15 |
16 | An HTML importer is registered to be able to turn HTML markup
17 | into a SimpleArticle instance.
18 | */
19 | export default {
20 | name: 'simple-writer',
21 | configure: function (config) {
22 | config.defineSchema({
23 | name: 'simple-article',
24 | ArticleClass: Document,
25 | defaultTextType: 'paragraph'
26 | })
27 |
28 | // BasePackage provides core functionaliy, such as undo/redo
29 | // and the SwitchTextTypeTool. However, you could import those
30 | // functionalities individually if you need more control
31 | config.import(BasePackage)
32 | config.import(SwitchTextTypePackage)
33 | // core nodes
34 | config.import(ParagraphPackage)
35 | config.import(HeadingPackage)
36 | config.import(CodeblockPackage)
37 | config.import(StrongPackage, {toolGroup: 'annotations'})
38 | config.import(EmphasisPackage, {toolGroup: 'annotations'})
39 | config.import(LinkPackage, {toolGroup: 'annotations'})
40 |
41 | // custom nodes
42 | config.import(BodyPackage)
43 | config.import(CommentPackage, {toolGroup: 'annotations'})
44 |
45 | // Override Importer/Exporter
46 | config.addImporter('html', SimpleHTMLImporter)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/simple-writer/_simple-writer.css:
--------------------------------------------------------------------------------
1 | .sc-simple-writer {}
2 | .sc-simple-writer .sc-container-editor {
3 | padding: var(--default-padding);
4 | margin: 0 auto;
5 | max-width: 800px;
6 | }
7 | /* Add padding to each child except last one */
8 | .sc-simple-writer .sc-container-editor > * {
9 | margin: var(--default-padding) 0px;
10 | }
11 |
12 | .sc-simple-writer .se-toolbar-wrapper {
13 | border-bottom: 1px solid var(--border-color);
14 | }
15 |
16 | .sc-simple-writer .se-toolbar-wrapper .sc-toolbar {
17 | max-width: 800px;
18 | margin: 0 auto;
19 | }
20 |
--------------------------------------------------------------------------------
/make.js:
--------------------------------------------------------------------------------
1 | let b = require('substance-bundler')
2 | let path = require('path')
3 |
4 | b.task('clean', function() {
5 | b.rm('./dist')
6 | })
7 |
8 | // copy assets
9 | b.task('assets', function() {
10 | b.copy('app/index.html', './dist/index.html')
11 | b.copy('./node_modules/font-awesome', './dist/font-awesome')
12 | })
13 |
14 | b.task('build', ['clean', 'assets'], function() {
15 | _client(false)
16 | })
17 |
18 | b.task('dev:build', ['clean', 'assets'], function() {
19 | _client(true)
20 | })
21 |
22 | b.task('default', ['build'])
23 |
24 | // starts a server when CLI argument '-s' is set
25 | b.setServerPort(5555)
26 | b.serve({
27 | static: true, route: '/', folder: 'dist'
28 | })
29 |
30 | function _client(devMode) {
31 | b.css('./app/app.css', 'dist/app.css', { variables: true })
32 | b.js('app/app.js', {
33 | target: {
34 | dest: './dist/app.js',
35 | format: 'umd',
36 | moduleName: 'app'
37 | },
38 | buble: !devMode
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "substance-simple-writer",
3 | "description": "Reference implementation for a custom Substance editor",
4 | "module": "index.es.js",
5 | "jsnext:main": "index.es.js",
6 | "devDependencies": {
7 | "substance": "1.0.0-beta.6.2",
8 | "font-awesome": "4.5.0",
9 | "substance-bundler": "0.7.2"
10 | },
11 | "scripts": {
12 | "build": "node make",
13 | "start": "node make -s -w"
14 | },
15 | "version": "1.0.0-beta.6",
16 | "files": [
17 | "app",
18 | "dist",
19 | "lib",
20 | "*.css",
21 | "*.js",
22 | "package.json"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/simple-writer.css:
--------------------------------------------------------------------------------
1 | /* comment */
2 | @import './lib/comment/_comment.css';
3 | /* simple-writer*/
4 | @import './lib/simple-writer/_simple-writer.css';
5 |
--------------------------------------------------------------------------------