├── .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 | --------------------------------------------------------------------------------