├── .bowerrc
├── .gitignore
├── Gruntfile.js
├── LICENSE
├── README.md
├── angular-markdown-editable.js
├── bower.json
├── example
└── index.html
├── extensions
├── iframe.js
└── video.js
├── karma.conf.js
├── package.json
└── unit-tests.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "lib"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | lib
4 | node_modules
5 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 |
3 | require('load-grunt-tasks')(grunt);
4 |
5 | grunt.initConfig({
6 | pkg: grunt.file.readJSON('package.json'),
7 |
8 | karma: {
9 | unit: {
10 | configFile: 'karma.conf.js',
11 | singleRun: true
12 | }
13 | }
14 | });
15 |
16 | grunt.registerTask('test', [
17 | 'karma'
18 | ]);
19 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Chris
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-markdown-editable
2 | =========================
3 |
4 | Markdown is awesome. ContentEditable is awesome. How about we display the parsed markdown, but let users edit the base markdown on focus?
5 |
6 | ### Usage
7 |
8 | You'll need to use three directives to make full use of this module.
9 |
10 | ```ng-model``` is required, and ```contenteditable="true"``` should be there to activate both native contentEditable functionality as well as the additional ```contenteditable``` directive included in this module, which wires up ```ngModel``` to ```contenteditable``` changes.
11 |
12 | Finally, add ```markdown-editable``` to have markdown parsed as html in non-focused states and parsed as text in focused states. A typical implementation looks like this:
13 |
14 | ```
15 |
{{ markdownText }}
16 | ```
17 |
18 | where ```$scope.markdownText = "# This is an h1. \n## This is an h2. \n- This is a line item\n- This is a second line item\n\nThis is a new paragraph\n\n# Another h1";```... or any valid markdown text that you like.
19 |
20 | ### Target
21 | If you'd like to change the target attribute for links in your markdown, try something like the following:
22 |
23 | ```
24 |
25 | ```
26 |
27 | Setting the ```target``` attribute on the markdown-editable directive will copy that attribute to all anchor tags.
28 |
29 | ### Testing
30 |
31 | 1. Install dependencies with ```bower install``` and ```npm install```.
32 | 2. Make sure you have grunt-cli... ```npm install -g grunt-cli```.
33 | 3. Run tests with ```grunt test```.
34 |
--------------------------------------------------------------------------------
/angular-markdown-editable.js:
--------------------------------------------------------------------------------
1 | //----------------------------------------------------------------------------------------------------------------------
2 | // A directive for rendering markdown in AngularJS.
3 | //
4 | // Written by John Lindquist (original author). Modified by Jonathan Rowny (ngModel support).
5 | // Adapted by Christopher S. Case
6 | // Extended even further by Chris Esplin (@deltaepsilon)
7 | //
8 | //
9 | // Taken from: http://blog.angularjs.org/2012/05/custom-components-part-1.html
10 | //
11 | // @module angular.markdown.js
12 | //----------------------------------------------------------------------------------------------------------------------
13 |
14 | angular.module("angular-markdown-editable", []).directive('markdownEditable', function($timeout) {
15 | var converter = new Showdown.converter();
16 |
17 | return {
18 | restrict: 'A',
19 | require: '?ngModel',
20 | priority: 1,
21 | link: function postLink(scope, element, attrs, model) {
22 | // Check for extensions
23 | var extAttr = attrs.extensions;
24 | var callPrettyPrint = false;
25 | if(extAttr) {
26 | var extensions = [];
27 |
28 | // Convert the comma separated string into a list.
29 | extAttr.split(',').forEach(function(val)
30 | {
31 | // Strip any whitespace from the beginning or end.
32 | extensions.push(val.replace(/^\s+|\s+$/g, ''));
33 | });
34 |
35 | if(extensions.indexOf('prettify') >= 0)
36 | {
37 | callPrettyPrint = true;
38 | } // end if
39 |
40 | // Create a new converter.
41 | converter = new Showdown.converter({extensions: extensions});
42 | } // end if
43 |
44 | // Check for option to strip whitespace
45 | var stripWS = attrs.strip;
46 | if(String(stripWS).toLowerCase() == 'true') {
47 | stripWS = true;
48 | } else {
49 | stripWS = false;
50 | } // end if
51 |
52 | // Check for option to translate line breaks
53 | var lineBreaks = attrs.lineBreaks;
54 | if (String(lineBreaks).toLowerCase() == 'true') {
55 | lineBreaks = true;
56 | } else {
57 | lineBreaks = false;
58 | } // end if
59 |
60 | var render = function() {
61 | var htmlText = "";
62 | var val = "";
63 |
64 | // Check to see if we're using a model.
65 | if(attrs.ngModel) {
66 | if (model.$modelValue) {
67 | val = model.$modelValue;
68 | } // end if
69 | } else {
70 | val = element.text();
71 | } // end if
72 |
73 | if(stripWS) {
74 | val = val.replace(/^[ /t]+/g, '').replace(/\n[ /t]+/g, '\n');
75 | } // end stripWS
76 |
77 | if (lineBreaks) {
78 | val = val.replace(/
/g, '\n');
79 | } // end lineBreaks
80 |
81 | // Compile the markdown, and set it.
82 | if (val) {
83 | htmlText = converter.makeHtml(val);
84 | element.html(htmlText);
85 |
86 | if(callPrettyPrint) {
87 | prettyPrint();
88 | } // end if
89 | }
90 |
91 | if (attrs.target) {
92 | $timeout(function () {
93 | if (element && element[0]) {
94 | var aTags = element[0].querySelectorAll('a');
95 | var i = aTags.length;
96 |
97 | while (i--) {
98 | aTags[i].setAttribute('target', attrs.target);
99 | }
100 | }
101 | });
102 | }
103 | };
104 |
105 | if(attrs.ngModel) {
106 | model.$render = render;
107 | $timeout(render);
108 | } // end if
109 |
110 | //Support for contenteditable
111 | if(attrs.contenteditable === "true" && attrs.ngModel) {
112 | var LINEBREAK_REGEX = /\n/g,
113 | BR_REGEX = /<(br|p|div)(\/)?>/g,
114 | TAG_REGEX = /<.+?>/g,
115 | NBSP_REGEX = / /g,
116 | BLOCKQUOTE_REGEX = />/g,
117 | OPEN_TAG_REGEX = /```</g,
118 | OPEN_TAG_REVERSE_REGEX = /```\");
131 | text = text.replace(DOUBLE_SPACE_REGEX, " ");
132 |
133 | element.html(text);
134 | }
135 |
136 | });
137 |
138 | element.on('blur', function () {
139 | var html = element.html();
140 |
141 |
142 | html = html.replace(BR_REGEX, "\n");
143 | html = html.replace(TAG_REGEX, "");
144 | html = html.replace(NBSP_REGEX, " ");
145 | html = html.replace(OPEN_TAG_REGEX, "```<");
146 | html = html.replace(OPEN_TAG_NEWLINE_REGEX, "```\n<");
147 | html = html.replace(BLOCKQUOTE_REGEX, ">");
148 | html = html.replace(TRIPLE_LINEBREAK_REGEX, "\n\n");
149 |
150 | // console.log('html', html);
151 |
152 | model.$setViewValue(html);
153 |
154 | $timeout(render);
155 |
156 | });
157 | }
158 |
159 | render();
160 | } // end link
161 | }
162 | }); // end markdown directive
163 |
164 | angular.module("angular-markdown-editable").directive('contenteditable', function($timeout) {
165 | return {
166 | require: 'ngModel',
167 | restrict: 'A',
168 | link: function postLink(scope, element, attrs, ctrl) {
169 | var maxLength = parseInt(attrs.ngMaxlength, 10);
170 |
171 | if (window.getSelection) {
172 | element.on('focus', function() {
173 | return $timeout(function() {
174 | var el, range, selection;
175 | selection = window.getSelection();
176 | range = document.createRange();
177 | el = element[0];
178 |
179 | if (el.firstChild) { // Empty elements will throw errors
180 | range.setStart(el.firstChild, 0);
181 | range.setEnd(el.lastChild, el.lastChild.length);
182 | selection.removeAllRanges();
183 | return selection.addRange(range);
184 | }
185 |
186 | });
187 | });
188 | }
189 | element.on('blur', function() {
190 | return scope.$apply(function() {
191 | var value = element.attr('value');
192 |
193 | if (value) {
194 | return ctrl.$setViewValue(value);
195 | } else {
196 | return ctrl.$setViewValue(element.text());
197 | }
198 |
199 | });
200 | });
201 | ctrl.$render = function() {
202 | return element.text(ctrl.$viewValue);
203 | };
204 | ctrl.$render();
205 | return element.on('keydown', function(e) {
206 | var del, el, esc, ret, tab;
207 | esc = e.which === 27;
208 | ret = e.which === 13;
209 | del = e.which === 8;
210 | tab = e.which === 9;
211 | el = angular.element(e.target);
212 | if (esc) {
213 | ctrl.$setViewValue(element.text());
214 | el.blur();
215 | return e.preventDefault();
216 | } else if (ret && attrs.oneLine) {
217 | return e.preventDefault();
218 | } else if (maxLength && el.text().length >= maxLength && !del && !tab) {
219 | return e.preventDefault();
220 | }
221 | });
222 | }
223 | }
224 | });
225 |
226 | //----------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-markdown-editable",
3 | "version": "1.0.0",
4 | "homepage": "https://github.com/deltaepsilon/angular-markdown-editable",
5 | "authors": [
6 | "Chris Esplin "
7 | ],
8 | "description": "ContentEditable + Markdown mashup",
9 | "main": "angular-markdown-editable.js",
10 | "keywords": [
11 | "angular",
12 | "markdown",
13 | "contenteditable",
14 | "quiver"
15 | ],
16 | "license": "MIT",
17 | "ignore": [
18 | "**/.*",
19 | "node_modules",
20 | "bower_components",
21 | "lib",
22 | "test",
23 | "tests",
24 | "example"
25 | ],
26 | "dependencies": {
27 | "showdown": "~0.3.1",
28 | "angular": "~1.2.14",
29 | "angular-mocks": "~1.2.14"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Angular Markdown Editable Example
4 |
5 |
6 |
7 |
14 |
27 |
28 |
29 |
30 | Angular-Markdown-Editable
31 |
32 | Use markdown and contenteditable at the same time... seamlessly???
33 |
34 |
35 |
36 |
37 |
38 | {{ markdownText }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |