├── .npmignore ├── doc ├── .DS_Store └── pic.png ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── src └── simple_markdown_editor.js /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /doc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seibelj/react-simple-markdown-editor/HEAD/doc/.DS_Store -------------------------------------------------------------------------------- /doc/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seibelj/react-simple-markdown-editor/HEAD/doc/pic.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | dist 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James Seibel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-simple-markdown-editor", 3 | "version": "1.1.0", 4 | "description": "Simple markdown editor widget you can attach to any TextArea element to provide rich markdown capabilities. Requires few dependencies", 5 | "main": "./dist/simple_markdown_editor.js", 6 | "scripts": { 7 | "build": "babel src --presets babel-preset-es2015,babel-preset-react --out-dir dist" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/seibelj/react-simple-markdown-editor.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "markdown", 16 | "editor", 17 | "simple", 18 | "remarkable" 19 | ], 20 | "author": "James Seibel", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/seibelj/react-simple-markdown-editor/issues" 24 | }, 25 | "homepage": "https://github.com/seibelj/react-simple-markdown-editor#readme", 26 | "devDependencies": { 27 | "babel-cli": "^6.6.5", 28 | "babel-preset-es2015": "^6.6.0", 29 | "babel-preset-react": "^6.5.0" 30 | }, 31 | "dependencies": { 32 | "lodash": "^4.6.1", 33 | "react": "^0.14.7" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Simple React Markdown Editor ## 2 | This makes it easy to add a simple markdown editing widget to any TextArea element. 3 | ![example](https://raw.githubusercontent.com/seibelj/react-simple-markdown-editor/master/doc/pic.png) 4 | 5 | ### Demo 6 | [CodePen Demo](http://codepen.io/seibelj/pen/rewRMe) 7 | 8 | ### Installation 9 | `npm install react-simple-markdown-editor` 10 | 11 | ### Features 12 | 13 | - Entirely customizable. Modify CSS easily with props, or add custom classes and modify CSS with stylesheets. Define which buttons are visible. 14 | - The only package dependencies are `react` and `lodash`, minimizing risk. 15 | 16 | 17 | 18 | ### Usage 19 | 20 | In your code: 21 | 22 | ES6: 23 | ```javascript 24 | import {SimpleMarkdownEditor} from 'react-simple-markdown-editor'; 25 | ``` 26 | 27 | Non-ES6: 28 | ```javascript 29 | var SimpleMarkdownEditor = require('react-simple-markdown-editor'); 30 | ``` 31 | 32 | In your React `render()` function: 33 | 34 | `` 35 | 36 | ### Rendering Markdown 37 | Use another library like [react-remarkable](https://github.com/acdlite/react-remarkable) in combination with this. Then set the `source` of the remarkable component to the value of your TextArea element. 38 | 39 | ### API 40 | Props: 41 | ```javascript 42 | SimpleMarkdownEditor.propTypes = { 43 | // Required props 44 | textAreaID: PropTypes.string.isRequired, 45 | 46 | // Optional props 47 | styles: PropTypes.object, 48 | containerClass: PropTypes.string, 49 | buttonClass: PropTypes.string, 50 | enabledButtons: PropTypes.object, 51 | buttonHtmlText: PropTypes.object, 52 | additionalProps: PropTypes.object 53 | }; 54 | ``` 55 | `textAreaID` (String, Required): The ID of the TextArea element you want the editor attached to. When you press buttons in this widget, the text in this TextArea will be modified. 56 | 57 | `styles`: (Object, optional): Used to overwrite inline CSS without using your own stylesheets. 58 | 59 | Existing properties: 60 | 61 | ```javascript 62 | container: { 63 | 64 | }, 65 | button: { 66 | fontFamily: 'Georgia, serif', 67 | backgroundColor: '#333536', 68 | color: 'white', 69 | marginRight: '5px', 70 | float: 'left', 71 | width: '25px', 72 | borderRadius: '4px', 73 | textAlign: 'center', 74 | cursor: 'pointer' 75 | } 76 | ``` 77 | 78 | For instance, if you want to add a border to each button: 79 | `` 80 | 81 | `containerClass` and `buttonClass` (String, optional): Provide classes to the container and button elements, so you can overwrite them using your own CSS stylesheets. An alternative to setting the `styles` prop. 82 | 83 | `enabledButtons`: (Object, optional): Hide any buttons you don't want to show. All of them default to showing. Buttons: 84 | 85 | ```javascript 86 | { 87 | bold: true, 88 | italic: true, 89 | strike: true, 90 | code: true, 91 | quote: true, 92 | h1: true, 93 | h2: true, 94 | h3: true, 95 | bullet: true, 96 | link: true, 97 | image: true 98 | } 99 | ``` 100 | For instance, if you want to hide the link button: 101 | `` 102 | 103 | `buttonHtmlText`: (Object, optional): Change the display text of any buttons, including any HTML markup. Defaults: 104 | 105 | ```javascript 106 | { 107 | bold: 'B', 108 | italic: 'I', 109 | strike: 'S', 110 | code: '< >', 111 | quote: '“ ”', 112 | h1: 'H1', 113 | h2: 'H2', 114 | h3: 'H3', 115 | bullet: '•', 116 | link: '#', 117 | image: '[i]' 118 | } 119 | ``` 120 | For instance, if you want to change `code` to be a square `quote` to be 2 right arrows: 121 | `` 122 | 123 | `additionalProps`: (Object, optional): Add arbitrary props to any button. For instance: 124 | `` 125 | 126 | ### License 127 | MIT, use for free. If you like this, give it a star. 128 | -------------------------------------------------------------------------------- /src/simple_markdown_editor.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import {merge} from 'lodash'; 3 | 4 | class SimpleMarkdownEditor extends React.Component { 5 | 6 | wrapText(symbol, endSymbol, insertAfter) { 7 | 8 | if (!endSymbol) { 9 | endSymbol = symbol; 10 | } 11 | 12 | let elem = document.getElementById(this.props.textAreaID), 13 | start = elem.selectionStart, 14 | end = elem.selectionEnd, 15 | text = elem.value; 16 | 17 | let afterText = insertAfter ? insertAfter : ''; 18 | 19 | elem.value = text.substring(0, start) + symbol + text.substring(start, end) + endSymbol + afterText + text.substring(end, text.length); 20 | elem.focus(); 21 | elem.setSelectionRange(start + symbol.length, end + endSymbol.length); 22 | } 23 | 24 | insertBold() { 25 | this.wrapText('**'); 26 | } 27 | 28 | insertItalics() { 29 | this.wrapText('_'); 30 | } 31 | 32 | insertStrike() { 33 | this.wrapText('~~'); 34 | } 35 | 36 | insertCode() { 37 | this.wrapText('`'); 38 | } 39 | 40 | insertAtBeginningOfLine(symbol) { 41 | let elem = document.getElementById(this.props.textAreaID), 42 | start = elem.selectionStart, 43 | end = elem.selectionEnd, 44 | text = elem.value; 45 | 46 | let newLineIndex = text.lastIndexOf('\n', start - 1); 47 | if (newLineIndex === -1) { 48 | elem.value = symbol + text; 49 | } 50 | else { 51 | elem.value = text.substring(0, newLineIndex + 1) + symbol + text.substring(newLineIndex + 1, text.length); 52 | } 53 | elem.focus(); 54 | elem.setSelectionRange(start + symbol.length, end + symbol.length); 55 | } 56 | 57 | insertH1() { 58 | this.insertAtBeginningOfLine('# '); 59 | } 60 | 61 | insertH2() { 62 | this.insertAtBeginningOfLine('## '); 63 | } 64 | 65 | insertH3() { 66 | this.insertAtBeginningOfLine('### '); 67 | } 68 | 69 | insertQuote() { 70 | this.insertAtBeginningOfLine('> '); 71 | } 72 | 73 | insertBullet() { 74 | this.insertAtBeginningOfLine('* '); 75 | } 76 | 77 | insertLink() { 78 | let elem = document.getElementById(this.props.textAreaID), 79 | start = elem.selectionStart, 80 | end = elem.selectionEnd, 81 | text = elem.value, 82 | link = "(http://www.mylink.com/)"; 83 | 84 | if (start === end) { 85 | elem.value = text.substring(0, start) + "[Link Text]" + link + text.substring(start, text.length); 86 | elem.focus(); 87 | elem.setSelectionRange(start, start); 88 | } 89 | else { 90 | this.wrapText('[', ']', link); 91 | } 92 | } 93 | 94 | insertImage() { 95 | let elem = document.getElementById(this.props.textAreaID), 96 | start = elem.selectionStart, 97 | end = elem.selectionEnd, 98 | text = elem.value, 99 | link = "(http://myhost.com/my_image.jpg)"; 100 | 101 | if (start === end) { 102 | elem.value = text.substring(0, start) + "![Image Description]" + link + text.substring(start, text.length); 103 | elem.focus(); 104 | elem.setSelectionRange(start, start); 105 | } 106 | else { 107 | this.wrapText('![', ']', link); 108 | } 109 | } 110 | 111 | render() { 112 | 113 | let styles = merge({}, this.constructor.styles, this.props.styles), 114 | enabledButtons = merge({}, this.constructor.enabledButtons, this.props.enabledButtons), 115 | buttonHtmlText = merge({}, this.constructor.buttonHtmlText, this.props.buttonHtmlText), 116 | additionalProps = merge({}, this.constructor.additionalProps, this.props.additionalProps); 117 | 118 | return ( 119 |
120 | {enabledButtons.bold && 121 |
123 | } 124 | {enabledButtons.italic && 125 |
127 | } 128 | {enabledButtons.strike && 129 |
131 | } 132 | {enabledButtons.code && 133 |
135 | } 136 | {enabledButtons.quote && 137 |
139 | } 140 | {enabledButtons.h1 && 141 |
143 | } 144 | {enabledButtons.h2 && 145 |
147 | } 148 | {enabledButtons.h3 && 149 |
151 | } 152 | {enabledButtons.bullet && 153 |
155 | } 156 | {enabledButtons.link && 157 |
159 | } 160 | {enabledButtons.image && 161 |
163 | } 164 | 165 |
166 | ); 167 | } 168 | } 169 | 170 | SimpleMarkdownEditor.styles = { 171 | container: { 172 | 173 | }, 174 | button: { 175 | fontFamily: 'Georgia, serif', 176 | backgroundColor: '#333536', 177 | color: 'white', 178 | marginRight: '5px', 179 | float: 'left', 180 | width: '25px', 181 | borderRadius: '4px', 182 | textAlign: 'center', 183 | cursor: 'pointer' 184 | } 185 | }; 186 | 187 | SimpleMarkdownEditor.enabledButtons = { 188 | bold: true, 189 | italic: true, 190 | strike: true, 191 | code: true, 192 | quote: true, 193 | h1: true, 194 | h2: true, 195 | h3: true, 196 | bullet: true, 197 | link: true, 198 | image: true 199 | } 200 | 201 | SimpleMarkdownEditor.buttonHtmlText = { 202 | bold: 'B', 203 | italic: 'I', 204 | strike: 'S', 205 | code: '< >', 206 | quote: '“ ”', 207 | h1: 'H1', 208 | h2: 'H2', 209 | h3: 'H3', 210 | bullet: '•', 211 | link: '#', 212 | image: '[i]' 213 | } 214 | 215 | SimpleMarkdownEditor.additionalProps = { 216 | bold: {}, 217 | italic: {}, 218 | strike: {}, 219 | code: {}, 220 | quote: {}, 221 | h1: {}, 222 | h2: {}, 223 | h3: {}, 224 | bullet: {}, 225 | link: {}, 226 | image: {} 227 | } 228 | 229 | SimpleMarkdownEditor.propTypes = { 230 | // Required props 231 | textAreaID: PropTypes.string.isRequired, 232 | 233 | // Optional props 234 | styles: PropTypes.object, 235 | containerClass: PropTypes.string, 236 | buttonClass: PropTypes.string, 237 | enabledButtons: PropTypes.object, 238 | buttonHtmlText: PropTypes.object, 239 | additionalProps: PropTypes.object 240 | }; 241 | 242 | exports.SimpleMarkdownEditor = SimpleMarkdownEditor; --------------------------------------------------------------------------------