├── LICENSE ├── README.md ├── bower.json ├── example └── index.html ├── fonts └── fontawesome-webfont.woff ├── javascripts ├── angular-editor.js └── simditor │ ├── module.js │ ├── simditor-all.js │ ├── simditor.js │ └── uploader.js └── stylesheets ├── font-awesome.css └── simditor.css /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 TomWan 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-editor,1 line is enough 2 | ============== 3 | 4 | 1 line is enough: 5 | ```html 6 |
7 | ``` 8 | 9 | angular rich text editor with simditor by tower.im 10 | 11 | [just visit the home page and check the source code:)](http://wanming.github.io/angular-editor) 12 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-editor", 3 | "homepage": "https://github.com/wanming/angular-editor", 4 | "main" : ["./javascripts/angular-editor.js"], 5 | "dependencies": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | angular-editor demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 35 | 36 | 37 |

angular-editor demo

38 |
39 |
40 |

the editor's value:

41 |
{{editor}}
42 |
43 |
44 |
45 | 46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wanming/angular-editor/f736b90ac6e82163ef42fe94819e6cfde7973f57/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /javascripts/angular-editor.js: -------------------------------------------------------------------------------- 1 | /*global window,location*/ 2 | (function (window) { 3 | 'use strict'; 4 | 5 | var Simditor = window.Simditor; 6 | var directives = angular.module('simditor',[]); 7 | 8 | directives.directive('simditor', function () { 9 | 10 | var TOOLBAR_DEFAULT = ['title', 'bold', 'italic', 'underline', 'strikethrough', '|', 'ol', 'ul', 'blockquote', 'code', 'table', '|', 'link', 'image', 'hr', '|', 'indent', 'outdent']; 11 | 12 | return { 13 | require: "?^ngModel", 14 | link: function (scope, element, attrs, ngModel) { 15 | element.append("
"); 16 | 17 | var toolbar = scope.$eval(attrs.toolbar) || TOOLBAR_DEFAULT; 18 | scope.simditor = new Simditor({ 19 | textarea: element.children()[0], 20 | pasteImage: true, 21 | toolbar: toolbar, 22 | defaultImage: 'assets/images/image.png', 23 | upload: location.search === '?upload' ? { 24 | url: '/upload' 25 | } : false 26 | }); 27 | 28 | var $target = element.find('.simditor-body'); 29 | 30 | function readViewText() { 31 | 32 | ngModel.$setViewValue($target.html()); 33 | 34 | if (attrs.ngRequired != undefined && attrs.ngRequired != "false") { 35 | 36 | var text = $target.text(); 37 | 38 | if(text.trim() === "") { 39 | ngModel.$setValidity("required", false); 40 | } else { 41 | ngModel.$setValidity("required", true); 42 | } 43 | } 44 | 45 | } 46 | 47 | ngModel.$render = function () { 48 | scope.simditor.focus(); 49 | $target.html(ngModel.$viewValue); 50 | }; 51 | 52 | scope.simditor.on('valuechanged', function () { 53 | scope.$apply(readViewText); 54 | }); 55 | } 56 | }; 57 | }); 58 | }(window)); 59 | -------------------------------------------------------------------------------- /javascripts/simditor/module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Module, Plugin, Widget, 3 | __slice = [].slice, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | Module = (function() { 8 | function Module() {} 9 | 10 | Module.extend = function(obj) { 11 | var key, val, _ref; 12 | if (!((obj != null) && typeof obj === 'object')) { 13 | return; 14 | } 15 | for (key in obj) { 16 | val = obj[key]; 17 | if (key !== 'included' && key !== 'extended') { 18 | this[key] = val; 19 | } 20 | } 21 | return (_ref = obj.extended) != null ? _ref.call(this) : void 0; 22 | }; 23 | 24 | Module.include = function(obj) { 25 | var key, val, _ref; 26 | if (!((obj != null) && typeof obj === 'object')) { 27 | return; 28 | } 29 | for (key in obj) { 30 | val = obj[key]; 31 | if (key !== 'included' && key !== 'extended') { 32 | this.prototype[key] = val; 33 | } 34 | } 35 | return (_ref = obj.included) != null ? _ref.call(this) : void 0; 36 | }; 37 | 38 | Module.prototype.on = function() { 39 | var args, _ref; 40 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 41 | return (_ref = $(this)).on.apply(_ref, args); 42 | }; 43 | 44 | Module.prototype.one = function() { 45 | var args, _ref; 46 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 47 | return (_ref = $(this)).one.apply(_ref, args); 48 | }; 49 | 50 | Module.prototype.off = function() { 51 | var args, _ref; 52 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 53 | return (_ref = $(this)).off.apply(_ref, args); 54 | }; 55 | 56 | Module.prototype.trigger = function() { 57 | var args, _ref; 58 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 59 | return (_ref = $(this)).trigger.apply(_ref, args); 60 | }; 61 | 62 | Module.prototype.triggerHandler = function() { 63 | var args, _ref; 64 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 65 | return (_ref = $(this)).triggerHandler.apply(_ref, args); 66 | }; 67 | 68 | return Module; 69 | 70 | })(); 71 | 72 | Widget = (function(_super) { 73 | __extends(Widget, _super); 74 | 75 | Widget.connect = function(cls) { 76 | if (typeof cls !== 'function') { 77 | return; 78 | } 79 | if (!cls.className) { 80 | throw new Error('Widget.connect: lack of class property "className"'); 81 | return; 82 | } 83 | if (!this._connectedClasses) { 84 | this._connectedClasses = []; 85 | } 86 | this._connectedClasses.push(cls); 87 | if (cls.className) { 88 | return this[cls.className] = cls; 89 | } 90 | }; 91 | 92 | Widget.prototype._init = function() {}; 93 | 94 | Widget.prototype.opts = {}; 95 | 96 | function Widget(opts) { 97 | var cls, instance, instances, name, _base, _i, _len; 98 | this.opts = $.extend({}, this.opts, opts); 99 | (_base = this.constructor)._connectedClasses || (_base._connectedClasses = []); 100 | instances = (function() { 101 | var _i, _len, _ref, _results; 102 | _ref = this.constructor._connectedClasses; 103 | _results = []; 104 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 105 | cls = _ref[_i]; 106 | name = cls.className.charAt(0).toLowerCase() + cls.className.slice(1); 107 | _results.push(this[name] = new cls(this)); 108 | } 109 | return _results; 110 | }).call(this); 111 | this._init(); 112 | for (_i = 0, _len = instances.length; _i < _len; _i++) { 113 | instance = instances[_i]; 114 | if (typeof instance._init === "function") { 115 | instance._init(); 116 | } 117 | } 118 | this.trigger('pluginconnected'); 119 | } 120 | 121 | Widget.prototype.destroy = function() {}; 122 | 123 | return Widget; 124 | 125 | })(Module); 126 | 127 | Plugin = (function(_super) { 128 | __extends(Plugin, _super); 129 | 130 | Plugin.className = 'Plugin'; 131 | 132 | Plugin.prototype.opts = {}; 133 | 134 | function Plugin(widget) { 135 | this.widget = widget; 136 | this.opts = $.extend({}, this.opts, this.widget.opts); 137 | } 138 | 139 | Plugin.prototype._init = function() {}; 140 | 141 | return Plugin; 142 | 143 | })(Module); 144 | 145 | window.Module = Module; 146 | 147 | window.Widget = Widget; 148 | 149 | window.Plugin = Plugin; 150 | 151 | }).call(this); 152 | -------------------------------------------------------------------------------- /javascripts/simditor/simditor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var BlockquoteButton, BoldButton, Button, CodeButton, CodePopover, Formatter, HrButton, ImageButton, ImagePopover, IndentButton, InputManager, ItalicButton, Keystroke, LinkButton, LinkPopover, ListButton, OrderListButton, OutdentButton, Popover, Selection, Simditor, StrikethroughButton, TableButton, Test, TestPlugin, TitleButton, Toolbar, UnderlineButton, UndoManager, UnorderListButton, Util, _ref, _ref1, _ref10, _ref11, _ref12, _ref13, _ref14, _ref15, _ref16, _ref17, _ref18, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 5 | __slice = [].slice, 6 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 7 | 8 | Selection = (function(_super) { 9 | __extends(Selection, _super); 10 | 11 | Selection.className = 'Selection'; 12 | 13 | function Selection() { 14 | var args; 15 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 16 | Selection.__super__.constructor.apply(this, args); 17 | this.sel = document.getSelection(); 18 | this.editor = this.widget; 19 | } 20 | 21 | Selection.prototype._init = function() {}; 22 | 23 | Selection.prototype.clear = function() { 24 | var e; 25 | try { 26 | return this.sel.removeAllRanges(); 27 | } catch (_error) { 28 | e = _error; 29 | } 30 | }; 31 | 32 | Selection.prototype.getRange = function() { 33 | if (!this.editor.inputManager.focused || !this.sel.rangeCount) { 34 | return null; 35 | } 36 | return this.sel.getRangeAt(0); 37 | }; 38 | 39 | Selection.prototype.selectRange = function(range) { 40 | this.clear(); 41 | return this.sel.addRange(range); 42 | }; 43 | 44 | Selection.prototype.rangeAtEndOf = function(node, range) { 45 | var endNode, endNodeLength, result, 46 | _this = this; 47 | if (range == null) { 48 | range = this.getRange(); 49 | } 50 | if (!((range != null) && range.collapsed)) { 51 | return; 52 | } 53 | node = $(node)[0]; 54 | endNode = range.endContainer; 55 | endNodeLength = this.editor.util.getNodeLength(endNode); 56 | if (!(range.endOffset === endNodeLength - 1 && $(endNode).contents().last().is('br')) && range.endOffset !== endNodeLength) { 57 | return false; 58 | } 59 | if (node === endNode) { 60 | return true; 61 | } else if (!$.contains(node, endNode)) { 62 | return false; 63 | } 64 | result = true; 65 | $(endNode).parentsUntil(node).addBack().each(function(i, n) { 66 | var $lastChild, nodes; 67 | nodes = $(n).parent().contents().filter(function() { 68 | return !(this !== n && this.nodeType === 3 && !this.nodeValue); 69 | }); 70 | $lastChild = nodes.last(); 71 | if (!($lastChild.get(0) === n || ($lastChild.is('br') && $lastChild.prev().get(0) === n))) { 72 | result = false; 73 | return false; 74 | } 75 | }); 76 | return result; 77 | }; 78 | 79 | Selection.prototype.rangeAtStartOf = function(node, range) { 80 | var result, startNode, 81 | _this = this; 82 | if (range == null) { 83 | range = this.getRange(); 84 | } 85 | if (!((range != null) && range.collapsed)) { 86 | return; 87 | } 88 | node = $(node)[0]; 89 | startNode = range.startContainer; 90 | if (range.startOffset !== 0) { 91 | return false; 92 | } 93 | if (node === startNode) { 94 | return true; 95 | } else if (!$.contains(node, startNode)) { 96 | return false; 97 | } 98 | result = true; 99 | $(startNode).parentsUntil(node).addBack().each(function(i, n) { 100 | var nodes; 101 | nodes = $(n).parent().contents().filter(function() { 102 | return !(this !== n && this.nodeType === 3 && !this.nodeValue); 103 | }); 104 | if (nodes.first().get(0) !== n) { 105 | return result = false; 106 | } 107 | }); 108 | return result; 109 | }; 110 | 111 | Selection.prototype.insertNode = function(node, range) { 112 | if (range == null) { 113 | range = this.getRange(); 114 | } 115 | if (range == null) { 116 | return; 117 | } 118 | node = $(node)[0]; 119 | range.insertNode(node); 120 | return this.setRangeAfter(node, range); 121 | }; 122 | 123 | Selection.prototype.setRangeAfter = function(node, range) { 124 | if (range == null) { 125 | range = this.getRange(); 126 | } 127 | if (range == null) { 128 | return; 129 | } 130 | node = $(node)[0]; 131 | range.setEndAfter(node); 132 | range.collapse(false); 133 | return this.selectRange(range); 134 | }; 135 | 136 | Selection.prototype.setRangeBefore = function(node, range) { 137 | if (range == null) { 138 | range = this.getRange(); 139 | } 140 | if (range == null) { 141 | return; 142 | } 143 | node = $(node)[0]; 144 | range.setEndBefore(node); 145 | range.collapse(false); 146 | return this.selectRange(range); 147 | }; 148 | 149 | Selection.prototype.setRangeAtStartOf = function(node, range) { 150 | if (range == null) { 151 | range = this.getRange(); 152 | } 153 | node = $(node).get(0); 154 | range.setEnd(node, 0); 155 | range.collapse(false); 156 | return this.selectRange(range); 157 | }; 158 | 159 | Selection.prototype.setRangeAtEndOf = function(node, range) { 160 | var $node, contents, lastChild, lastText, nodeLength; 161 | if (range == null) { 162 | range = this.getRange(); 163 | } 164 | $node = $(node); 165 | node = $node.get(0); 166 | if ($node.is('pre')) { 167 | contents = $node.contents(); 168 | if (contents.length > 0) { 169 | lastChild = contents.last(); 170 | lastText = lastChild.text(); 171 | if (lastText.charAt(lastText.length - 1) === '\n') { 172 | range.setEnd(lastChild[0], this.editor.util.getNodeLength(lastChild[0]) - 1); 173 | } else { 174 | range.setEnd(lastChild[0], this.editor.util.getNodeLength(lastChild[0])); 175 | } 176 | } else { 177 | range.setEnd(node, 0); 178 | } 179 | } else { 180 | nodeLength = this.editor.util.getNodeLength(node); 181 | if (node.nodeType !== 3 && nodeLength > 0 && $(node).contents().last().is('br')) { 182 | nodeLength -= 1; 183 | } 184 | range.setEnd(node, nodeLength); 185 | } 186 | range.collapse(false); 187 | return this.selectRange(range); 188 | }; 189 | 190 | Selection.prototype.deleteRangeContents = function(range) { 191 | if (range == null) { 192 | range = this.getRange(); 193 | } 194 | return range.deleteContents(); 195 | }; 196 | 197 | Selection.prototype.breakBlockEl = function(el, range) { 198 | var $el; 199 | if (range == null) { 200 | range = this.getRange(); 201 | } 202 | $el = $(el); 203 | if (!range.collapsed) { 204 | return $el; 205 | } 206 | range.setStartBefore($el.get(0)); 207 | if (range.collapsed) { 208 | return $el; 209 | } 210 | return $el.before(range.extractContents()); 211 | }; 212 | 213 | Selection.prototype.save = function() { 214 | var endCaret, range, startCaret; 215 | if (this._selectionSaved) { 216 | return; 217 | } 218 | range = this.getRange(); 219 | startCaret = $('').addClass('simditor-caret-start'); 220 | endCaret = $('').addClass('simditor-caret-end'); 221 | range.insertNode(startCaret[0]); 222 | range.collapse(false); 223 | range.insertNode(endCaret[0]); 224 | this.clear(); 225 | return this._selectionSaved = true; 226 | }; 227 | 228 | Selection.prototype.restore = function() { 229 | var endCaret, endContainer, endOffset, range, startCaret, startContainer, startOffset; 230 | if (!this._selectionSaved) { 231 | return false; 232 | } 233 | startCaret = this.editor.body.find('.simditor-caret-start'); 234 | endCaret = this.editor.body.find('.simditor-caret-end'); 235 | if (startCaret.length && endCaret.length) { 236 | startContainer = startCaret.parent(); 237 | startOffset = startContainer.contents().index(startCaret); 238 | endContainer = endCaret.parent(); 239 | endOffset = endContainer.contents().index(endCaret); 240 | if (startContainer[0] === endContainer[0]) { 241 | endOffset -= 1; 242 | } 243 | range = document.createRange(); 244 | range.setStart(startContainer.get(0), startOffset); 245 | range.setEnd(endContainer.get(0), endOffset); 246 | startCaret.remove(); 247 | endCaret.remove(); 248 | this.selectRange(range); 249 | if (this.editor.util.browser.firefox) { 250 | this.editor.body.focus(); 251 | } 252 | } else { 253 | startCaret.remove(); 254 | endCaret.remove(); 255 | } 256 | this._selectionSaved = false; 257 | return range; 258 | }; 259 | 260 | return Selection; 261 | 262 | })(Plugin); 263 | 264 | Formatter = (function(_super) { 265 | __extends(Formatter, _super); 266 | 267 | Formatter.className = 'Formatter'; 268 | 269 | function Formatter() { 270 | var args; 271 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 272 | Formatter.__super__.constructor.apply(this, args); 273 | this.editor = this.widget; 274 | } 275 | 276 | Formatter.prototype._init = function() { 277 | var _this = this; 278 | return this.editor.body.on('click', 'a', function(e) { 279 | return false; 280 | }); 281 | }; 282 | 283 | Formatter.prototype._allowedTags = ['br', 'a', 'img', 'b', 'strong', 'i', 'u', 'p', 'ul', 'ol', 'li', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'hr']; 284 | 285 | Formatter.prototype._allowedAttributes = { 286 | img: ['src', 'alt', 'width', 'height', 'data-origin-src', 'data-origin-size', 'data-origin-name'], 287 | a: ['href', 'target'], 288 | pre: ['data-lang'], 289 | p: ['data-indent'], 290 | h1: ['data-indent'], 291 | h2: ['data-indent'], 292 | h3: ['data-indent'], 293 | h4: ['data-indent'] 294 | }; 295 | 296 | Formatter.prototype.decorate = function($el) { 297 | if ($el == null) { 298 | $el = this.editor.body; 299 | } 300 | return this.editor.trigger('decorate', [$el]); 301 | }; 302 | 303 | Formatter.prototype.undecorate = function($el) { 304 | if ($el == null) { 305 | $el = this.editor.body.clone(); 306 | } 307 | this.editor.trigger('undecorate', [$el]); 308 | return $.trim($el.html()); 309 | }; 310 | 311 | Formatter.prototype.autolink = function($el) { 312 | var $node, findLinkNode, lastIndex, linkNodes, match, re, replaceEls, text, uri, _i, _len; 313 | if ($el == null) { 314 | $el = this.editor.body; 315 | } 316 | linkNodes = []; 317 | findLinkNode = function($parentNode) { 318 | return $parentNode.contents().each(function(i, node) { 319 | var $node, text; 320 | $node = $(node); 321 | if ($node.is('a') || $node.closest('a', $el).length) { 322 | return; 323 | } 324 | if ($node.contents().length) { 325 | return findLinkNode($node); 326 | } else if ((text = $node.text()) && /https?:\/\/|www\./ig.test(text)) { 327 | return linkNodes.push($node); 328 | } 329 | }); 330 | }; 331 | findLinkNode($el); 332 | re = /(https?:\/\/|www\.)[\w\-\.\?&=\/#%:]+/ig; 333 | for (_i = 0, _len = linkNodes.length; _i < _len; _i++) { 334 | $node = linkNodes[_i]; 335 | text = $node.text(); 336 | replaceEls = []; 337 | match = null; 338 | lastIndex = 0; 339 | while ((match = re.exec(text)) !== null) { 340 | replaceEls.push(document.createTextNode(text.substring(lastIndex, match.index))); 341 | lastIndex = re.lastIndex; 342 | uri = /^(http(s)?:\/\/|\/)/.test(match[0]) ? match[0] : 'http://' + match[0]; 343 | replaceEls.push($('' + match[0] + '')[0]); 344 | } 345 | replaceEls.push(document.createTextNode(text.substring(lastIndex))); 346 | $node.replaceWith($(replaceEls)); 347 | } 348 | return $el; 349 | }; 350 | 351 | Formatter.prototype.format = function($el) { 352 | var $node, blockNode, n, node, _i, _j, _len, _len1, _ref, _ref1; 353 | if ($el == null) { 354 | $el = this.editor.body; 355 | } 356 | if ($el.is(':empty')) { 357 | $el.append('

' + this.editor.util.phBr + '

'); 358 | return $el; 359 | } 360 | _ref = $el.contents(); 361 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 362 | n = _ref[_i]; 363 | this.cleanNode(n, true); 364 | } 365 | _ref1 = $el.contents(); 366 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 367 | node = _ref1[_j]; 368 | $node = $(node); 369 | if ($node.is('br')) { 370 | if (typeof blockNode !== "undefined" && blockNode !== null) { 371 | blockNode = null; 372 | } 373 | $node.remove(); 374 | } else if (this.editor.util.isBlockNode(node) || $node.is('img')) { 375 | if ($node.is('li')) { 376 | if (blockNode && blockNode.is('ul, ol')) { 377 | blockNode.append(node); 378 | } else { 379 | blockNode = $('