├── .gitignore ├── src ├── font │ ├── fontello.eot │ ├── fontello.ttf │ ├── fontello.woff │ └── fontello.svg ├── markdown.js ├── pen.css └── pen.js ├── .travis.yml ├── bower.json ├── .jshintrc ├── package.json ├── Gruntfile.js ├── license.txt ├── readme.md └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.lock 3 | *.log 4 | node_modules 5 | build 6 | -------------------------------------------------------------------------------- /src/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sofish/pen/HEAD/src/font/fontello.eot -------------------------------------------------------------------------------- /src/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sofish/pen/HEAD/src/font/fontello.ttf -------------------------------------------------------------------------------- /src/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sofish/pen/HEAD/src/font/fontello.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_script: 5 | - npm install -g grunt-cli 6 | 7 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pen", 3 | "homepage": "https://github.com/sofish/pen", 4 | "description": "enjoy live editing(+markdown)", 5 | "main": [ 6 | "src/pen.js", 7 | "src/pen.css" 8 | ], 9 | "keywords": [ 10 | "editor", 11 | "wysiwyg", 12 | "markdown" 13 | ], 14 | "authors": [ 15 | "sofish" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "console": false 4 | }, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "eqnull": true, 10 | "forin": true, 11 | "immed": true, 12 | "indent": 2, 13 | "latedef": true, 14 | "laxcomma": true, 15 | "newcap": true, 16 | "noarg": true, 17 | "nonew": true, 18 | "noempty": true, 19 | "undef": true, 20 | "unused": true, 21 | "strict": false, 22 | "trailing": true, 23 | "maxlen": 200, 24 | "browser": true 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pen", 3 | "version": "0.0.1", 4 | "main": "src/pen.js", 5 | "author": "sofish ", 6 | "description": "enjoy live editing (+markdown) http://sofish.github.io/pen", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/sofish/pen" 10 | }, 11 | "keywords": [ 12 | "editor", 13 | "markdown", 14 | "medium" 15 | ], 16 | "license": { 17 | "type": "MIT", 18 | "url": "https://github.com/sofish/pen/blob/master/license.txt" 19 | }, 20 | "devDependencies": { 21 | "grunt": "^0.4.5", 22 | "grunt-contrib-jshint": "~0.7.2", 23 | "grunt-contrib-uglify": "~0.2.7", 24 | "grunt-contrib-watch": "~0.5.3" 25 | }, 26 | "scripts": { 27 | "test": "grunt --verbose" 28 | }, 29 | "readmeFilename": "README.md" 30 | } 31 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module: false */ 2 | module.exports = function(grunt) { 3 | "use strict"; 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | 9 | jshint: { 10 | options: { 11 | jshintrc: true 12 | }, 13 | files: ['Gruntfile.js', 'src/**/*.js'] 14 | }, 15 | 16 | uglify: { 17 | build: { 18 | src: 'src/**/*.js', 19 | dest: 'build/pen-<%= pkg.version %>.min.js' 20 | } 21 | }, 22 | 23 | watch: { 24 | files: ['<%= jshint.files %>'], 25 | tasks: ['jshint'] 26 | } 27 | }); 28 | 29 | // Plugins 30 | grunt.loadNpmTasks('grunt-contrib-jshint'); 31 | grunt.loadNpmTasks('grunt-contrib-uglify'); 32 | grunt.loadNpmTasks('grunt-contrib-watch'); 33 | 34 | // Default task(s). 35 | grunt.registerTask('default', ['jshint', 'uglify']); 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Pen - enjoy live editing 2 | 3 | Copyright (C) 2013 https://github.com/sofish/pen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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 | -------------------------------------------------------------------------------- /src/markdown.js: -------------------------------------------------------------------------------- 1 | /*! Licensed under MIT, https://github.com/sofish/pen */ 2 | (function(root) { 3 | 4 | // only works with Pen 5 | if(!root.Pen) return; 6 | 7 | // markdown covertor obj 8 | var covertor = { 9 | keymap: { '96': '`', '62': '>', '49': '1', '46': '.', '45': '-', '42': '*', '35': '#'}, 10 | stack : [] 11 | }; 12 | 13 | // return valid markdown syntax 14 | covertor.valid = function(str) { 15 | var len = str.length; 16 | 17 | if(str.match(/[#]{1,6}/)) { 18 | return ['h' + len, len]; 19 | } else if(str === '```') { 20 | return ['pre', len]; 21 | } else if(str === '>') { 22 | return ['blockquote', len]; 23 | } else if(str === '1.') { 24 | return ['insertorderedlist', len]; 25 | } else if(str === '-' || str === '*') { 26 | return ['insertunorderedlist', len]; 27 | } else if(str.match(/(?:\.|\*|\-){3,}/)) { 28 | return ['inserthorizontalrule', len]; 29 | } 30 | }; 31 | 32 | // parse command 33 | covertor.parse = function(e) { 34 | var code = e.keyCode || e.which; 35 | 36 | // when `space` is pressed 37 | if (code === 32) { 38 | var markdownSyntax = this.stack.join(''); 39 | // reset stack 40 | this.stack = []; 41 | 42 | var cmd = this.valid(markdownSyntax); 43 | if (cmd) { 44 | // prevents leading space after executing command 45 | e.preventDefault(); 46 | return cmd; 47 | } 48 | } 49 | 50 | // make cmd 51 | if(this.keymap[code]) this.stack.push(this.keymap[code]); 52 | 53 | return false; 54 | }; 55 | 56 | // exec command 57 | covertor.action = function(pen, cmd) { 58 | 59 | // only apply effect at line start 60 | if(pen.selection.focusOffset > cmd[1]) return; 61 | 62 | var node = pen.selection.focusNode; 63 | node.textContent = node.textContent.slice(cmd[1]); 64 | pen.execCommand(cmd[0]); 65 | }; 66 | 67 | // init covertor 68 | covertor.init = function(pen) { 69 | pen.on('keypress', function(e) { 70 | var cmd = covertor.parse(e); 71 | if(cmd) return covertor.action(pen, cmd); 72 | }); 73 | }; 74 | 75 | // append to Pen 76 | root.Pen.prototype.markdown = covertor; 77 | 78 | }(window)); 79 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Pen Editor 2 | 3 | - **LIVE DEMO:** [http://sofish.github.io/pen](http://sofish.github.io/pen) 4 | - **Markdown is supported** 5 | - **Build status:** [![Build Status](https://travis-ci.org/sofish/pen.png?branch=master)](https://travis-ci.org/sofish/pen) 6 | 7 | ****************** 8 | 9 | ![pen editor - screenshot ](https://f.cloud.github.com/assets/153183/1093671/61d4c0d2-16a9-11e3-88ed-01b1758a9a42.png) 10 | 11 | ****************** 12 | 13 | ## 0. source code 14 | 15 | You can clone the source code from github, or using bower. 16 | 17 | ``` 18 | bower install pen 19 | ``` 20 | 21 | 22 | ## 1. installation 23 | 24 | #### 1.1 init with id attribute 25 | 26 | ```js 27 | var editor = new Pen('#editor'); 28 | ``` 29 | 30 | #### 1.2 init with an element 31 | 32 | ```js 33 | var editor = new Pen(document.getElementById('editor')); 34 | ``` 35 | 36 | #### 1.3 init with options 37 | 38 | ```js 39 | var options = { 40 | editor: document.body, // {DOM Element} [required] 41 | class: 'pen', // {String} class of the editor, 42 | debug: false, // {Boolean} false by default 43 | textarea: '', // fallback for old browsers 44 | list: ['bold', 'italic', 'underline'], // editor menu list 45 | linksInNewWindow: true // open hyperlinks in a new windows/tab 46 | } 47 | 48 | var editor = new Pen(options); 49 | ``` 50 | 51 | ## 2. configure 52 | 53 | The following object sets up the default settings of Pen: 54 | 55 | ```js 56 | defaults = { 57 | class: 'pen', 58 | debug: false, 59 | textarea: '', 60 | list: [ 61 | 'blockquote', 'h2', 'h3', 'p', 'insertorderedlist', 'insertunorderedlist', 62 | 'indent', 'outdent', 'bold', 'italic', 'underline', 'createlink' 63 | ], 64 | stay: true, 65 | linksInNewWindow: false 66 | } 67 | ``` 68 | 69 | If you want to customize the toolbar to fit your own project, you can instanciate `Pen` constructor with an `options` object like [#1.3: init with options](https://github.com/sofish/pen#13-init-with-options): 70 | 71 | #### 2.1 Fallback for old browser 72 | 73 | You can set `defaults.textarea` to a piece of HTML string, by default, it's ``。This will be set as `innerHTML` of your `#editor`. 74 | 75 | #### 2.2 Change the editor class 76 | 77 | Pen will add `.pen` to your editor by default, if you want to change the class, make sure to replace the class name `pen` to your own in `src/pen.css`. 78 | 79 | #### 2.3 Enable debug mode 80 | 81 | If `options.debug` is set to `true`, Pen will output logs to the Console of your browser. 82 | 83 | ![debugger](https://f.cloud.github.com/assets/153183/1078426/e1d40758-1527-11e3-9a68-12c58225c93c.png) 84 | 85 | #### 2.4 Customize the toolbar 86 | 87 | You can set `options.list` to an `Array`, add the following strings to make your own: 88 | 89 | - `blockquote`, `h2`, `h3`, `p`, `pre`: create a tag as its literal meaning 90 | - `insertorderedlist`: create an `ol>li` list 91 | - `insertunorderedlist`: create a `ul>li` list 92 | - `indent`: indent list / blockquote block 93 | - `outdent`: outdent list / blockquote block 94 | - `bold`: wrap the text selection in a `b` tag 95 | - `italic`: wrap the text selection in an `i` tag 96 | - `underline`: wrap the text selection in a `u` tag 97 | - `createlink`: insert link to the text selection 98 | - `inserthorizontalrule`: insert a `hr` tag 99 | - `insertimage`: insert an image (`img`) tag 100 | 101 | #### 2.5 Add tooltips to the toolbar icons 102 | 103 | You can set `options.titles` to an object with properties that match the toolbar actions. The value of each property 104 | will be used as the title attribute on the icon. Most browsers will display the title attribute as a tooltip when 105 | the mouse hovers over the icon. 106 | 107 | ```js 108 | options.title = { 109 | 'blockquote': 'Blockquote' 110 | 'createlink': 'Hyperlink' 111 | 'insertimage': 'Image' 112 | } 113 | ``` 114 | 115 | If you are using Bootstrap or jQueryUI, you can standardize the tooltip style by adding `$('i.pen-icon').tooltip()` 116 | to your JavaScript. 117 | 118 | #### 2.6 Prevent unsafe page redirect 119 | 120 | By default, Pen will prevent unsafe page redirect when editing, to shut down it, specific `options.stay` to `false`. 121 | 122 | __NOTE:__ if `defaults.debug` is set to `true` and `default.stay` is not set: `defaults.stay == !defaults.debug`. 123 | 124 | #### 2.7 Disable and Re-enable editor 125 | 126 | You can disable the pen editor by call `destroy()` method of the `var pen = new Pen(options)` object. like: 127 | 128 | ```js 129 | var pen = new Pen('#editor'); 130 | 131 | pen.destroy(); // return itself 132 | ``` 133 | 134 | And, there's a corresponding method called `rebuild()` to re-enable the editor: 135 | 136 | ```js 137 | pen.rebuild(); // return itself 138 | ``` 139 | 140 | #### 2.8 Export content as markdown 141 | 142 | It's an experimental feature 143 | 144 | ```js 145 | var pen = new Pen('#editor'); 146 | 147 | pen.toMd(); // return a markdown string 148 | ``` 149 | 150 | 151 | ## 3. markdown syntax support 152 | 153 | #### 3.1 install 154 | The syntax convertor will be enabled automatically by linking `markdown.js` after `pen.js: 155 | 156 | ```html 157 | 158 | 159 | ``` 160 | 161 | #### 3.2 usage 162 | To use it, you can type `action cmd` + `space key` at a line start. like: 163 | 164 | ``` 165 | ### This will create a h3 tag 166 | ``` 167 | 168 | The following cmds are allowed: 169 | 170 | - Headings: type 1~6 `#` at the line start 171 | - Unordered List: type `- ` or `* ` 172 | - Ordered List: type `1. ` 173 | - Code block: type **\`\`\`** 174 | - Block Quote: type `> ` 175 | - Horizontal Rule: more than 3 `-`, `*`, `.` will create a `
`, like `......` 176 | 177 | ## 4. license 178 | 179 | Licensed under MIT. 180 | 181 | ## 5. trusted by * 182 | 183 | [![Teambition](https://dn-project-site.qbox.me/images/logo.png)](https://github.com/teambition) 184 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pen - What You See Is What You Get (WYSIWYG) 7 | 114 | 115 | 116 | 117 | 118 | 119 |
120 | 121 | 122 | MD 123 |
124 | 125 | 142 | 143 |
144 |

Enjoy live editing (+markdown)

145 | 146 |

Click anywhere to start editing, select and click items on the popup toolbar to toggle effects.

147 |
148 |

Horizontal-Rule can be inserted by click「...」on the toolbar or just type「... 」/「--- 」/「*** 」at the start of a line. 149 | Note, the shorthand command should be followed by a SPACE to enable the convert, otherwise, they will just stay what 150 | they look like.

151 |

oh my god

153 |
154 |

To add a link, please type your URL into the input field and hit ENTER/RETURN key. And, a link 155 | can be removed by clearing up the input field. 156 | 157 |

158 |

162 |
You can quote texts by typing「>」at the start of a line.
163 |

What about add underline to texts? "Stay Hungry, Stay Foolish - Steve Jobs".

164 |
Code block is also supported by typing 「```」 at the start of a line, don't forget the trailing SPACE.
165 |

For more detail, please check out: https://github.com/sofish/pen#readme 166 |

167 |
168 | 169 | Fork me on GitHub 171 | 172 | 173 | 174 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /src/pen.css: -------------------------------------------------------------------------------- 1 | /*! Licensed under MIT, https://github.com/sofish/pen */ 2 | 3 | /* basic reset */ 4 | .pen, .pen-menu, .pen-input, .pen textarea{font:400 1.16em/1.45 Palatino, Optima, Georgia, serif;color:#331;} 5 | .pen:focus{outline:none;} 6 | .pen fieldset, img {border: 0;} 7 | .pen blockquote{padding-left:10px;margin-left:-14px;border-left:4px solid #1abf89;} 8 | .pen a{color:#1abf89;} 9 | .pen del{text-decoration:line-through;} 10 | .pen sub, .pen sup {font-size:75%;position:relative;vertical-align:text-top;} 11 | :root .pen sub, :root .pen sup{vertical-align:baseline; /* for ie9 and other mordern browsers */} 12 | .pen sup {top:-0.5em;} 13 | .pen sub {bottom:-0.25em;} 14 | .pen hr{border:none;border-bottom:1px solid #cfcfcf;margin-bottom:25px;*color:pink;*filter:chroma(color=pink);height:10px;*margin:-7px 0 15px;} 15 | .pen small{font-size:0.8em;color:#888;} 16 | .pen em, .pen b, .pen strong{font-weight:700;} 17 | .pen pre{white-space:pre-wrap;padding:0.85em;background:#f8f8f8;} 18 | 19 | /* block-level element margin */ 20 | .pen p, .pen pre, .pen ul, .pen ol, .pen dl, .pen form, .pen table, .pen blockquote{margin-bottom:16px;} 21 | 22 | /* headers */ 23 | .pen h1, .pen h2, .pen h3, .pen h4, .pen h5, .pen h6{margin-bottom:16px;font-weight:700;line-height:1.2;} 24 | .pen h1{font-size:2em;} 25 | .pen h2{font-size:1.8em;} 26 | .pen h3{font-size:1.6em;} 27 | .pen h4{font-size:1.4em;} 28 | .pen h5, .pen h6{font-size:1.2em;} 29 | 30 | /* list */ 31 | .pen ul, .pen ol{margin-left:1.2em;} 32 | .pen ul, .pen-ul{list-style:disc;} 33 | .pen ol, .pen-ol{list-style:decimal;} 34 | .pen li ul, .pen li ol, .pen-ul ul, .pen-ul ol, .pen-ol ul, .pen-ol ol{margin:0 2em 0 1.2em;} 35 | .pen li ul, .pen-ul ul, .pen-ol ul{list-style: circle;} 36 | 37 | /* pen menu */ 38 | .pen-menu [class^="icon-"], .pen-menu [class*=" icon-"] { /* reset to avoid conflicts with Bootstrap */ 39 | background: transparent; 40 | background-image: none; 41 | } 42 | .pen-menu { min-width: 320px; } 43 | .pen-menu, .pen-input{font-size:14px;line-height:1;} 44 | .pen-menu{white-space:nowrap;box-shadow:1px 2px 3px -2px #222;background:#333;background-image:linear-gradient(to bottom, #222, #333);opacity:0.9;position:fixed;height:36px;border:1px solid #333;border-radius:3px;display:none;z-index:1000;} 45 | .pen-menu:after {top:100%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none;} 46 | .pen-menu:after {border-color:rgba(51, 51, 51, 0);border-top-color:#333;border-width:6px;left:50%;margin-left:-6px;} 47 | .pen-menu-below:after {top: -11px; display:block; -moz-transform: rotate(180deg); -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg);} 48 | .pen-icon{font:normal 900 16px/40px Georgia serif;min-width:20px;display:inline-block;padding:0 10px;height:36px;overflow:hidden;color:#fff;text-align:center;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;} 49 | .pen-icon:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;} 50 | .pen-icon:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;} 51 | .pen-icon:hover{background:#000;} 52 | .pen-icon.active{color:#1abf89;background:#000;box-shadow:inset 2px 2px 4px #000;} 53 | .pen-input{position:absolute;width:100%;left:0;top:0;height:36px;line-height:20px;background:#333;color:#fff;border:none;text-align:center;display:none;font-family:arial, sans-serif;} 54 | .pen-input:focus{outline:none;} 55 | 56 | .pen-textarea{display:block;background:#f8f8f8;padding:20px;} 57 | .pen textarea{font-size:14px;border:none;background:none;width:100%;_height:200px;min-height:200px;resize:none;} 58 | 59 | @font-face { 60 | font-family: 'pen'; 61 | src: url('font/fontello.eot?370dad08'); 62 | src: url('font/fontello.eot?370dad08#iefix') format('embedded-opentype'), 63 | url('font/fontello.woff?370dad08') format('woff'), 64 | url('font/fontello.ttf?370dad08') format('truetype'), 65 | url('font/fontello.svg?370dad08#fontello') format('svg'); 66 | font-weight: normal; 67 | font-style: normal; 68 | } 69 | 70 | .pen-menu [class^="icon-"]:before, .pen-menu [class*=" icon-"]:before { 71 | font-family: "pen"; 72 | font-style: normal; 73 | font-weight: normal; 74 | speak: none; 75 | display: inline-block; 76 | text-decoration: inherit; 77 | width: 1em; 78 | margin-right: .2em; 79 | text-align: center; 80 | font-variant: normal; 81 | text-transform: none; 82 | line-height: 1em; 83 | margin-left: .2em; 84 | } 85 | .pen-menu .icon-location:before { content: '\e815'; } /* '' */ 86 | .pen-menu .icon-fit:before { content: '\e80f'; } /* '' */ 87 | .pen-menu .icon-bold:before { content: '\e805'; } /* '' */ 88 | .pen-menu .icon-italic:before { content: '\e806'; } /* '' */ 89 | .pen-menu .icon-justifyleft:before { content: '\e80a'; } /* '' */ 90 | .pen-menu .icon-justifycenter:before { content: '\e80b'; } /* '' */ 91 | .pen-menu .icon-justifyright:before { content: '\e80c'; } /* '' */ 92 | .pen-menu .icon-justifyfull:before { content: '\e80d'; } /* '' */ 93 | .pen-menu .icon-outdent:before { content: '\e800'; } /* '' */ 94 | .pen-menu .icon-indent:before { content: '\e801'; } /* '' */ 95 | .pen-menu .icon-mode:before { content: '\e813'; } /* '' */ 96 | .pen-menu .icon-fullscreen:before { content: '\e80e'; } /* '' */ 97 | .pen-menu .icon-insertunorderedlist:before { content: '\e802'; } /* '' */ 98 | .pen-menu .icon-insertorderedlist:before { content: '\e803'; } /* '' */ 99 | .pen-menu .icon-strikethrough:before { content: '\e807'; } /* '' */ 100 | .pen-menu .icon-underline:before { content: '\e804'; } /* '' */ 101 | .pen-menu .icon-blockquote:before { content: '\e814'; } /* '' */ 102 | .pen-menu .icon-undo:before { content: '\e817'; } /* '' */ 103 | .pen-menu .icon-code:before { content: '\e816'; } /* '' */ 104 | .pen-menu .icon-pre:before { content: '\e816'; } /* '' */ 105 | .pen-menu .icon-unlink:before { content: '\e811'; } /* '' */ 106 | .pen-menu .icon-superscript:before { content: '\e808'; } /* '' */ 107 | .pen-menu .icon-subscript:before { content: '\e809'; } /* '' */ 108 | .pen-menu .icon-inserthorizontalrule:before { content: '\e818'; } /* '' */ 109 | .pen-menu .icon-pin:before { content: '\e812'; } /* '' */ 110 | .pen-menu .icon-createlink:before { content: '\e810'; } /* '' */ 111 | .pen-menu .icon-h1:before { content: 'H1'; } 112 | .pen-menu .icon-h2:before { content: 'H2'; } 113 | .pen-menu .icon-h3:before { content: 'H3'; } 114 | .pen-menu .icon-h4:before { content: 'H4'; } 115 | .pen-menu .icon-h5:before { content: 'H5'; } 116 | .pen-menu .icon-h6:before { content: 'H6'; } 117 | .pen-menu .icon-p:before { content: 'P'; } 118 | .pen-menu .icon-insertimage:before { width:1.8em;margin:0;position:relative;top:-2px;content:'IMG';font-size:12px;border:1px solid #fff;padding:2px;border-radius:2px; } 119 | .pen { 120 | position: relative; 121 | } 122 | .pen.hinted h1:before, 123 | .pen.hinted h2:before, 124 | .pen.hinted h3:before, 125 | .pen.hinted h4:before, 126 | .pen.hinted h5:before, 127 | .pen.hinted h6:before, 128 | .pen.hinted blockquote:before, 129 | .pen.hinted hr:before { 130 | color: #eee; 131 | position: absolute; 132 | right: 100%; 133 | white-space: nowrap; 134 | padding-right: 10px; 135 | } 136 | .pen.hinted blockquote { border-left: 0; margin-left: 0; padding-left: 0; } 137 | .pen.hinted blockquote:before { 138 | color: #1abf89; 139 | content: ">"; 140 | font-weight: bold; 141 | vertical-align: center; 142 | } 143 | .pen.hinted h1:before { content: "#";} 144 | .pen.hinted h2:before { content: "##";} 145 | .pen.hinted h3:before { content: "###";} 146 | .pen.hinted h4:before { content: "####";} 147 | .pen.hinted h5:before { content: "#####";} 148 | .pen.hinted h6:before { content: "######";} 149 | .pen.hinted hr:before { content: "﹘﹘﹘"; line-height: 1.2; vertical-align: bottom; } 150 | 151 | .pen.hinted pre:before, .pen.hinted pre:after { 152 | content: "```"; 153 | display: block; 154 | color: #ccc; 155 | } 156 | 157 | .pen.hinted ul { list-style: none; } 158 | .pen.hinted ul li:before { 159 | content: "*"; 160 | color: #999; 161 | line-height: 1; 162 | vertical-align: bottom; 163 | margin-left: -1.2em; 164 | display: inline-block; 165 | width: 1.2em; 166 | } 167 | 168 | .pen.hinted b:before, .pen.hinted b:after { content: "**"; color: #eee; font-weight: normal; } 169 | .pen.hinted i:before, .pen.hinted i:after { content: "*"; color: #eee; } 170 | 171 | .pen.hinted a { text-decoration: none; } 172 | .pen.hinted a:before {content: "["; color: #ddd; } 173 | .pen.hinted a:after { content: "](" attr(href) ")"; color: #ddd; } 174 | 175 | .pen-placeholder:after { position: absolute; top: 0; left: 0; content: attr(data-placeholder); color: #999; cursor: text; } 176 | -------------------------------------------------------------------------------- /src/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2012 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/pen.js: -------------------------------------------------------------------------------- 1 | /*! Licensed under MIT, https://github.com/sofish/pen */ 2 | (function(root, doc) { 3 | 4 | var Pen, debugMode, selection, utils = {}; 5 | var toString = Object.prototype.toString; 6 | var slice = Array.prototype.slice; 7 | 8 | // allow command list 9 | var commandsReg = { 10 | block: /^(?:p|h[1-6]|blockquote|pre)$/, 11 | inline: /^(?:bold|italic|underline|insertorderedlist|insertunorderedlist|indent|outdent)$/, 12 | source: /^(?:createlink|unlink)$/, 13 | insert: /^(?:inserthorizontalrule|insertimage|insert)$/, 14 | wrap: /^(?:code)$/ 15 | }; 16 | 17 | var lineBreakReg = /^(?:blockquote|pre|div)$/i; 18 | 19 | var effectNodeReg = /(?:[pubia]|h[1-6]|blockquote|[uo]l|li)/i; 20 | 21 | var strReg = { 22 | whiteSpace: /(^\s+)|(\s+$)/g, 23 | mailTo: /^(?!mailto:|.+\/|.+#|.+\?)(.*@.*\..+)$/, 24 | http: /^(?!\w+?:\/\/|mailto:|\/|\.\/|\?|#)(.*)$/ 25 | }; 26 | 27 | var autoLinkReg = { 28 | url: /((https?|ftp):\/\/|www\.)[^\s<]{3,}/gi, 29 | prefix: /^(?:https?|ftp):\/\//i, 30 | notLink: /^(?:img|a|input|audio|video|source|code|pre|script|head|title|style)$/i, 31 | maxLength: 100 32 | }; 33 | 34 | // type detect 35 | utils.is = function(obj, type) { 36 | return toString.call(obj).slice(8, -1) === type; 37 | }; 38 | 39 | utils.forEach = function(obj, iterator, arrayLike) { 40 | if (!obj) return; 41 | if (arrayLike == null) arrayLike = utils.is(obj, 'Array'); 42 | if (arrayLike) { 43 | for (var i = 0, l = obj.length; i < l; i++) iterator(obj[i], i, obj); 44 | } else { 45 | for (var key in obj) { 46 | if (obj.hasOwnProperty(key)) iterator(obj[key], key, obj); 47 | } 48 | } 49 | }; 50 | 51 | // copy props from a obj 52 | utils.copy = function(defaults, source) { 53 | utils.forEach(source, function (value, key) { 54 | defaults[key] = utils.is(value, 'Object') ? utils.copy({}, value) : 55 | utils.is(value, 'Array') ? utils.copy([], value) : value; 56 | }); 57 | return defaults; 58 | }; 59 | 60 | // log 61 | utils.log = function(message, force) { 62 | if (debugMode || force) 63 | console.log('%cPEN DEBUGGER: %c' + message, 'font-family:arial,sans-serif;color:#1abf89;line-height:2em;', 'font-family:cursor,monospace;color:#333;'); 64 | }; 65 | 66 | utils.delayExec = function (fn) { 67 | var timer = null; 68 | return function (delay) { 69 | clearTimeout(timer); 70 | timer = setTimeout(function() { 71 | fn(); 72 | }, delay || 1); 73 | }; 74 | }; 75 | 76 | // merge: make it easy to have a fallback 77 | utils.merge = function(config) { 78 | 79 | // default settings 80 | var defaults = { 81 | class: 'pen', 82 | debug: false, 83 | toolbar: null, // custom toolbar 84 | stay: config.stay || !config.debug, 85 | stayMsg: 'Are you going to leave here?', 86 | textarea: '', 87 | list: [ 88 | 'blockquote', 'h2', 'h3', 'p', 'code', 'insertorderedlist', 'insertunorderedlist', 'inserthorizontalrule', 89 | 'indent', 'outdent', 'bold', 'italic', 'underline', 'createlink', 'insertimage' 90 | ], 91 | titles: {}, 92 | cleanAttrs: ['id', 'class', 'style', 'name'], 93 | cleanTags: ['script'], 94 | linksInNewWindow: false 95 | }; 96 | 97 | // user-friendly config 98 | if (config.nodeType === 1) { 99 | defaults.editor = config; 100 | } else if (config.match && config.match(/^#[\S]+$/)) { 101 | defaults.editor = doc.getElementById(config.slice(1)); 102 | } else { 103 | defaults = utils.copy(defaults, config); 104 | } 105 | 106 | return defaults; 107 | }; 108 | 109 | function commandOverall(ctx, cmd, val) { 110 | var message = ' to exec 「' + cmd + '」 command' + (val ? (' with value: ' + val) : ''); 111 | 112 | try { 113 | doc.execCommand(cmd, false, val); 114 | } catch(err) { 115 | // TODO: there's an error when insert a image to document, but not a bug 116 | return utils.log('fail' + message, true); 117 | } 118 | 119 | utils.log('success' + message); 120 | } 121 | 122 | function commandInsert(ctx, name, val) { 123 | var node = getNode(ctx); 124 | if (!node) return; 125 | ctx._range.selectNode(node); 126 | ctx._range.collapse(false); 127 | 128 | // hide menu when a image was inserted 129 | if(name === 'insertimage' && ctx._menu) toggleNode(ctx._menu, true); 130 | 131 | return commandOverall(ctx, name, val); 132 | } 133 | 134 | function commandBlock(ctx, name) { 135 | var list = effectNode(ctx, getNode(ctx), true); 136 | if (list.indexOf(name) !== -1) name = 'p'; 137 | return commandOverall(ctx, 'formatblock', name); 138 | } 139 | 140 | function commandWrap(ctx, tag, value) { 141 | value = '<' + tag + '>' + (value||selection.toString()) + ''; 142 | return commandOverall(ctx, 'insertHTML', value); 143 | } 144 | 145 | function commandLink(ctx, tag, value) { 146 | if (ctx.config.linksInNewWindow) { 147 | value = '< a href="' + value + '" target="_blank">' + (selection.toString()) + ''; 148 | return commandOverall(ctx, 'insertHTML', value); 149 | } else { 150 | return commandOverall(ctx, tag, value); 151 | } 152 | } 153 | 154 | function initToolbar(ctx) { 155 | var icons = '', inputStr = ''; 156 | 157 | ctx._toolbar = ctx.config.toolbar; 158 | if (!ctx._toolbar) { 159 | var toolList = ctx.config.list; 160 | utils.forEach(toolList, function (name) { 161 | var klass = 'pen-icon icon-' + name; 162 | var title = ctx.config.titles[name] || ''; 163 | icons += ''; 164 | }, true); 165 | if (toolList.indexOf('createlink') >= 0 || toolList.indexOf('insertimage') >= 0) 166 | icons += inputStr; 167 | } else if (ctx._toolbar.querySelectorAll('[data-action=createlink]').length || 168 | ctx._toolbar.querySelectorAll('[data-action=insertimage]').length) { 169 | icons += inputStr; 170 | } 171 | 172 | if (icons) { 173 | ctx._menu = doc.createElement('div'); 174 | ctx._menu.setAttribute('class', ctx.config.class + '-menu pen-menu'); 175 | ctx._menu.innerHTML = icons; 176 | ctx._inputBar = ctx._menu.querySelector('input'); 177 | toggleNode(ctx._menu, true); 178 | doc.body.appendChild(ctx._menu); 179 | } 180 | if (ctx._toolbar && ctx._inputBar) toggleNode(ctx._inputBar); 181 | } 182 | 183 | function initEvents(ctx) { 184 | var toolbar = ctx._toolbar || ctx._menu, editor = ctx.config.editor; 185 | 186 | var toggleMenu = utils.delayExec(function() { 187 | ctx.highlight().menu(); 188 | }); 189 | var outsideClick = function() {}; 190 | 191 | function updateStatus(delay) { 192 | ctx._range = ctx.getRange(); 193 | toggleMenu(delay); 194 | } 195 | 196 | if (ctx._menu) { 197 | var setpos = function() { 198 | if (ctx._menu.style.display === 'block') ctx.menu(); 199 | }; 200 | 201 | // change menu offset when window resize / scroll 202 | addListener(ctx, root, 'resize', setpos); 203 | addListener(ctx, root, 'scroll', setpos); 204 | 205 | // toggle toolbar on mouse select 206 | var selecting = false; 207 | addListener(ctx, editor, 'mousedown', function() { 208 | selecting = true; 209 | }); 210 | addListener(ctx, editor, 'mouseleave', function() { 211 | if (selecting) updateStatus(800); 212 | selecting = false; 213 | }); 214 | addListener(ctx, editor, 'mouseup', function() { 215 | if (selecting) updateStatus(100); 216 | selecting = false; 217 | }); 218 | // Hide menu when focusing outside of editor 219 | outsideClick = function(e) { 220 | if (ctx._menu && !containsNode(editor, e.target) && !containsNode(ctx._menu, e.target)) { 221 | removeListener(ctx, doc, 'click', outsideClick); 222 | toggleMenu(100); 223 | } 224 | }; 225 | } else { 226 | addListener(ctx, editor, 'click', function() { 227 | updateStatus(0); 228 | }); 229 | } 230 | 231 | addListener(ctx, editor, 'keyup', function(e) { 232 | if (e.which === 8 && ctx.isEmpty()) return lineBreak(ctx, true); 233 | // toggle toolbar on key select 234 | if (e.which !== 13 || e.shiftKey) return updateStatus(400); 235 | var node = getNode(ctx, true); 236 | if (!node || !node.nextSibling || !lineBreakReg.test(node.nodeName)) return; 237 | if (node.nodeName !== node.nextSibling.nodeName) return; 238 | // hack for webkit, make 'enter' behavior like as firefox. 239 | if (node.lastChild.nodeName !== 'BR') node.appendChild(doc.createElement('br')); 240 | utils.forEach(node.nextSibling.childNodes, function(child) { 241 | if (child) node.appendChild(child); 242 | }, true); 243 | node.parentNode.removeChild(node.nextSibling); 244 | focusNode(ctx, node.lastChild, ctx.getRange()); 245 | }); 246 | 247 | // check line break 248 | addListener(ctx, editor, 'keydown', function(e) { 249 | editor.classList.remove('pen-placeholder'); 250 | if (e.which !== 13 || e.shiftKey) return; 251 | var node = getNode(ctx, true); 252 | if (!node || !lineBreakReg.test(node.nodeName)) return; 253 | var lastChild = node.lastChild; 254 | if (!lastChild || !lastChild.previousSibling) return; 255 | if (lastChild.previousSibling.textContent || lastChild.textContent) return; 256 | // quit block mode for 2 'enter' 257 | e.preventDefault(); 258 | var p = doc.createElement('p'); 259 | p.innerHTML = '
'; 260 | node.removeChild(lastChild); 261 | if (!node.nextSibling) node.parentNode.appendChild(p); 262 | else node.parentNode.insertBefore(p, node.nextSibling); 263 | focusNode(ctx, p, ctx.getRange()); 264 | }); 265 | 266 | var menuApply = function(action, value) { 267 | ctx.execCommand(action, value); 268 | ctx._range = ctx.getRange(); 269 | ctx.highlight().menu(); 270 | }; 271 | 272 | // toggle toolbar on key select 273 | addListener(ctx, toolbar, 'click', function(e) { 274 | var node = e.target, action; 275 | 276 | while (node !== toolbar && !(action = node.getAttribute('data-action'))) { 277 | node = node.parentNode; 278 | } 279 | 280 | if (!action) return; 281 | if (!/(?:createlink)|(?:insertimage)/.test(action)) return menuApply(action); 282 | if (!ctx._inputBar) return; 283 | 284 | // create link 285 | var input = ctx._inputBar; 286 | if (toolbar === ctx._menu) toggleNode(input); 287 | else { 288 | ctx._inputActive = true; 289 | ctx.menu(); 290 | } 291 | if (ctx._menu.style.display === 'none') return; 292 | 293 | setTimeout(function() { input.focus(); }, 400); 294 | var createlink = function() { 295 | var inputValue = input.value; 296 | 297 | if (!inputValue) action = 'unlink'; 298 | else { 299 | inputValue = input.value 300 | .replace(strReg.whiteSpace, '') 301 | .replace(strReg.mailTo, 'mailto:$1') 302 | .replace(strReg.http, 'http://$1'); 303 | } 304 | menuApply(action, inputValue); 305 | if (toolbar === ctx._menu) toggleNode(input, false); 306 | else toggleNode(ctx._menu, true); 307 | }; 308 | 309 | input.onkeypress = function(e) { 310 | if (e.which === 13) return createlink(); 311 | }; 312 | 313 | }); 314 | 315 | // listen for placeholder 316 | addListener(ctx, editor, 'focus', function() { 317 | if (ctx.isEmpty()) lineBreak(ctx, true); 318 | addListener(ctx, doc, 'click', outsideClick); 319 | }); 320 | 321 | addListener(ctx, editor, 'blur', function() { 322 | checkPlaceholder(ctx); 323 | ctx.checkContentChange(); 324 | }); 325 | 326 | // listen for paste and clear style 327 | addListener(ctx, editor, 'paste', function() { 328 | setTimeout(function() { 329 | ctx.cleanContent(); 330 | }); 331 | }); 332 | } 333 | 334 | function addListener(ctx, target, type, listener) { 335 | if (ctx._events.hasOwnProperty(type)) { 336 | ctx._events[type].push(listener); 337 | } else { 338 | ctx._eventTargets = ctx._eventTargets || []; 339 | ctx._eventsCache = ctx._eventsCache || []; 340 | var index = ctx._eventTargets.indexOf(target); 341 | if (index < 0) index = ctx._eventTargets.push(target) - 1; 342 | ctx._eventsCache[index] = ctx._eventsCache[index] || {}; 343 | ctx._eventsCache[index][type] = ctx._eventsCache[index][type] || []; 344 | ctx._eventsCache[index][type].push(listener); 345 | 346 | target.addEventListener(type, listener, false); 347 | } 348 | return ctx; 349 | } 350 | 351 | // trigger local events 352 | function triggerListener(ctx, type) { 353 | if (!ctx._events.hasOwnProperty(type)) return; 354 | var args = slice.call(arguments, 2); 355 | utils.forEach(ctx._events[type], function (listener) { 356 | listener.apply(ctx, args); 357 | }); 358 | } 359 | 360 | function removeListener(ctx, target, type, listener) { 361 | var events = ctx._events[type]; 362 | if (!events) { 363 | var _index = ctx._eventTargets.indexOf(target); 364 | if (_index >= 0) events = ctx._eventsCache[_index][type]; 365 | } 366 | if (!events) return ctx; 367 | var index = events.indexOf(listener); 368 | if (index >= 0) events.splice(index, 1); 369 | target.removeEventListener(type, listener, false); 370 | return ctx; 371 | } 372 | 373 | function removeAllListeners(ctx) { 374 | utils.forEach(this._events, function (events) { 375 | events.length = 0; 376 | }, false); 377 | if (!ctx._eventsCache) return ctx; 378 | utils.forEach(ctx._eventsCache, function (events, index) { 379 | var target = ctx._eventTargets[index]; 380 | utils.forEach(events, function (listeners, type) { 381 | utils.forEach(listeners, function (listener) { 382 | target.removeEventListener(type, listener, false); 383 | }, true); 384 | }, false); 385 | }, true); 386 | ctx._eventTargets = []; 387 | ctx._eventsCache = []; 388 | return ctx; 389 | } 390 | 391 | function checkPlaceholder(ctx) { 392 | ctx.config.editor.classList[ctx.isEmpty() ? 'add' : 'remove']('pen-placeholder'); 393 | } 394 | 395 | function trim(str) { 396 | return (str || '').replace(/^\s+|\s+$/g, ''); 397 | } 398 | 399 | // node.contains is not implemented in IE10/IE11 400 | function containsNode(parent, child) { 401 | if (parent === child) return true; 402 | child = child.parentNode; 403 | while (child) { 404 | if (child === parent) return true; 405 | child = child.parentNode; 406 | } 407 | return false; 408 | } 409 | 410 | function getNode(ctx, byRoot) { 411 | var node, root = ctx.config.editor; 412 | ctx._range = ctx._range || ctx.getRange(); 413 | node = ctx._range.commonAncestorContainer; 414 | if (!node || node === root) return null; 415 | while (node && (node.nodeType !== 1) && (node.parentNode !== root)) node = node.parentNode; 416 | while (node && byRoot && (node.parentNode !== root)) node = node.parentNode; 417 | return containsNode(root, node) ? node : null; 418 | } 419 | 420 | // node effects 421 | function effectNode(ctx, el, returnAsNodeName) { 422 | var nodes = []; 423 | el = el || ctx.config.editor; 424 | while (el && el !== ctx.config.editor) { 425 | if (el.nodeName.match(effectNodeReg)) { 426 | nodes.push(returnAsNodeName ? el.nodeName.toLowerCase() : el); 427 | } 428 | el = el.parentNode; 429 | } 430 | return nodes; 431 | } 432 | 433 | // breakout from node 434 | function lineBreak(ctx, empty) { 435 | var range = ctx._range = ctx.getRange(), node = doc.createElement('p'); 436 | if (empty) ctx.config.editor.innerHTML = ''; 437 | node.innerHTML = '
'; 438 | range.insertNode(node); 439 | focusNode(ctx, node.childNodes[0], range); 440 | } 441 | 442 | function focusNode(ctx, node, range) { 443 | range.setStartAfter(node); 444 | range.setEndBefore(node); 445 | range.collapse(false); 446 | ctx.setRange(range); 447 | } 448 | 449 | function autoLink(node) { 450 | if (node.nodeType === 1) { 451 | if (autoLinkReg.notLink.test(node.tagName)) return; 452 | utils.forEach(node.childNodes, function (child) { 453 | autoLink(child); 454 | }, true); 455 | } else if (node.nodeType === 3) { 456 | var result = urlToLink(node.nodeValue || ''); 457 | if (!result.links) return; 458 | var frag = doc.createDocumentFragment(), 459 | div = doc.createElement('div'); 460 | div.innerHTML = result.text; 461 | while (div.childNodes.length) frag.appendChild(div.childNodes[0]); 462 | node.parentNode.replaceChild(frag, node); 463 | } 464 | } 465 | 466 | function urlToLink(str) { 467 | var count = 0; 468 | str = str.replace(autoLinkReg.url, function(url) { 469 | var realUrl = url, displayUrl = url; 470 | count++; 471 | if (url.length > autoLinkReg.maxLength) displayUrl = url.slice(0, autoLinkReg.maxLength) + '...'; 472 | // Add http prefix if necessary 473 | if (!autoLinkReg.prefix.test(realUrl)) realUrl = 'http://' + realUrl; 474 | return '' + displayUrl + ''; 475 | }); 476 | return {links: count, text: str}; 477 | } 478 | 479 | function toggleNode(node, hide) { 480 | node.style.display = hide ? 'none' : 'block'; 481 | } 482 | 483 | Pen = function(config) { 484 | 485 | if (!config) throw new Error('Can\'t find config'); 486 | 487 | debugMode = config.debug; 488 | 489 | // merge user config 490 | var defaults = utils.merge(config); 491 | 492 | var editor = defaults.editor; 493 | 494 | if (!editor || editor.nodeType !== 1) throw new Error('Can\'t find editor'); 495 | 496 | // set default class 497 | editor.classList.add(defaults.class); 498 | 499 | // set contenteditable 500 | editor.setAttribute('contenteditable', 'true'); 501 | 502 | // assign config 503 | this.config = defaults; 504 | 505 | // set placeholder 506 | if (defaults.placeholder) editor.setAttribute('data-placeholder', defaults.placeholder); 507 | checkPlaceholder(this); 508 | 509 | // save the selection obj 510 | this.selection = selection; 511 | 512 | // define local events 513 | this._events = {change: []}; 514 | 515 | // enable toolbar 516 | initToolbar(this); 517 | 518 | // init events 519 | initEvents(this); 520 | 521 | // to check content change 522 | this._prevContent = this.getContent(); 523 | 524 | // enable markdown covert 525 | if (this.markdown) this.markdown.init(this); 526 | 527 | // stay on the page 528 | if (this.config.stay) this.stay(this.config); 529 | 530 | if(this.config.input) { 531 | this.addOnSubmitListener(this.config.input); 532 | } 533 | }; 534 | 535 | Pen.prototype.on = function(type, listener) { 536 | addListener(this, this.config.editor, type, listener); 537 | return this; 538 | }; 539 | 540 | Pen.prototype.addOnSubmitListener = function(inputElement) { 541 | var form = inputElement.form; 542 | var me = this; 543 | form.addEventListener("submit", function() { 544 | inputElement.value = me.config.saveAsMarkdown ? me.toMd(me.config.editor.innerHTML) : me.config.editor.innerHTML; 545 | }); 546 | }; 547 | 548 | Pen.prototype.isEmpty = function(node) { 549 | node = node || this.config.editor; 550 | return !(node.querySelector('img')) && !(node.querySelector('blockquote')) && 551 | !(node.querySelector('li')) && !trim(node.textContent); 552 | }; 553 | 554 | Pen.prototype.getContent = function() { 555 | return this.isEmpty() ? '' : trim(this.config.editor.innerHTML); 556 | }; 557 | 558 | Pen.prototype.setContent = function(html) { 559 | this.config.editor.innerHTML = html; 560 | this.cleanContent(); 561 | return this; 562 | }; 563 | 564 | Pen.prototype.checkContentChange = function () { 565 | var prevContent = this._prevContent, currentContent = this.getContent(); 566 | if (prevContent === currentContent) return; 567 | this._prevContent = currentContent; 568 | triggerListener(this, 'change', currentContent, prevContent); 569 | }; 570 | 571 | Pen.prototype.getRange = function() { 572 | var editor = this.config.editor, range = selection.rangeCount && selection.getRangeAt(0); 573 | if (!range) range = doc.createRange(); 574 | if (!containsNode(editor, range.commonAncestorContainer)) { 575 | range.selectNodeContents(editor); 576 | range.collapse(false); 577 | } 578 | return range; 579 | }; 580 | 581 | Pen.prototype.setRange = function(range) { 582 | range = range || this._range; 583 | if (!range) { 584 | range = this.getRange(); 585 | range.collapse(false); // set to end 586 | } 587 | try { 588 | selection.removeAllRanges(); 589 | selection.addRange(range); 590 | } catch (e) {/* IE throws error sometimes*/} 591 | return this; 592 | }; 593 | 594 | Pen.prototype.focus = function(focusStart) { 595 | if (!focusStart) this.setRange(); 596 | this.config.editor.focus(); 597 | return this; 598 | }; 599 | 600 | Pen.prototype.execCommand = function(name, value) { 601 | name = name.toLowerCase(); 602 | this.setRange(); 603 | 604 | if (commandsReg.block.test(name)) { 605 | commandBlock(this, name); 606 | } else if (commandsReg.inline.test(name)) { 607 | commandOverall(this, name, value); 608 | } else if (commandsReg.source.test(name)) { 609 | commandLink(this, name, value); 610 | } else if (commandsReg.insert.test(name)) { 611 | commandInsert(this, name, value); 612 | } else if (commandsReg.wrap.test(name)) { 613 | commandWrap(this, name, value); 614 | } else { 615 | utils.log('can not find command function for name: ' + name + (value ? (', value: ' + value) : ''), true); 616 | } 617 | if (name === 'indent') this.checkContentChange(); 618 | else this.cleanContent({cleanAttrs: ['style']}); 619 | }; 620 | 621 | // remove attrs and tags 622 | // pen.cleanContent({cleanAttrs: ['style'], cleanTags: ['id']}) 623 | Pen.prototype.cleanContent = function(options) { 624 | var editor = this.config.editor; 625 | 626 | if (!options) options = this.config; 627 | utils.forEach(options.cleanAttrs, function (attr) { 628 | utils.forEach(editor.querySelectorAll('[' + attr + ']'), function(item) { 629 | item.removeAttribute(attr); 630 | }, true); 631 | }, true); 632 | utils.forEach(options.cleanTags, function (tag) { 633 | utils.forEach(editor.querySelectorAll(tag), function(item) { 634 | item.parentNode.removeChild(item); 635 | }, true); 636 | }, true); 637 | 638 | checkPlaceholder(this); 639 | this.checkContentChange(); 640 | return this; 641 | }; 642 | 643 | // auto link content, return content 644 | Pen.prototype.autoLink = function() { 645 | autoLink(this.config.editor); 646 | return this.getContent(); 647 | }; 648 | 649 | // highlight menu 650 | Pen.prototype.highlight = function() { 651 | var toolbar = this._toolbar || this._menu 652 | , node = getNode(this); 653 | // remove all highlights 654 | utils.forEach(toolbar.querySelectorAll('.active'), function(el) { 655 | el.classList.remove('active'); 656 | }, true); 657 | 658 | if (!node) return this; 659 | 660 | var effects = effectNode(this, node) 661 | , inputBar = this._inputBar 662 | , highlight; 663 | 664 | if (inputBar && toolbar === this._menu) { 665 | // display link input if createlink enabled 666 | inputBar.style.display = 'none'; 667 | // reset link input value 668 | inputBar.value = ''; 669 | } 670 | 671 | highlight = function(str) { 672 | if (!str) return; 673 | var el = toolbar.querySelector('[data-action=' + str + ']'); 674 | return el && el.classList.add('active'); 675 | }; 676 | utils.forEach(effects, function(item) { 677 | var tag = item.nodeName.toLowerCase(); 678 | switch(tag) { 679 | case 'a': 680 | if (inputBar) inputBar.value = item.getAttribute('href'); 681 | tag = 'createlink'; 682 | break; 683 | case 'img': 684 | if (inputBar) inputBar.value = item.getAttribute('src'); 685 | tag = 'insertimage'; 686 | break; 687 | case 'i': 688 | tag = 'italic'; 689 | break; 690 | case 'u': 691 | tag = 'underline'; 692 | break; 693 | case 'b': 694 | tag = 'bold'; 695 | break; 696 | case 'pre': 697 | case 'code': 698 | tag = 'code'; 699 | break; 700 | case 'ul': 701 | tag = 'insertunorderedlist'; 702 | break; 703 | case 'ol': 704 | tag = 'insertorderedlist'; 705 | break; 706 | case 'li': 707 | tag = 'indent'; 708 | break; 709 | } 710 | highlight(tag); 711 | }, true); 712 | 713 | return this; 714 | }; 715 | 716 | // show menu 717 | Pen.prototype.menu = function() { 718 | if (!this._menu) return this; 719 | if (selection.isCollapsed) { 720 | this._menu.style.display = 'none'; //hide menu 721 | this._inputActive = false; 722 | return this; 723 | } 724 | if (this._toolbar) { 725 | if (!this._inputBar || !this._inputActive) return this; 726 | } 727 | var offset = this._range.getBoundingClientRect() 728 | , menuPadding = 10 729 | , top = offset.top - menuPadding 730 | , left = offset.left + (offset.width / 2) 731 | , menu = this._menu 732 | , menuOffset = {x: 0, y: 0} 733 | , stylesheet = this._stylesheet; 734 | 735 | // fixes some browser double click visual discontinuity 736 | // if the offset has no width or height it should not be used 737 | if (offset.width === 0 && offset.height === 0) return this; 738 | 739 | // store the stylesheet used for positioning the menu horizontally 740 | if (this._stylesheet === undefined) { 741 | var style = document.createElement("style"); 742 | document.head.appendChild(style); 743 | this._stylesheet = stylesheet = style.sheet; 744 | } 745 | // display block to caculate its width & height 746 | menu.style.display = 'block'; 747 | 748 | menuOffset.x = left - (menu.clientWidth / 2); 749 | menuOffset.y = top - menu.clientHeight; 750 | 751 | // check to see if menu has over-extended its bounding box. if it has, 752 | // 1) apply a new class if overflowed on top; 753 | // 2) apply a new rule if overflowed on the left 754 | if (stylesheet.cssRules.length > 0) { 755 | stylesheet.deleteRule(0); 756 | } 757 | if (menuOffset.x < 0) { 758 | menuOffset.x = 0; 759 | stylesheet.insertRule('.pen-menu:after {left: ' + left + 'px;}', 0); 760 | } else { 761 | stylesheet.insertRule('.pen-menu:after {left: 50%; }', 0); 762 | } 763 | if (menuOffset.y < 0) { 764 | menu.classList.add('pen-menu-below'); 765 | menuOffset.y = offset.top + offset.height + menuPadding; 766 | } else { 767 | menu.classList.remove('pen-menu-below'); 768 | } 769 | 770 | menu.style.top = menuOffset.y + 'px'; 771 | menu.style.left = menuOffset.x + 'px'; 772 | return this; 773 | }; 774 | 775 | Pen.prototype.stay = function(config) { 776 | var ctx = this; 777 | if (!window.onbeforeunload) { 778 | window.onbeforeunload = function() { 779 | if (!ctx._isDestroyed) return config.stayMsg; 780 | }; 781 | } 782 | }; 783 | 784 | Pen.prototype.destroy = function(isAJoke) { 785 | var destroy = isAJoke ? false : true 786 | , attr = isAJoke ? 'setAttribute' : 'removeAttribute'; 787 | 788 | if (!isAJoke) { 789 | removeAllListeners(this); 790 | try { 791 | selection.removeAllRanges(); 792 | if (this._menu) this._menu.parentNode.removeChild(this._menu); 793 | } catch (e) {/* IE throws error sometimes*/} 794 | } else { 795 | initToolbar(this); 796 | initEvents(this); 797 | } 798 | this._isDestroyed = destroy; 799 | this.config.editor[attr]('contenteditable', ''); 800 | 801 | return this; 802 | }; 803 | 804 | Pen.prototype.rebuild = function() { 805 | return this.destroy('it\'s a joke'); 806 | }; 807 | 808 | // a fallback for old browers 809 | root.Pen = function(config) { 810 | if (!config) return utils.log('can\'t find config', true); 811 | 812 | var defaults = utils.merge(config) 813 | , klass = defaults.editor.getAttribute('class'); 814 | 815 | klass = klass ? klass.replace(/\bpen\b/g, '') + ' pen-textarea ' + defaults.class : 'pen pen-textarea'; 816 | defaults.editor.setAttribute('class', klass); 817 | defaults.editor.innerHTML = defaults.textarea; 818 | return defaults.editor; 819 | }; 820 | 821 | // export content as markdown 822 | var regs = { 823 | a: [/]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'], 824 | img: [/]*src=["']([^\"+|[^']+)[^>]*>/ig, '![]($1)'], 825 | b: [/]*>(.*?)<\/b>/ig, '**$1**'], 826 | i: [/]*>(.*?)<\/i>/ig, '***$1***'], 827 | h: [/]*>(.*?)<\/h\1>/ig, function(a, b, c) { 828 | return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n'; 829 | }], 830 | li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'], 831 | blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'], 832 | pre: [/]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'], 833 | code: [/]*>(.*?)<\/code>/ig, '\n`\n$1\n`\n'], 834 | p: [/]*>(.*?)<\/p>/ig, '\n$1\n'], 835 | hr: [/]*>/ig, '\n---\n'] 836 | }; 837 | 838 | Pen.prototype.toMd = function() { 839 | var html = this.getContent() 840 | .replace(/\n+/g, '') // remove line break 841 | .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol 842 | 843 | for(var p in regs) { 844 | if (regs.hasOwnProperty(p)) 845 | html = html.replace.apply(html, regs[p]); 846 | } 847 | return html.replace(/\*{5}/g, '**'); 848 | }; 849 | 850 | // make it accessible 851 | if (doc.getSelection) { 852 | selection = doc.getSelection(); 853 | root.Pen = Pen; 854 | } 855 | 856 | }(window, document)); 857 | --------------------------------------------------------------------------------