├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── README.md ├── demo.gif ├── demo.html ├── dist ├── pell.css ├── pell.js ├── pell.min.css └── pell.min.js ├── examples ├── react.md └── vue.md ├── gulpfile.js ├── images ├── browserstack.png └── logo.png ├── package-lock.json ├── package.json └── src ├── pell.js └── pell.scss /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"modules": false}], 4 | "stage-0" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "jared" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jared Reich 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### [v2 working branch](https://github.com/jaredreich/pell/tree/v2) and [v2 project board](https://github.com/jaredreich/pell/projects/1) 2 | 3 | --- 4 | 5 | Logo 6 | 7 | [![npm](https://img.shields.io/npm/v/pell.svg)](https://www.npmjs.com/package/pell) 8 | [![cdnjs](https://img.shields.io/cdnjs/v/pell.svg)](https://cdnjs.com/libraries/pell) 9 | 10 | > pell is the simplest and smallest WYSIWYG text editor for web, with no dependencies 11 | 12 | ![Demo](/demo.gif?raw=true "Demo") 13 | 14 | ## Comparisons 15 | 16 | | library | size (min+gzip) | size (min) | jquery | bootstrap | react | link | 17 | |---------------|-----------------|------------|--------|-----------|-------|------| 18 | | pell | 1.38kB | 3.54kB | | | | https://github.com/jaredreich/pell | 19 | | squire | 16kB | 49kB | | | | https://github.com/neilj/Squire | 20 | | medium-editor | 27kB | 105kB | | | | https://github.com/yabwe/medium-editor | 21 | | quill | 43kB | 205kB | | | | https://github.com/quilljs/quill | 22 | | trix | 47kB | 204kB | | | | https://github.com/basecamp/trix | 23 | | ckeditor | 163kB | 551kB | | | | https://ckeditor.com | 24 | | trumbowyg | 8kB | 23kB | x | | | https://github.com/Alex-D/Trumbowyg | 25 | | summernote | 26kB | 93kB | x | x | | https://github.com/summernote/summernote | 26 | | draft | 46kB | 147kB | | | x | https://github.com/facebook/draft-js | 27 | | froala | 52kB | 186kB | x | | | https://github.com/froala/wysiwyg-editor | 28 | | tinymce | 157kB | 491kB | x | | | https://github.com/tinymce/tinymce | 29 | 30 | ## Features 31 | 32 | * Pure JavaScript, no dependencies, written in ES6 33 | * Easily customizable with the sass file (pell.scss) or overwrite the CSS 34 | 35 | Included actions: 36 | - Bold 37 | - Italic 38 | - Underline 39 | - Strike-through 40 | - Heading 1 41 | - Heading 2 42 | - Paragraph 43 | - Quote 44 | - Ordered List 45 | - Unordered List 46 | - Code 47 | - Horizontal Rule 48 | - Link 49 | - Image 50 | 51 | Other available actions (listed at https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand): 52 | - Justify Center 53 | - Justify Full 54 | - Justify Left 55 | - Justify Right 56 | - Subscript 57 | - Superscript 58 | - Font Name 59 | - Font Size 60 | - Indent 61 | - Outdent 62 | - Clear Formatting 63 | - Undo 64 | - Redo 65 | 66 | Or create any custom action! 67 | 68 | ## Browser Support 69 | 70 | * IE 9+ (theoretically, but good luck) 71 | * Chrome 5+ 72 | * Firefox 4+ 73 | * Safari 5+ 74 | * Opera 11.6+ 75 | 76 | ## Installation 77 | 78 | #### npm: 79 | 80 | ```bash 81 | npm install --save pell 82 | ``` 83 | 84 | #### HTML: 85 | 86 | ```html 87 | 88 | ... 89 | 90 | 96 | 97 | 98 | ... 99 | 100 | 101 | 102 | ``` 103 | 104 | ## Usage 105 | 106 | #### API 107 | 108 | ```js 109 | // ES6 110 | import pell from 'pell' 111 | // or 112 | import { exec, init } from 'pell' 113 | ``` 114 | 115 | ```js 116 | // Browser 117 | pell 118 | // or 119 | window.pell 120 | ``` 121 | 122 | ```js 123 | // Initialize pell on an HTMLElement 124 | pell.init({ 125 | // , required 126 | element: document.getElementById('some-id'), 127 | 128 | // , required 129 | // Use the output html, triggered by element's `oninput` event 130 | onChange: html => console.log(html), 131 | 132 | // , optional, default = 'div' 133 | // Instructs the editor which element to inject via the return key 134 | defaultParagraphSeparator: 'div', 135 | 136 | // , optional, default = false 137 | // Outputs instead of 138 | styleWithCSS: false, 139 | 140 | // , string if overwriting, object if customizing/creating 141 | // action.name (only required if overwriting) 142 | // action.icon (optional if overwriting, required if custom action) 143 | // action.title (optional) 144 | // action.result (required) 145 | // Specify the actions you specifically want (in order) 146 | actions: [ 147 | 'bold', 148 | { 149 | name: 'custom', 150 | icon: 'C', 151 | title: 'Custom Action', 152 | result: () => console.log('Do something!') 153 | }, 154 | 'underline' 155 | ], 156 | 157 | // classes (optional) 158 | // Choose your custom class names 159 | classes: { 160 | actionbar: 'pell-actionbar', 161 | button: 'pell-button', 162 | content: 'pell-content', 163 | selected: 'pell-button-selected' 164 | } 165 | }) 166 | 167 | // Execute a document command, see reference: 168 | // https://developer.mozilla.org/en/docs/Web/API/Document/execCommand 169 | // this is just `document.execCommand(command, false, value)` 170 | pell.exec(command, value) 171 | ``` 172 | 173 | #### List of overwriteable action names 174 | - bold 175 | - italic 176 | - underline 177 | - strikethrough 178 | - heading1 179 | - heading2 180 | - paragraph 181 | - quote 182 | - olist 183 | - ulist 184 | - code 185 | - line 186 | - link 187 | - image 188 | 189 | ## Examples 190 | 191 | #### General 192 | 193 | ```html 194 |
195 |
196 | HTML output: 197 |
198 |
199 | ``` 200 | 201 | ```js 202 | import { exec, init } from 'pell' 203 | 204 | const editor = init({ 205 | element: document.getElementById('editor'), 206 | onChange: html => { 207 | document.getElementById('html-output').textContent = html 208 | }, 209 | defaultParagraphSeparator: 'p', 210 | styleWithCSS: true, 211 | actions: [ 212 | 'bold', 213 | 'underline', 214 | { 215 | name: 'italic', 216 | result: () => exec('italic') 217 | }, 218 | { 219 | name: 'backColor', 220 | icon: '
A
', 221 | title: 'Highlight Color', 222 | result: () => exec('backColor', 'pink') 223 | }, 224 | { 225 | name: 'image', 226 | result: () => { 227 | const url = window.prompt('Enter the image URL') 228 | if (url) exec('insertImage', url) 229 | } 230 | }, 231 | { 232 | name: 'link', 233 | result: () => { 234 | const url = window.prompt('Enter the link URL') 235 | if (url) exec('createLink', url) 236 | } 237 | } 238 | ], 239 | classes: { 240 | actionbar: 'pell-actionbar-custom-name', 241 | button: 'pell-button-custom-name', 242 | content: 'pell-content-custom-name', 243 | selected: 'pell-button-selected-custom-name' 244 | } 245 | }) 246 | 247 | // editor.content 248 | // To change the editor's content: 249 | editor.content.innerHTML = 'Initial content!' 250 | ``` 251 | 252 | #### Example (Markdown) 253 | 254 | ```html 255 |
256 |
257 | Markdown output: 258 |
259 |
260 | ``` 261 | 262 | ```js 263 | import { init } from 'pell' 264 | import Turndown from 'turndown' 265 | 266 | const { turndown } = new Turndown({ headingStyle: 'atx' }) 267 | 268 | init({ 269 | element: document.getElementById('editor'), 270 | actions: ['bold', 'italic', 'heading1', 'heading2', 'olist', 'ulist'], 271 | onChange: html => { 272 | document.getElementById('markdown-output').innerHTML = turndown(html) 273 | } 274 | }) 275 | ``` 276 | 277 | #### Frameworks 278 | 279 | - [React](/examples/react.md) 280 | - [Vue](/examples/vue.md) 281 | 282 | ## Custom Styles 283 | 284 | #### SCSS 285 | 286 | ```scss 287 | $pell-content-height: 400px; 288 | // See all overwriteable variables in src/pell.scss 289 | 290 | // Then import pell.scss into styles: 291 | @import '../../node_modules/pell/src/pell'; 292 | ``` 293 | 294 | #### CSS 295 | 296 | ```css 297 | /* After pell styles are applied to DOM: */ 298 | .pell-content { 299 | height: 400px; 300 | } 301 | ``` 302 | 303 | ## License 304 | 305 | MIT 306 | 307 | ## Credits 308 | 309 | BrowserStack for cross browser testing: 310 | 311 | BrowserStack logo 312 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredreich/pell/d5556baa2e0bfc1411621ee4d780752200b367fd/demo.gif -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pell 8 | 9 | 10 | 11 | 28 | 29 | 30 | 31 | 32 |
33 |

pell

34 |
35 |
36 |

Text output:

37 |
38 |
39 |
40 |

HTML output:

41 |

42 |       
43 |
44 | 45 | 46 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /dist/pell.css: -------------------------------------------------------------------------------- 1 | .pell { 2 | border: 1px solid rgba(10, 10, 10, 0.1); 3 | box-sizing: border-box; } 4 | 5 | .pell-content { 6 | box-sizing: border-box; 7 | height: 300px; 8 | outline: 0; 9 | overflow-y: auto; 10 | padding: 10px; } 11 | 12 | .pell-actionbar { 13 | background-color: #FFF; 14 | border-bottom: 1px solid rgba(10, 10, 10, 0.1); } 15 | 16 | .pell-button { 17 | background-color: transparent; 18 | border: none; 19 | cursor: pointer; 20 | height: 30px; 21 | outline: 0; 22 | width: 30px; 23 | vertical-align: bottom; } 24 | 25 | .pell-button-selected { 26 | background-color: #F0F0F0; } 27 | -------------------------------------------------------------------------------- /dist/pell.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (factory((global.pell = {}))); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var defaultParagraphSeparatorString = 'defaultParagraphSeparator'; 10 | var formatBlock = 'formatBlock'; 11 | var addEventListener = function addEventListener(parent, type, listener) { 12 | return parent.addEventListener(type, listener); 13 | }; 14 | var appendChild = function appendChild(parent, child) { 15 | return parent.appendChild(child); 16 | }; 17 | var createElement = function createElement(tag) { 18 | return document.createElement(tag); 19 | }; 20 | var queryCommandState = function queryCommandState(command) { 21 | return document.queryCommandState(command); 22 | }; 23 | var queryCommandValue = function queryCommandValue(command) { 24 | return document.queryCommandValue(command); 25 | }; 26 | 27 | var exec = function exec(command) { 28 | var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 29 | return document.execCommand(command, false, value); 30 | }; 31 | 32 | var defaultActions = { 33 | bold: { 34 | icon: 'B', 35 | title: 'Bold', 36 | state: function state() { 37 | return queryCommandState('bold'); 38 | }, 39 | result: function result() { 40 | return exec('bold'); 41 | } 42 | }, 43 | italic: { 44 | icon: 'I', 45 | title: 'Italic', 46 | state: function state() { 47 | return queryCommandState('italic'); 48 | }, 49 | result: function result() { 50 | return exec('italic'); 51 | } 52 | }, 53 | underline: { 54 | icon: 'U', 55 | title: 'Underline', 56 | state: function state() { 57 | return queryCommandState('underline'); 58 | }, 59 | result: function result() { 60 | return exec('underline'); 61 | } 62 | }, 63 | strikethrough: { 64 | icon: 'S', 65 | title: 'Strike-through', 66 | state: function state() { 67 | return queryCommandState('strikeThrough'); 68 | }, 69 | result: function result() { 70 | return exec('strikeThrough'); 71 | } 72 | }, 73 | heading1: { 74 | icon: 'H1', 75 | title: 'Heading 1', 76 | result: function result() { 77 | return exec(formatBlock, '

'); 78 | } 79 | }, 80 | heading2: { 81 | icon: 'H2', 82 | title: 'Heading 2', 83 | result: function result() { 84 | return exec(formatBlock, '

'); 85 | } 86 | }, 87 | paragraph: { 88 | icon: '¶', 89 | title: 'Paragraph', 90 | result: function result() { 91 | return exec(formatBlock, '

'); 92 | } 93 | }, 94 | quote: { 95 | icon: '“ ”', 96 | title: 'Quote', 97 | result: function result() { 98 | return exec(formatBlock, '

'); 99 | } 100 | }, 101 | olist: { 102 | icon: '#', 103 | title: 'Ordered List', 104 | result: function result() { 105 | return exec('insertOrderedList'); 106 | } 107 | }, 108 | ulist: { 109 | icon: '•', 110 | title: 'Unordered List', 111 | result: function result() { 112 | return exec('insertUnorderedList'); 113 | } 114 | }, 115 | code: { 116 | icon: '</>', 117 | title: 'Code', 118 | result: function result() { 119 | return exec(formatBlock, '
');
120 |     }
121 |   },
122 |   line: {
123 |     icon: '―',
124 |     title: 'Horizontal Line',
125 |     result: function result() {
126 |       return exec('insertHorizontalRule');
127 |     }
128 |   },
129 |   link: {
130 |     icon: '🔗',
131 |     title: 'Link',
132 |     result: function result() {
133 |       var url = window.prompt('Enter the link URL');
134 |       if (url) exec('createLink', url);
135 |     }
136 |   },
137 |   image: {
138 |     icon: '📷',
139 |     title: 'Image',
140 |     result: function result() {
141 |       var url = window.prompt('Enter the image URL');
142 |       if (url) exec('insertImage', url);
143 |     }
144 |   }
145 | };
146 | 
147 | var defaultClasses = {
148 |   actionbar: 'pell-actionbar',
149 |   button: 'pell-button',
150 |   content: 'pell-content',
151 |   selected: 'pell-button-selected'
152 | };
153 | 
154 | var init = function init(settings) {
155 |   var actions = settings.actions ? settings.actions.map(function (action) {
156 |     if (typeof action === 'string') return defaultActions[action];else if (defaultActions[action.name]) return _extends({}, defaultActions[action.name], action);
157 |     return action;
158 |   }) : Object.keys(defaultActions).map(function (action) {
159 |     return defaultActions[action];
160 |   });
161 | 
162 |   var classes = _extends({}, defaultClasses, settings.classes);
163 | 
164 |   var defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div';
165 | 
166 |   var actionbar = createElement('div');
167 |   actionbar.className = classes.actionbar;
168 |   appendChild(settings.element, actionbar);
169 | 
170 |   var content = settings.element.content = createElement('div');
171 |   content.contentEditable = true;
172 |   content.className = classes.content;
173 |   content.oninput = function (_ref) {
174 |     var firstChild = _ref.target.firstChild;
175 | 
176 |     if (firstChild && firstChild.nodeType === 3) exec(formatBlock, '<' + defaultParagraphSeparator + '>');else if (content.innerHTML === '
') content.innerHTML = ''; 177 | settings.onChange(content.innerHTML); 178 | }; 179 | content.onkeydown = function (event) { 180 | if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') { 181 | setTimeout(function () { 182 | return exec(formatBlock, '<' + defaultParagraphSeparator + '>'); 183 | }, 0); 184 | } 185 | }; 186 | appendChild(settings.element, content); 187 | 188 | actions.forEach(function (action) { 189 | var button = createElement('button'); 190 | button.className = classes.button; 191 | button.innerHTML = action.icon; 192 | button.title = action.title; 193 | button.setAttribute('type', 'button'); 194 | button.onclick = function () { 195 | return action.result() && content.focus(); 196 | }; 197 | 198 | if (action.state) { 199 | var handler = function handler() { 200 | return button.classList[action.state() ? 'add' : 'remove'](classes.selected); 201 | }; 202 | addEventListener(content, 'keyup', handler); 203 | addEventListener(content, 'mouseup', handler); 204 | addEventListener(button, 'click', handler); 205 | } 206 | 207 | appendChild(actionbar, button); 208 | }); 209 | 210 | if (settings.styleWithCSS) exec('styleWithCSS'); 211 | exec(defaultParagraphSeparatorString, defaultParagraphSeparator); 212 | 213 | return settings.element; 214 | }; 215 | 216 | var pell = { exec: exec, init: init }; 217 | 218 | exports.exec = exec; 219 | exports.init = init; 220 | exports['default'] = pell; 221 | 222 | Object.defineProperty(exports, '__esModule', { value: true }); 223 | 224 | }))); 225 | -------------------------------------------------------------------------------- /dist/pell.min.css: -------------------------------------------------------------------------------- 1 | .pell{border:1px solid hsla(0,0%,4%,.1)}.pell,.pell-content{box-sizing:border-box}.pell-content{height:300px;outline:0;overflow-y:auto;padding:10px}.pell-actionbar{background-color:#fff;border-bottom:1px solid hsla(0,0%,4%,.1)}.pell-button{background-color:transparent;border:none;cursor:pointer;height:30px;outline:0;width:30px;vertical-align:bottom}.pell-button-selected{background-color:#f0f0f0} -------------------------------------------------------------------------------- /dist/pell.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.pell={})}(this,function(t){"use strict";var e=Object.assign||function(t){for(var e=1;eB",title:"Bold",state:function(){return n("bold")},result:function(){return f("bold")}},italic:{icon:"I",title:"Italic",state:function(){return n("italic")},result:function(){return f("italic")}},underline:{icon:"U",title:"Underline",state:function(){return n("underline")},result:function(){return f("underline")}},strikethrough:{icon:"S",title:"Strike-through",state:function(){return n("strikeThrough")},result:function(){return f("strikeThrough")}},heading1:{icon:"H1",title:"Heading 1",result:function(){return f(l,"

")}},heading2:{icon:"H2",title:"Heading 2",result:function(){return f(l,"

")}},paragraph:{icon:"¶",title:"Paragraph",result:function(){return f(l,"

")}},quote:{icon:"“ ”",title:"Quote",result:function(){return f(l,"

")}},olist:{icon:"#",title:"Ordered List",result:function(){return f("insertOrderedList")}},ulist:{icon:"•",title:"Unordered List",result:function(){return f("insertUnorderedList")}},code:{icon:"</>",title:"Code",result:function(){return f(l,"
")}},line:{icon:"―",title:"Horizontal Line",result:function(){return f("insertHorizontalRule")}},link:{icon:"🔗",title:"Link",result:function(){var t=window.prompt("Enter the link URL");t&&f("createLink",t)}},image:{icon:"📷",title:"Image",result:function(){var t=window.prompt("Enter the image URL");t&&f("insertImage",t)}}},m={actionbar:"pell-actionbar",button:"pell-button",content:"pell-content",selected:"pell-button-selected"},r=function(n){var t=n.actions?n.actions.map(function(t){return"string"==typeof t?p[t]:p[t.name]?e({},p[t.name],t):t}):Object.keys(p).map(function(t){return p[t]}),r=e({},m,n.classes),i=n[c]||"div",o=d("div");o.className=r.actionbar,s(n.element,o);var u=n.element.content=d("div");return u.contentEditable=!0,u.className=r.content,u.oninput=function(t){var e=t.target.firstChild;e&&3===e.nodeType?f(l,"<"+i+">"):"
"===u.innerHTML&&(u.innerHTML=""),n.onChange(u.innerHTML)},u.onkeydown=function(t){var e;"Enter"===t.key&&"blockquote"===(e=l,document.queryCommandValue(e))&&setTimeout(function(){return f(l,"<"+i+">")},0)},s(n.element,u),t.forEach(function(t){var e=d("button");if(e.className=r.button,e.innerHTML=t.icon,e.title=t.title,e.setAttribute("type","button"),e.onclick=function(){return t.result()&&u.focus()},t.state){var n=function(){return e.classList[t.state()?"add":"remove"](r.selected)};a(u,"keyup",n),a(u,"mouseup",n),a(e,"click",n)}s(o,e)}),n.styleWithCSS&&f("styleWithCSS"),f(c,i),n.element},i={exec:f,init:r};t.exec=f,t.init=r,t.default=i,Object.defineProperty(t,"__esModule",{value:!0})}); 2 | -------------------------------------------------------------------------------- /examples/react.md: -------------------------------------------------------------------------------- 1 | ```js 2 | // App.js 3 | 4 | import React, { Component } from 'react'; 5 | import { init } from 'pell'; 6 | 7 | import 'pell/dist/pell.css' 8 | 9 | class App extends Component { 10 | editor = null 11 | 12 | constructor (props) { 13 | super(props) 14 | this.state = { html: null } 15 | } 16 | 17 | componentDidMount () { 18 | this.editor = init({ 19 | element: document.getElementById('editor'), 20 | onChange: html => this.setState({ html }), 21 | actions: ['bold', 'underline', 'italic'], 22 | }) 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Editor:

29 |
30 |

HTML Output:

31 |
{this.state.html}
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default App; 38 | ``` 39 | -------------------------------------------------------------------------------- /examples/vue.md: -------------------------------------------------------------------------------- 1 | ```vue 2 | 10 | 11 | 56 | 57 | 69 | ``` 70 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel') 2 | const cssnano = require('gulp-cssnano') 3 | const del = require('del') 4 | const gulp = require('gulp') 5 | const rename = require('gulp-rename') 6 | const Rollup = require('rollup') 7 | const rollup = require('gulp-rollup') 8 | const run = require('run-sequence') 9 | const sass = require('gulp-sass') 10 | const size = require('gulp-size') 11 | const uglify = require('rollup-plugin-uglify') 12 | 13 | gulp.task('clean', () => del(['./dist'])) 14 | 15 | const rollupConfig = minimize => ({ 16 | rollup: Rollup, 17 | entry: './src/pell.js', 18 | moduleName: 'pell', 19 | format: 'umd', 20 | exports: 'named', 21 | plugins: [babel({ exclude: 'node_modules/**' })].concat( 22 | minimize 23 | ? [ 24 | uglify({ 25 | compress: { warnings: false }, 26 | mangle: true, 27 | sourceMap: false 28 | }) 29 | ] 30 | : [] 31 | ) 32 | }) 33 | 34 | gulp.task('script', () => { 35 | gulp.src('./src/*.js') 36 | .pipe(rollup(rollupConfig(false))) 37 | .pipe(size({ showFiles: true })) 38 | .pipe(gulp.dest('./dist')) 39 | 40 | gulp.src('./src/*.js') 41 | .pipe(rollup(rollupConfig(true))) 42 | .pipe(rename('pell.min.js')) 43 | .pipe(size({ showFiles: true })) 44 | .pipe(size({ gzip: true, showFiles: true })) 45 | .pipe(gulp.dest('./dist')) 46 | }) 47 | 48 | gulp.task('style', () => { 49 | gulp.src(['./src/pell.scss']) 50 | .pipe(sass().on('error', sass.logError)) 51 | .pipe(gulp.dest('./dist')) 52 | .pipe(cssnano()) 53 | .pipe(rename('pell.min.css')) 54 | .pipe(gulp.dest('./dist')) 55 | }) 56 | 57 | gulp.task('default', ['clean'], () => { 58 | run('script', 'style') 59 | gulp.watch('./src/pell.scss', ['style']) 60 | gulp.watch('./src/pell.js', ['script']) 61 | }) 62 | -------------------------------------------------------------------------------- /images/browserstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredreich/pell/d5556baa2e0bfc1411621ee4d780752200b367fd/images/browserstack.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredreich/pell/d5556baa2e0bfc1411621ee4d780752200b367fd/images/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pell", 3 | "description": "pell - the simplest and smallest WYSIWYG text editor for web, with no dependencies", 4 | "author": "Jared Reich", 5 | "version": "1.0.6", 6 | "main": "./dist/pell.min.js", 7 | "scripts": { 8 | "dev": "gulp", 9 | "build": "gulp clean && gulp script && gulp style", 10 | "lint": "./node_modules/.bin/eslint .js ./" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jaredreich/pell.git" 15 | }, 16 | "keywords": [ 17 | "text editor", 18 | "editor", 19 | "rich text", 20 | "wysiwyg", 21 | "contenteditable" 22 | ], 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jaredreich/pell/issues" 26 | }, 27 | "homepage": "https://github.com/jaredreich/pell", 28 | "devDependencies": { 29 | "babel-core": "6.23.1", 30 | "babel-preset-es2015": "6.22.0", 31 | "babel-preset-stage-0": "6.22.0", 32 | "del": "2.2.2", 33 | "eslint-config-jared": "1.2.0", 34 | "gulp": "3.9.1", 35 | "gulp-cssnano": "2.1.2", 36 | "gulp-rename": "1.2.2", 37 | "gulp-rollup": "2.14.0", 38 | "gulp-sass": "3.1.0", 39 | "gulp-size": "2.1.0", 40 | "rollup": "0.45.1", 41 | "rollup-plugin-babel": "2.7.1", 42 | "rollup-plugin-uglify": "2.0.1", 43 | "run-sequence": "1.2.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pell.js: -------------------------------------------------------------------------------- 1 | const defaultParagraphSeparatorString = 'defaultParagraphSeparator' 2 | const formatBlock = 'formatBlock' 3 | const addEventListener = (parent, type, listener) => parent.addEventListener(type, listener) 4 | const appendChild = (parent, child) => parent.appendChild(child) 5 | const createElement = tag => document.createElement(tag) 6 | const queryCommandState = command => document.queryCommandState(command) 7 | const queryCommandValue = command => document.queryCommandValue(command) 8 | 9 | export const exec = (command, value = null) => document.execCommand(command, false, value) 10 | 11 | const defaultActions = { 12 | bold: { 13 | icon: 'B', 14 | title: 'Bold', 15 | state: () => queryCommandState('bold'), 16 | result: () => exec('bold') 17 | }, 18 | italic: { 19 | icon: 'I', 20 | title: 'Italic', 21 | state: () => queryCommandState('italic'), 22 | result: () => exec('italic') 23 | }, 24 | underline: { 25 | icon: 'U', 26 | title: 'Underline', 27 | state: () => queryCommandState('underline'), 28 | result: () => exec('underline') 29 | }, 30 | strikethrough: { 31 | icon: 'S', 32 | title: 'Strike-through', 33 | state: () => queryCommandState('strikeThrough'), 34 | result: () => exec('strikeThrough') 35 | }, 36 | heading1: { 37 | icon: 'H1', 38 | title: 'Heading 1', 39 | result: () => exec(formatBlock, '

') 40 | }, 41 | heading2: { 42 | icon: 'H2', 43 | title: 'Heading 2', 44 | result: () => exec(formatBlock, '

') 45 | }, 46 | paragraph: { 47 | icon: '¶', 48 | title: 'Paragraph', 49 | result: () => exec(formatBlock, '

') 50 | }, 51 | quote: { 52 | icon: '“ ”', 53 | title: 'Quote', 54 | result: () => exec(formatBlock, '

') 55 | }, 56 | olist: { 57 | icon: '#', 58 | title: 'Ordered List', 59 | result: () => exec('insertOrderedList') 60 | }, 61 | ulist: { 62 | icon: '•', 63 | title: 'Unordered List', 64 | result: () => exec('insertUnorderedList') 65 | }, 66 | code: { 67 | icon: '</>', 68 | title: 'Code', 69 | result: () => exec(formatBlock, '
')
 70 |   },
 71 |   line: {
 72 |     icon: '―',
 73 |     title: 'Horizontal Line',
 74 |     result: () => exec('insertHorizontalRule')
 75 |   },
 76 |   link: {
 77 |     icon: '🔗',
 78 |     title: 'Link',
 79 |     result: () => {
 80 |       const url = window.prompt('Enter the link URL')
 81 |       if (url) exec('createLink', url)
 82 |     }
 83 |   },
 84 |   image: {
 85 |     icon: '📷',
 86 |     title: 'Image',
 87 |     result: () => {
 88 |       const url = window.prompt('Enter the image URL')
 89 |       if (url) exec('insertImage', url)
 90 |     }
 91 |   }
 92 | }
 93 | 
 94 | const defaultClasses = {
 95 |   actionbar: 'pell-actionbar',
 96 |   button: 'pell-button',
 97 |   content: 'pell-content',
 98 |   selected: 'pell-button-selected'
 99 | }
100 | 
101 | export const init = settings => {
102 |   const actions = settings.actions
103 |     ? (
104 |       settings.actions.map(action => {
105 |         if (typeof action === 'string') return defaultActions[action]
106 |         else if (defaultActions[action.name]) return { ...defaultActions[action.name], ...action }
107 |         return action
108 |       })
109 |     )
110 |     : Object.keys(defaultActions).map(action => defaultActions[action])
111 | 
112 |   const classes = { ...defaultClasses, ...settings.classes }
113 | 
114 |   const defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div'
115 | 
116 |   const actionbar = createElement('div')
117 |   actionbar.className = classes.actionbar
118 |   appendChild(settings.element, actionbar)
119 | 
120 |   const content = settings.element.content = createElement('div')
121 |   content.contentEditable = true
122 |   content.className = classes.content
123 |   content.oninput = ({ target: { firstChild } }) => {
124 |     if (firstChild && firstChild.nodeType === 3) exec(formatBlock, `<${defaultParagraphSeparator}>`)
125 |     else if (content.innerHTML === '
') content.innerHTML = '' 126 | settings.onChange(content.innerHTML) 127 | } 128 | content.onkeydown = event => { 129 | if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') { 130 | setTimeout(() => exec(formatBlock, `<${defaultParagraphSeparator}>`), 0) 131 | } 132 | } 133 | appendChild(settings.element, content) 134 | 135 | actions.forEach(action => { 136 | const button = createElement('button') 137 | button.className = classes.button 138 | button.innerHTML = action.icon 139 | button.title = action.title 140 | button.setAttribute('type', 'button') 141 | button.onclick = () => action.result() && content.focus() 142 | 143 | if (action.state) { 144 | const handler = () => button.classList[action.state() ? 'add' : 'remove'](classes.selected) 145 | addEventListener(content, 'keyup', handler) 146 | addEventListener(content, 'mouseup', handler) 147 | addEventListener(button, 'click', handler) 148 | } 149 | 150 | appendChild(actionbar, button) 151 | }) 152 | 153 | if (settings.styleWithCSS) exec('styleWithCSS') 154 | exec(defaultParagraphSeparatorString, defaultParagraphSeparator) 155 | 156 | return settings.element 157 | } 158 | 159 | export default { exec, init } 160 | -------------------------------------------------------------------------------- /src/pell.scss: -------------------------------------------------------------------------------- 1 | $pell-actionbar-color: #FFF !default; 2 | $pell-border-color: rgba(10, 10, 10, 0.1) !default; 3 | $pell-border-style: solid !default; 4 | $pell-border-width: 1px !default; 5 | $pell-button-height: 30px !default; 6 | $pell-button-selected-color: #F0F0F0 !default; 7 | $pell-button-width: 30px !default; 8 | $pell-content-height: 300px !default; 9 | $pell-content-padding: 10px !default; 10 | 11 | .pell { 12 | border: $pell-border-width $pell-border-style $pell-border-color; 13 | box-sizing: border-box; 14 | } 15 | 16 | .pell-content { 17 | box-sizing: border-box; 18 | height: $pell-content-height; 19 | outline: 0; 20 | overflow-y: auto; 21 | padding: $pell-content-padding; 22 | } 23 | 24 | .pell-actionbar { 25 | background-color: $pell-actionbar-color; 26 | border-bottom: $pell-border-width $pell-border-style $pell-border-color; 27 | } 28 | 29 | .pell-button { 30 | background-color: transparent; 31 | border: none; 32 | cursor: pointer; 33 | height: $pell-button-height; 34 | outline: 0; 35 | width: $pell-button-width; 36 | vertical-align: bottom; 37 | } 38 | 39 | .pell-button-selected { 40 | background-color: $pell-button-selected-color; 41 | } 42 | --------------------------------------------------------------------------------