Skip to content

Commit f0d3baf

Browse files
SimeonCSimeonC
SimeonC
authored and
SimeonC
committed
feat(taBind): Textarea basic formatting of html with tabs and newlines
Fixes #307
1 parent c4b7bdd commit f0d3baf

11 files changed

+320
-3
lines changed

dist/textAngular.min.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

karma-jqlite.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = function (config) {
1919
'src/textAngular-sanitize.js',
2020
'src/textAngularSetup.js',
2121
'src/textAngular.js',
22+
'test/helpers.js',
2223
'bower_components/jquery/jquery.min.js',
2324
'test/**/*.spec.js'
2425
],

karma-jquery.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = function (config) {
2020
'src/textAngular-sanitize.js',
2121
'src/textAngularSetup.js',
2222
'src/textAngular.js',
23+
'test/helpers.js',
2324
'test/**/*.spec.js'
2425
],
2526

lib/factories.js

+10
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ angular.module('textAngular.factories', [])
132132
} catch (e){
133133
safe = oldsafe || '';
134134
}
135+
var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
136+
safe = safe.replace(/(&#(9|10);)*/ig, '');
137+
var re = /<pre[^>]*>.*?<\/pre[^>]*>/i;
138+
var index = 0;
139+
var origTag;
140+
while((origTag = re.exec(safe)) !== null && index < _preTags.length){
141+
safe = safe.substring(0, origTag.index) + _preTags[index] + safe.substring(origTag.index + origTag[0].length);
142+
re.lastIndex = Math.max(0, re.lastIndex + _preTags[index].length - origTag[0].length);
143+
index++;
144+
}
135145
return safe;
136146
};
137147
}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){

lib/taBind.js

+73
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,79 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
160160
element.on('change blur', scope.events.change = scope.events.blur = function(){
161161
if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
162162
});
163+
164+
element.on('keydown', scope.events.keydown = function(event, eventData){
165+
/* istanbul ignore else: this is for catching the jqLite testing*/
166+
if(eventData) angular.extend(event, eventData);
167+
// Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
168+
/* istanbul ignore else: otherwise normal functionality */
169+
if(event.keyCode === 9){ // tab was pressed
170+
// get caret position/selection
171+
var start = this.selectionStart;
172+
var end = this.selectionEnd;
173+
174+
var value = element.val();
175+
if(event.shiftKey){
176+
// find \t
177+
var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
178+
if(_tab !== -1 && _tab >= _linebreak){
179+
// set textarea value to: text before caret + tab + text after caret
180+
element.val(value.substring(0, _tab) + value.substring(_tab + 1));
181+
182+
// put caret at right position again (add one for the tab)
183+
this.selectionStart = this.selectionEnd = start - 1;
184+
}
185+
}else{
186+
// set textarea value to: text before caret + tab + text after caret
187+
element.val(value.substring(0, start) + "\t" + value.substring(end));
188+
189+
// put caret at right position again (add one for the tab)
190+
this.selectionStart = this.selectionEnd = start + 1;
191+
}
192+
// prevent the focus lose
193+
event.preventDefault();
194+
}
195+
});
196+
197+
var _repeat = function(string, n){
198+
var result = '';
199+
for(var _n = 0; _n < n; _n++) result += string;
200+
return result;
201+
};
202+
203+
var recursiveListFormat = function(listNode, tablevel){
204+
var _html = '', _children = listNode.childNodes;
205+
tablevel++;
206+
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, listNode.outerHTML.indexOf('<li'));
207+
for(var _i = 0; _i < _children.length; _i++){
208+
/* istanbul ignore next: browser catch */
209+
if(!_children[_i].outerHTML) continue;
210+
if(_children[_i].nodeName.toLowerCase() === 'ul' || _children[_i].nodeName.toLowerCase() === 'ol')
211+
_html += '\n' + recursiveListFormat(_children[_i], tablevel);
212+
else
213+
_html += '\n' + _repeat('\t', tablevel) + _children[_i].outerHTML;
214+
}
215+
_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
216+
return _html;
217+
};
218+
219+
ngModel.$formatters.push(function(htmlValue){
220+
// tabulate the HTML so it looks nicer
221+
var _children = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
222+
if(_children.length > 0){
223+
htmlValue = '';
224+
for(var i = 0; i < _children.length; i++){
225+
/* istanbul ignore next: browser catch */
226+
if(!_children[i].outerHTML) continue;
227+
if(htmlValue.length > 0) htmlValue += '\n';
228+
if(_children[i].nodeName.toLowerCase() === 'ul' || _children[i].nodeName.toLowerCase() === 'ol')
229+
htmlValue += '' + recursiveListFormat(_children[i], 0);
230+
else htmlValue += '' + _children[i].outerHTML;
231+
}
232+
}
233+
234+
return htmlValue;
235+
});
163236
}else{
164237
// all the code specific to contenteditable divs
165238
var waitforpastedata = function(savedcontent, _savedSelection, cb) {

src/textAngular.js

+83
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,16 @@ angular.module('textAngular.factories', [])
311311
} catch (e){
312312
safe = oldsafe || '';
313313
}
314+
var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
315+
safe = safe.replace(/(&#(9|10);)*/ig, '');
316+
var re = /<pre[^>]*>.*?<\/pre[^>]*>/i;
317+
var index = 0;
318+
var origTag;
319+
while((origTag = re.exec(safe)) !== null && index < _preTags.length){
320+
safe = safe.substring(0, origTag.index) + _preTags[index] + safe.substring(origTag.index + origTag[0].length);
321+
re.lastIndex = Math.max(0, re.lastIndex + _preTags[index].length - origTag[0].length);
322+
index++;
323+
}
314324
return safe;
315325
};
316326
}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
@@ -953,6 +963,79 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
953963
element.on('change blur', scope.events.change = scope.events.blur = function(){
954964
if(!_isReadonly) ngModel.$setViewValue(_compileHtml());
955965
});
966+
967+
element.on('keydown', scope.events.keydown = function(event, eventData){
968+
/* istanbul ignore else: this is for catching the jqLite testing*/
969+
if(eventData) angular.extend(event, eventData);
970+
// Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
971+
/* istanbul ignore else: otherwise normal functionality */
972+
if(event.keyCode === 9){ // tab was pressed
973+
// get caret position/selection
974+
var start = this.selectionStart;
975+
var end = this.selectionEnd;
976+
977+
var value = element.val();
978+
if(event.shiftKey){
979+
// find \t
980+
var _linebreak = value.lastIndexOf('\n', start), _tab = value.lastIndexOf('\t', start);
981+
if(_tab !== -1 && _tab >= _linebreak){
982+
// set textarea value to: text before caret + tab + text after caret
983+
element.val(value.substring(0, _tab) + value.substring(_tab + 1));
984+
985+
// put caret at right position again (add one for the tab)
986+
this.selectionStart = this.selectionEnd = start - 1;
987+
}
988+
}else{
989+
// set textarea value to: text before caret + tab + text after caret
990+
element.val(value.substring(0, start) + "\t" + value.substring(end));
991+
992+
// put caret at right position again (add one for the tab)
993+
this.selectionStart = this.selectionEnd = start + 1;
994+
}
995+
// prevent the focus lose
996+
event.preventDefault();
997+
}
998+
});
999+
1000+
var _repeat = function(string, n){
1001+
var result = '';
1002+
for(var _n = 0; _n < n; _n++) result += string;
1003+
return result;
1004+
};
1005+
1006+
var recursiveListFormat = function(listNode, tablevel){
1007+
var _html = '', _children = listNode.childNodes;
1008+
tablevel++;
1009+
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, listNode.outerHTML.indexOf('<li'));
1010+
for(var _i = 0; _i < _children.length; _i++){
1011+
/* istanbul ignore next: browser catch */
1012+
if(!_children[_i].outerHTML) continue;
1013+
if(_children[_i].nodeName.toLowerCase() === 'ul' || _children[_i].nodeName.toLowerCase() === 'ol')
1014+
_html += '\n' + recursiveListFormat(_children[_i], tablevel);
1015+
else
1016+
_html += '\n' + _repeat('\t', tablevel) + _children[_i].outerHTML;
1017+
}
1018+
_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
1019+
return _html;
1020+
};
1021+
1022+
ngModel.$formatters.push(function(htmlValue){
1023+
// tabulate the HTML so it looks nicer
1024+
var _children = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
1025+
if(_children.length > 0){
1026+
htmlValue = '';
1027+
for(var i = 0; i < _children.length; i++){
1028+
/* istanbul ignore next: browser catch */
1029+
if(!_children[i].outerHTML) continue;
1030+
if(htmlValue.length > 0) htmlValue += '\n';
1031+
if(_children[i].nodeName.toLowerCase() === 'ul' || _children[i].nodeName.toLowerCase() === 'ol')
1032+
htmlValue += '' + recursiveListFormat(_children[i], 0);
1033+
else htmlValue += '' + _children[i].outerHTML;
1034+
}
1035+
}
1036+
1037+
return htmlValue;
1038+
});
9561039
}else{
9571040
// all the code specific to contenteditable divs
9581041
var waitforpastedata = function(savedcontent, _savedSelection, cb) {

test/helpers.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var triggerEvent = function(event, element, options){
2+
var event;
3+
if(angular.element === jQuery){
4+
event = jQuery.Event(event);
5+
angular.extend(event, options);
6+
element.triggerHandler(event);
7+
}else{
8+
element.triggerHandler(event, options);
9+
}
10+
};
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
describe('taBind.$formatters', function () {
2+
'use strict';
3+
var $rootScope, element;
4+
beforeEach(module('textAngular'));
5+
beforeEach(inject(function(_$rootScope_, $compile){
6+
$rootScope = _$rootScope_;
7+
$rootScope.html = '';
8+
element = $compile('<textarea ta-bind ng-model="html"></textarea>')($rootScope);
9+
}));
10+
afterEach(inject(function($document){
11+
$document.find('body').html('');
12+
}));
13+
14+
describe('should format textarea html for readability', function(){
15+
it('adding newlines after immediate child tags', function(){
16+
$rootScope.html = '<p>Test Line 1</p><div>Test Line 2</div><span>Test Line 3</span>';
17+
$rootScope.$digest();
18+
expect(element.val()).toBe('<p>Test Line 1</p>\n<div>Test Line 2</div>\n<span>Test Line 3</span>');
19+
});
20+
it('ignore nested tags', function(){
21+
$rootScope.html = '<p><b>Test</b> Line 1</p><div>Test <i>Line</i> 2</div><span>Test Line <u>3</u></span>';
22+
$rootScope.$digest();
23+
expect(element.val()).toBe('<p><b>Test</b> Line 1</p>\n<div>Test <i>Line</i> 2</div>\n<span>Test Line <u>3</u></span>');
24+
});
25+
it('tab out li elements', function(){
26+
$rootScope.html = '<ul><li>Test Line 1</li><li>Test Line 2</li><li>Test Line 3</li></ul>';
27+
$rootScope.$digest();
28+
expect(element.val()).toBe('<ul>\n\t<li>Test Line 1</li>\n\t<li>Test Line 2</li>\n\t<li>Test Line 3</li>\n</ul>');
29+
});
30+
it('handle nested lists', function(){
31+
$rootScope.html = '<ol><li>Test Line 1</li><ul><li>Nested Line 1</li><li>Nested Line 2</li></ul><li>Test Line 3</li></ol>';
32+
$rootScope.$digest();
33+
expect(element.val()).toBe('<ol>\n\t<li>Test Line 1</li>\n\t<ul>\n\t\t<li>Nested Line 1</li>\n\t\t<li>Nested Line 2</li>\n\t</ul>\n\t<li>Test Line 3</li>\n</ol>');
34+
});
35+
});
36+
});

test/taBind/taBind.events.spec.js

+41
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,45 @@ describe('taBind.events', function () {
303303
expect(test).toBe(targetElement[0]);
304304
});
305305
});
306+
307+
describe('handles tab key in textarea mode', function(){
308+
var $rootScope, element;
309+
beforeEach(inject(function (_$compile_, _$rootScope_) {
310+
$rootScope = _$rootScope_;
311+
$rootScope.html = '';
312+
element = _$compile_('<textarea ta-bind ng-model="html"></div>')($rootScope);
313+
$rootScope.html = '<p><a>Test Contents</a><img/></p>';
314+
$rootScope.$digest();
315+
}));
316+
317+
it('should insert \\t on tab key', function(){
318+
element.val('<p><a>Test Contents</a><img/></p>');
319+
triggerEvent('keydown', element, {keyCode: 9});
320+
expect(element.val()).toBe('\t<p><a>Test Contents</a><img/></p>');
321+
});
322+
323+
describe('should remove \\t on shift-tab', function(){
324+
it('should remove \\t at start of line', function(){
325+
element.val('\t<p><a>Test Contents</a><img/></p>');
326+
triggerEvent('keydown', element, {keyCode: 9, shiftKey: true});
327+
expect(element.val()).toBe('<p><a>Test Contents</a><img/></p>');
328+
});
329+
it('should remove only one \\t at start of line', function(){
330+
element.val('\t\t<p><a>Test Contents</a><img/></p>');
331+
triggerEvent('keydown', element, {keyCode: 9, shiftKey: true});
332+
expect(element.val()).toBe('\t<p><a>Test Contents</a><img/></p>');
333+
});
334+
it('should do nothing if no \\t at start of line', function(){
335+
element.val('<p><a>Test Contents</a><img/></p>');
336+
triggerEvent('keydown', element, {keyCode: 9, shiftKey: true});
337+
expect(element.val()).toBe('<p><a>Test Contents</a><img/></p>');
338+
});
339+
// Issue with phantomjs not setting target to end? It works just not in tests.
340+
it('should remove only one \\t at start of the current line', function(){
341+
element.val('\t\t<p><a>Test Contents</a><img/></p>\n\t\t<p><a>Test Contents</a><img/></p>');
342+
triggerEvent('keydown', element, {keyCode: 9, shiftKey: true});
343+
expect(element.val()).toBe('\t<p><a>Test Contents</a><img/></p>\n\t\t<p><a>Test Contents</a><img/></p>');
344+
});
345+
});
346+
});
306347
});

test/taSanitize.spec.js

+62
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,68 @@ describe('taSanitize', function(){
3737
}));
3838
});
3939

40+
describe('clears out unnecessary &#10; &#9;', function(){
41+
it('at start both', inject(function(taSanitize){
42+
var result = taSanitize('<p>&#10;&#9;Test Test 2</p>', 'safe');
43+
expect(result).toBe('<p>Test Test 2</p>');
44+
}));
45+
46+
it('at start &#10;', inject(function(taSanitize){
47+
var result = taSanitize('<p>&#10;Test Test 2</p>', 'safe');
48+
expect(result).toBe('<p>Test Test 2</p>');
49+
}));
50+
51+
it('at start &#9;', inject(function(taSanitize){
52+
var result = taSanitize('<p>&#9;Test Test 2</p>', 'safe');
53+
expect(result).toBe('<p>Test Test 2</p>');
54+
}));
55+
56+
it('at middle both', inject(function(taSanitize){
57+
var result = taSanitize('<p>Test &#10;&#9;Test 2</p>', 'safe');
58+
expect(result).toBe('<p>Test Test 2</p>');
59+
}));
60+
61+
it('at middle &#10;', inject(function(taSanitize){
62+
var result = taSanitize('<p>Test &#10;Test 2</p>', 'safe');
63+
expect(result).toBe('<p>Test Test 2</p>');
64+
}));
65+
66+
it('at middle &#9;', inject(function(taSanitize){
67+
var result = taSanitize('<p>Test &#9;Test 2</p>', 'safe');
68+
expect(result).toBe('<p>Test Test 2</p>');
69+
}));
70+
71+
it('at end both', inject(function(taSanitize){
72+
var result = taSanitize('<p>Test Test 2&#10;&#9;</p>', 'safe');
73+
expect(result).toBe('<p>Test Test 2</p>');
74+
}));
75+
76+
it('at end &#10;', inject(function(taSanitize){
77+
var result = taSanitize('<p>Test Test 2&#10;</p>', 'safe');
78+
expect(result).toBe('<p>Test Test 2</p>');
79+
}));
80+
81+
it('at end &#9;', inject(function(taSanitize){
82+
var result = taSanitize('<p>Test Test 2&#9;</p>', 'safe');
83+
expect(result).toBe('<p>Test Test 2</p>');
84+
}));
85+
86+
it('combination', inject(function(taSanitize){
87+
var result = taSanitize('<p>&#10;Test &#10; &#9;Test 2&#10;&#9;</p>', 'safe');
88+
expect(result).toBe('<p>Test Test 2</p>');
89+
}));
90+
91+
it('leaves them inbetween <pre> tags', inject(function(taSanitize){
92+
var result = taSanitize('<pre>&#9;Test &#10; &#9;Test 2&#10;&#9;</pre>', 'safe');
93+
expect(result).toBe('<pre>&#9;Test &#10; &#9;Test 2&#10;&#9;</pre>');
94+
}));
95+
96+
it('correctly handles a mixture', inject(function(taSanitize){
97+
var result = taSanitize('<p>&#10;Test &#10; &#9;Test 2&#10;&#9;</p><pre>&#9;Test &#10; &#9;Test 2&#10;&#9;</pre>', 'safe');
98+
expect(result).toBe('<p>Test Test 2</p><pre>&#9;Test &#10; &#9;Test 2&#10;&#9;</pre>');
99+
}));
100+
});
101+
40102
describe('only certain style attributes are allowed', function(){
41103
describe('validated color attribute', function(){
42104
it('name', inject(function(taSanitize){

test/textAngular.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ describe('textAngular', function(){
608608
element.append('<bad-tag>Test 2 Content</bad-tag>');
609609
element.triggerHandler('keyup');
610610
$rootScope.$digest();
611-
expect(element2.val()).toBe('<p>Test Contents</p><bad-tag>Test 2 Content</bad-tag>');
611+
expect(element2.val()).toBe('<p>Test Contents</p>\n<bad-tag>Test 2 Content</bad-tag>');
612612
});
613613

614614
it('not allow malformed html', function () {

0 commit comments

Comments
 (0)