');
237 | });
238 |
239 | describe('explicitly disallow', function() {
240 | it('should not allow attributes', function() {
241 | writer.start('div', {id:'a', name:'a', style:'a'});
242 | expect(html).toEqual('
');
243 | });
244 |
245 | it('should not allow tags', function() {
246 | function tag(name) {
247 | writer.start(name, {});
248 | writer.end(name);
249 | }
250 | tag('frameset');
251 | tag('frame');
252 | tag('form');
253 | tag('param');
254 | tag('object');
255 | tag('embed');
256 | tag('textarea');
257 | tag('input');
258 | tag('button');
259 | tag('option');
260 | tag('select');
261 | tag('script');
262 | tag('style');
263 | tag('link');
264 | tag('base');
265 | tag('basefont');
266 | expect(html).toEqual('');
267 | });
268 | });
269 |
270 | describe('uri validation', function() {
271 | it('should call the uri validator', function() {
272 | writer.start('a', {href:'someUrl'}, false);
273 | expect(uriValidator).toHaveBeenCalledWith('someUrl', false);
274 | uriValidator.reset();
275 | writer.start('img', {src:'someImgUrl'}, false);
276 | expect(uriValidator).toHaveBeenCalledWith('someImgUrl', true);
277 | uriValidator.reset();
278 | writer.start('someTag', {src:'someNonUrl'}, false);
279 | expect(uriValidator).not.toHaveBeenCalled();
280 | });
281 |
282 | it('should drop non valid uri attributes', function() {
283 | uriValidator.andReturn(false);
284 | writer.start('a', {href:'someUrl'}, false);
285 | expect(html).toEqual('
');
286 |
287 | html = '';
288 | uriValidator.andReturn(true);
289 | writer.start('a', {href:'someUrl'}, false);
290 | expect(html).toEqual(' ');
291 | });
292 | });
293 | });
294 |
295 | describe('uri checking', function() {
296 | beforeEach(function() {
297 | this.addMatchers({
298 | toBeValidUrl: function() {
299 | var sanitize;
300 | inject(function($sanitize) {
301 | sanitize = $sanitize;
302 | });
303 | var input = ' ';
304 | return sanitize(input) === input;
305 | },
306 | toBeValidImageSrc: function() {
307 | var sanitize;
308 | inject(function($sanitize) {
309 | sanitize = $sanitize;
310 | });
311 | var input = '
';
312 | return sanitize(input) === input;
313 | }
314 | });
315 | });
316 |
317 | it('should use $$sanitizeUri for links', function() {
318 | var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri');
319 | module(function($provide) {
320 | $provide.value('$$sanitizeUri', $$sanitizeUri);
321 | });
322 | inject(function() {
323 | $$sanitizeUri.andReturn('someUri');
324 |
325 | expectHTML('
').toEqual('
');
326 | expect($$sanitizeUri).toHaveBeenCalledWith('someUri', false);
327 |
328 | $$sanitizeUri.andReturn('unsafe:someUri');
329 | expectHTML('
').toEqual('
');
330 | });
331 | });
332 |
333 | it('should use $$sanitizeUri for links', function() {
334 | var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri');
335 | module(function($provide) {
336 | $provide.value('$$sanitizeUri', $$sanitizeUri);
337 | });
338 | inject(function() {
339 | $$sanitizeUri.andReturn('someUri');
340 |
341 | expectHTML('
').toEqual('
');
342 | expect($$sanitizeUri).toHaveBeenCalledWith('someUri', true);
343 |
344 | $$sanitizeUri.andReturn('unsafe:someUri');
345 | expectHTML('
').toEqual('
');
346 | });
347 | });
348 |
349 | it('should be URI', function() {
350 | expect('').toBeValidUrl();
351 | expect('http://abc').toBeValidUrl();
352 | expect('HTTP://abc').toBeValidUrl();
353 | expect('https://abc').toBeValidUrl();
354 | expect('HTTPS://abc').toBeValidUrl();
355 | expect('ftp://abc').toBeValidUrl();
356 | expect('FTP://abc').toBeValidUrl();
357 | expect('mailto:me@example.com').toBeValidUrl();
358 | expect('MAILTO:me@example.com').toBeValidUrl();
359 | expect('tel:123-123-1234').toBeValidUrl();
360 | expect('TEL:123-123-1234').toBeValidUrl();
361 | expect('#anchor').toBeValidUrl();
362 | expect('/page1.md').toBeValidUrl();
363 | });
364 |
365 | it('should not be URI', function() {
366 | expect('javascript:alert').not.toBeValidUrl();
367 | });
368 |
369 | describe('javascript URLs', function() {
370 | it('should ignore javascript:', function() {
371 | expect('JavaScript:abc').not.toBeValidUrl();
372 | expect(' \n Java\n Script:abc').not.toBeValidUrl();
373 | expect('http://JavaScript/my.js').toBeValidUrl();
374 | });
375 |
376 | it('should ignore dec encoded javascript:', function() {
377 | expect('javascript:').not.toBeValidUrl();
378 | expect('javascript:').not.toBeValidUrl();
379 | expect('j avascript:').not.toBeValidUrl();
380 | });
381 |
382 | it('should ignore decimal with leading 0 encodede javascript:', function() {
383 | expect('javascript:').not.toBeValidUrl();
384 | expect('j avascript:').not.toBeValidUrl();
385 | expect('j avascript:').not.toBeValidUrl();
386 | });
387 |
388 | it('should ignore hex encoded javascript:', function() {
389 | expect('javascript:').not.toBeValidUrl();
390 | expect('javascript:').not.toBeValidUrl();
391 | expect('j avascript:').not.toBeValidUrl();
392 | });
393 |
394 | it('should ignore hex encoded whitespace javascript:', function() {
395 | expect('jav ascript:alert();').not.toBeValidUrl();
396 | expect('jav
ascript:alert();').not.toBeValidUrl();
397 | expect('jav
ascript:alert();').not.toBeValidUrl();
398 | expect('jav\u0000ascript:alert();').not.toBeValidUrl();
399 | expect('java\u0000\u0000script:alert();').not.toBeValidUrl();
400 | expect(' java\u0000\u0000script:alert();').not.toBeValidUrl();
401 | });
402 | });
403 | });
404 | });
--------------------------------------------------------------------------------
/test/textAngularManager.spec.js:
--------------------------------------------------------------------------------
1 | describe('textAngularManager', function(){
2 | 'use strict';
3 | beforeEach(module('textAngular'));
4 |
5 | describe('toolbar', function(){
6 | describe('registration', function(){
7 | it('should require a scope object', inject(function(textAngularManager){
8 | expect(textAngularManager.registerToolbar).toThrow("textAngular Error: A toolbar requires a scope");
9 | }));
10 |
11 | it('should require a name', inject(function(textAngularManager){
12 | expect(function(){textAngularManager.registerToolbar({});}).toThrow("textAngular Error: A toolbar requires a name");
13 | expect(function(){textAngularManager.registerToolbar({name: ''});}).toThrow("textAngular Error: A toolbar requires a name");
14 | }));
15 |
16 | it('should require a unique name', inject(function(textAngularManager){
17 | textAngularManager.registerToolbar({name: 'test'});
18 | expect(function(){textAngularManager.registerToolbar({name: 'test'});}).toThrow('textAngular Error: A toolbar with name "test" already exists');
19 | }));
20 | });
21 |
22 | describe('retrieval', function(){
23 | it('should be undefined for no registered toolbar', inject(function(textAngularManager){
24 | expect(textAngularManager.retrieveToolbar('test')).toBeUndefined();
25 | }));
26 |
27 | it('should get the correct toolbar', inject(function(textAngularManager){
28 | var scope = {name: 'test'};
29 | textAngularManager.registerToolbar(scope);
30 | expect(textAngularManager.retrieveToolbar('test')).toBe(scope);
31 | }));
32 |
33 | it('should get the correct toolbar via editor', inject(function(textAngularManager){
34 | var scope = {name: 'test'};
35 | textAngularManager.registerToolbar(scope);
36 | textAngularManager.registerEditor('testeditor', {}, ['test']);
37 | expect(textAngularManager.retrieveToolbarsViaEditor('testeditor')[0]).toBe(scope);
38 | }));
39 | });
40 |
41 | describe('unregister', function(){
42 | it('should get the correct toolbar', inject(function(textAngularManager){
43 | textAngularManager.registerToolbar({name: 'test'});
44 | textAngularManager.unregisterToolbar('test');
45 | expect(textAngularManager.retrieveToolbar('test')).toBeUndefined();
46 | }));
47 | });
48 |
49 | describe('modification', function(){
50 | var $rootScope, toolbar1, toolbar2, textAngularManager;
51 | beforeEach(inject(function(_textAngularManager_){
52 | textAngularManager = _textAngularManager_;
53 | }));
54 | beforeEach(inject(function (_$compile_, _$rootScope_) {
55 | $rootScope = _$rootScope_;
56 | toolbar1 = _$compile_('
')($rootScope);
57 | toolbar2 = _$compile_('
')($rootScope);
58 | $rootScope.$digest();
59 | }));
60 |
61 | describe('throws error on no toolbar', function(){
62 | it('when update tool', function(){
63 | expect(function(){
64 | textAngularManager.updateToolbarToolDisplay('test', 'h1', {iconclass: 'test-icon-class'});
65 | }).toThrow('textAngular Error: No Toolbar with name "test" exists');
66 | });
67 | it('when reset tool', function(){
68 | expect(function(){
69 | textAngularManager.resetToolbarToolDisplay('test', 'h1');
70 | }).toThrow('textAngular Error: No Toolbar with name "test" exists');
71 | });
72 | });
73 |
74 | describe('single toolbar', function(){
75 | // we test these by adding an icon with a specific class and then testing for it's existance
76 | it('should update only one button on one toolbar', function(){
77 | textAngularManager.updateToolbarToolDisplay('test1', 'h1', {iconclass: 'test-icon-class'});
78 | expect(jQuery('i.test-icon-class', toolbar1).length).toBe(1);
79 | expect(jQuery('i.test-icon-class', toolbar2).length).toBe(0);
80 | });
81 | it('should reset one toolbar button on one toolbar', function(){
82 | textAngularManager.updateToolbarToolDisplay('test1', 'h1', {iconclass: 'test-icon-class'});
83 | textAngularManager.updateToolbarToolDisplay('test1', 'h2', {iconclass: 'test-icon-class2'});
84 | textAngularManager.resetToolbarToolDisplay('test1', 'h1');
85 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar1).length).toBe(0);
86 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar2).length).toBe(0);
87 | expect(jQuery('[name="h2"] i.test-icon-class2', toolbar1).length).toBe(1);
88 | });
89 | });
90 | describe('multi toolbar', function(){
91 | it('should update only one button on multiple toolbars', function(){
92 | textAngularManager.updateToolDisplay('h1', {iconclass: 'test-icon-class'});
93 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar1).length).toBe(1);
94 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar2).length).toBe(1);
95 | });
96 | it('should reset one toolbar button', function(){
97 | textAngularManager.updateToolDisplay('h1', {iconclass: 'test-icon-class'});
98 | textAngularManager.updateToolDisplay('h2', {iconclass: 'test-icon-class2'});
99 | textAngularManager.resetToolDisplay('h1');
100 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar1).length).toBe(0);
101 | expect(jQuery('[name="h1"] i.test-icon-class', toolbar2).length).toBe(0);
102 | expect(jQuery('[name="h2"] i.test-icon-class2', toolbar1).length).toBe(1);
103 | });
104 | it('should update multiple buttons on multiple toolbars', function(){
105 | textAngularManager.updateToolsDisplay({'h1': {iconclass: 'test-icon-class'},'h2': {iconclass: 'test-icon-class2'}});
106 | expect(jQuery('[name="h1"] i.test-icon-class, [name="h2"] i.test-icon-class2', toolbar1).length).toBe(2);
107 | expect(jQuery('[name="h1"] i.test-icon-class, [name="h2"] i.test-icon-class2', toolbar2).length).toBe(2);
108 | });
109 | it('should reset all toolbar buttons', function(){
110 | textAngularManager.updateToolsDisplay({'h1': {iconclass: 'test-icon-class'},'h2': {iconclass: 'test-icon-class2'}});
111 | textAngularManager.resetToolsDisplay();
112 | expect(jQuery('[name="h1"] i.test-icon-class, [name="h2"] i.test-icon-class2', toolbar1).length).toBe(0);
113 | expect(jQuery('[name="h1"] i.test-icon-class, [name="h2"] i.test-icon-class2', toolbar2).length).toBe(0);
114 | });
115 | });
116 | });
117 | });
118 |
119 | describe('editor', function(){
120 | describe('registration', function(){
121 | it('should require a name', inject(function(textAngularManager){
122 | expect(textAngularManager.registerEditor).toThrow("textAngular Error: An editor requires a name");
123 | expect(function(){textAngularManager.registerEditor('');}).toThrow("textAngular Error: An editor requires a name");
124 | }));
125 |
126 | it('should require a scope object', inject(function(textAngularManager){
127 | expect(function(){textAngularManager.registerEditor('test');}).toThrow("textAngular Error: An editor requires a scope");
128 | }));
129 |
130 | it('should require a unique name', inject(function(textAngularManager){
131 | textAngularManager.registerEditor('test', {});
132 | expect(function(){textAngularManager.registerEditor('test', {});}).toThrow('textAngular Error: An Editor with name "test" already exists');
133 | }));
134 |
135 | it('should return a disable function', inject(function(textAngularManager){
136 | expect(textAngularManager.registerEditor('test', {}).disable).toBeDefined();
137 | }));
138 |
139 | it('should return a enable function', inject(function(textAngularManager){
140 | expect(textAngularManager.registerEditor('test', {}).enable).toBeDefined();
141 | }));
142 |
143 | it('should return a focus function', inject(function(textAngularManager){
144 | expect(textAngularManager.registerEditor('test', {}).focus).toBeDefined();
145 | }));
146 |
147 | it('should return a unfocus function', inject(function(textAngularManager){
148 | expect(textAngularManager.registerEditor('test', {}).unfocus).toBeDefined();
149 | }));
150 |
151 | it('should return a updateSelectedStyles function', inject(function(textAngularManager){
152 | expect(textAngularManager.registerEditor('test', {}).updateSelectedStyles).toBeDefined();
153 | }));
154 | });
155 |
156 | describe('retrieval', function(){
157 | it('should be undefined for no registered editor', inject(function(textAngularManager){
158 | expect(textAngularManager.retrieveEditor('test')).toBeUndefined();
159 | }));
160 |
161 | it('should get the correct editor', inject(function(textAngularManager){
162 | var scope = {};
163 | textAngularManager.registerEditor('test', scope);
164 | expect(textAngularManager.retrieveEditor('test').scope).toBe(scope);
165 | }));
166 | });
167 |
168 | describe('unregister', function(){
169 | it('should get the correct editor', inject(function(textAngularManager){
170 | textAngularManager.registerEditor('test', {});
171 | textAngularManager.unregisterEditor('test');
172 | expect(textAngularManager.retrieveEditor('test')).toBeUndefined();
173 | }));
174 | });
175 |
176 | describe('interacting', function(){
177 | var $rootScope, textAngularManager, editorFuncs, testbar1, testbar2, testbar3;
178 | var editorScope = {};
179 | beforeEach(inject(function(_textAngularManager_){
180 | textAngularManager = _textAngularManager_;
181 | }));
182 |
183 | describe('active state', function(){
184 | beforeEach(inject(function (_$rootScope_) {
185 | $rootScope = _$rootScope_;
186 | textAngularManager.registerToolbar((testbar1 = {name: 'testbar1', disabled: true}));
187 | textAngularManager.registerToolbar((testbar2 = {name: 'testbar2', disabled: true}));
188 | textAngularManager.registerToolbar((testbar3 = {name: 'testbar3', disabled: true}));
189 | editorFuncs = textAngularManager.registerEditor('test', editorScope, ['testbar1','testbar2']);
190 | $rootScope.$digest();
191 | }));
192 | describe('focus', function(){
193 | beforeEach(function(){
194 | editorFuncs.focus();
195 | $rootScope.$digest();
196 | });
197 | it('should set disabled to false on toolbars', function(){
198 | expect(!testbar1.disabled);
199 | expect(!testbar2.disabled);
200 | expect(testbar3.disabled);
201 | });
202 | it('should set the active editor to the editor', function(){
203 | expect(testbar1._parent).toBe(editorScope);
204 | expect(testbar2._parent).toBe(editorScope);
205 | expect(testbar3._parent).toNotBe(editorScope);
206 | });
207 | });
208 | describe('unfocus', function(){
209 | beforeEach(function(){
210 | editorFuncs.unfocus();
211 | $rootScope.$digest();
212 | });
213 | it('should set disabled to false on toolbars', function(){
214 | expect(testbar1.disabled);
215 | expect(testbar2.disabled);
216 | expect(!testbar3.disabled);
217 | });
218 | });
219 | describe('disable', function(){
220 | beforeEach(function(){
221 | editorFuncs.disable();
222 | $rootScope.$digest();
223 | });
224 | it('should set disabled to false on toolbars', function(){
225 | expect(testbar1.disabled).toBe(true);
226 | expect(testbar2.disabled).toBe(true);
227 | expect(testbar3.disabled).toBe(true);
228 | });
229 | });
230 | describe('enable', function(){
231 | beforeEach(function(){
232 | editorFuncs.disable();
233 | $rootScope.$digest();
234 | editorFuncs.enable();
235 | $rootScope.$digest();
236 | });
237 | it('should set disabled to false on toolbars', function(){
238 | expect(testbar1.disabled).toBe(false);
239 | expect(testbar2.disabled).toBe(false);
240 | expect(testbar3.disabled).toBe(true);
241 | });
242 | });
243 | });
244 |
245 | describe('actions passthrough', function(){
246 | var editorScope, element;
247 | beforeEach(inject(function(taRegisterTool, taOptions, _$rootScope_, _$compile_){
248 | // add a tool that is ALLWAYS active
249 | taRegisterTool('activeonrangyrange', {
250 | buttontext: 'Active On Rangy Rangy',
251 | action: function(){
252 | return this.$element.attr('hit-this', 'true');
253 | },
254 | commandKeyCode: 21,
255 | activeState: function(rangyrange){ return rangyrange !== undefined; }
256 | });
257 | taRegisterTool('inactiveonrangyrange', {
258 | buttontext: 'Inactive On Rangy Rangy',
259 | action: function(){
260 | return this.$element.attr('hit-this', 'true');
261 | },
262 | commandKeyCode: 23,
263 | activeState: function(rangyrange){ return rangyrange === undefined; }
264 | });
265 | taRegisterTool('noactivestate', {
266 | buttontext: 'Shouldnt error, Shouldnt be active either',
267 | action: function(){
268 | return this.$element.attr('hit-this', 'true');
269 | }
270 | });
271 | taOptions.toolbar = [['noactivestate','activeonrangyrange','inactiveonrangyrange']];
272 | $rootScope = _$rootScope_;
273 | element = _$compile_('
Test Content
')($rootScope);
274 | $rootScope.$digest();
275 | editorScope = textAngularManager.retrieveEditor('test');
276 | }));
277 | describe('updateSelectedStyles', function(){
278 | describe('should activate buttons correctly', function(){
279 | it('without rangyrange passed through', function(){
280 | editorScope.editorFunctions.updateSelectedStyles();
281 | $rootScope.$digest();
282 | expect(element.find('.ta-toolbar button.active').length).toBe(1);
283 | });
284 | it('with rangyrange passed through', function(){
285 | editorScope.editorFunctions.updateSelectedStyles({});
286 | $rootScope.$digest();
287 | expect(element.find('.ta-toolbar button.active').length).toBe(1);
288 | });
289 | });
290 | });
291 |
292 | describe('sendKeyCommand', function(){
293 | it('should return true if there is a relevantCommandKeyCode on a tool', function(){
294 | expect(editorScope.editorFunctions.sendKeyCommand({metaKey: true, which: 21})).toBe(true);
295 | });
296 |
297 | it('should call the action of the specified tool', function(){
298 | editorScope.editorFunctions.sendKeyCommand({metaKey: true, which: 21});
299 | $rootScope.$digest();
300 | expect(element.find('.ta-toolbar button[name=activeonrangyrange]').attr('hit-this')).toBe('true');
301 | });
302 |
303 | it('should react only when modifiers present', function(){
304 | editorScope.editorFunctions.sendKeyCommand({which: 21});
305 | $rootScope.$digest();
306 | expect(element.find('.ta-toolbar button[name=activeonrangyrange]').attr('hit-this')).toBeUndefined();
307 | });
308 |
309 | it('should react to metaKey', function(){
310 | editorScope.editorFunctions.sendKeyCommand({metaKey: true, which: 21});
311 | $rootScope.$digest();
312 | expect(element.find('.ta-toolbar button[name=activeonrangyrange]').attr('hit-this')).toBe('true');
313 | });
314 |
315 | it('should react to ctrlKey', function(){
316 | editorScope.editorFunctions.sendKeyCommand({ctrlKey: true, which: 21});
317 | $rootScope.$digest();
318 | expect(element.find('.ta-toolbar button[name=activeonrangyrange]').attr('hit-this')).toBe('true');
319 | });
320 | });
321 | });
322 | });
323 |
324 | describe('linking toolbar to existing editor', function(){
325 | it('should link when referenced', inject(function(textAngularManager) {
326 | var scope = {name: 'test'};
327 | textAngularManager.registerEditor('testeditor', {}, ['test']);
328 | textAngularManager.registerToolbar(scope);
329 | expect(textAngularManager.retrieveToolbarsViaEditor('testeditor')[0]).toBe(scope);
330 | }));
331 | it('should not link when not referenced', inject(function(textAngularManager) {
332 | var scope = {name: 'test'};
333 | textAngularManager.registerEditor('testeditor', {}, []);
334 | textAngularManager.registerToolbar(scope);
335 | expect(textAngularManager.retrieveToolbarsViaEditor('testeditor').length).toBe(0);
336 | }));
337 | });
338 |
339 | describe('updating', function(){
340 | var $rootScope, element;
341 | beforeEach(inject(function (_$compile_, _$rootScope_) {
342 | $rootScope = _$rootScope_;
343 | $rootScope.htmlcontent = '
Test Content
';
344 | element = _$compile_('
')($rootScope);
345 | $rootScope.$digest();
346 | }));
347 | it('should throw error for named editor that doesn\'t exist', inject(function(textAngularManager){
348 | expect(function(){textAngularManager.refreshEditor('non-editor');}).toThrow('textAngular Error: No Editor with name "non-editor" exists');
349 | }));
350 | it('should update from text view to model', inject(function(textAngularManager){
351 | jQuery('.ta-text', element).append('
Test 2 Content
');
352 | textAngularManager.refreshEditor('test');
353 | expect($rootScope.htmlcontent).toBe('
Test Content
Test 2 Content
');
354 | }));
355 | });
356 | });
357 | });
--------------------------------------------------------------------------------
/bootstrap-colorpicker-module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('colorpicker.module', [])
4 | .factory('Helper', function () {
5 | return {
6 | closestSlider: function (elem) {
7 | var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
8 | if (matchesSelector.bind(elem)('I')) {
9 | return elem.parentNode;
10 | }
11 | return elem;
12 | },
13 | getOffset: function (elem) {
14 | var
15 | x = 0,
16 | y = 0,
17 | scrollX = 0,
18 | scrollY = 0;
19 | while (elem && !isNaN(elem.offsetLeft) && !isNaN(elem.offsetTop)) {
20 | x += elem.offsetLeft;
21 | y += elem.offsetTop;
22 | scrollX += elem.scrollLeft;
23 | scrollY += elem.scrollTop;
24 | elem = elem.offsetParent;
25 | }
26 | return {
27 | top: y,
28 | left: x,
29 | scrollX: scrollX,
30 | scrollY: scrollY
31 | };
32 | },
33 | // a set of RE's that can match strings and generate color tuples. https://github.com/jquery/jquery-color/
34 | stringParsers: [
35 | {
36 | re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
37 | parse: function (execResult) {
38 | return [
39 | execResult[1],
40 | execResult[2],
41 | execResult[3],
42 | execResult[4]
43 | ];
44 | }
45 | },
46 | {
47 | re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,
48 | parse: function (execResult) {
49 | return [
50 | 2.55 * execResult[1],
51 | 2.55 * execResult[2],
52 | 2.55 * execResult[3],
53 | execResult[4]
54 | ];
55 | }
56 | },
57 | {
58 | re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,
59 | parse: function (execResult) {
60 | return [
61 | parseInt(execResult[1], 16),
62 | parseInt(execResult[2], 16),
63 | parseInt(execResult[3], 16)
64 | ];
65 | }
66 | },
67 | {
68 | re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/,
69 | parse: function (execResult) {
70 | return [
71 | parseInt(execResult[1] + execResult[1], 16),
72 | parseInt(execResult[2] + execResult[2], 16),
73 | parseInt(execResult[3] + execResult[3], 16)
74 | ];
75 | }
76 | }
77 | ]
78 | };
79 | })
80 | .factory('Color', ['Helper', function (Helper) {
81 | return {
82 | value: {
83 | h: 1,
84 | s: 1,
85 | b: 1,
86 | a: 1
87 | },
88 | // translate a format from Color object to a string
89 | 'rgb': function () {
90 | var rgb = this.toRGB();
91 | return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
92 | },
93 | 'rgba': function () {
94 | var rgb = this.toRGB();
95 | return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + rgb.a + ')';
96 | },
97 | 'hex': function () {
98 | return this.toHex();
99 | },
100 |
101 | // HSBtoRGB from RaphaelJS
102 | RGBtoHSB: function (r, g, b, a) {
103 | r /= 255;
104 | g /= 255;
105 | b /= 255;
106 |
107 | var H, S, V, C;
108 | V = Math.max(r, g, b);
109 | C = V - Math.min(r, g, b);
110 | H = (C === 0 ? null :
111 | V == r ? (g - b) / C :
112 | V == g ? (b - r) / C + 2 :
113 | (r - g) / C + 4
114 | );
115 | H = ((H + 360) % 6) * 60 / 360;
116 | S = C === 0 ? 0 : C / V;
117 | return {h: H || 1, s: S, b: V, a: a || 1};
118 | },
119 |
120 | //parse a string to HSB
121 | setColor: function (val) {
122 | val = val.toLowerCase();
123 | for (var key in Helper.stringParsers) {
124 | if (Helper.stringParsers.hasOwnProperty(key)) {
125 | var parser = Helper.stringParsers[key];
126 | var match = parser.re.exec(val),
127 | values = match && parser.parse(match),
128 | space = parser.space || 'rgba';
129 | if (values) {
130 | this.value = this.RGBtoHSB.apply(null, values);
131 | return false;
132 | }
133 | }
134 | }
135 | },
136 |
137 | setHue: function (h) {
138 | this.value.h = 1 - h;
139 | },
140 |
141 | setSaturation: function (s) {
142 | this.value.s = s;
143 | },
144 |
145 | setLightness: function (b) {
146 | this.value.b = 1 - b;
147 | },
148 |
149 | setAlpha: function (a) {
150 | this.value.a = parseInt((1 - a) * 100, 10) / 100;
151 | },
152 |
153 | // HSBtoRGB from RaphaelJS
154 | // https://github.com/DmitryBaranovskiy/raphael/
155 | toRGB: function (h, s, b, a) {
156 | if (!h) {
157 | h = this.value.h;
158 | s = this.value.s;
159 | b = this.value.b;
160 | }
161 | h *= 360;
162 | var R, G, B, X, C;
163 | h = (h % 360) / 60;
164 | C = b * s;
165 | X = C * (1 - Math.abs(h % 2 - 1));
166 | R = G = B = b - C;
167 |
168 | h = ~~h;
169 | R += [C, X, 0, 0, X, C][h];
170 | G += [X, C, C, X, 0, 0][h];
171 | B += [0, 0, X, C, C, X][h];
172 | return {
173 | r: Math.round(R * 255),
174 | g: Math.round(G * 255),
175 | b: Math.round(B * 255),
176 | a: a || this.value.a
177 | };
178 | },
179 |
180 | toHex: function (h, s, b, a) {
181 | var rgb = this.toRGB(h, s, b, a);
182 | return '#' + ((1 << 24) | (parseInt(rgb.r, 10) << 16) | (parseInt(rgb.g, 10) << 8) | parseInt(rgb.b, 10)).toString(16).substr(1);
183 | }
184 | };
185 | }])
186 | .factory('Slider', ['Helper', function (Helper) {
187 | var
188 | slider = {
189 | maxLeft: 0,
190 | maxTop: 0,
191 | callLeft: null,
192 | callTop: null,
193 | knob: {
194 | top: 0,
195 | left: 0
196 | }
197 | },
198 | pointer = {};
199 |
200 | return {
201 | getSlider: function() {
202 | return slider;
203 | },
204 | getLeftPosition: function(event) {
205 | return Math.max(0, Math.min(slider.maxLeft, slider.left + ((event.pageX || pointer.left) - pointer.left)));
206 | },
207 | getTopPosition: function(event) {
208 | return Math.max(0, Math.min(slider.maxTop, slider.top + ((event.pageY || pointer.top) - pointer.top)));
209 | },
210 | setSlider: function (event) {
211 | var
212 | target = Helper.closestSlider(event.target),
213 | targetOffset = Helper.getOffset(target);
214 | slider.knob = target.children[0].style;
215 | slider.left = event.pageX - targetOffset.left - window.pageXOffset + targetOffset.scrollX;
216 | slider.top = event.pageY - targetOffset.top - window.pageYOffset + targetOffset.scrollY;
217 |
218 | pointer = {
219 | left: event.pageX,
220 | top: event.pageY
221 | };
222 | },
223 | setSaturation: function(event) {
224 | slider = {
225 | maxLeft: 100,
226 | maxTop: 100,
227 | callLeft: 'setSaturation',
228 | callTop: 'setLightness'
229 | };
230 | this.setSlider(event)
231 | },
232 | setHue: function(event) {
233 | slider = {
234 | maxLeft: 0,
235 | maxTop: 100,
236 | callLeft: false,
237 | callTop: 'setHue'
238 | };
239 | this.setSlider(event)
240 | },
241 | setAlpha: function(event) {
242 | slider = {
243 | maxLeft: 0,
244 | maxTop: 100,
245 | callLeft: false,
246 | callTop: 'setAlpha'
247 | };
248 | this.setSlider(event)
249 | },
250 | setKnob: function(top, left) {
251 | slider.knob.top = top + 'px';
252 | slider.knob.left = left + 'px';
253 | }
254 | };
255 | }])
256 | .directive('colorpicker', ['$document', '$compile', '$timeout', 'Color', 'Slider', 'Helper', function ($document, $compile, $timeout, Color, Slider, Helper) {
257 | return {
258 | require: '?ngModel',
259 | restrict: 'A',
260 | link: function ($scope, elem, attrs, ngModel) {
261 | var
262 | thisFormat = attrs.colorpicker ? attrs.colorpicker : 'hex',
263 | position = angular.isDefined(attrs.colorpickerPosition) ? attrs.colorpickerPosition : 'bottom',
264 | fixedPosition = angular.isDefined(attrs.colorpickerFixedPosition) ? attrs.colorpickerFixedPosition : false,
265 | target = angular.isDefined(attrs.colorpickerParent) ? elem.parent() : angular.element(document.body),
266 | withInput = angular.isDefined(attrs.colorpickerWithInput) ? attrs.colorpickerWithInput : false,
267 | textEditor = angular.isDefined(attrs.colorpickerTextEditor) ? attrs.colorpickerTextEditor : false,
268 | inputTemplate = setTemplate(),
269 | template =
270 | '
' +
271 | '' +
279 | '
',
280 | colorpickerTemplate = angular.element(template),
281 | pickerColor = Color,
282 | sliderAlpha,
283 | sliderHue = colorpickerTemplate.find('colorpicker-hue'),
284 | sliderSaturation = colorpickerTemplate.find('colorpicker-saturation'),
285 | colorpickerPreview = colorpickerTemplate.find('colorpicker-preview'),
286 | pickerColorPointers = colorpickerTemplate.find('i');
287 |
288 | // Build inputTemplate based on attrs values
289 | function setTemplate() {
290 | if (textEditor) {
291 | return '
' +
292 | '
{{selectedColor}}
' +
293 | '
Add HEX value
' +
294 | '
';
295 | } else if (withInput) {
296 | return '
';
297 | } else {
298 | return ''
299 | }
300 | }
301 |
302 | // Opens propmt to set color value when
303 | $scope.customColor = function() {
304 | var newColor;
305 | newColor = prompt('Please enter a HEX value', '#');
306 | if (newColor !== '') {
307 | elem.val(newColor);
308 |
309 | $scope.action(newColor);
310 | }
311 | };
312 |
313 | $compile(colorpickerTemplate)($scope);
314 |
315 | if (withInput) {
316 | var pickerColorInput = colorpickerTemplate.find('input');
317 | pickerColorInput
318 | .on('mousedown', function() {
319 | event.stopPropagation();
320 | })
321 | .on('keyup', function(event) {
322 | var newColor = this.value;
323 | elem.val(newColor);
324 | if(ngModel) {
325 | $scope.$apply(ngModel.$setViewValue(newColor));
326 | }
327 | event.stopPropagation();
328 | event.preventDefault();
329 | });
330 | elem.on('keyup', function() {
331 | pickerColorInput.val(elem.val());
332 | });
333 | }
334 |
335 | var bindMouseEvents = function() {
336 | $document.on('mousemove', mousemove);
337 | $document.on('mouseup', mouseup);
338 | };
339 |
340 | if (thisFormat === 'rgba') {
341 | colorpickerTemplate.addClass('alpha');
342 | sliderAlpha = colorpickerTemplate.find('colorpicker-alpha');
343 | sliderAlpha
344 | .on('click', function(event) {
345 | Slider.setAlpha(event);
346 | mousemove(event);
347 | })
348 | .on('mousedown', function(event) {
349 | Slider.setAlpha(event);
350 | bindMouseEvents();
351 | });
352 | }
353 |
354 | sliderHue
355 | .on('click', function(event) {
356 | Slider.setHue(event);
357 | mousemove(event);
358 | })
359 | .on('mousedown', function(event) {
360 | Slider.setHue(event);
361 | bindMouseEvents();
362 | });
363 |
364 | sliderSaturation
365 | .on('click', function(event) {
366 | Slider.setSaturation(event);
367 | mousemove(event);
368 | })
369 | .on('mousedown', function(event) {
370 | Slider.setSaturation(event);
371 | bindMouseEvents();
372 | });
373 |
374 | if (fixedPosition) {
375 | colorpickerTemplate.addClass('colorpicker-fixed-position');
376 | }
377 |
378 | colorpickerTemplate.addClass('colorpicker-position-' + position);
379 |
380 | target.append(colorpickerTemplate);
381 |
382 | if(ngModel) {
383 | ngModel.$render = function () {
384 | elem.val(ngModel.$viewValue);
385 | };
386 | $scope.$watch(attrs.ngModel, function() {
387 | update();
388 | });
389 | }
390 |
391 | elem.on('$destroy', function() {
392 | colorpickerTemplate.remove();
393 | });
394 |
395 | var previewColor = function () {
396 | try {
397 | colorpickerPreview.css('backgroundColor', pickerColor[thisFormat]());
398 | } catch (e) {
399 | colorpickerPreview.css('backgroundColor', pickerColor.toHex());
400 | }
401 | sliderSaturation.css('backgroundColor', pickerColor.toHex(pickerColor.value.h, 1, 1, 1));
402 | if (thisFormat === 'rgba') {
403 | sliderAlpha.css.backgroundColor = pickerColor.toHex();
404 | }
405 |
406 | if (textEditor) {
407 | $timeout(function() {
408 | $scope.selectedColor = pickerColor.toHex();
409 | });
410 | }
411 | };
412 |
413 | var mousemove = function (event) {
414 | var
415 | left = Slider.getLeftPosition(event),
416 | top = Slider.getTopPosition(event),
417 | slider = Slider.getSlider();
418 |
419 | Slider.setKnob(top, left);
420 |
421 | if (slider.callLeft) {
422 | pickerColor[slider.callLeft].call(pickerColor, left / 100);
423 | }
424 | if (slider.callTop) {
425 | pickerColor[slider.callTop].call(pickerColor, top / 100);
426 | }
427 | previewColor();
428 | var newColor = pickerColor[thisFormat]();
429 | elem.val(newColor);
430 | if(ngModel) {
431 | $scope.$apply(ngModel.$setViewValue(newColor));
432 | }
433 | if (withInput) {
434 | pickerColorInput.val(newColor);
435 | }
436 | if (textEditor) {
437 | $scope.selectedColor = newColor;
438 | }
439 | return false;
440 | };
441 |
442 | var mouseup = function () {
443 | $document.off('mousemove', mousemove);
444 | $document.off('mouseup', mouseup);
445 |
446 | if (textEditor) {
447 | $scope.action($scope.selectedColor);
448 | }
449 | };
450 |
451 | var update = function () {
452 | pickerColor.setColor(elem.val());
453 | pickerColorPointers.eq(0).css({
454 | left: pickerColor.value.s * 100 + 'px',
455 | top: 100 - pickerColor.value.b * 100 + 'px'
456 | });
457 | pickerColorPointers.eq(1).css('top', 100 * (1 - pickerColor.value.h) + 'px');
458 | pickerColorPointers.eq(2).css('top', 100 * (1 - pickerColor.value.a) + 'px');
459 | previewColor();
460 | };
461 |
462 | var getColorpickerTemplatePosition = function() {
463 | var
464 | positionValue,
465 | positionOffset = Helper.getOffset(elem[0]);
466 |
467 | if(angular.isDefined(attrs.colorpickerParent)) {
468 | positionOffset.left = 0;
469 | positionOffset.top = 0;
470 | }
471 |
472 | if (position === 'top') {
473 | positionValue = {
474 | 'top': positionOffset.top - 147,
475 | 'left': positionOffset.left
476 | };
477 | } else if (position === 'right') {
478 | positionValue = {
479 | 'top': positionOffset.top,
480 | 'left': positionOffset.left + 126
481 | };
482 | } else if (position === 'bottom') {
483 | positionValue = {
484 | 'top': positionOffset.top + elem[0].offsetHeight + 2,
485 | 'left': positionOffset.left
486 | };
487 | } else if (position === 'left') {
488 | positionValue = {
489 | 'top': positionOffset.top,
490 | 'left': positionOffset.left - 150
491 | };
492 | }
493 | return {
494 | 'top': positionValue.top + 'px',
495 | 'left': positionValue.left + 'px'
496 | };
497 | };
498 |
499 | elem.on('click', function () {
500 | update();
501 | colorpickerTemplate
502 | .addClass('colorpicker-visible')
503 | .css(getColorpickerTemplatePosition());
504 | });
505 |
506 | colorpickerTemplate.on('mousedown', function (event) {
507 | event.stopPropagation();
508 | event.preventDefault();
509 | });
510 |
511 | var hideColorpickerTemplate = function() {
512 | if (colorpickerTemplate.hasClass('colorpicker-visible')) {
513 | colorpickerTemplate.removeClass('colorpicker-visible');
514 | }
515 | };
516 |
517 | colorpickerTemplate.find('button').on('click', function () {
518 | hideColorpickerTemplate();
519 | });
520 |
521 | $document.on('mousedown', function () {
522 | hideColorpickerTemplate();
523 | });
524 | }
525 | };
526 | }]);
--------------------------------------------------------------------------------
/textAngular.min.js:
--------------------------------------------------------------------------------
1 | !function(){"Use Strict";function a(a,b){if(!a||""===a||c.hasOwnProperty(a))throw"textAngular Error: A unique name is required for a Tool Definition";if(b.display&&(""===b.display||0===angular.element(b.display).length)||!b.display&&!b.buttontext&&!b.iconclass)throw'textAngular Error: Tool Definition for "'+a+'" does not have a valid display/iconclass/buttontext value';c[a]=b}var b=angular.module("textAngular",["ngSanitize"]);b.value("taOptions",{toolbar:[["h1","h2","h3","h4","h5","h6","p","pre","quote"],["bold","italics","underline","ul","ol","redo","undo","clear"],["justifyLeft","justifyCenter","justifyRight"],["html","insertImage","insertLink","unlink"]],classes:{focussed:"focussed",toolbar:"btn-toolbar",toolbarGroup:"btn-group",toolbarButton:"btn btn-default",toolbarButtonActive:"active",disabled:"disabled",textEditor:"form-control",htmlEditor:"form-control"},setup:{textEditorSetup:function(){},htmlEditorSetup:function(){}}});var c={};b.constant("taRegisterTool",a),b.value("taTools",c),b.config(["taRegisterTool",function(a){angular.forEach(c,function(a,b){delete c[b]}),a("html",{buttontext:"Toggle HTML",action:function(){this.$editor().switchView()},activeState:function(){return this.$editor().showHtml}});var b=function(a){return function(){return this.$editor().queryFormatBlockState(a)}},d=function(){return this.$editor().wrapSelection("formatBlock","<"+this.name.toUpperCase()+">")};angular.forEach(["h1","h2","h3","h4","h5","h6"],function(c){a(c.toLowerCase(),{buttontext:c.toUpperCase(),action:d,activeState:b(c.toLowerCase())})}),a("p",{buttontext:"P",action:function(){return this.$editor().wrapSelection("formatBlock","
")},activeState:function(){return this.$editor().queryFormatBlockState("p")}}),a("pre",{buttontext:"pre",action:function(){return this.$editor().wrapSelection("formatBlock","
")},activeState:function(){return this.$editor().queryFormatBlockState("pre")}}),a("ul",{iconclass:"fa fa-list-ul",action:function(){return this.$editor().wrapSelection("insertUnorderedList",null)},activeState:function(){return document.queryCommandState("insertUnorderedList")}}),a("ol",{iconclass:"fa fa-list-ol",action:function(){return this.$editor().wrapSelection("insertOrderedList",null)},activeState:function(){return document.queryCommandState("insertOrderedList")}}),a("quote",{iconclass:"fa fa-quote-right",action:function(){return this.$editor().wrapSelection("formatBlock","")},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("undo",{iconclass:"fa fa-undo",action:function(){return this.$editor().wrapSelection("undo",null)}}),a("redo",{iconclass:"fa fa-repeat",action:function(){return this.$editor().wrapSelection("redo",null)}}),a("bold",{iconclass:"fa fa-bold",action:function(){return this.$editor().wrapSelection("bold",null)},activeState:function(){return document.queryCommandState("bold")},commandKeyCode:98}),a("justifyLeft",{iconclass:"fa fa-align-left",action:function(){return this.$editor().wrapSelection("justifyLeft",null)},activeState:function(a){var b=!1;return a&&(b="left"===a.css("text-align")||"left"===a.attr("align")||"right"!==a.css("text-align")&&"center"!==a.css("text-align")&&!document.queryCommandState("justifyRight")&&!document.queryCommandState("justifyCenter")),b=b||document.queryCommandState("justifyLeft")}}),a("justifyRight",{iconclass:"fa fa-align-right",action:function(){return this.$editor().wrapSelection("justifyRight",null)},activeState:function(a){var b=!1;return a&&(b="right"===a.css("text-align")),b=b||document.queryCommandState("justifyRight")}}),a("justifyCenter",{iconclass:"fa fa-align-center",action:function(){return this.$editor().wrapSelection("justifyCenter",null)},activeState:function(a){var b=!1;return a&&(b="center"===a.css("text-align")),b=b||document.queryCommandState("justifyCenter")}}),a("italics",{iconclass:"fa fa-italic",action:function(){return this.$editor().wrapSelection("italic",null)},activeState:function(){return document.queryCommandState("italic")},commandKeyCode:105}),a("underline",{iconclass:"fa fa-underline",action:function(){return this.$editor().wrapSelection("underline",null)},activeState:function(){return document.queryCommandState("underline")},commandKeyCode:117}),a("clear",{iconclass:"fa fa-ban",action:function(a,b){this.$editor().wrapSelection("removeFormat",null);var c=[];if(this.$window.rangy&&this.$window.rangy.getSelection&&1===(c=this.$window.rangy.getSelection().getAllRanges()).length){var d=angular.element(c[0].commonAncestorContainer),e=function(a){a=angular.element(a);var b=a;angular.forEach(a.children(),function(a){var c=angular.element("
");c.html(angular.element(a).html()),b.after(c),b=c}),a.remove()};angular.forEach(d.find("ul"),e),angular.forEach(d.find("ol"),e);var f=this.$editor(),g=function(a){a=angular.element(a),a[0]!==f.displayElements.text[0]&&a.removeAttr("class"),angular.forEach(a.children(),g)};angular.forEach(d,g),"li"!==d[0].tagName.toLowerCase()&&"ol"!==d[0].tagName.toLowerCase()&&"ul"!==d[0].tagName.toLowerCase()&&this.$editor().wrapSelection("formatBlock","")}else this.$editor().wrapSelection("formatBlock","
");b()}}),a("insertImage",{iconclass:"fa fa-picture-o",action:function(){var a;return a=prompt("Please enter an image URL to insert","http://"),""!==a&&"http://"!==a?this.$editor().wrapSelection("insertImage",a):void 0}}),a("insertLink",{iconclass:"fa fa-link",action:function(){var a;return a=prompt("Please enter an URL to insert","http://"),""!==a&&"http://"!==a?this.$editor().wrapSelection("createLink",a):void 0},activeState:function(a){return a?"A"===a[0].tagName:!1}}),a("unlink",{iconclass:"fa fa-unlink",action:function(){return this.$editor().wrapSelection("unlink",null)},activeState:function(a){return a?"A"===a[0].tagName:!1}})}]),b.directive("textAngular",["$compile","$timeout","taOptions","taSanitize","textAngularManager","$window",function(a,b,c,d,e,f){return{require:"?ngModel",scope:{},restrict:"EA",link:function(d,g,h,i){var j,k,l,m,n,o,p,q,r=Math.floor(1e16*Math.random()),s=h.name?h.name:"textAngularEditor"+r;angular.extend(d,angular.copy(c),{wrapSelection:function(a,b){try{document.execCommand(a,!1,b)}catch(c){}d.displayElements.text[0].focus()},showHtml:!1}),h.taFocussedClass&&(d.classes.focussed=h.taFocussedClass),h.taTextEditorClass&&(d.classes.textEditor=h.taTextEditorClass),h.taHtmlEditorClass&&(d.classes.htmlEditor=h.taHtmlEditorClass),h.taTextEditorSetup&&(d.setup.textEditorSetup=d.$parent.$eval(h.taTextEditorSetup)),h.taHtmlEditorSetup&&(d.setup.htmlEditorSetup=d.$parent.$eval(h.taHtmlEditorSetup)),p=g[0].innerHTML,g[0].innerHTML="",d.displayElements={forminput:angular.element(" "),html:angular.element(""),text:angular.element("
")},d.setup.htmlEditorSetup(d.displayElements.html),d.setup.textEditorSetup(d.displayElements.text),d.displayElements.html.attr({id:"taHtmlElement","ng-show":"showHtml","ta-bind":"ta-bind","ng-model":"html"}),d.displayElements.text.attr({id:"taTextElement",contentEditable:"true","ng-hide":"showHtml","ta-bind":"ta-bind","ng-model":"html"}),g.append(d.displayElements.text),g.append(d.displayElements.html),d.displayElements.forminput.attr("name",s),g.append(d.displayElements.forminput),h.tabindex&&(g.removeAttr("tabindex"),d.displayElements.text.attr("tabindex",h.tabindex),d.displayElements.html.attr("tabindex",h.tabindex)),h.placeholder&&(d.displayElements.text.attr("placeholder",h.placeholder),d.displayElements.html.attr("placeholder",h.placeholder)),h.taDisabled&&(d.displayElements.text.attr("ta-readonly","disabled"),d.displayElements.html.attr("ta-readonly","disabled"),d.disabled=d.$parent.$eval(h.taDisabled),d.$parent.$watch(h.taDisabled,function(a){d.disabled=a,d.disabled?g.addClass(d.classes.disabled):g.removeClass(d.classes.disabled)})),a(d.displayElements.text)(d),a(d.displayElements.html)(d),g.addClass("ta-root"),d.displayElements.text.addClass("ta-text ta-editor "+d.classes.textEditor),d.displayElements.html.addClass("ta-html ta-editor "+d.classes.textEditor),d._actionRunning=!1;var t=!1;if(d.startAction=function(){return d._actionRunning=!0,f.rangy&&f.rangy.saveSelection?(t=f.rangy.saveSelection(),function(){t&&f.rangy.restoreSelection(t)}):void 0},d.endAction=function(){d._actionRunning=!1,t&&f.rangy.removeMarkers(t),t=!1,d.updateSelectedStyles(),d.showHtml||d.updateTaBindtaTextElement()},n=function(){g.addClass(d.classes.focussed),q.focus()},d.displayElements.html.on("focus",n),d.displayElements.text.on("focus",n),o=function(a){return d._actionRunning||document.activeElement===d.displayElements.html[0]||document.activeElement===d.displayElements.text[0]||(g.removeClass(d.classes.focussed),q.unfocus(),b(function(){g.triggerHandler("blur")},0)),a.preventDefault(),!1},d.displayElements.html.on("blur",o),d.displayElements.text.on("blur",o),d.queryFormatBlockState=function(a){return a.toLowerCase()===document.queryCommandValue("formatBlock").toLowerCase()},d.switchView=function(){d.showHtml=!d.showHtml,d.showHtml?b(function(){return d.displayElements.html[0].focus()},100):b(function(){return d.displayElements.text[0].focus()},100)},h.ngModel){var u=!0;i.$render=function(){if(u){u=!1;var a=d.$parent.$eval(h.ngModel);void 0!==a&&null!==a||!p||""===p||i.$setViewValue(p)}d.displayElements.forminput.val(i.$viewValue),document.activeElement!==d.displayElements.html[0]&&document.activeElement!==d.displayElements.text[0]&&(d.html=i.$viewValue||"")}}else d.displayElements.forminput.val(p),d.html=p;if(d.$watch("html",function(a,b){a!==b&&(h.ngModel&&i.$setViewValue(a),d.displayElements.forminput.val(a))}),h.taTargetToolbars)q=e.registerEditor(s,d,h.taTargetToolbars.split(","));else{var v=angular.element('');h.taToolbar&&v.attr("ta-toolbar",h.taToolbar),h.taToolbarClass&&v.attr("ta-toolbar-class",h.taToolbarClass),h.taToolbarGroupClass&&v.attr("ta-toolbar-group-class",h.taToolbarGroupClass),h.taToolbarButtonClass&&v.attr("ta-toolbar-button-class",h.taToolbarButtonClass),h.taToolbarActiveButtonClass&&v.attr("ta-toolbar-active-button-class",h.taToolbarActiveButtonClass),h.taFocussedClass&&v.attr("ta-focussed-class",h.taFocussedClass),g.prepend(v),a(v)(d.$parent),q=e.registerEditor(s,d,["textAngularToolbar"+r])}d.$on("$destroy",function(){e.unregisterEditor(s)}),d._bUpdateSelectedStyles=!1,d.updateSelectedStyles=function(){var a;f.rangy&&f.rangy.getSelection&&1===(a=f.rangy.getSelection().getAllRanges()).length&&a[0].commonAncestorContainer.parentNode!==d.displayElements.text[0]?q.updateSelectedStyles(angular.element(a[0].commonAncestorContainer.parentNode)):q.updateSelectedStyles(),d._bUpdateSelectedStyles&&b(d.updateSelectedStyles,200)},j=function(){d._bUpdateSelectedStyles||(d._bUpdateSelectedStyles=!0,d.$apply(function(){d.updateSelectedStyles()}))},d.displayElements.html.on("keydown",j),d.displayElements.text.on("keydown",j),k=function(){d._bUpdateSelectedStyles=!1},d.displayElements.html.on("keyup",k),d.displayElements.text.on("keyup",k),l=function(a){d.$apply(function(){return q.sendKeyCommand(a)?(d._bUpdateSelectedStyles||d.updateSelectedStyles(),a.preventDefault(),!1):void 0})},d.displayElements.html.on("keypress",l),d.displayElements.text.on("keypress",l),m=function(){d._bUpdateSelectedStyles=!1,d.$apply(function(){d.updateSelectedStyles()})},d.displayElements.html.on("mouseup",m),d.displayElements.text.on("mouseup",m)}}}]).directive("taBind",["taSanitize","$timeout","taFixChrome",function(a,b,c){return{require:"ngModel",scope:{},link:function(d,e,f,g){var h=void 0!==e.attr("contenteditable")&&e.attr("contenteditable"),i=h||"textarea"===e[0].tagName.toLowerCase()||"input"===e[0].tagName.toLowerCase(),j=!1,k=function(){if(h)return e[0].innerHTML;if(i)return e.val();throw"textAngular Error: attempting to update non-editable taBind"};d.$parent["updateTaBind"+(f.id||"")]=function(){j||g.$setViewValue(k())},i&&(e.on("paste cut",function(){j||b(function(){g.$setViewValue(k())},0)}),h?(e.on("keyup",function(){j||g.$setViewValue(k())}),e.on("blur",function(){var a=k();""===a&&e.attr("placeholder")&&e.addClass("placeholder-text"),j||g.$setViewValue(k()),g.$render()}),e.attr("placeholder")&&(e.addClass("placeholder-text"),e.on("focus",function(){e.removeClass("placeholder-text"),g.$render()}))):e.on("change blur",function(){j||g.$setViewValue(k())}));var l=function(b){return g.$oldViewValue=a(c(b),g.$oldViewValue)};g.$parsers.push(l),g.$formatters.push(l),g.$render=function(){if(document.activeElement!==e[0]){var a=g.$viewValue||"";h?(e[0].innerHTML=""===a&&e.attr("placeholder")&&e.hasClass("placeholder-text")?e.attr("placeholder"):a,j||e.find("a").on("click",function(a){return a.preventDefault(),!1})):"textarea"!==e[0].tagName.toLowerCase()&&"input"!==e[0].tagName.toLowerCase()?e[0].innerHTML=a:e.val(a)}},f.taReadonly&&(j=d.$parent.$eval(f.taReadonly),j?(("textarea"===e[0].tagName.toLowerCase()||"input"===e[0].tagName.toLowerCase())&&e.attr("disabled","disabled"),void 0!==e.attr("contenteditable")&&e.attr("contenteditable")&&e.removeAttr("contenteditable")):"textarea"===e[0].tagName.toLowerCase()||"input"===e[0].tagName.toLowerCase()?e.removeAttr("disabled"):h&&e.attr("contenteditable","true"),d.$parent.$watch(f.taReadonly,function(a,b){b!==a&&(a?(("textarea"===e[0].tagName.toLowerCase()||"input"===e[0].tagName.toLowerCase())&&e.attr("disabled","disabled"),void 0!==e.attr("contenteditable")&&e.attr("contenteditable")&&e.removeAttr("contenteditable")):"textarea"===e[0].tagName.toLowerCase()||"input"===e[0].tagName.toLowerCase()?e.removeAttr("disabled"):h&&e.attr("contenteditable","true"),j=a)}))}}}]).factory("taFixChrome",function(){var a=function(a){for(var b=angular.element("
"+a+"
"),c=angular.element(b).find("span"),d=0;d
0&&"BR"===e.next()[0].tagName&&e.next().remove(),e.replaceWith(e[0].innerHTML)))}var f=b[0].innerHTML.replace(/style="[^"]*?(line-height: 1.428571429;|color: inherit; line-height: 1.1;)[^"]*"/gi,"");return f!==b[0].innerHTML&&(b[0].innerHTML=f),b[0].innerHTML};return a}).factory("taSanitize",["$sanitize",function(a){function b(a,c){var d=[],e=a.children();return e.length&&angular.forEach(e,function(a){d=d.concat(b(angular.element(a),c))}),a.attr(c)&&d.push(a),d}return function(c,d){var e=angular.element(""+c+"
");angular.forEach(b(e,"align"),function(a){a.css("text-align",a.attr("align")),a.removeAttr("align")}),c=e[0].innerHTML;var f;try{f=a(c)}catch(g){f=d||""}return f}}]).directive("textAngularToolbar",["$compile","textAngularManager","taOptions","taTools","taToolExecuteAction","$window",function(a,b,c,d,e,f){return{scope:{name:"@"},restrict:"EA",link:function(g,h,i){if(!g.name||""===g.name)throw"textAngular Error: A toolbar requires a name";angular.extend(g,angular.copy(c)),i.taToolbar&&(g.toolbar=g.$parent.$eval(i.taToolbar)),i.taToolbarClass&&(g.classes.toolbar=i.taToolbarClass),i.taToolbarGroupClass&&(g.classes.toolbarGroup=i.taToolbarGroupClass),i.taToolbarButtonClass&&(g.classes.toolbarButton=i.taToolbarButtonClass),i.taToolbarActiveButtonClass&&(g.classes.toolbarButtonActive=i.taToolbarActiveButtonClass),i.taFocussedClass&&(g.classes.focussed=i.taFocussedClass),g.disabled=!0,g.focussed=!1,h[0].innerHTML="",h.addClass("ta-toolbar "+g.classes.toolbar),g.$watch("focussed",function(){g.focussed?h.addClass(g.classes.focussed):h.removeClass(g.classes.focussed)}),setupToolElement=function(b,c){var d;if(d=angular.element(b&&b.display?b.display:""),d.addClass(g.classes.toolbarButton),d.attr("name",c.name),d.attr("unselectable","on"),d.attr("ng-disabled","isDisabled()"),d.attr("tabindex","-1"),d.attr("ng-click","executeAction()"),d.attr("ng-class","displayActiveToolClass(active)"),d.on("mousedown",function(a){return a.preventDefault(),!1}),b&&!b.display&&!c._display&&(d[0].innerHTML="",b.buttontext&&(d[0].innerHTML=b.buttontext),b.iconclass)){var e=angular.element(""),f=d[0].innerHTML;e.addClass(b.iconclass),d[0].innerHTML="",d.append(e),f&&""!==f&&d.append(" "+f)}return c._lastToolDefinition=angular.copy(b),a(d)(c)},g.tools={},g._parent={disabled:!0,showHtml:!1,queryFormatBlockState:function(){return!1}};var j={$window:f,$editor:function(){return g._parent},isDisabled:function(){return this.$eval("disabled")||this.$eval("disabled()")||"html"!==this.name&&this.$editor().showHtml||this.$parent.disabled||this.$editor().disabled},displayActiveToolClass:function(a){return a?g.classes.toolbarButtonActive:""},executeAction:e};angular.forEach(g.toolbar,function(a){groupElement=angular.element(""),groupElement.addClass(g.classes.toolbarGroup),angular.forEach(a,function(a){g.tools[a]=angular.extend(g.$new(!0),d[a],j,{name:a}),g.tools[a].$element=setupToolElement(d[a],g.tools[a]),groupElement.append(g.tools[a].$element)}),h.append(groupElement)}),g.updateToolDisplay=function(a,b,c){var d=g.tools[a];if(d){if(d._lastToolDefinition&&!c&&(b=angular.extend({},d._lastToolDefinition,b)),null===b.buttontext&&null===b.iconclass&&null===b.display)throw'textAngular Error: Tool Definition for updating "'+a+'" does not have a valid display/iconclass/buttontext value';null===b.buttontext&&delete b.buttontext,null===b.iconclass&&delete b.iconclass,null===b.display&&delete b.display,toolElement=setupToolElement(b,d),d.$element.replaceWith(toolElement),d.$element=toolElement}},b.registerToolbar(g),g.$on("$destroy",function(){b.unregisterToolbar(g.name)})}}}]).service("taToolExecuteAction",["$q",function(a){return function(b){void 0!==b&&(this.$editor=function(){return b});var c=a.defer(),d=c.promise,e=this.$editor();d["finally"](function(){e.endAction.call(e)});var f;try{f=this.action(c,e.startAction())}catch(g){}(f||void 0===f)&&c.resolve()}}]).service("textAngularManager",["taToolExecuteAction",function(a){var b={},d={};return{registerEditor:function(e,f,g){if(!e||""===e)throw"textAngular Error: An editor requires a name";if(!f)throw"textAngular Error: An editor requires a scope";if(d[e])throw'textAngular Error: An Editor with name "'+e+'" already exists';var h=[];return angular.forEach(g,function(a){b[a]&&h.push(b[a])}),d[e]={scope:f,toolbars:g,_registerToolbar:function(a){this.toolbars.indexOf(a.name)>=0&&h.push(a)},editorFunctions:{disable:function(){angular.forEach(h,function(a){a.disabled=!0})},enable:function(){angular.forEach(h,function(a){a.disabled=!1})},focus:function(){angular.forEach(h,function(a){a._parent=f,a.disabled=!1,a.focussed=!0})},unfocus:function(){angular.forEach(h,function(a){a.disabled=!0,a.focussed=!1})},updateSelectedStyles:function(a){angular.forEach(h,function(b){angular.forEach(b.tools,function(b){b.activeState&&(b.active=b.activeState(a))})})},sendKeyCommand:function(b){var d=!1;return(b.ctrlKey||b.metaKey)&&angular.forEach(c,function(c,e){if(c.commandKeyCode&&c.commandKeyCode===b.which)for(var g=0;g
");
10 | },
11 | activeState: function(){ return true; }
12 | });
13 | taOptions.toolbar = [
14 | ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote', 'active'],
15 | ['bold', 'italics', 'underline', 'ul', 'ol', 'redo', 'undo', 'clear'],
16 | ['justifyLeft','justifyCenter','justifyRight'],
17 | ['html', 'insertImage', 'insertLink', 'unlink']
18 | ];
19 | }));
20 | describe('initiation', function(){
21 | describe('requires a name attribute', function(){
22 | it('errors when missing', inject(function($rootScope, $compile){
23 | expect(function(){
24 | $compile(' ')($rootScope);
25 | }).toThrow('textAngular Error: A toolbar requires a name');
26 | }));
27 | });
28 |
29 | describe('respects the taToolbar attribute compiled string', function(){
30 | it('should output the correct toolbar', inject(function($rootScope, $compile){
31 | var element = $compile(' ')($rootScope);
32 | expect(jQuery('button', element).length).toBe(6);
33 | }));
34 |
35 | it('via text-angular should output the correct toolbar', inject(function($rootScope, $compile){
36 | var element = $compile(' ')($rootScope);
37 | expect(jQuery('button', element).length).toBe(6);
38 | }));
39 | });
40 |
41 | describe('respects the taToolbar attribute variable name', function(){
42 | it('should output the correct toolbar', inject(function($rootScope, $compile){
43 | $rootScope.toolbar = [['h1','h2','h3','h4','h5','h6']];
44 | var element = $compile(' ')($rootScope);
45 | expect(jQuery('button', element).length).toBe(6);
46 | }));
47 |
48 | it('via text-angular should output the correct toolbar', inject(function($rootScope, $compile){
49 | $rootScope.toolbar = [['h1','h2','h3','h4','h5','h6']];
50 | var element = $compile(' ')($rootScope);
51 | expect(jQuery('button', element).length).toBe(6);
52 | }));
53 | });
54 |
55 | describe('respects the Class attribute taToolbarClass', function(){
56 | it('on the toolbar', inject(function($rootScope, $compile){
57 | var element = $compile(' ')($rootScope);
58 | expect(jQuery('.test-class', element).length).toBe(0);
59 | expect(jQuery(element).hasClass('test-class'));
60 | }));
61 |
62 | it('via text-angular on the toolbar', inject(function($rootScope, $compile){
63 | var element = $compile(' ')($rootScope);
64 | expect(jQuery('.test-class', element).length).toBe(1);
65 | }));
66 | });
67 |
68 | describe('respects the Class attribute taToolbarGroupClass', function(){
69 | it('on the toolbar group', inject(function($rootScope, $compile){
70 | var element = $compile(' ')($rootScope);
71 | expect(jQuery('.test-class', element).length).toBe(4);
72 | }));
73 |
74 | it('via text-angular on the toolbar group', inject(function($rootScope, $compile){
75 | var element = $compile(' ')($rootScope);
76 | expect(jQuery('.test-class', element).length).toBe(4);
77 | }));
78 | });
79 |
80 | describe('respects the Class attribute taToolbarButtonClass', function(){
81 | it('adds to all buttons', inject(function($rootScope, $compile){
82 | var element = $compile(' ')($rootScope);
83 | expect(jQuery('button:not(.test-class)', element).length).toBe(0);
84 | }));
85 |
86 | it('via text-angular adds to all buttons', inject(function($rootScope, $compile){
87 | var element = $compile(' ')($rootScope);
88 | expect(jQuery('button:not(.test-class)', element).length).toBe(0);
89 | }));
90 | });
91 |
92 | describe('respects the Class attribute taToolbarActiveButtonClass', function(){
93 | it('on an active button', inject(function($rootScope, $compile, textAngularManager){
94 | var element = $compile(' ')($rootScope);
95 | var toolbarScope = textAngularManager.retrieveToolbar('test');
96 | toolbarScope.disabled = false;
97 | toolbarScope.focussed = true;
98 | angular.forEach(toolbarScope.tools, function(toolScope){
99 | if(toolScope.activeState){
100 | toolScope.active = toolScope.activeState();
101 | }
102 | });
103 | $rootScope.$digest();
104 | expect(jQuery('button.test-class', element).length).toBe(1);
105 | }));
106 |
107 | it('via text-angular on an active button', inject(function($rootScope, $compile, textAngularManager){
108 | var element = $compile(' ')($rootScope);
109 | var toolbarScope = textAngularManager.retrieveToolbarsViaEditor('test')[0];
110 | toolbarScope.disabled = false;
111 | toolbarScope.focussed = true;
112 | angular.forEach(toolbarScope.tools, function(toolScope){
113 | if(toolScope.activeState){
114 | toolScope.active = toolScope.activeState();
115 | }
116 | });
117 | $rootScope.$digest();
118 | expect(jQuery('button.test-class', element).length).toBe(1);
119 | }));
120 | });
121 |
122 | describe('is added to the textAngularManager', function(){
123 | it('successfully', inject(function($rootScope, $compile, textAngularManager){
124 | $compile(' ')($rootScope);
125 | expect(textAngularManager.retrieveToolbar('test')).not.toBeUndefined();
126 | }));
127 | });
128 | });
129 |
130 | describe('focussed class', function(){
131 | var $rootScope, element, toolbarScope;
132 | beforeEach(inject(function(_$rootScope_, $compile, textAngularManager){
133 | $rootScope = _$rootScope_;
134 | element = $compile(' ')($rootScope);
135 | toolbarScope = textAngularManager.retrieveToolbar('test');
136 | toolbarScope.disabled = false;
137 | }));
138 |
139 | describe('initially not focussed', function(){
140 | it('should not have class', function(){
141 | expect(!jQuery(element).hasClass('test-class'));
142 | });
143 |
144 | it('should add class on focussed change', function(){
145 | toolbarScope.focussed = true;
146 | $rootScope.$digest();
147 | expect(jQuery(element).hasClass('test-class'));
148 | });
149 | });
150 | });
151 |
152 | describe('enables and disables from editor', function(){
153 | var $rootScope, element, $timeout;
154 | beforeEach(inject(function (_$compile_, _$rootScope_, _$timeout_) {
155 | $timeout = _$timeout_;
156 | $rootScope = _$rootScope_;
157 | element = _$compile_(' ')($rootScope);
158 | $rootScope.$digest();
159 | }));
160 |
161 | describe('should have activated all buttons', function(){
162 | it('on trigger focus on ta-text', function(){
163 | element.find('.ta-text').triggerHandler('focus');
164 | $rootScope.$digest();
165 | expect(element.find('button').attr('disabled')).toBeUndefined();
166 | });
167 | it('on trigger focus on ta-html', function(){
168 | element.find('.ta-html').triggerHandler('focus');
169 | $rootScope.$digest();
170 | expect(element.find('button').attr('disabled')).toBeUndefined();
171 | });
172 | });
173 |
174 | describe('should have disabled all buttons', function(){
175 | it('on ta-text trigger blur', function(){
176 | element.find('.ta-text').triggerHandler('focus');
177 | $rootScope.$digest();
178 | element.find('.ta-text').triggerHandler('blur');
179 | $rootScope.$digest();
180 | $timeout.flush();
181 | $rootScope.$digest();
182 | expect(element.find('button').attr('disabled')).toBe('disabled');
183 | });
184 | it('on ta-html trigger blur', function(){
185 | element.find('.ta-html').triggerHandler('focus');
186 | $rootScope.$digest();
187 | element.find('.ta-html').triggerHandler('blur');
188 | $rootScope.$digest();
189 | $timeout.flush();
190 | $rootScope.$digest();
191 | expect(element.find('button').attr('disabled')).toBe('disabled');
192 | });
193 | });
194 | });
195 |
196 | describe('registration', function(){
197 | it('should add itself to the textAngularManager', inject(function($rootScope, $compile, textAngularManager){
198 | $compile(' ')($rootScope);
199 | expect(textAngularManager.retrieveToolbar('test')).not.toBeUndefined();
200 | }));
201 | });
202 |
203 | describe('unregistration', function(){
204 | it('should remove itself from the textAngularManager on $destroy', inject(function($rootScope, $compile, textAngularManager){
205 | var element = $compile(' ')($rootScope);
206 | $rootScope.$digest();
207 | textAngularManager.retrieveToolbar('test').$destroy();
208 | expect(textAngularManager.retrieveToolbar('test')).toBeUndefined();
209 | }));
210 | });
211 |
212 | describe('check for required tool attributes', function(){
213 | var $rootScope, element, toolbarScope;
214 | beforeEach(inject(function(_$rootScope_, $compile, textAngularManager){
215 | $rootScope = _$rootScope_;
216 | element = $compile(' ')($rootScope);
217 | toolbarScope = textAngularManager.retrieveToolbar('test');
218 | toolbarScope.disabled = false;
219 | toolbarScope.focussed = true;
220 | $rootScope.$digest();
221 | element = $('button', element).eq(0);
222 | }));
223 |
224 | it('should have a name', function(){
225 | expect(element.attr('name')).not.toBeUndefined();
226 | });
227 |
228 | it('should be unselectable', function(){
229 | expect(element.attr('unselectable')).toBe('on');
230 | });
231 |
232 | it('should have ng-disabled set', function(){
233 | expect(element.attr('ng-disabled')).toBe('isDisabled()');
234 | });
235 |
236 | it('should have a negative tabindex', function(){
237 | expect(element.attr('tabindex')).toBe('-1');
238 | });
239 |
240 | it('should have ng-click to executeAction()', function(){
241 | expect(element.attr('ng-click')).toBe('executeAction()');
242 | });
243 |
244 | it('should have ng-disabled set', function(){
245 | expect(element.attr('ng-disabled')).toBe('isDisabled()');
246 | });
247 |
248 | it('should prevent event on mousedown', function(){
249 | element.on('mousedown', function(e){
250 | expect(e.isDefaultPrevented());
251 | });
252 | element.triggerHandler('mousedown');
253 | });
254 | });
255 |
256 | describe('test custom tool attributes', function(){
257 | var $rootScope, element, toolbarScope;
258 | beforeEach(inject(function(_$rootScope_, $compile, textAngularManager, taRegisterTool, taOptions){
259 | taRegisterTool('display', {display: 'THIS IS A TEST DIV
', iconclass: 'badclass', buttontext: 'badtext'});
260 | taRegisterTool('buttontext', {buttontext: 'Only Text'});
261 | taRegisterTool('iconclass', {iconclass: 'onlyiconclass'});
262 | taRegisterTool('iconandtext', {iconclass: 'iconclass', buttontext: 'good text'});
263 | taOptions.toolbar = [['display','buttontext','iconclass','iconandtext']];
264 | $rootScope = _$rootScope_;
265 | element = $compile(' ')($rootScope);
266 | toolbarScope = textAngularManager.retrieveToolbar('test');
267 | toolbarScope.disabled = false;
268 | toolbarScope.focussed = true;
269 | $rootScope.$digest();
270 | }));
271 |
272 | describe('displaying the button', function(){
273 | it('should override everything with the html in the display attribute', function(){
274 | expect($('div[name=display]', element).html()).toBe('THIS IS A TEST DIV');
275 | });
276 |
277 | it('should display only buttontext in the button', function(){
278 | expect($('button[name=buttontext]', element).html()).toBe('Only Text');
279 | });
280 |
281 | it('should display only icon in the button', function(){
282 | expect($('button[name=iconclass]', element).html()).toBe(' ');
283 | });
284 |
285 | it('should display both icon and buttontext in the button', function(){
286 | expect($('button[name=iconandtext]', element).html()).toBe(' good text');
287 | });
288 | });
289 |
290 | describe('updating the button display', function(){
291 | beforeEach(inject(function(textAngularManager){
292 | textAngularManager.updateToolsDisplay({
293 | 'display': {
294 | display: 'Replaced Text
'
295 | },
296 | 'buttontext': {
297 | buttontext: 'otherstuff'
298 | },
299 | 'iconclass': {
300 | iconclass: 'test-icon-class'
301 | },
302 | 'iconandtext': {
303 | buttontext: 'otherstuff',
304 | iconclass: 'test-icon-class'
305 | }
306 | });
307 | $rootScope.$digest();
308 | }));
309 |
310 | it('should override the old display with the html in the new display attribute', function(){
311 | expect($('div[name=display]', element).html()).toBe('Replaced Text');
312 | });
313 |
314 | it('should display only new buttontext in the button', function(){
315 | expect($('button[name=buttontext]', element).html()).toBe('otherstuff');
316 | });
317 |
318 | it('should display only new icon in the button', function(){
319 | expect($('button[name=iconclass]', element).html()).toBe(' ');
320 | });
321 |
322 | it('should display both new icon and new buttontext in the button', function(){
323 | expect($('button[name=iconandtext]', element).html()).toBe(' otherstuff');
324 | });
325 | });
326 |
327 | describe('updating the button display', function(){
328 | beforeEach(inject(function(textAngularManager){
329 | textAngularManager.updateToolsDisplay({
330 | 'display': {
331 | display: 'Replaced Text
'
332 | },
333 | 'buttontext': {
334 | buttontext: 'otherstuff'
335 | },
336 | 'iconclass': {
337 | iconclass: 'test-icon-class'
338 | },
339 | 'iconandtext': {
340 | buttontext: 'otherstuff',
341 | iconclass: 'test-icon-class'
342 | }
343 | });
344 | $rootScope.$digest();
345 | }));
346 |
347 | it('should override the old display with the html in the new display attribute', function(){
348 | expect($('div[name=display]', element).html()).toBe('Replaced Text');
349 | });
350 |
351 | it('should display only new buttontext in the button', function(){
352 | expect($('button[name=buttontext]', element).html()).toBe('otherstuff');
353 | });
354 |
355 | it('should display only new icon in the button', function(){
356 | expect($('button[name=iconclass]', element).html()).toBe(' ');
357 | });
358 |
359 | it('should display both new icon and new buttontext in the button', function(){
360 | expect($('button[name=iconandtext]', element).html()).toBe(' otherstuff');
361 | });
362 | });
363 |
364 | describe('resetting part of the button display', function(){
365 | beforeEach(inject(function(textAngularManager){
366 | textAngularManager.updateToolsDisplay({
367 | 'display': {
368 | display: 'Replaced Text
',
369 | buttontext: 'This isnt a test'
370 | },
371 | 'buttontext': {
372 | buttontext: 'otherstuff',
373 | iconclass: 'newest-test-class'
374 | },
375 | 'iconclass': {
376 | iconclass: 'test-icon-class',
377 | buttontext: 'More text to insert'
378 | }
379 | });
380 | $rootScope.$digest();
381 | textAngularManager.updateToolsDisplay({
382 | 'display': {
383 | display: null
384 | },
385 | 'buttontext': {
386 | buttontext: null
387 | },
388 | 'iconclass': {
389 | iconclass: null
390 | }
391 | });
392 | $rootScope.$digest();
393 | }));
394 |
395 | it('should remove the display attribute and follow the other rules', function(){
396 | // note as it is reset this is now a button not a div
397 | expect($('button[name=display]', element).html()).toBe(' This isnt a test');
398 | });
399 |
400 | it('should remove the button text', function(){
401 | expect($('button[name=buttontext]', element).html()).toBe(' ');
402 | });
403 |
404 | it('should remove the icon tag', function(){
405 | expect($('button[name=iconclass]', element).html()).toBe('More text to insert');
406 | });
407 |
408 | it('should error on attempting to set all 3 to null', inject(function(textAngularManager){
409 | expect(function(){
410 | textAngularManager.updateToolsDisplay({'iconandtext': {
411 | display: null,
412 | buttontext: null,
413 | iconclass: null
414 | }});
415 | }).toThrow('textAngular Error: Tool Definition for updating "iconandtext" does not have a valid display/iconclass/buttontext value');
416 | }));
417 | });
418 | });
419 |
420 | describe('update and reset tools buttons and multi toolbars', function(){
421 | var $rootScope, elements, toolbarScopes, manager;
422 | beforeEach(inject(function(_$rootScope_, $compile, textAngularManager, taRegisterTool, taOptions){
423 | manager = textAngularManager;
424 | taOptions.toolbar = [['h1','h2','h3','h4']];
425 | $rootScope = _$rootScope_;
426 | elements = jQuery('
');
427 | elements.append($compile(' ')($rootScope));
428 | elements.append($compile(' ')($rootScope));
429 | elements.append($compile(' ')($rootScope));
430 | elements.append($compile(' ')($rootScope));
431 | toolbarScopes = [
432 | textAngularManager.retrieveToolbar('test1'),
433 | textAngularManager.retrieveToolbar('test2'),
434 | textAngularManager.retrieveToolbar('test3'),
435 | textAngularManager.retrieveToolbar('test4')
436 | ];
437 | angular.forEach(toolbarScopes, function(toolbarScope){
438 | toolbarScope.disabled = false;
439 | toolbarScope.focussed = true;
440 | });
441 | $rootScope.$digest();
442 | }));
443 |
444 | it('updateToolbarToolDisplay should update one tool on one toolbar', function(){
445 | manager.updateToolbarToolDisplay('test1', 'h1', {buttontext: 'h1_changed'});
446 | var matches = 0;
447 | elements.find('button').each(function(i, element){
448 | if(jQuery(element).html() === 'h1_changed') matches++;
449 | });
450 | expect(matches).toBe(1);
451 | });
452 |
453 | it('updateToolDisplay should update one tool on all toolbars', function(){
454 | manager.updateToolDisplay('h1', {buttontext: 'h1_changed'});
455 | var matches = 0;
456 | elements.find('button').each(function(i, element){
457 | if(jQuery(element).html() === 'h1_changed') matches++;
458 | });
459 | expect(matches).toBe(4);
460 | });
461 |
462 | it('updateToolsDisplay should update multiple tools on all toolbars', function(){
463 | manager.updateToolsDisplay({'h1': {buttontext: 'changed'}, 'h2': {buttontext: 'changed'}});
464 | var matches = 0;
465 | elements.find('button').each(function(i, element){
466 | if(jQuery(element).html() === 'changed') matches++;
467 | });
468 | expect(matches).toBe(8);
469 | });
470 |
471 | it('resetToolbarToolDisplay should reset one tool on one toolbar to the default', function(){
472 | manager.updateToolsDisplay({'h1': {buttontext: 'changed'}, 'h2': {buttontext: 'changed'}});
473 | manager.resetToolbarToolDisplay('test1', 'h1');
474 | var matches = 0;
475 | elements.find('button').each(function(i, element){
476 | if(jQuery(element).html() === 'changed') matches++;
477 | });
478 | expect(matches).toBe(7);
479 | });
480 |
481 | it('updateToolDisplay should update one tool on all toolbars', function(){
482 | manager.updateToolsDisplay({'h1': {buttontext: 'changed'}, 'h2': {buttontext: 'changed'}});
483 | manager.resetToolDisplay('h1');
484 | var matches = 0;
485 | elements.find('button').each(function(i, element){
486 | if(jQuery(element).html() === 'changed') matches++;
487 | });
488 | expect(matches).toBe(4);
489 | });
490 |
491 | it('updateToolsDisplay should update multiple tools on all toolbars', function(){
492 | manager.updateToolsDisplay({'h1': {buttontext: 'changed'}, 'h2': {buttontext: 'changed'}});
493 | manager.resetToolsDisplay();
494 | var matches = 0;
495 | elements.find('button').each(function(i, element){
496 | if(jQuery(element).html() === 'changed') matches++;
497 | });
498 | expect(matches).toBe(0);
499 | });
500 | });
501 |
502 | describe('test custom tool functions', function(){
503 | var $rootScope, element, toolbarScope;
504 | beforeEach(inject(function(_$rootScope_, $compile, textAngularManager, taRegisterTool, taOptions){
505 | taRegisterTool('disabled1', {buttontext: 'allways-disabled', disabled: true});
506 | taRegisterTool('disabled2', {buttontext: 'allways-disabled', disabled: function(){ return true;}});
507 | taOptions.toolbar = [['disabled1','disabled2']];
508 | $rootScope = _$rootScope_;
509 | element = $compile(' ')($rootScope);
510 | toolbarScope = textAngularManager.retrieveToolbar('test');
511 | toolbarScope.disabled = false;
512 | toolbarScope.focussed = true;
513 | $rootScope.$digest();
514 | }));
515 |
516 | it('should respect a disabled value', function(){
517 | expect(element.find('[name=disabled1]').is(":disabled"));
518 | });
519 |
520 | it('should respect a disabled functions return value', function(){
521 | expect(element.find('[name=disabled2]').is(":disabled"));
522 | });
523 | // disabled as value and as function on scope
524 | // action is called in correct deferred pattern
525 | });
526 | });
--------------------------------------------------------------------------------
/test/taBind.spec.js:
--------------------------------------------------------------------------------
1 | describe('taBind', function () {
2 | 'use strict';
3 | beforeEach(module('textAngular'));
4 | var $rootScope;
5 |
6 | it('should require ngModel', inject(function (_$compile_, _$rootScope_) {
7 | expect(function () {
8 | _$compile_('
')(_$rootScope_);
9 | $rootScope.$digest();
10 | }).toThrow();
11 | }));
12 |
13 | describe('should respect HTML5 placeholder', function () {
14 | var $rootScope, element;
15 | beforeEach(inject(function (_$compile_, _$rootScope_) {
16 | $rootScope = _$rootScope_;
17 | $rootScope.html = '';
18 | element = _$compile_('')($rootScope);
19 | $rootScope.$digest();
20 | }));
21 |
22 | it('should add the placeholder-text class', function () {
23 | expect(element.hasClass('placeholder-text')).toBe(true);
24 | });
25 | it('should add the placeholder text', function () {
26 | expect(element.text()).toEqual('Add Comment');
27 | });
28 | it('should remove the placeholder text on focusin', function () {
29 | element.triggerHandler('focus');
30 | expect(element.text()).toEqual('');
31 | });
32 | it('should remove the placeholder-text class on focusin', function () {
33 | element.triggerHandler('focus');
34 | expect(element.hasClass('placeholder-text')).toBe(false);
35 | });
36 | it('should add the placeholder text back on blur if the input is blank', function () {
37 | element.triggerHandler('focus');
38 | expect(element.text()).toEqual('');
39 | element.triggerHandler('blur');
40 | expect(element.text()).toEqual('Add Comment');
41 | });
42 | it('should add the placeholder-text class back on blur if the input is blank', function () {
43 | element.triggerHandler('focus');
44 | expect(element.hasClass('placeholder-text')).toBe(false);
45 | element.triggerHandler('blur');
46 | expect(element.hasClass('placeholder-text')).toBe(true);
47 | });
48 | it('should not add the placeholder text back on blur if the input is not blank', function () {
49 | element.triggerHandler('focus');
50 | expect(element.text()).toEqual('');
51 | element.text('Lorem Ipsum');
52 | element.triggerHandler('blur');
53 | expect(element.text()).toEqual('Lorem Ipsum');
54 | });
55 | it('should not add the placeholder-text class back on blur if the input is not blank', function () {
56 | element.triggerHandler('focus');
57 | expect(element.hasClass('placeholder-text')).toBe(false);
58 | element.text('Lorem Ipsum');
59 | element.triggerHandler('blur');
60 | expect(element.hasClass('placeholder-text')).toBe(false);
61 | });
62 | });
63 |
64 | describe('should function as a display div', function () {
65 | var $rootScope, element;
66 | beforeEach(inject(function (_$compile_, _$rootScope_) {
67 | $rootScope = _$rootScope_;
68 | $rootScope.html = 'Test Contents
';
69 | element = _$compile_('
')($rootScope);
70 | $rootScope.$digest();
71 | }));
72 |
73 | it('should display model contents', function () {
74 | expect(element.html()).toBe('Test Contents
');
75 | });
76 | it('should NOT update model from keyup', function () {
77 | element.html('Test 2 Content
');
78 | element.triggerHandler('keyup');
79 | $rootScope.$digest();
80 | expect($rootScope.html).toBe('Test Contents
');
81 | });
82 | it('should error on update model from update function', function () {
83 | element.html('Test 2 Content
');
84 | expect(function () {
85 | $rootScope.updateTaBind();
86 | }).toThrow('textAngular Error: attempting to update non-editable taBind');
87 | });
88 | it('should update display from model change', function () {
89 | $rootScope.html = 'Test 2 Content
';
90 | $rootScope.$digest();
91 | expect(element.html()).toBe('Test 2 Content
');
92 | });
93 | });
94 |
95 | describe('should function as an WYSIWYG div', function () {
96 | var $rootScope, element;
97 | beforeEach(inject(function (_$compile_, _$rootScope_) {
98 | $rootScope = _$rootScope_;
99 | $rootScope.html = 'Test Contents
';
100 | element = _$compile_('
')($rootScope);
101 | $rootScope.$digest();
102 | }));
103 |
104 | it('should display model contents', function () {
105 | expect(element.html()).toBe('Test Contents
');
106 | });
107 | it('should update model from keyup', function () {
108 | element.html('Test 2 Content
');
109 | element.triggerHandler('keyup');
110 | $rootScope.$digest();
111 | expect($rootScope.html).toBe('Test 2 Content
');
112 | });
113 | it('should update model from update function', function () {
114 | element.html('Test 2 Content
');
115 | $rootScope.updateTaBind();
116 | $rootScope.$digest();
117 | expect($rootScope.html).toBe('Test 2 Content
');
118 | });
119 | it('should update display from model change', function () {
120 | $rootScope.html = 'Test 2 Content
';
121 | $rootScope.$digest();
122 | expect(element.html()).toBe('Test 2 Content
');
123 | });
124 |
125 | it('should prevent links from being clicked', function () {
126 | $rootScope.html = '';
127 | $rootScope.$digest();
128 | element.find('a').on('click', function(e){
129 | expect(e.isDefaultPrevented());
130 | });
131 | element.find('a').triggerHandler('click');
132 | });
133 | });
134 |
135 | describe('should function as an textarea', function () {
136 | var $rootScope, element;
137 | beforeEach(inject(function (_$compile_, _$rootScope_) {
138 | $rootScope = _$rootScope_;
139 | $rootScope.html = 'Test Contents
';
140 | element = _$compile_('')($rootScope);
141 | $rootScope.$digest();
142 | }));
143 |
144 | it('should display model contents', function () {
145 | expect(element.val()).toBe('Test Contents
');
146 | });
147 | it('should update model from change', function () {
148 | element.val('Test 2 Content
');
149 | element.trigger('blur');
150 | $rootScope.$digest();
151 | expect($rootScope.html).toBe('Test 2 Content
');
152 | });
153 | it('should update model from update function', function () {
154 | element.val('Test 2 Content
');
155 | $rootScope.updateTaBind();
156 | $rootScope.$digest();
157 | expect($rootScope.html).toBe('Test 2 Content
');
158 | });
159 | it('should update display from model change', function () {
160 | $rootScope.html = 'Test 2 Content
';
161 | $rootScope.$digest();
162 | expect(element.val()).toBe('Test 2 Content
');
163 | });
164 | });
165 |
166 | describe('should function as an input', function () {
167 | var $rootScope, element;
168 | beforeEach(inject(function (_$compile_, _$rootScope_) {
169 | $rootScope = _$rootScope_;
170 | $rootScope.html = 'Test Contents
';
171 | element = _$compile_(' ')($rootScope);
172 | $rootScope.$digest();
173 | }));
174 |
175 | it('should display model contents', function () {
176 | expect(element.val()).toBe('Test Contents
');
177 | });
178 | it('should update model from change', function () {
179 | element.val('Test 2 Content
');
180 | element.trigger('blur');
181 | $rootScope.$digest();
182 | expect($rootScope.html).toBe('Test 2 Content
');
183 | });
184 | it('should update model from update function', function () {
185 | element.val('Test 2 Content
');
186 | $rootScope.updateTaBind();
187 | $rootScope.$digest();
188 | expect($rootScope.html).toBe('Test 2 Content
');
189 | });
190 | it('should update display from model change', function () {
191 | $rootScope.html = 'Test 2 Content
';
192 | $rootScope.$digest();
193 | expect(element.val()).toBe('Test 2 Content
');
194 | });
195 | });
196 |
197 | describe('should update from cut and paste events', function () {
198 | var $rootScope, element, $timeout;
199 | beforeEach(inject(function (_$compile_, _$rootScope_, _$timeout_) {
200 | $rootScope = _$rootScope_;
201 | $timeout = _$timeout_;
202 | $rootScope.html = 'Test Contents
';
203 | element = _$compile_('')($rootScope);
204 | $rootScope.$digest();
205 | }));
206 |
207 | it('should update model from paste', function () {
208 | element.val('Test 2 Content
');
209 | element.trigger('paste');
210 | $rootScope.$digest();
211 | $timeout.flush();
212 | $rootScope.$digest();
213 | expect($rootScope.html).toBe('Test 2 Content
');
214 | });
215 |
216 | it('should update model from cut', function () {
217 | element.val('Test 2 Content
');
218 | element.trigger('cut');
219 | $timeout.flush();
220 | $rootScope.$digest();
221 | expect($rootScope.html).toBe('Test 2 Content
');
222 | });
223 | });
224 |
225 | describe('should create the updateTaBind function on parent scope', function () {
226 | describe('without id', function () {
227 | it('should exist', inject(function (_$compile_, _$rootScope_) {
228 | _$rootScope_.html = 'Test Contents
';
229 | _$compile_('')(_$rootScope_);
230 | _$rootScope_.$digest();
231 | expect(_$rootScope_.updateTaBind).toBeDefined();
232 | }));
233 | });
234 |
235 | describe('with id', function () {
236 | it('should exist', inject(function (_$compile_, _$rootScope_) {
237 | _$rootScope_.html = 'Test Contents
';
238 | _$compile_('')(_$rootScope_);
239 | _$rootScope_.$digest();
240 | expect(_$rootScope_.updateTaBindTest).toBeDefined();
241 | }));
242 | });
243 | });
244 |
245 | describe('should use taSanitize to', function () {
246 | var $rootScope, element;
247 | beforeEach(inject(function (_$compile_, _$rootScope_) {
248 | $rootScope = _$rootScope_;
249 | $rootScope.html = 'Test Contents
';
250 | element = _$compile_('
')($rootScope);
251 | $rootScope.$digest();
252 | }));
253 |
254 | it('parse from change events', function () {
255 | element.append('Test 2 Content ');
256 | $rootScope.updateTaBind();
257 | $rootScope.$digest();
258 | expect($rootScope.html).toBe('Test Contents
Test 2 Content');
259 | });
260 |
261 | it('formatt from model change', function () {
262 | $rootScope.html += 'Test 2 Content ';
263 | $rootScope.$digest();
264 | expect(element.html()).toBe('Test Contents
Test 2 Content');
265 | });
266 | });
267 |
268 | describe('should respect taReadonly value', function () {
269 | describe('initially true', function () {
270 | it('as a textarea', inject(function (_$compile_, _$rootScope_) {
271 | _$rootScope_.html = 'Test Contents
';
272 | _$rootScope_.readonly = true;
273 | var element = _$compile_('')(_$rootScope_);
274 | _$rootScope_.$digest();
275 | expect(element.attr('disabled')).toBe('disabled');
276 | }));
277 | it('as an input', inject(function (_$compile_, _$rootScope_) {
278 | _$rootScope_.html = 'Test Contents
';
279 | _$rootScope_.readonly = true;
280 | var element = _$compile_(' ')(_$rootScope_);
281 | _$rootScope_.$digest();
282 | expect(element.attr('disabled')).toBe('disabled');
283 | }));
284 | it('as an editable div', inject(function (_$compile_, _$rootScope_) {
285 | _$rootScope_.html = 'Test Contents
';
286 | _$rootScope_.readonly = true;
287 | var element = _$compile_('
')(_$rootScope_);
288 | _$rootScope_.$digest();
289 | expect(element.attr('contenteditable')).not.toBeDefined();
290 | }));
291 | it('as an un-editable div', inject(function (_$compile_, _$rootScope_) {
292 | _$rootScope_.html = 'Test Contents
';
293 | _$rootScope_.readonly = true;
294 | var element = _$compile_('
')(_$rootScope_);
295 | _$rootScope_.$digest();
296 | expect(element.attr('contenteditable')).not.toBeDefined();
297 | }));
298 | });
299 |
300 | describe('initially false', function () {
301 | it('as a textarea', inject(function (_$compile_, _$rootScope_) {
302 | _$rootScope_.html = 'Test Contents
';
303 | _$rootScope_.readonly = false;
304 | var element = _$compile_('')(_$rootScope_);
305 | _$rootScope_.$digest();
306 | expect(element.attr('disabled')).not.toBeDefined();
307 | }));
308 | it('as an input', inject(function (_$compile_, _$rootScope_) {
309 | _$rootScope_.html = 'Test Contents
';
310 | _$rootScope_.readonly = false;
311 | var element = _$compile_(' ')(_$rootScope_);
312 | _$rootScope_.$digest();
313 | expect(element.attr('disabled')).not.toBeDefined();
314 | }));
315 | it('as an editable div', inject(function (_$compile_, _$rootScope_) {
316 | _$rootScope_.html = 'Test Contents
';
317 | _$rootScope_.readonly = false;
318 | var element = _$compile_('
')(_$rootScope_);
319 | _$rootScope_.$digest();
320 | expect(element.attr('contenteditable')).toBe('true');
321 | }));
322 | it('as an un-editable div', inject(function (_$compile_, _$rootScope_) {
323 | _$rootScope_.html = 'Test Contents
';
324 | _$rootScope_.readonly = false;
325 | var element = _$compile_('
')(_$rootScope_);
326 | _$rootScope_.$digest();
327 | expect(element.attr('contenteditable')).not.toBeDefined();
328 | }));
329 | });
330 |
331 |
332 | describe('changed to true', function () {
333 | it('as a textarea', inject(function (_$compile_, _$rootScope_) {
334 | _$rootScope_.html = 'Test Contents
';
335 | _$rootScope_.readonly = false;
336 | var element = _$compile_('')(_$rootScope_);
337 | _$rootScope_.$digest();
338 | _$rootScope_.readonly = true;
339 | _$rootScope_.$digest();
340 | expect(element.attr('disabled')).toBe('disabled');
341 | }));
342 | it('as an input', inject(function (_$compile_, _$rootScope_) {
343 | _$rootScope_.html = 'Test Contents
';
344 | _$rootScope_.readonly = false;
345 | var element = _$compile_(' ')(_$rootScope_);
346 | _$rootScope_.$digest();
347 | _$rootScope_.readonly = true;
348 | _$rootScope_.$digest();
349 | expect(element.attr('disabled')).toBe('disabled');
350 | }));
351 | it('as an editable div', inject(function (_$compile_, _$rootScope_) {
352 | _$rootScope_.html = 'Test Contents
';
353 | _$rootScope_.readonly = false;
354 | var element = _$compile_('
')(_$rootScope_);
355 | _$rootScope_.$digest();
356 | _$rootScope_.readonly = true;
357 | _$rootScope_.$digest();
358 | expect(element.attr('contenteditable')).not.toBeDefined();
359 | }));
360 | it('as an un-editable div', inject(function (_$compile_, _$rootScope_) {
361 | _$rootScope_.html = 'Test Contents
';
362 | _$rootScope_.readonly = false;
363 | var element = _$compile_('
')(_$rootScope_);
364 | _$rootScope_.$digest();
365 | _$rootScope_.readonly = true;
366 | _$rootScope_.$digest();
367 | expect(element.attr('contenteditable')).not.toBeDefined();
368 | }));
369 | });
370 |
371 | describe('changed to false', function () {
372 | it('as a textarea', inject(function (_$compile_, _$rootScope_) {
373 | _$rootScope_.html = 'Test Contents
';
374 | _$rootScope_.readonly = true;
375 | var element = _$compile_('')(_$rootScope_);
376 | _$rootScope_.$digest();
377 | _$rootScope_.readonly = false;
378 | _$rootScope_.$digest();
379 | expect(element.attr('disabled')).not.toBeDefined();
380 | }));
381 | it('as an input', inject(function (_$compile_, _$rootScope_) {
382 | _$rootScope_.html = 'Test Contents
';
383 | _$rootScope_.readonly = true;
384 | var element = _$compile_(' ')(_$rootScope_);
385 | _$rootScope_.$digest();
386 | _$rootScope_.readonly = false;
387 | _$rootScope_.$digest();
388 | expect(element.attr('disabled')).not.toBeDefined();
389 | }));
390 | it('as an editable div', inject(function (_$compile_, _$rootScope_) {
391 | _$rootScope_.html = 'Test Contents
';
392 | _$rootScope_.readonly = true;
393 | var element = _$compile_('
')(_$rootScope_);
394 | _$rootScope_.$digest();
395 | _$rootScope_.readonly = false;
396 | _$rootScope_.$digest();
397 | expect(element.attr('contenteditable')).toBe('true');
398 | }));
399 | it('as an un-editable div', inject(function (_$compile_, _$rootScope_) {
400 | _$rootScope_.html = 'Test Contents
';
401 | _$rootScope_.readonly = true;
402 | var element = _$compile_('
')(_$rootScope_);
403 | _$rootScope_.$digest();
404 | _$rootScope_.readonly = false;
405 | _$rootScope_.$digest();
406 | expect(element.attr('contenteditable')).not.toBeDefined();
407 | }));
408 | });
409 |
410 | describe('when true don\'t update model', function () {
411 | describe('from cut and paste events', function () {
412 | describe('on textarea', function () {
413 | var $rootScope, element;
414 | beforeEach(inject(function (_$compile_, _$rootScope_) {
415 | $rootScope = _$rootScope_;
416 | $rootScope.html = 'Test Contents
';
417 | $rootScope.readonly = true;
418 | element = _$compile_('')($rootScope);
419 | $rootScope.$digest();
420 | }));
421 |
422 | it('should update model from paste', function () {
423 | element.val('Test 2 Content
');
424 | element.trigger('paste');
425 | $rootScope.$digest();
426 | expect($rootScope.html).toBe('Test Contents
');
427 | });
428 |
429 | it('should update model from cut', function () {
430 | element.val('Test 2 Content
');
431 | element.trigger('cut');
432 | $rootScope.$digest();
433 | expect($rootScope.html).toBe('Test Contents
');
434 | });
435 | });
436 |
437 | describe('on input', function () {
438 | var $rootScope, element;
439 | beforeEach(inject(function (_$compile_, _$rootScope_) {
440 | $rootScope = _$rootScope_;
441 | $rootScope.html = 'Test Contents
';
442 | $rootScope.readonly = true;
443 | element = _$compile_(' ')($rootScope);
444 | $rootScope.$digest();
445 | }));
446 |
447 | it('should update model from paste', function () {
448 | element.val('Test 2 Content
');
449 | element.trigger('paste');
450 | $rootScope.$digest();
451 | expect($rootScope.html).toBe('Test Contents
');
452 | });
453 |
454 | it('should update model from cut', function () {
455 | element.val('Test 2 Content
');
456 | element.trigger('cut');
457 | $rootScope.$digest();
458 | expect($rootScope.html).toBe('Test Contents
');
459 | });
460 | });
461 |
462 | describe('on editable div', function () {
463 | var $rootScope, element;
464 | beforeEach(inject(function (_$compile_, _$rootScope_) {
465 | $rootScope = _$rootScope_;
466 | $rootScope.html = 'Test Contents
';
467 | $rootScope.readonly = true;
468 | element = _$compile_('
')($rootScope);
469 | $rootScope.$digest();
470 | }));
471 |
472 | it('should update model from paste', function () {
473 | element.html('Test 2 Content
');
474 | element.trigger('paste');
475 | $rootScope.$digest();
476 | expect($rootScope.html).toBe('Test Contents
');
477 | });
478 |
479 | it('should update model from cut', function () {
480 | element.html('Test 2 Content
');
481 | element.trigger('cut');
482 | $rootScope.$digest();
483 | expect($rootScope.html).toBe('Test Contents
');
484 | });
485 | });
486 | });
487 |
488 | describe('from updateTaBind function', function () {
489 | describe('on textarea', function () {
490 | var $rootScope, element;
491 | beforeEach(inject(function (_$compile_, _$rootScope_) {
492 | $rootScope = _$rootScope_;
493 | $rootScope.html = 'Test Contents
';
494 | $rootScope.readonly = true;
495 | element = _$compile_('')($rootScope);
496 | $rootScope.$digest();
497 | }));
498 |
499 | it('should update model', function () {
500 | element.val('Test 2 Content
');
501 | $rootScope.updateTaBind();
502 | $rootScope.$digest();
503 | expect($rootScope.html).toBe('Test Contents
');
504 | });
505 | });
506 |
507 | describe('on input', function () {
508 | var $rootScope, element;
509 | beforeEach(inject(function (_$compile_, _$rootScope_) {
510 | $rootScope = _$rootScope_;
511 | $rootScope.html = 'Test Contents
';
512 | $rootScope.readonly = true;
513 | element = _$compile_(' ')($rootScope);
514 | $rootScope.$digest();
515 | }));
516 |
517 | it('should update model', function () {
518 | element.val('Test 2 Content
');
519 | $rootScope.updateTaBind();
520 | $rootScope.$digest();
521 | expect($rootScope.html).toBe('Test Contents
');
522 | });
523 | });
524 |
525 | describe('on editable div', function () {
526 | var $rootScope, element;
527 | beforeEach(inject(function (_$compile_, _$rootScope_) {
528 | $rootScope = _$rootScope_;
529 | $rootScope.html = 'Test Contents
';
530 | $rootScope.readonly = true;
531 | element = _$compile_('
')($rootScope);
532 | $rootScope.$digest();
533 | }));
534 |
535 | it('should update model', function () {
536 | element.html('Test 2 Content
');
537 | $rootScope.updateTaBind();
538 | $rootScope.$digest();
539 | expect($rootScope.html).toBe('Test Contents
');
540 | });
541 | });
542 | });
543 |
544 | describe('from blur function', function () {
545 | describe('on textarea', function () {
546 | var $rootScope, element;
547 | beforeEach(inject(function (_$compile_, _$rootScope_) {
548 | $rootScope = _$rootScope_;
549 | $rootScope.html = 'Test Contents
';
550 | $rootScope.readonly = true;
551 | element = _$compile_('')($rootScope);
552 | $rootScope.$digest();
553 | }));
554 |
555 | it('should update model', function () {
556 | element.val('Test 2 Content
');
557 | element.trigger('blur');
558 | $rootScope.$digest();
559 | expect($rootScope.html).toBe('Test Contents
');
560 | });
561 | });
562 |
563 | describe('on input', function () {
564 | var $rootScope, element;
565 | beforeEach(inject(function (_$compile_, _$rootScope_) {
566 | $rootScope = _$rootScope_;
567 | $rootScope.html = 'Test Contents
';
568 | $rootScope.readonly = true;
569 | element = _$compile_(' ')($rootScope);
570 | $rootScope.$digest();
571 | }));
572 |
573 | it('should update model', function () {
574 | element.val('Test 2 Content
');
575 | element.trigger('blur');
576 | $rootScope.$digest();
577 | expect($rootScope.html).toBe('Test Contents
');
578 | });
579 | });
580 | });
581 |
582 | describe('from keyup function', function () {
583 | describe('on editable div', function () {
584 | var $rootScope, element;
585 | beforeEach(inject(function (_$compile_, _$rootScope_) {
586 | $rootScope = _$rootScope_;
587 | $rootScope.html = 'Test Contents
';
588 | $rootScope.readonly = true;
589 | element = _$compile_('
')($rootScope);
590 | $rootScope.$digest();
591 | }));
592 |
593 | it('should update model', function () {
594 | element.html('Test 2 Content
');
595 | element.trigger('keyup');
596 | $rootScope.$digest();
597 | expect($rootScope.html).toBe('Test Contents
');
598 | });
599 | });
600 | });
601 | });
602 | });
603 | });
--------------------------------------------------------------------------------
/textAngular-sanitize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.2.8-build.2102+sha.5a9cb8b
3 | * (c) 2010-2014 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular, undefined) {'use strict';
7 |
8 | var $sanitizeMinErr = angular.$$minErr('$sanitize');
9 |
10 | /**
11 | * @ngdoc overview
12 | * @name ngSanitize
13 | * @description
14 | *
15 | * # ngSanitize
16 | *
17 | * The `ngSanitize` module provides functionality to sanitize HTML.
18 | *
19 | * {@installModule sanitize}
20 | *
21 | *
22 | *
23 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
24 | */
25 |
26 | /*
27 | * HTML Parser By Misko Hevery (misko@hevery.com)
28 | * based on: HTML Parser By John Resig (ejohn.org)
29 | * Original code by Erik Arvidsson, Mozilla Public License
30 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
31 | *
32 | * // Use like so:
33 | * htmlParser(htmlString, {
34 | * start: function(tag, attrs, unary) {},
35 | * end: function(tag) {},
36 | * chars: function(text) {},
37 | * comment: function(text) {}
38 | * });
39 | *
40 | */
41 |
42 |
43 | /**
44 | * @ngdoc service
45 | * @name ngSanitize.$sanitize
46 | * @function
47 | *
48 | * @description
49 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
50 | * then serialized back to properly escaped html string. This means that no unsafe input can make
51 | * it into the returned string, however, since our parser is more strict than a typical browser
52 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
53 | * browser, won't make it through the sanitizer.
54 | * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
55 | * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
56 | *
57 | * @param {string} html Html input.
58 | * @returns {string} Sanitized html.
59 | *
60 | * @example
61 |
62 |
63 |
74 |
75 | Snippet:
76 |
77 |
78 | Directive
79 | How
80 | Source
81 | Rendered
82 |
83 |
84 | ng-bind-html
85 | Automatically uses $sanitize
86 | <div ng-bind-html="snippet"> </div>
87 |
88 |
89 |
90 | ng-bind-html
91 | Bypass $sanitize by explicitly trusting the dangerous value
92 |
93 | <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
94 | </div>
95 |
96 |
97 |
98 |
99 | ng-bind
100 | Automatically escapes
101 | <div ng-bind="snippet"> </div>
102 |
103 |
104 |
105 |
106 |
107 |
108 | it('should sanitize the html snippet by default', function() {
109 | expect(using('#bind-html-with-sanitize').element('div').html()).
110 | toBe('an html\nclick here \nsnippet
');
111 | });
112 |
113 | it('should inline raw snippet if bound to a trusted value', function() {
114 | expect(using('#bind-html-with-trust').element("div").html()).
115 | toBe("an html\n" +
116 | "click here \n" +
117 | "snippet
");
118 | });
119 |
120 | it('should escape snippet without any filter', function() {
121 | expect(using('#bind-default').element('div').html()).
122 | toBe("<p style=\"color:blue\">an html\n" +
123 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
124 | "snippet</p>");
125 | });
126 |
127 | it('should update', function() {
128 | input('snippet').enter('new text ');
129 | expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new text ');
130 | expect(using('#bind-html-with-trust').element('div').html()).toBe(
131 | 'new text ');
132 | expect(using('#bind-default').element('div').html()).toBe(
133 | "new <b onclick=\"alert(1)\">text</b>");
134 | });
135 |
136 |
137 | */
138 | function $SanitizeProvider() {
139 | this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
140 | return function(html) {
141 | var buf = [];
142 | htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
143 | return !/^unsafe/.test($$sanitizeUri(uri, isImage));
144 | }));
145 | return buf.join('');
146 | };
147 | }];
148 | }
149 |
150 | function sanitizeText(chars) {
151 | var buf = [];
152 | var writer = htmlSanitizeWriter(buf, angular.noop);
153 | writer.chars(chars);
154 | return buf.join('');
155 | }
156 |
157 |
158 | // Regular Expressions for parsing tags and attributes
159 | var START_TAG_REGEXP =
160 | /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
161 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
162 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
163 | BEGIN_TAG_REGEXP = /^,
164 | BEGING_END_TAGE_REGEXP = /^<\s*\//,
165 | COMMENT_REGEXP = //g,
166 | DOCTYPE_REGEXP = /]*?)>/i,
167 | CDATA_REGEXP = //g,
168 | // Match everything outside of normal chars and " (quote character)
169 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
170 |
171 |
172 | // Good source of info about elements and attributes
173 | // http://dev.w3.org/html5/spec/Overview.html#semantics
174 | // http://simon.html5.org/html-elements
175 |
176 | // Safe Void Elements - HTML5
177 | // http://dev.w3.org/html5/spec/Overview.html#void-elements
178 | var voidElements = makeMap("area,br,col,hr,img,wbr");
179 |
180 | // Elements that you can, intentionally, leave open (and which close themselves)
181 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags
182 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
183 | optionalEndTagInlineElements = makeMap("rp,rt"),
184 | optionalEndTagElements = angular.extend({},
185 | optionalEndTagInlineElements,
186 | optionalEndTagBlockElements);
187 |
188 | // Safe Block Elements - HTML5
189 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
190 | "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
191 | "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
192 |
193 | // Inline Elements - HTML5
194 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
195 | "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
196 | "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
197 |
198 |
199 | // Special Elements (can contain anything)
200 | var specialElements = makeMap("script,style");
201 |
202 | var validElements = angular.extend({},
203 | voidElements,
204 | blockElements,
205 | inlineElements,
206 | optionalEndTagElements);
207 |
208 | //Attributes that have href and hence need to be sanitized
209 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
210 | var validAttrs = angular.extend({}, uriAttrs, makeMap(
211 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
212 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
213 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
214 | 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
215 | 'valign,value,vspace,width'));
216 |
217 | function makeMap(str) {
218 | var obj = {}, items = str.split(','), i;
219 | for (i = 0; i < items.length; i++) obj[items[i]] = true;
220 | return obj;
221 | }
222 |
223 |
224 | /**
225 | * @example
226 | * htmlParser(htmlString, {
227 | * start: function(tag, attrs, unary) {},
228 | * end: function(tag) {},
229 | * chars: function(text) {},
230 | * comment: function(text) {}
231 | * });
232 | *
233 | * @param {string} html string
234 | * @param {object} handler
235 | */
236 | function htmlParser( html, handler ) {
237 | var index, chars, match, stack = [], last = html;
238 | stack.last = function() { return stack[ stack.length - 1 ]; };
239 |
240 | while ( html ) {
241 | chars = true;
242 |
243 | // Make sure we're not in a script or style element
244 | if ( !stack.last() || !specialElements[ stack.last() ] ) {
245 |
246 | // Comment
247 | if ( html.indexOf("", index) === index) {
252 | if (handler.comment) handler.comment( html.substring( 4, index ) );
253 | html = html.substring( index + 3 );
254 | chars = false;
255 | }
256 | // DOCTYPE
257 | } else if ( DOCTYPE_REGEXP.test(html) ) {
258 | match = html.match( DOCTYPE_REGEXP );
259 |
260 | if ( match ) {
261 | html = html.replace( match[0] , '');
262 | chars = false;
263 | }
264 | // end tag
265 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
266 | match = html.match( END_TAG_REGEXP );
267 |
268 | if ( match ) {
269 | html = html.substring( match[0].length );
270 | match[0].replace( END_TAG_REGEXP, parseEndTag );
271 | chars = false;
272 | }
273 |
274 | // start tag
275 | } else if ( BEGIN_TAG_REGEXP.test(html) ) {
276 | match = html.match( START_TAG_REGEXP );
277 |
278 | if ( match ) {
279 | html = html.substring( match[0].length );
280 | match[0].replace( START_TAG_REGEXP, parseStartTag );
281 | chars = false;
282 | }
283 | }
284 |
285 | if ( chars ) {
286 | index = html.indexOf("<");
287 |
288 | var text = index < 0 ? html : html.substring( 0, index );
289 | html = index < 0 ? "" : html.substring( index );
290 |
291 | if (handler.chars) handler.chars( decodeEntities(text) );
292 | }
293 |
294 | } else {
295 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
296 | function(all, text){
297 | text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
298 |
299 | if (handler.chars) handler.chars( decodeEntities(text) );
300 |
301 | return "";
302 | });
303 |
304 | parseEndTag( "", stack.last() );
305 | }
306 |
307 | if ( html == last ) {
308 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
309 | "of html: {0}", html);
310 | }
311 | last = html;
312 | }
313 |
314 | // Clean up any remaining tags
315 | parseEndTag();
316 |
317 | function parseStartTag( tag, tagName, rest, unary ) {
318 | tagName = angular.lowercase(tagName);
319 | if ( blockElements[ tagName ] ) {
320 | while ( stack.last() && inlineElements[ stack.last() ] ) {
321 | parseEndTag( "", stack.last() );
322 | }
323 | }
324 |
325 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
326 | parseEndTag( "", tagName );
327 | }
328 |
329 | unary = voidElements[ tagName ] || !!unary;
330 |
331 | if ( !unary )
332 | stack.push( tagName );
333 |
334 | var attrs = {};
335 |
336 | rest.replace(ATTR_REGEXP,
337 | function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
338 | var value = doubleQuotedValue
339 | || singleQuotedValue
340 | || unquotedValue
341 | || '';
342 |
343 | attrs[name] = decodeEntities(value);
344 | });
345 | if (handler.start) handler.start( tagName, attrs, unary );
346 | }
347 |
348 | function parseEndTag( tag, tagName ) {
349 | var pos = 0, i;
350 | tagName = angular.lowercase(tagName);
351 | if ( tagName )
352 | // Find the closest opened tag of the same type
353 | for ( pos = stack.length - 1; pos >= 0; pos-- )
354 | if ( stack[ pos ] == tagName )
355 | break;
356 |
357 | if ( pos >= 0 ) {
358 | // Close all the open elements, up the stack
359 | for ( i = stack.length - 1; i >= pos; i-- )
360 | if (handler.end) handler.end( stack[ i ] );
361 |
362 | // Remove the open elements from the stack
363 | stack.length = pos;
364 | }
365 | }
366 | }
367 |
368 | var hiddenPre=document.createElement("pre");
369 | var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
370 | /**
371 | * decodes all entities into regular string
372 | * @param value
373 | * @returns {string} A string with decoded entities.
374 | */
375 | function decodeEntities(value) {
376 | if (!value) { return ''; }
377 |
378 | // Note: IE8 does not preserve spaces at the start/end of innerHTML
379 | // so we must capture them and reattach them afterward
380 | var parts = spaceRe.exec(value);
381 | var spaceBefore = parts[1];
382 | var spaceAfter = parts[3];
383 | var content = parts[2];
384 | if (content) {
385 | hiddenPre.innerHTML=content.replace(/= 1536 && c <= 1540) ||
414 | c == 1807 ||
415 | c == 6068 ||
416 | c == 6069 ||
417 | (c >= 8204 && c <= 8207) ||
418 | (c >= 8232 && c <= 8239) ||
419 | (c >= 8288 && c <= 8303) ||
420 | c == 65279 ||
421 | (c >= 65520 && c <= 65535)) return '' + c + ';';
422 | return value; // avoids multilingual issues
423 | }).
424 | replace(//g, '>');
426 | }
427 |
428 | var trim = (function() {
429 | // native trim is way faster: http://jsperf.com/angular-trim-test
430 | // but IE doesn't have it... :-(
431 | // TODO: we should move this into IE/ES5 polyfill
432 | if (!String.prototype.trim) {
433 | return function(value) {
434 | return angular.isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value;
435 | };
436 | }
437 | return function(value) {
438 | return angular.isString(value) ? value.trim() : value;
439 | };
440 | })();
441 |
442 | // Custom logic for accepting certain style options only - textAngular
443 | // Currently allows only the color attribute, all other attributes should be easily done through classes.
444 | function validStyles(styleAttr){
445 | var result = '';
446 | var styleArray = styleAttr.split(';');
447 | angular.forEach(styleArray, function(value){
448 | var v = value.split(':');
449 | if(v.length == 2){
450 | var key = trim(angular.lowercase(v[0]));
451 | var value = trim(angular.lowercase(v[1]));
452 | if(
453 | key === 'color' && (
454 | value.match(/^rgb\([0-9%,\. ]*\)$/i)
455 | || value.match(/^rgba\([0-9%,\. ]*\)$/i)
456 | || value.match(/^hsl\([0-9%,\. ]*\)$/i)
457 | || value.match(/^hsla\([0-9%,\. ]*\)$/i)
458 | || value.match(/^#[0-9a-f]{3,6}$/i)
459 | || value.match(/^[a-z]*$/i)
460 | )
461 | ||
462 | key === 'text-align' && (
463 | value === 'left'
464 | || value === 'right'
465 | || value === 'center'
466 | || value === 'justify'
467 | )
468 | ) result += key + ': ' + value + ';';
469 | }
470 | });
471 | return result;
472 | }
473 |
474 | /**
475 | * create an HTML/XML writer which writes to buffer
476 | * @param {Array} buf use buf.jain('') to get out sanitized html string
477 | * @returns {object} in the form of {
478 | * start: function(tag, attrs, unary) {},
479 | * end: function(tag) {},
480 | * chars: function(text) {},
481 | * comment: function(text) {}
482 | * }
483 | */
484 | function htmlSanitizeWriter(buf, uriValidator){
485 | var ignore = false;
486 | var out = angular.bind(buf, buf.push);
487 | return {
488 | start: function(tag, attrs, unary){
489 | tag = angular.lowercase(tag);
490 | if (!ignore && specialElements[tag]) {
491 | ignore = tag;
492 | }
493 | if (!ignore && validElements[tag] === true) {
494 | out('<');
495 | out(tag);
496 | angular.forEach(attrs, function(value, key){
497 | var lkey=angular.lowercase(key);
498 | var isImage=(tag === 'img' && lkey === 'src') || (lkey === 'background');
499 | if ((lkey === 'style' && (value = validStyles(value)) !== '') || validAttrs[lkey] === true &&
500 | (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
501 | out(' ');
502 | out(key);
503 | out('="');
504 | out(encodeEntities(value));
505 | out('"');
506 | }
507 | });
508 | out(unary ? '/>' : '>');
509 | }
510 | },
511 | end: function(tag){
512 | tag = angular.lowercase(tag);
513 | if (!ignore && validElements[tag] === true) {
514 | out('');
515 | out(tag);
516 | out('>');
517 | }
518 | if (tag == ignore) {
519 | ignore = false;
520 | }
521 | },
522 | chars: function(chars){
523 | if (!ignore) {
524 | out(encodeEntities(chars));
525 | }
526 | }
527 | };
528 | }
529 |
530 |
531 | // define ngSanitize module and register $sanitize service
532 | angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
533 |
534 | /* global sanitizeText: false */
535 |
536 | /**
537 | * @ngdoc filter
538 | * @name ngSanitize.filter:linky
539 | * @function
540 | *
541 | * @description
542 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
543 | * plain email address links.
544 | *
545 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
546 | *
547 | * @param {string} text Input text.
548 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
549 | * @returns {string} Html-linkified text.
550 | *
551 | * @usage
552 |
553 | *
554 | * @example
555 |
556 |
557 |
568 |
569 | Snippet:
570 |
571 |
572 | Filter
573 | Source
574 | Rendered
575 |
576 |
577 | linky filter
578 |
579 | <div ng-bind-html="snippet | linky"> </div>
580 |
581 |
582 |
583 |
584 |
585 |
586 | linky target
587 |
588 | <div ng-bind-html="snippetWithTarget | linky:'_blank'"> </div>
589 |
590 |
591 |
592 |
593 |
594 |
595 | no filter
596 | <div ng-bind="snippet"> </div>
597 |
598 |
599 |
600 |
601 |
602 | it('should linkify the snippet with urls', function() {
603 | expect(using('#linky-filter').binding('snippet | linky')).
604 | toBe('Pretty text with some links:
' +
605 | 'http://angularjs.org/ ,
' +
606 | 'us@somewhere.org ,
' +
607 | 'another@somewhere.org ,
' +
608 | 'and one more: ftp://127.0.0.1/ .');
609 | });
610 |
611 | it ('should not linkify snippet without the linky filter', function() {
612 | expect(using('#escaped-html').binding('snippet')).
613 | toBe("Pretty text with some links:\n" +
614 | "http://angularjs.org/,\n" +
615 | "mailto:us@somewhere.org,\n" +
616 | "another@somewhere.org,\n" +
617 | "and one more: ftp://127.0.0.1/.");
618 | });
619 |
620 | it('should update', function() {
621 | input('snippet').enter('new http://link.');
622 | expect(using('#linky-filter').binding('snippet | linky')).
623 | toBe('new http://link .');
624 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
625 | });
626 |
627 | it('should work with the target property', function() {
628 | expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")).
629 | toBe('http://angularjs.org/ ');
630 | });
631 |
632 |
633 | */
634 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
635 | var LINKY_URL_REGEXP =
636 | /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
637 | MAILTO_REGEXP = /^mailto:/;
638 |
639 | return function(text, target) {
640 | if (!text) return text;
641 | var match;
642 | var raw = text;
643 | var html = [];
644 | var url;
645 | var i;
646 | while ((match = raw.match(LINKY_URL_REGEXP))) {
647 | // We can not end in these as they are sometimes found at the end of the sentence
648 | url = match[0];
649 | // if we did not match ftp/http/mailto then assume mailto
650 | if (match[2] == match[3]) url = 'mailto:' + url;
651 | i = match.index;
652 | addText(raw.substr(0, i));
653 | addLink(url, match[0].replace(MAILTO_REGEXP, ''));
654 | raw = raw.substring(i + match[0].length);
655 | }
656 | addText(raw);
657 | return $sanitize(html.join(''));
658 |
659 | function addText(text) {
660 | if (!text) {
661 | return;
662 | }
663 | html.push(sanitizeText(text));
664 | }
665 |
666 | function addLink(url, text) {
667 | html.push('
');
676 | addText(text);
677 | html.push(' ');
678 | }
679 | };
680 | }]);
681 |
682 |
683 | })(window, window.angular);
684 |
--------------------------------------------------------------------------------