├── img └── color-picker │ ├── hue.png │ ├── alpha.png │ └── saturation.png ├── less ├── wysiwyg.less ├── variables.less ├── dropdowns.less └── colorpicker.less ├── .gitignore ├── bower.json ├── test ├── textAngularSanitize │ ├── ngBindHtml.spec.js │ ├── linky.spec.js │ └── sanitize.spec.js ├── taFixChrome.spec.js ├── taRegisterTool.spec.js ├── taSanitize.spec.js ├── taTools.spec.js ├── textAngularManager.spec.js ├── textAngularToolbar.spec.js └── taBind.spec.js ├── textAngular-dropdownToggle.js ├── package.json ├── Gruntfile.js ├── karma.conf.js ├── changelog.md ├── demo ├── demo.html ├── static-demo.html └── textAngular.com.html ├── textAngular-sanitize.min.js ├── README.md ├── bootstrap-colorpicker-module.js ├── textAngular.min.js └── textAngular-sanitize.js /img/color-picker/hue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfrye/textAngular/HEAD/img/color-picker/hue.png -------------------------------------------------------------------------------- /img/color-picker/alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfrye/textAngular/HEAD/img/color-picker/alpha.png -------------------------------------------------------------------------------- /less/wysiwyg.less: -------------------------------------------------------------------------------- 1 | @import 'variables.less'; 2 | @import 'dropdowns.less'; 3 | @import 'colorpicker.less'; -------------------------------------------------------------------------------- /img/color-picker/saturation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfrye/textAngular/HEAD/img/color-picker/saturation.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage/* 3 | lib-cov 4 | bower_components/* 5 | node_modules/* 6 | *.seed 7 | *.log 8 | *.csv 9 | *.dat 10 | *.out 11 | *.pid 12 | *.gz 13 | *.iml 14 | 15 | pids 16 | logs 17 | results 18 | 19 | npm-debug.log 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textAngular", 3 | "version": "1.2.0", 4 | "main": "./textAngular.js", 5 | "description": "A radically powerful Text-Editor/Wysiwyg editor for Angular.js", 6 | "keywords": [ 7 | "editor", 8 | "angular", 9 | "wysiwyg", 10 | "jquery" 11 | ], 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test*", 17 | "demo*", 18 | "Gruntfile.js", 19 | "package.json" 20 | ], 21 | "dependencies": { 22 | "angular": "~1.2.x" 23 | }, 24 | "devDependencies": { 25 | "angular-mocks": "~1.x", 26 | "jquery": "1.9.x", 27 | "bootstrap": "3.0.x", 28 | "font-awesome": "4.0.x", 29 | "rangy": "1.2.x" 30 | }, 31 | "license": "MIT", 32 | "homepage": "https://github.com/fraywing/textAngular" 33 | } 34 | -------------------------------------------------------------------------------- /less/variables.less: -------------------------------------------------------------------------------- 1 | 2 | // Color-picker img path 3 | @img-path: '../../img/color-picker'; 4 | 5 | // WYSIWYG 6 | @brand-primary: #428bca; 7 | 8 | //** Unit-less `line-height` for use in components like buttons. 9 | @line-height-base: 1.428571429; // 20/14 10 | 11 | //** Global color for active items (e.g., navs or dropdowns). 12 | @component-active-color: #fff; 13 | //** Global background color for active items (e.g., navs or dropdowns). 14 | @component-active-bg: @brand-primary; 15 | 16 | //** Dropdown link text color. 17 | @dropdown-link-color: @gray-dark; 18 | //** Hover color for dropdown links. 19 | @dropdown-link-hover-color: darken(@gray-dark, 5%); 20 | //** Hover background for dropdown links. 21 | @dropdown-link-hover-bg: #f5f5f5; 22 | 23 | //** Active dropdown menu item text color. 24 | @dropdown-link-active-color: @component-active-color; 25 | //** Active dropdown menu item background color. 26 | @dropdown-link-active-bg: @component-active-bg; -------------------------------------------------------------------------------- /test/textAngularSanitize/ngBindHtml.spec.js: -------------------------------------------------------------------------------- 1 | describe('ngBindHtml', function() { 2 | 'use strict'; 3 | beforeEach(module('ngSanitize')); 4 | 5 | it('should set html', inject(function($rootScope, $compile) { 6 | var element = $compile('
')($rootScope); 7 | $rootScope.html = '
hello
'; 8 | $rootScope.$digest(); 9 | expect(angular.lowercase(element.html())).toEqual('
hello
'); 10 | })); 11 | 12 | 13 | it('should reset html when value is null or undefined', inject(function($compile, $rootScope) { 14 | var element = $compile('
')($rootScope); 15 | 16 | angular.forEach([null, undefined, ''], function(val) { 17 | $rootScope.html = 'some val'; 18 | $rootScope.$digest(); 19 | expect(angular.lowercase(element.html())).toEqual('some val'); 20 | 21 | $rootScope.html = val; 22 | $rootScope.$digest(); 23 | expect(angular.lowercase(element.html())).toEqual(''); 24 | }); 25 | })); 26 | }); 27 | -------------------------------------------------------------------------------- /textAngular-dropdownToggle.js: -------------------------------------------------------------------------------- 1 | angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) { 2 | var openElement = null, 3 | closeMenu = angular.noop; 4 | return { 5 | restrict: 'CA', 6 | link: function(scope, element, attrs) { 7 | scope.$watch('$location.path', function() { closeMenu(); }); 8 | element.parent().bind('click', function() { closeMenu(); }); 9 | element.bind('click', function (event) { 10 | 11 | var elementWasOpen = (element === openElement); 12 | 13 | event.preventDefault(); 14 | event.stopPropagation(); 15 | 16 | if (!!openElement) { 17 | closeMenu(); 18 | } 19 | 20 | if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) { 21 | element.parent().addClass('open'); 22 | openElement = element; 23 | closeMenu = function (event) { 24 | if (event) { 25 | event.preventDefault(); 26 | event.stopPropagation(); 27 | } 28 | $document.unbind('click', closeMenu); 29 | element.parent().removeClass('open'); 30 | closeMenu = angular.noop; 31 | openElement = null; 32 | }; 33 | $document.bind('click', closeMenu); 34 | } 35 | }); 36 | } 37 | }; 38 | }]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textAngular", 3 | "filename": "textAngular.min.js", 4 | "version": "1.2.0", 5 | "description": "A radically powerful Text-Editor/Wysiwyg editor for Angular.js", 6 | "maintainers": [{"name": "Fraywing"},{"name": "SimeonC"}], 7 | "keywords": [ 8 | "AngularJS", 9 | "text editor", 10 | "WYSIWYG", 11 | "directive" 12 | ], 13 | "license": "MIT", 14 | "homepage": "http://textangular.com", 15 | "main": "./textAngular.js", 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "bower": "*", 19 | "grunt": "~0.4.2", 20 | "grunt-cli": "~0.1.11", 21 | "grunt-contrib-jshint": "~0.8.0", 22 | "grunt-contrib-uglify": "~0.2.7", 23 | "grunt-contrib-clean": "~0.5.0", 24 | "grunt-istanbul-coverage": "0.0.2", 25 | "grunt-karma": "~0.6.2", 26 | "karma-coverage": "~0.1.4", 27 | "karma-script-launcher": "~0.1.0", 28 | "karma-chrome-launcher": "~0.1.2", 29 | "karma-phantomjs-launcher": "~0.1.2", 30 | "karma-html2js-preprocessor": "~0.1.0", 31 | "karma-jasmine": "~0.1.5", 32 | "karma-coffee-preprocessor": "~0.1.2", 33 | "requirejs": "~2.1.10", 34 | "karma-requirejs": "~0.2.1", 35 | "phantomjs" : "1.9.1-0", 36 | "karma": "~0.10.9" 37 | }, 38 | "scripts": { 39 | "test": "grunt", 40 | "postinstall": "./node_modules/bower/bin/bower install" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git://github.com/fraywing/textAngular.git" 45 | } 46 | } -------------------------------------------------------------------------------- /test/textAngularSanitize/linky.spec.js: -------------------------------------------------------------------------------- 1 | describe('linky', function() { 2 | 'use strict'; 3 | 4 | var linky; 5 | 6 | beforeEach(module('ngSanitize')); 7 | 8 | beforeEach(inject(function($filter){ 9 | linky = $filter('linky'); 10 | })); 11 | 12 | it('should do basic filter', function() { 13 | expect(linky("http://ab/ (http://a/) http://1.2/v:~-123. c")). 14 | toEqual('http://ab/ ' + 15 | '(http://a/) ' + 16 | '<http://a/> ' + 17 | 'http://1.2/v:~-123. c'); 18 | expect(linky(undefined)).not.toBeDefined(); 19 | }); 20 | 21 | it('should handle mailto:', function() { 22 | expect(linky("mailto:me@example.com")). 23 | toEqual('me@example.com'); 24 | expect(linky("me@example.com")). 25 | toEqual('me@example.com'); 26 | expect(linky("send email to me@example.com, but")). 27 | toEqual('send email to me@example.com, but'); 28 | }); 29 | 30 | it('should handle target:', function() { 31 | expect(linky("http://example.com", "_blank")). 32 | toEqual('http://example.com'); 33 | expect(linky("http://example.com", "someNamedIFrame")). 34 | toEqual('http://example.com'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/taFixChrome.spec.js: -------------------------------------------------------------------------------- 1 | describe('taFixChrome', function(){ 2 | 'Use Strict'; 3 | beforeEach(module('textAngular')); 4 | var taFixChrome; 5 | beforeEach(inject(function(_taFixChrome_){ 6 | taFixChrome = _taFixChrome_; 7 | })); 8 | 9 | describe('should cleanse the following HTML samples from chrome', function(){ 10 | it('should remove the style attributes on a non-span', function(){ 11 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 12 | }); 13 | 14 | it('should remove a span with only those attributes', function(){ 15 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 16 | }); 17 | 18 | it('should the style attributes on a span with other attributes', function(){ 19 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 20 | }); 21 | 22 | it('should leave a span with none of those attributes', function(){ 23 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 24 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 25 | }); 26 | 27 | it('should remove a matching span with its following br', function(){ 28 | expect(taFixChrome('
Test Content
')).toBe('
Test Content
'); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /test/taRegisterTool.spec.js: -------------------------------------------------------------------------------- 1 | describe('Adding tools to taTools', function(){ 2 | 'use strict'; 3 | beforeEach(module('textAngular')); 4 | 5 | it('should require a unique name', inject(function(taRegisterTool){ 6 | expect(taRegisterTool).toThrow("textAngular Error: A unique name is required for a Tool Definition"); 7 | expect(function(){taRegisterTool('');}).toThrow("textAngular Error: A unique name is required for a Tool Definition"); 8 | expect(function(){ 9 | taRegisterTool('test', {iconclass: 'test'}); 10 | taRegisterTool('test', {iconclass: 'test'}); 11 | }).toThrow("textAngular Error: A unique name is required for a Tool Definition"); 12 | })); 13 | it('should require a display element/iconclass/buttontext', inject(function(taRegisterTool){ 14 | expect(function(){taRegisterTool('test1', {});}).toThrow('textAngular Error: Tool Definition for "test1" does not have a valid display/iconclass/buttontext value'); 15 | expect(function(){taRegisterTool('test2', {display: 'testbad'});}).toThrow('textAngular Error: Tool Definition for "test2" does not have a valid display/iconclass/buttontext value'); 16 | expect(function(){taRegisterTool('test3', {iconclass: 'test'});}).not.toThrow('textAngular Error: Tool Definition for "test3" does not have a valid display/iconclass/buttontext value'); 17 | expect(function(){taRegisterTool('test4', {buttontext: 'test'});}).not.toThrow('textAngular Error: Tool Definition for "test4" does not have a valid display/iconclass/buttontext value'); 18 | })); 19 | it('should add a valid tool to taTools', inject(function(taRegisterTool, taTools){ 20 | var toolDef = {iconclass: 'test'}; 21 | taRegisterTool('test5', toolDef); 22 | expect(taTools['test5']).toBe(toolDef); 23 | })); 24 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | // load all grunt tasks 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-contrib-uglify'); 6 | grunt.loadNpmTasks('grunt-contrib-clean'); 7 | grunt.loadNpmTasks('grunt-istanbul-coverage'); 8 | grunt.loadNpmTasks('grunt-karma'); 9 | 10 | // Default task. 11 | grunt.registerTask('default', ['uglify','test']); 12 | grunt.registerTask('test', ['clean', 'jshint', 'karma', 'coverage']); 13 | 14 | var testConfig = function (configFile, customOptions) { 15 | var options = { configFile: configFile, keepalive: true }; 16 | var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; 17 | return grunt.util._.extend(options, customOptions, travisOptions); 18 | }; 19 | 20 | // Project configuration. 21 | grunt.initConfig({ 22 | clean: ["coverage/*.json"], 23 | coverage: { 24 | options: { 25 | thresholds: { 26 | 'statements': 100, 27 | 'branches': 98, 28 | 'lines': 100, 29 | 'functions': 100 30 | }, 31 | dir: 'coverage/' 32 | } 33 | }, 34 | karma: { 35 | unit: { 36 | options: testConfig('karma.conf.js') 37 | } 38 | }, 39 | jshint: { 40 | files: ['textAngular.js', 'test/*.spec.js'],// don't hint the textAngularSanitize as they will fail 41 | options: { 42 | eqeqeq: true, 43 | immed: true, 44 | latedef: true, 45 | newcap: true, 46 | noarg: true, 47 | sub: true, 48 | boss: true, 49 | eqnull: true, 50 | globals: {} 51 | } 52 | }, 53 | uglify: { 54 | options: { 55 | mangle: true, 56 | compress: true 57 | }, 58 | my_target: { 59 | files: { 60 | 'textAngular.min.js': ['textAngular.js'], 61 | 'textAngular-sanitize.min.js': ['textAngular-sanitize.js'] 62 | } 63 | } 64 | } 65 | }); 66 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (config) { 3 | 'use strict'; 4 | config.set({ 5 | 6 | frameworks: ['jasmine'], 7 | 8 | plugins: [ 9 | 'karma-jasmine', 10 | 'karma-phantomJS-launcher', 11 | 'karma-coverage' 12 | ], 13 | 14 | files: [ 15 | 'bower_components/jquery/jquery.min.js', 16 | 'bower_components/rangy/rangy-core.js', 17 | 'bower_components/rangy/rangy-selectionsaverestore.js', 18 | 'bower_components/angular/angular.min.js', 19 | 'bower_components/angular-mocks/angular-mocks.js', 20 | 'textAngular-sanitize.js', 21 | 'textAngular.js', 22 | 'test/**/*.spec.js' 23 | ], 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | 28 | ], 29 | 30 | preprocessors: { 31 | 'textAngular.js': ['coverage'] 32 | }, 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress', 'junit' 36 | reporters: ['progress', 'coverage'], 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // cli runner port 43 | runnerPort: 9100, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: false, 57 | 58 | 59 | // Start these browsers, currently available: 60 | // - Chrome 61 | // - ChromeCanary 62 | // - Firefox 63 | // - Opera 64 | // - Safari (only Mac) 65 | // - PhantomJS 66 | // - IE (only Windows) 67 | browsers: ['PhantomJS'], 68 | 69 | 70 | // If browser does not capture in given timeout [ms], kill it 71 | captureTimeout: 60000, 72 | 73 | 74 | // Continuous Integration mode 75 | // if true, it capture browsers, run tests and exit 76 | singleRun: true 77 | }); 78 | }; -------------------------------------------------------------------------------- /less/dropdowns.less: -------------------------------------------------------------------------------- 1 | #taHtmlElement, #taTextElement { 2 | height: 400px; 3 | overflow-y: scroll; 4 | } 5 | 6 | // Toolbar buttons 7 | .bar-btn-dropdown { 8 | padding: 0; 9 | border: none; 10 | } 11 | 12 | .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) button { 13 | border-radius: 0; 14 | } 15 | 16 | .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) button { 17 | border-bottom-right-radius: 0; 18 | border-top-right-radius: 0; 19 | } 20 | 21 | .btn-group > .btn:last-child:not(:first-child):not(.dropdown-toggle) button { 22 | border-bottom-left-radius: 0; 23 | border-top-left-radius: 0; 24 | } 25 | 26 | // Links within the dropdown menu 27 | .bar-btn-dropdown .dropdown-menu > li > button { 28 | display: block; 29 | padding: 3px 20px; 30 | clear: both; 31 | font-weight: normal; 32 | line-height: @line-height-base; 33 | color: @dropdown-link-color; 34 | white-space: nowrap; // prevent links from randomly breaking onto new lines 35 | background-color: @white; 36 | -webkit-appearance: none; 37 | width: 100%; 38 | border: none; 39 | } 40 | 41 | // Dropdowns with active checkmark 42 | .bar-btn-dropdown .dropdown-menu > li > .checked-dropdown { 43 | padding-left: 30px; 44 | position: relative; 45 | text-align: left; 46 | } 47 | 48 | .bar-btn-dropdown .dropdown-menu > li > button .fa-check { 49 | position: absolute; 50 | left: 5px; 51 | top: 50%; 52 | margin-top: -5px; 53 | font-size: 14px; 54 | } 55 | 56 | .dropdown-menu.inline-opts { 57 | min-width: 0; 58 | } 59 | 60 | // Hover/Focus state 61 | .dropdown-menu > li > button { 62 | &:hover, 63 | &:focus { 64 | text-decoration: none; 65 | color: @dropdown-link-hover-color; 66 | background-color: @dropdown-link-hover-bg; 67 | } 68 | } 69 | 70 | // Active state 71 | .dropdown-menu > .active > button { 72 | &, 73 | &:hover, 74 | &:focus { 75 | color: @dropdown-link-active-color; 76 | text-decoration: none; 77 | outline: 0; 78 | background-color: @dropdown-link-active-bg; 79 | } 80 | } -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ###Changelog 2 | 3 | 2014-02-28 v1.2.0 4 | 5 | - Lots and Lots of changes, too many to list. Structural changes and added functionality. Supports down to IE8 and all other browsers. 6 | 7 | 2013-12-11 v1.1.2 8 | 9 | - Updated to work correctly with IE (console.log bug) 10 | 11 | 2013-12-11 v1.1.2-pre3 12 | 13 | - Added support for .focussed class and ng-focus to allow dynamic styling on focus events. #47 14 | - Updates to fix Angular.JS breaking with parameter renaming minification. #49 15 | - Minor bug fix to disable links from being 'clickable' in the editor. 16 | - Updated the default toolbar to include ALL default tools. 17 | - Update the tools to use activeState better. 18 | - Small update to allow use of ta-bind outside of textAngular. 19 | - Changed the raw html view to use a text-area for better compatability. 20 | 21 | 2013-12-09 v1.1.2-pre2 22 | 23 | - Added input for form submission. #43 24 | - Slight restructure and update into of /demo. 25 | - Moved a lot of the README.md to the online Wiki. #34 26 | - Changed pre-release tag names to -preX as we aren't really doing alpha - beta - RC format. 27 | 28 | 2013-12-05 v1.1.2-alpha (v1.1.2-pre1) 29 | 30 | - Added bundled demo pages. 31 | - Fixed Escaping of < and > #30 32 | - Fixed stripping of style and class attributes and other parsing issues whilst maintaining the chrome fixes. #35 #30 #5 33 | - Fixed two-way-binding not working #38 34 | - Updated Readme.md and consolidated the readme out of the textAngular.js file. 35 | 36 | 2013-12-2 v1.1.1 37 | 38 | - Fixed buttons still submitting form. #29 39 | - Fix for Null ngModel value. Thanks to @slobo #22 40 | - Added Ability to override just "display" for default button set. Thanks to @slobo #27 41 | 42 | 2013-11-9 v1.1.0 43 | 44 | - Re-written to only depend on Angular and Angular-Sanitize. No More jQuery. 45 | - Re-worked to be more angular-esq in it's initiation and use. Less reliance on global variables except for defaults and more use of bindings on attributes. 46 | - Default styles are Bootstrap 3 classes, options to change these classes. 47 | - Restructured the Toolbar to make it more plugin friendly, all tool buttons are encapsulated in their own scope that is a child of the individual textAngular bound scope. 48 | 49 | 2013-11-6 v1.0.3 50 | 51 | - $sce isn't required anymore* Thanks to @nadeeshacabral 52 | - bower support added* Thanks to @edouard-lopez 53 | 54 | 2013-10-11 v1.0.2 55 | 56 | - Fixed issue with images not calling the compileHTML method* 57 | - Fixed issue in chrome where styling was getting added for unordered lists* 58 | - You can now change the model from the outside and have it affect the textAngular instance contents* 59 | - Cleaned up code* 60 | 61 | 2013-10-10 v1.0.1 62 | 63 | - Added Tooltip Option, title has been renamed icon, and title is now the tooltip* 64 | - The minified version actually works now* 65 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | textAngular 1.2.0 Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

Editor

31 | 32 |
33 |

Raw HTML in a text area

34 | 35 |

Bound with ng-bind-html

36 |
37 |

Bound with ta-bind, our internal html-binding directive

38 |
39 | 40 |

Note: although we support classes and styles, angularjs' ng-bind-html directive will strip out all style attributes.

41 | 42 |

Option to masquerade as a fancy text-area - complete with form submission and optional ngModel

43 | 44 |

Any HTML we put in-between the text-angular tags gets automatically put into the editor if there is not a value assigned to the ngModel.

45 |

If there is a value assigned to the ngModel, it replaces any html here. To see this, uncomment the line at the bottom of demo.html

46 |
47 |

Bound with ta-bind, our internal html-binding directive

48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /less/colorpicker.less: -------------------------------------------------------------------------------- 1 | // Colorpicker 2 | .colorpicker-visible, 3 | .colorpicker-visible .dropdown-menu { 4 | display: block !important; 5 | } 6 | 7 | colorpicker-saturation { 8 | display: block; 9 | width: 100px; 10 | height: 100px; 11 | background-image: data-uri('@{img-path}/saturation.png'); 12 | cursor: crosshair; 13 | float: left; 14 | i { 15 | display: block; 16 | height: 7px; 17 | width: 7px; 18 | border: 1px solid #000; 19 | border-radius: 5px; 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | margin: -4px 0 0 -4px; 24 | &::after { 25 | content: ''; 26 | display: block; 27 | height: 7px; 28 | width: 7px; 29 | border: 1px solid #fff; 30 | border-radius: 5px; 31 | } 32 | } 33 | } 34 | 35 | colorpicker-hue, 36 | colorpicker-alpha { 37 | width: 15px; 38 | height: 100px; 39 | float: left; 40 | cursor: row-resize; 41 | margin-left: 4px; 42 | margin-bottom: 4px; 43 | i { 44 | display: block; 45 | height: 2px; 46 | background: #000; 47 | border-top: 1px solid #fff; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | width: 100%; 52 | margin-top: -1px; 53 | } 54 | } 55 | 56 | colorpicker-hue { 57 | background-image: data-uri('@{img-path}/hue.png'); 58 | } 59 | 60 | colorpicker-alpha { 61 | display: none; 62 | } 63 | 64 | colorpicker-alpha, 65 | .colorpicker-color { 66 | background-image: data-uri('@{img-path}/alpha.png'); 67 | } 68 | 69 | .colorpicker { 70 | top: 0; 71 | left: 0; 72 | position: absolute; 73 | z-index: 9999; 74 | display: none; 75 | colorpicker-hue, 76 | colorpicker-alpha, 77 | colorpicker-saturation { 78 | position: relative; 79 | } 80 | input { 81 | width: 100px; 82 | font-size: 11px; 83 | color: #000; 84 | background-color: #fff; 85 | } 86 | &.alpha { 87 | min-width: 140px; 88 | colorpicker-alpha { 89 | display: block; 90 | } 91 | } 92 | &.colorpicker-fixed-position { 93 | position: fixed; 94 | } 95 | .dropdown-menu { 96 | &::after, 97 | &::before { 98 | content: ''; 99 | display: inline-block; 100 | position: absolute; 101 | } 102 | &::after { 103 | clear: both; 104 | border: 6px solid transparent; 105 | top: -5px; 106 | left: 7px; 107 | } 108 | &::before { 109 | border: 7px solid transparent; 110 | top: -6px; 111 | left: 6px; 112 | } 113 | } 114 | .dropdown-menu { 115 | position: static; 116 | top: 0; 117 | left: 0; 118 | min-width: 129px; 119 | padding: 4px; 120 | margin-top: 0; 121 | } 122 | } 123 | 124 | .colorpicker-position-top { 125 | .dropdown-menu { 126 | &::after { 127 | border-top: 6px solid #fff; 128 | border-bottom: 0; 129 | top: auto; 130 | bottom: -5px; 131 | } 132 | &::before { 133 | border-top: 7px solid rgba(0, 0, 0, 0.2); 134 | border-bottom: 0; 135 | top: auto; 136 | bottom: -6px; 137 | } 138 | } 139 | } 140 | 141 | .colorpicker-position-right { 142 | .dropdown-menu { 143 | &::after { 144 | border-right: 6px solid #fff; 145 | border-left: 0; 146 | top: 11px; 147 | left: -5px; 148 | } 149 | &::before { 150 | border-right: 7px solid rgba(0, 0, 0, 0.2); 151 | border-left: 0; 152 | top: 10px; 153 | left: -6px; 154 | } 155 | } 156 | } 157 | 158 | .colorpicker-position-bottom { 159 | .dropdown-menu { 160 | &::after { 161 | border-bottom: 6px solid #fff; 162 | border-top: 0; 163 | } 164 | &::before { 165 | border-bottom: 7px solid rgba(0, 0, 0, 0.2); 166 | border-top: 0; 167 | } 168 | } 169 | } 170 | 171 | .colorpicker-position-left { 172 | .dropdown-menu { 173 | &::after { 174 | border-left: 6px solid #fff; 175 | border-right: 0; 176 | top: 11px; 177 | left: auto; 178 | right: -5px; 179 | } 180 | &::before { 181 | border-left: 7px solid rgba(0, 0, 0, 0.2); 182 | border-right: 0; 183 | top: 10px; 184 | left: auto; 185 | right: -6px; 186 | } 187 | } 188 | } 189 | 190 | colorpicker-preview { 191 | display: block; 192 | height: 10px; 193 | margin: 5px 0 3px 0; 194 | clear: both; 195 | background-position: 0 100%; 196 | } -------------------------------------------------------------------------------- /textAngular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";function c(){this.$get=["$$sanitizeUri",function(a){return function(b){var c=[];return f(b,j(c,function(b,c){return!/^unsafe/.test(a(b,c))})),c.join("")}}]}function d(a){var c=[],d=j(c,b.noop);return d.chars(a),c.join("")}function e(a){var b,c={},d=a.split(",");for(b=0;b=0&&j[f]!=d;f--);if(f>=0){for(e=j.length-1;e>=f;e--)c.end&&c.end(j[e]);j.length=f}}var f,h,i,j=[],t=a;for(j.last=function(){return j[j.length-1]};a;){if(h=!0,j.last()&&A[j.last()])a=a.replace(new RegExp("(.*)<\\s*\\/\\s*"+j.last()+"[^>]*>","i"),function(a,b){return b=b.replace(q,"$1").replace(s,"$1"),c.chars&&c.chars(g(b)),""}),e("",j.last());else if(0===a.indexOf("",f)===f&&(c.comment&&c.comment(a.substring(4,f)),a=a.substring(f+3),h=!1)):r.test(a)?(i=a.match(r),i&&(a=a.replace(i[0],""),h=!1)):p.test(a)?(i=a.match(m),i&&(a=a.substring(i[0].length),i[0].replace(m,e),h=!1)):o.test(a)&&(i=a.match(l),i&&(a=a.substring(i[0].length),i[0].replace(l,d),h=!1)),h){f=a.indexOf("<");var v=0>f?a:a.substring(0,f);a=0>f?"":a.substring(f),c.chars&&c.chars(g(v))}if(a==t)throw k("badparse","The sanitizer was unable to parse the following block of html: {0}",a);t=a}e()}function g(a){if(!a)return"";var b=F.exec(a),c=b[1],d=b[3],e=b[2];return e&&(E.innerHTML=e.replace(/=b||173==b||b>=1536&&1540>=b||1807==b||6068==b||6069==b||b>=8204&&8207>=b||b>=8232&&8239>=b||b>=8288&&8303>=b||65279==b||b>=65520&&65535>=b?"&#"+b+";":a}).replace(//g,">")}function i(a){var c="",d=a.split(";");return b.forEach(d,function(a){var d=a.split(":");if(2==d.length){var e=G(b.lowercase(d[0])),a=G(b.lowercase(d[1]));("color"===e&&(a.match(/^rgb\([0-9%,\. ]*\)$/i)||a.match(/^rgba\([0-9%,\. ]*\)$/i)||a.match(/^hsl\([0-9%,\. ]*\)$/i)||a.match(/^hsla\([0-9%,\. ]*\)$/i)||a.match(/^#[0-9a-f]{3,6}$/i)||a.match(/^[a-z]*$/i))||"text-align"===e&&("left"===a||"right"===a||"center"===a||"justify"===a))&&(c+=e+": "+a+";")}}),c}function j(a,c){var d=!1,e=b.bind(a,a.push);return{start:function(a,f,g){a=b.lowercase(a),!d&&A[a]&&(d=a),d||B[a]!==!0||(e("<"),e(a),b.forEach(f,function(d,f){var g=b.lowercase(f),j="img"===a&&"src"===g||"background"===g;("style"===g&&""!==(d=i(d))||D[g]===!0&&(C[g]!==!0||c(d,j)))&&(e(" "),e(f),e('="'),e(h(d)),e('"'))}),e(g?"/>":">"))},end:function(a){a=b.lowercase(a),d||B[a]!==!0||(e("")),a==d&&(d=!1)},chars:function(a){d||e(h(a))}}}var k=b.$$minErr("$sanitize"),l=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,m=/^<\s*\/\s*([\w:-]+)[^>]*>/,n=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,o=/^/g,r=/]*?)>/i,s=//g,t=/([^\#-~| |!])/g,u=e("area,br,col,hr,img,wbr"),v=e("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),w=e("rp,rt"),x=b.extend({},w,v),y=b.extend({},v,e("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),z=b.extend({},w,e("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),A=e("script,style"),B=b.extend({},u,y,z,x),C=e("background,cite,href,longdesc,src,usemap"),D=b.extend({},C,e("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width")),E=document.createElement("pre"),F=/^(\s*)([\s\S]*?)(\s*)$/,G=function(){return String.prototype.trim?function(a){return b.isString(a)?a.trim():a}:function(a){return b.isString(a)?a.replace(/^\s\s*/,"").replace(/\s\s*$/,""):a}}();b.module("ngSanitize",[]).provider("$sanitize",c),b.module("ngSanitize").filter("linky",["$sanitize",function(a){var c=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,e=/^mailto:/;return function(f,g){function h(a){a&&n.push(d(a))}function i(a,c){n.push("'),h(c),n.push("")}if(!f)return f;for(var j,k,l,m=f,n=[];j=m.match(c);)k=j[0],j[2]==j[3]&&(k="mailto:"+k),l=j.index,h(m.substr(0,l)),i(k,j[0].replace(e,"")),m=m.substring(l+j[0].length);return h(m),a(n.join(""))}}])}(window,window.angular); -------------------------------------------------------------------------------- /test/taSanitize.spec.js: -------------------------------------------------------------------------------- 1 | describe('taSanitize', function(){ 2 | 'use strict'; 3 | beforeEach(module('textAngular')); 4 | beforeEach(module('ngSanitize')); 5 | describe('should change all align attributes to text-align styles for HTML5 compatability', function(){ 6 | it('should correct left align', inject(function(taSanitize){ 7 | var safe = angular.element(taSanitize('
')); 8 | expect(safe.attr('align')).not.toBeDefined(); 9 | expect(safe.css('text-align')).toBe('left'); 10 | })); 11 | it('should correct right align', inject(function(taSanitize){ 12 | var safe = angular.element(taSanitize('
')); 13 | expect(safe.attr('align')).not.toBeDefined(); 14 | expect(safe.css('text-align')).toBe('right'); 15 | })); 16 | it('should correct center align', inject(function(taSanitize){ 17 | var safe = angular.element(taSanitize('
')); 18 | expect(safe.attr('align')).not.toBeDefined(); 19 | expect(safe.css('text-align')).toBe('center'); 20 | })); 21 | it('should correct justify align', inject(function(taSanitize){ 22 | var safe = angular.element(taSanitize('
')); 23 | expect(safe.attr('align')).not.toBeDefined(); 24 | expect(safe.css('text-align')).toBe('justify'); 25 | })); 26 | }); 27 | 28 | describe('if invalid HTML', function(){ 29 | it('should return the oldsafe passed in', inject(function(taSanitize){ 30 | var result = taSanitize('')); 44 | expect(result.attr('style')).toBe('color: blue;'); 45 | })); 46 | it('hex value', inject(function(taSanitize){ 47 | var result = angular.element(taSanitize('
')); 48 | expect(result.attr('style')).toBe('color: #000000;'); 49 | })); 50 | it('rgba', inject(function(taSanitize){ 51 | var result = angular.element(taSanitize('
')); 52 | expect(result.attr('style')).toBe('color: rgba(20, 20, 20, 0.5);'); 53 | })); 54 | it('rgb', inject(function(taSanitize){ 55 | var result = angular.element(taSanitize('
')); 56 | expect(result.attr('style')).toBe('color: rgb(20, 20, 20);'); 57 | })); 58 | it('hsl', inject(function(taSanitize){ 59 | var result = angular.element(taSanitize('
')); 60 | expect(result.attr('style')).toBe('color: hsl(20, 20%, 20%);'); 61 | })); 62 | it('hlsa', inject(function(taSanitize){ 63 | var result = angular.element(taSanitize('
')); 64 | expect(result.attr('style')).toBe('color: hsla(20, 20%, 20%, 0.5);'); 65 | })); 66 | it('bad value not accepted', inject(function(taSanitize){ 67 | var result = taSanitize('
'); 68 | expect(result).toBe('
'); 69 | })); 70 | }); 71 | 72 | describe('validated text-align attribute', function(){ 73 | it('left', inject(function(taSanitize){ 74 | var result = angular.element(taSanitize('
')); 75 | expect(result.attr('style')).toBe('text-align: left;'); 76 | })); 77 | it('right', inject(function(taSanitize){ 78 | var result = angular.element(taSanitize('
')); 79 | expect(result.attr('style')).toBe('text-align: right;'); 80 | })); 81 | it('center', inject(function(taSanitize){ 82 | var result = angular.element(taSanitize('
')); 83 | expect(result.attr('style')).toBe('text-align: center;'); 84 | })); 85 | it('justify', inject(function(taSanitize){ 86 | var result = angular.element(taSanitize('
')); 87 | expect(result.attr('style')).toBe('text-align: justify;'); 88 | })); 89 | it('bad value not accepted', inject(function(taSanitize){ 90 | var result = taSanitize('
'); 91 | expect(result).toBe('
'); 92 | })); 93 | }); 94 | 95 | describe('un-validated are removed', function(){ 96 | it('removes non whitelisted values', inject(function(taSanitize){ 97 | var result = taSanitize('
'); 98 | expect(result).toBe('
'); 99 | })); 100 | it('removes non whitelisted values leaving valid values', inject(function(taSanitize){ 101 | var result = angular.element(taSanitize('
')); 102 | expect(result.attr('style')).toBe('text-align: left;'); 103 | })); 104 | }); 105 | }); 106 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Full Featured textAngular 2 | ========================= 3 | 4 | I built this because I needed a lot more stuff that wasn't included in the default textAngular. This is a work in progress and currently includes the following extras: 5 | 6 | - Colorpicker via angular-bootstrap-colorpicker 7 | - Font family 8 | - Font size 9 | - Indent / outdent 10 | - Added angular-ui bootstrap dropdown toggle to hold common items 11 | - h1-h6 and p formatBlocks 12 | - text alignment 13 | 14 | Many thanks for the hard work put in by @fraywing on textAngular and @buberdds on angular-bootstrap-colorpicker. 15 | 16 | ## Requirements 17 | 18 | 1. `AngularJS` ≥ `1.2.x` ; 19 | 2. `Angular Sanitize` ≥ `1.2.x`. 20 | 21 | ### Optional requirements 22 | 23 | 1. [Bootstrap 3.x](http://getbootstrap.com/) for the default styles 24 | 2. [Font-Awesome 4.x](http://fortawesome.github.io/Font-Awesome/) for the default icons on the toolbar 25 | 3. [Rangy 1.x](https://code.google.com/p/rangy/) for better activeState detection and more dynamic plugins, also the selectionsaverestore module. 26 | 27 | ### Where to get it 28 | 29 | **NOTE:** If you are using `angular-sanitize` anywhere you need to pick one of `angular-sanitize` OR `textAngular-sanitize` as the latter is our fork of the `angular-sanitize` file so loading both may cause conflicts. 30 | 31 | **Via Bower:** 32 | 33 | Run `bower install textAngular` from the command line. 34 | Include script tags similar to the following: 35 | ```html 36 | 37 | 38 | ``` 39 | 40 | **Via CDNJS:** 41 | 42 | Include script tags similar to the following: 43 | ```html 44 | 45 | 46 | ``` 47 | 48 | **Via jsDelivr:** 49 | 50 | Include script tags similar to the following: 51 | ```html 52 | 53 | 54 | ``` 55 | 56 | **Via Github** 57 | 58 | Download the code from [https://github.com/fraywing/textAngular/releases/latest](https://github.com/fraywing/textAngular/releases/latest), unzip the files then add script tags similar to the following: 59 | ```html 60 | 61 | 62 | ``` 63 | 64 | ### Usage 65 | 66 | 1. Include `textAngular.js` or `textAngular.min.js` and `textAngular-sanitize.js` or `textAngular-sanitize.min.js` in your project using script tags 67 | 2. Add a dependency to `textAngular` in your app module, for example: ```angular.module('myModule', ['textAngular'])```. 68 | 3. Create an element to hold the editor and add an `ng-model="htmlVariable"` attribute where `htmtlVariable` is the scope variable that will hold the HTML entered into the editor: 69 | ```html 70 |
71 | ``` 72 | OR 73 | ```html 74 | 75 | ``` 76 | This acts similar to a regular AngularJS / form input if you give it a name attribute, allowing for form submission and AngularJS form validation. 77 | 78 | Have fun! 79 | 80 | **Important Note:** Though textAngular supports the use of all attributes in it's input, please note that angulars ng-bind-html **WILL** strip out all of your style attributes. 81 | 82 | For Additional options see the [github Wiki](https://github.com/fraywing/textAngular/wiki). 83 | 84 | ### Issues? 85 | 86 | textAngular uses ```execCommand``` for the rich-text functionality. 87 | That being said, its still a fairly experimental browser feature-set, and may not behave the same in all browsers - see http://tifftiff.de/contenteditable/compliance_test.html for a full compliance list. 88 | It has been tested to work on Chrome, Safari, Opera, Firefox and Internet Explorer 8+. 89 | If you find something, please let me know - throw me a message, or submit a issue request! 90 | 91 | ## Developer Notes 92 | 93 | When checking out, you need a node.js installation, running `npm install` will get you setup with everything to run the unit tests and minification. 94 | 95 | ## License 96 | 97 | This project is licensed under the [MIT license](http://opensource.org/licenses/MIT). 98 | 99 | 100 | ## Contributers 101 | 102 | Special thanks to all the contributions thus far! 103 | 104 | Including those from: 105 | 106 | * [SimeonC](https://github.com/SimeonC) 107 | * [slobo](https://github.com/slobo) 108 | * [edouard-lopez](https://github.com/edouard-lopez) 109 | * [108ium](https://github.com/108ium) 110 | * [nadeeshacabral](https://github.com/nadeeshacabral) 111 | * [netbubu17](https://github.com/netbubu17) 112 | * [worldspawn](https://github.com/worldspawn) 113 | * [JonathanGawrych](https://github.com/JonathanGawrych) 114 | * [kanakiyajay](https://github.com/kanakiyajay) 115 | * [kencaron](https://github.com/kencaron) 116 | * [gintau](https://github.com/gintau) 117 | * [uxtx](https://github.com/uxtx) -------------------------------------------------------------------------------- /demo/static-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | textAngular 1.2.0 Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |

Toolbar

41 |

42 | 43 |

Editor

44 | 45 | 46 |
47 |
48 |

Raw HTML in a text area

49 | 50 |

Bound with ng-bind-html

51 |
52 |

Bound with ta-bind, our internal html-binding directive

53 |
54 | 55 | 56 |

Note: although we support classes and styles, angularjs' ng-bind-html directive will strip out all style attributes.

57 | 58 | 59 |
60 | 61 | 62 | Bio:
63 |
64 | 65 |

Option to masquerade as a fancy text-area - complete with form submission and optional ngModel

66 |

Any HTML we put in-between the text-angular tags gets automatically put into the editor if there is not a ngModel

67 |
68 | 69 | 70 | 71 | 72 | 73 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /demo/textAngular.com.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | textAngular :: Lightweight Angular.js, Javascript Wysiwyg/Text-Editor 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 109 | 110 | 111 |
112 |
113 |
114 | 115 |
116 |
117 |
118 | A Lightweight, Two-Way-Bound & Totally Awesome Angular.js Text-Editor 119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |

127 |
128 |

129 |
130 | //cdnjs.cloudflare.com/ajax/libs/textAngular/1.2.0/textAngular.min.js    131 |
Download v1.2.0
132 |
133 |
134 | 135 | 136 | 137 |
138 |
139 |
140 |
Create by fraywing
141 |
2013 Licensed under MIT
142 |
143 |
144 |
145 | 146 | 153 | 154 | -------------------------------------------------------------------------------- /test/taTools.spec.js: -------------------------------------------------------------------------------- 1 | describe('taToolsExecuteFunction', function(){ 2 | var scope, startActionResult, editor, $rootScope; 3 | beforeEach(module('textAngular')); 4 | beforeEach(inject(function(taToolExecuteAction, _$rootScope_){ 5 | $rootScope = _$rootScope_; 6 | startActionResult = Math.random(); 7 | scope = { 8 | taToolExecuteAction: taToolExecuteAction, 9 | $editor: function(){ 10 | return editor = { 11 | startCount: 0, 12 | startAction: function(){ 13 | this.startCount++; 14 | return startActionResult; 15 | }, 16 | finishCount: 0, 17 | endAction: function(){ this.finishCount++; } 18 | }; 19 | } 20 | }; 21 | })); 22 | 23 | describe('executes the action passing the correct parameters', function(){ 24 | it('should pass the result of startAction Result', function(){ 25 | scope.action = function(deferred, startActionResult){ 26 | expect(startActionResult).toBe(startActionResult); 27 | }; 28 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 29 | }); 30 | it('should pass a valid deferred object', function(){ 31 | scope.action = function(deferred, startActionResult){ 32 | expect(deferred.resolve).toBeDefined(); 33 | expect(deferred.reject).toBeDefined(); 34 | expect(deferred.notify).toBeDefined(); 35 | expect(deferred.promise).toBeDefined(); 36 | }; 37 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 38 | }); 39 | }); 40 | 41 | it('doesn\'t error when action not present', function(){ 42 | expect(function(){ 43 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 44 | }).not.toThrow(); 45 | }); 46 | 47 | it('sets the correct editor if passed', function(){ 48 | var _editor = {endAction: function(){}, startAction: function(){}}; 49 | scope.taToolExecuteAction(_editor); 50 | expect(scope.$editor()).toBe(_editor); 51 | }); 52 | 53 | describe('calls editor action', function(){ 54 | it('start and end when action returns truthy', function(){ 55 | scope.action = function(deferred, startActionResult){ return true; }; 56 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 57 | expect(editor.startCount).toBe(1); 58 | expect(editor.finishCount).toBe(1); 59 | }); 60 | 61 | it('start and end when action returns undefined', function(){ 62 | scope.action = function(deferred, startActionResult){}; 63 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 64 | expect(editor.startCount).toBe(1); 65 | expect(editor.finishCount).toBe(1); 66 | }); 67 | 68 | it('start and not end when action returns false', function(){ 69 | scope.action = function(deferred, startActionResult){ return false; }; 70 | $rootScope.$apply(function(){ scope.taToolExecuteAction(); }); 71 | expect(editor.startCount).toBe(1); 72 | expect(editor.finishCount).toBe(0); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('taTools test tool actions', function(){ 78 | 'use strict'; 79 | var $rootScope, element, button, editorScope, $window; 80 | var findAndTriggerButton = function(name){ 81 | var button = element.find('button[name=' + name + ']'); 82 | button.scope().executeAction(editorScope); 83 | editorScope.endAction(); 84 | $rootScope.$digest(); 85 | return button; 86 | }; 87 | 88 | // We use an assumption here and only test whether the button reports as being activated 89 | // it ended up being too difficult to reselect and un-apply 90 | 91 | var testAllButtons = function(){ 92 | it('h1 button should function correctly', function(){ 93 | button = findAndTriggerButton('h1'); 94 | expect(button.hasClass('active')); 95 | }); 96 | 97 | it('h2 button should function correctly', function(){ 98 | button = findAndTriggerButton('h2'); 99 | expect(button.hasClass('active')); 100 | }); 101 | 102 | it('h3 button should function correctly', function(){ 103 | button = findAndTriggerButton('h3'); 104 | expect(button.hasClass('active')); 105 | }); 106 | 107 | it('h4 button should function correctly', function(){ 108 | button = findAndTriggerButton('h4'); 109 | expect(button.hasClass('active')); 110 | }); 111 | 112 | it('h5 button should function correctly', function(){ 113 | button = findAndTriggerButton('h5'); 114 | expect(button.hasClass('active')); 115 | }); 116 | 117 | it('h6 button should function correctly', function(){ 118 | button = findAndTriggerButton('h6'); 119 | expect(button.hasClass('active')); 120 | }); 121 | 122 | it('p button should function correctly', function(){ 123 | button = findAndTriggerButton('p'); 124 | expect(button.hasClass('active')); 125 | }); 126 | 127 | it('pre button should function correctly', function(){ 128 | button = findAndTriggerButton('pre'); 129 | expect(button.hasClass('active')); 130 | }); 131 | 132 | it('quote button should function correctly', function(){ 133 | button = findAndTriggerButton('quote'); 134 | expect(button.hasClass('active')); 135 | }); 136 | 137 | it('bold button should function correctly', function(){ 138 | button = findAndTriggerButton('bold'); 139 | expect(button.hasClass('active')); 140 | }); 141 | 142 | it('italics button should function correctly', function(){ 143 | button = findAndTriggerButton('italics'); 144 | expect(button.hasClass('active')); 145 | }); 146 | 147 | it('underline button should function correctly', function(){ 148 | button = findAndTriggerButton('underline'); 149 | expect(button.hasClass('active')); 150 | }); 151 | 152 | it('ul button should function correctly', function(){ 153 | button = findAndTriggerButton('ul'); 154 | expect(button.hasClass('active')); 155 | }); 156 | 157 | it('ol button should function correctly', function(){ 158 | button = findAndTriggerButton('ol'); 159 | expect(button.hasClass('active')); 160 | }); 161 | 162 | it('justifyLeft button should function correctly', function(){ 163 | button = findAndTriggerButton('justifyLeft'); 164 | expect(button.hasClass('active')); 165 | }); 166 | 167 | it('justifyCenter button should function correctly', function(){ 168 | button = findAndTriggerButton('justifyCenter'); 169 | expect(button.hasClass('active')); 170 | }); 171 | 172 | it('justifyRight button should function correctly', function(){ 173 | button = findAndTriggerButton('justifyRight'); 174 | expect(button.hasClass('active')); 175 | }); 176 | 177 | it('html button should function correctly', inject(function($timeout){ 178 | button = findAndTriggerButton('html'); 179 | $timeout.flush(); 180 | expect(button.hasClass('active')); 181 | expect(!element.find('.ta-text').is(":visible")); 182 | expect(element.find('.ta-html').is(":visible")); 183 | expect(element.find('.ta-html').is(":focus")); 184 | button = findAndTriggerButton('html'); // retrigger to reset to non html view 185 | $timeout.flush(); 186 | expect(!element.find('.ta-html').is(":visible")); 187 | expect(element.find('.ta-text').is(":visible")); 188 | expect(element.find('.ta-text').is(":focus")); 189 | })); 190 | 191 | it('check untestables don\'t error', function(){ 192 | expect(function(){ 193 | findAndTriggerButton('redo'); 194 | }).not.toThrow(); 195 | }); 196 | it('check untestables don\'t error', function(){ 197 | expect(function(){ 198 | findAndTriggerButton('undo'); 199 | }).not.toThrow(); 200 | }); 201 | it('check untestables don\'t error', function(){ 202 | expect(function(){ 203 | findAndTriggerButton('insertImage'); 204 | }).not.toThrow(); 205 | }); 206 | it('check untestables don\'t error', function(){ 207 | expect(function(){ 208 | findAndTriggerButton('insertLink'); 209 | }).not.toThrow(); 210 | }); 211 | it('check untestables don\'t error', function(){ 212 | expect(function(){ 213 | findAndTriggerButton('unlink'); 214 | }).not.toThrow(); 215 | }); 216 | }; 217 | 218 | describe('with un-wrapped content', function(){ 219 | beforeEach(module('textAngular')); 220 | beforeEach(inject(function (_$compile_, _$rootScope_, $document, textAngularManager, _$window_) { 221 | $window = _$window_; 222 | $window.prompt = function(){ return 'soehusoaehusnahoeusnt'; }; 223 | $rootScope = _$rootScope_; 224 | element = _$compile_('Test Content')($rootScope); 225 | $document.find('body').append(element); 226 | $rootScope.$digest(); 227 | editorScope = textAngularManager.retrieveEditor('test').scope; 228 | var sel = $window.rangy.getSelection(); 229 | var range = $window.rangy.createRangyRange(); 230 | range.selectNode(element.find('.ta-text')[0].childNodes[0]); 231 | sel.setSingleRange(range); 232 | })); 233 | afterEach(function(){ 234 | element.remove(); 235 | }); 236 | 237 | testAllButtons(); 238 | }); 239 | 240 | describe('with wrapped content', function(){ 241 | beforeEach(module('textAngular')); 242 | beforeEach(inject(function (_$compile_, _$rootScope_, $document, textAngularManager, _$window_) { 243 | $window = _$window_; 244 | $window.prompt = function(){ return ''; }; 245 | $rootScope = _$rootScope_; 246 | element = _$compile_('

Test Content

')($rootScope); 247 | $document.find('body').append(element); 248 | $rootScope.$digest(); 249 | editorScope = textAngularManager.retrieveEditor('test').scope; 250 | var sel = $window.rangy.getSelection(); 251 | var range = $window.rangy.createRangyRange(); 252 | range.selectNodeContents(element.find('.ta-text p')[0]); 253 | sel.setSingleRange(range); 254 | })); 255 | afterEach(function(){ 256 | element.remove(); 257 | }); 258 | 259 | testAllButtons(); 260 | }); 261 | 262 | describe('test clear button', function(){ 263 | beforeEach(module('textAngular')); 264 | beforeEach(inject(function (_$compile_, _$rootScope_, $document, textAngularManager, _$window_) { 265 | $window = _$window_; 266 | $window.prompt = function(){ return ''; }; 267 | $rootScope = _$rootScope_; 268 | $rootScope.htmlcontent = '

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2
'; 269 | element = _$compile_('')($rootScope); 270 | $document.find('body').append(element); 271 | $rootScope.$digest(); 272 | editorScope = textAngularManager.retrieveEditor('testclearbutton').scope; 273 | var sel = $window.rangy.getSelection(); 274 | var range = $window.rangy.createRangyRange(); 275 | range.selectNodeContents(element.find('.ta-text')[0]); 276 | sel.setSingleRange(range); 277 | })); 278 | afterEach(function(){ 279 | element.remove(); 280 | }); 281 | 282 | it('doesn\'t error', function(){ 283 | expect(function(){ 284 | findAndTriggerButton('clear'); 285 | }).not.toThrow(); 286 | }); 287 | 288 | it('clears out all formatting', function(){ 289 | findAndTriggerButton('clear'); 290 | //expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

Test 1

Test 2

'); 291 | // bug in phantom JS 292 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

Test 1

Test 2

'); 293 | }); 294 | 295 | it('doesn\'t remove partially selected list elements, but clears them of formatting', function(){ 296 | var sel = $window.rangy.getSelection(); 297 | var range = $window.rangy.createRangyRange(); 298 | range.setStartBefore(element.find('.ta-text ul li:first-child b')[0]); 299 | range.setEndBefore(element.find('.ta-text ul li:last-child')[0]); 300 | sel.setSingleRange(range); 301 | sel.refresh(); 302 | findAndTriggerButton('clear'); 303 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2
'); 304 | }); 305 | 306 | it('doesn\'t clear wholly selected list elements, but clears them of formatting', function(){ 307 | var sel = $window.rangy.getSelection(); 308 | var range = $window.rangy.createRangyRange(); 309 | range.selectNodeContents(element.find('.ta-text ul')[0]); 310 | sel.setSingleRange(range); 311 | sel.refresh(); 312 | findAndTriggerButton('clear'); 313 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2
'); 314 | }); 315 | 316 | it('doesn\'t clear singly selected list elements, but clears them of formatting', function(){ 317 | var sel = $window.rangy.getSelection(); 318 | var range = $window.rangy.createRangyRange(); 319 | range.selectNodeContents(element.find('.ta-text ul li:first-child')[0]); 320 | range.setEndAfter(element.find('.ta-text ul li:first-child')[0]); 321 | sel.setSingleRange(range); 322 | sel.refresh(); 323 | findAndTriggerButton('clear'); 324 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2
'); 325 | }); 326 | 327 | it('doesn\'t clear singly selected list elements, but clears them of formatting', function(){ 328 | var sel = $window.rangy.getSelection(); 329 | var range = $window.rangy.createRangyRange(); 330 | range.selectNode(element.find('.ta-text ul li:first-child')[0]); 331 | range.setEndAfter(element.find('.ta-text ul li:first-child')[0]); 332 | sel.setSingleRange(range); 333 | sel.refresh(); 334 | findAndTriggerButton('clear'); 335 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2
'); 336 | }); 337 | 338 | it('works without rangy', function(){ 339 | var button = element.find('button[name=clear]'); 340 | var _rangy = button.scope().$window.rangy; 341 | button.scope().$window.rangy = undefined; 342 | button.scope().executeAction(editorScope); 343 | editorScope.endAction(); 344 | $rootScope.$digest(); 345 | expect($rootScope.htmlcontent).toBe('

Test Content that should be cleared

Test Other Tags

  • Test 1
  • Test 2

'); 346 | button.scope().$window.rangy = _rangy; 347 | }); 348 | }); 349 | }); -------------------------------------------------------------------------------- /test/textAngularSanitize/sanitize.spec.js: -------------------------------------------------------------------------------- 1 | describe('HTML', function() { 2 | var expectHTML; 3 | 4 | beforeEach(module('ngSanitize')); 5 | beforeEach(function() { 6 | expectHTML = function(html){ 7 | var sanitize; 8 | inject(function($sanitize) { 9 | sanitize = $sanitize; 10 | }); 11 | return expect(sanitize(html)); 12 | }; 13 | }); 14 | 15 | describe('htmlParser', function() { 16 | if (angular.isUndefined(window.htmlParser)) return; 17 | 18 | var handler, start, text, comment; 19 | beforeEach(function() { 20 | handler = { 21 | start: function(tag, attrs, unary){ 22 | start = { 23 | tag: tag, 24 | attrs: attrs, 25 | unary: unary 26 | }; 27 | // Since different browsers handle newlines differently we trim 28 | // so that it is easier to write tests. 29 | angular.forEach(attrs, function(value, key) { 30 | attrs[key] = value.replace(/^\s*/, '').replace(/\s*$/, ''); 31 | }); 32 | }, 33 | chars: function(text_){ 34 | text = text_; 35 | }, 36 | end:function(tag) { 37 | expect(tag).toEqual(start.tag); 38 | }, 39 | comment:function(comment_) { 40 | comment = comment_; 41 | } 42 | }; 43 | }); 44 | 45 | it('should parse comments', function() { 46 | htmlParser('', handler); 47 | expect(comment).toEqual('FOOBAR'); 48 | }); 49 | 50 | it('should throw an exception for invalid comments', function() { 51 | var caught=false; 52 | try { 53 | htmlParser('', handler); 54 | } 55 | catch (ex) { 56 | caught = true; 57 | // expected an exception due to a bad parse 58 | } 59 | expect(caught).toBe(true); 60 | }); 61 | 62 | it('double-dashes are not allowed in a comment', function() { 63 | var caught=false; 64 | try { 65 | htmlParser('', handler); 66 | } 67 | catch (ex) { 68 | caught = true; 69 | // expected an exception due to a bad parse 70 | } 71 | expect(caught).toBe(true); 72 | }); 73 | 74 | it('should parse basic format', function() { 75 | htmlParser('text', handler); 76 | expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); 77 | expect(text).toEqual('text'); 78 | }); 79 | 80 | it('should parse newlines in tags', function() { 81 | htmlParser('<\ntag\n attr="value"\n>text<\n/\ntag\n>', handler); 82 | expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); 83 | expect(text).toEqual('text'); 84 | }); 85 | 86 | it('should parse newlines in attributes', function() { 87 | htmlParser('text', handler); 88 | expect(start).toEqual({tag:'tag', attrs:{attr:'value'}, unary:false}); 89 | expect(text).toEqual('text'); 90 | }); 91 | 92 | it('should parse namespace', function() { 93 | htmlParser('text', handler); 94 | expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'value'}, unary:false}); 95 | expect(text).toEqual('text'); 96 | }); 97 | 98 | it('should parse empty value attribute of node', function() { 99 | htmlParser('', handler); 100 | expect(start).toEqual({tag:'option', attrs:{selected:'', value:''}, unary:false}); 101 | expect(text).toEqual('abc'); 102 | }); 103 | }); 104 | 105 | // THESE TESTS ARE EXECUTED WITH COMPILED ANGULAR 106 | it('should echo html', function() { 107 | expectHTML('helloworld.'). 108 | toEqual('helloworld.'); 109 | }); 110 | 111 | it('should remove script', function() { 112 | expectHTML('ac.').toEqual('ac.'); 136 | }); 137 | 138 | it('should remove double nested script', function() { 139 | expectHTML('ailc.').toEqual('ac.'); 140 | }); 141 | 142 | it('should remove unknown names', function() { 143 | expectHTML('abc').toEqual('abc'); 144 | }); 145 | 146 | it('should remove unsafe value', function() { 147 | expectHTML('').toEqual(''); 148 | }); 149 | 150 | it('should handle self closed elements', function() { 151 | expectHTML('a
c').toEqual('a
c'); 152 | }); 153 | 154 | it('should handle namespace', function() { 155 | expectHTML('abc').toEqual('abc'); 156 | }); 157 | 158 | it('should handle entities', function() { 159 | var everything = '
' + 160 | '!@#$%^&*()_+-={}[]:";\'<>?,./`~
'; 161 | expectHTML(everything).toEqual(everything); 162 | }); 163 | 164 | it('should handle improper html', function() { 165 | expectHTML('< div rel="" alt=abc dir=\'"\' >text< /div>'). 166 | toEqual('
text
'); 167 | }); 168 | 169 | it('should handle improper html2', function() { 170 | expectHTML('< div rel="" / >'). 171 | toEqual('
'); 172 | }); 173 | 174 | it('should ignore back slash as escape', function() { 175 | expectHTML('xxx\\'). 176 | toEqual('xxx\\'); 177 | }); 178 | 179 | it('should ignore object attributes', function() { 180 | expectHTML(':)'). 181 | toEqual(':)'); 182 | expectHTML(':)'). 183 | toEqual(''); 184 | }); 185 | 186 | it('should keep spaces as prefix/postfix', function() { 187 | expectHTML(' a ').toEqual(' a '); 188 | }); 189 | 190 | it('should allow multiline strings', function() { 191 | expectHTML('\na\n').toEqual(' a '); 192 | }); 193 | 194 | describe('htmlSanitizerWriter', function() { 195 | if (angular.isUndefined(window.htmlSanitizeWriter)) return; 196 | 197 | var writer, html, uriValidator; 198 | beforeEach(function() { 199 | html = ''; 200 | uriValidator = jasmine.createSpy('uriValidator'); 201 | writer = htmlSanitizeWriter({push:function(text){html+=text;}}, uriValidator); 202 | }); 203 | 204 | it('should write basic HTML', function() { 205 | writer.chars('before'); 206 | writer.start('div', {rel:'123'}, false); 207 | writer.chars('in'); 208 | writer.end('div'); 209 | writer.chars('after'); 210 | 211 | expect(html).toEqual('before
in
after'); 212 | }); 213 | 214 | it('should escape text nodes', function() { 215 | writer.chars('a
&
c'); 216 | expect(html).toEqual('a<div>&</div>c'); 217 | }); 218 | 219 | it('should escape IE script', function() { 220 | writer.chars('&<>{}'); 221 | expect(html).toEqual('&<>{}'); 222 | }); 223 | 224 | it('should escape attributes', function() { 225 | writer.start('div', {rel:'!@#$%^&*()_+-={}[]:";\'<>?,./`~ \n\0\r\u0127'}); 226 | expect(html).toEqual('
'); 227 | }); 228 | 229 | it('should ignore missformed elements', function() { 230 | writer.start('d>i&v', {}); 231 | expect(html).toEqual(''); 232 | }); 233 | 234 | it('should ignore unknown attributes', function() { 235 | writer.start('div', {unknown:""}); 236 | expect(html).toEqual('
'); 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 | '', 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 | '
' + 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;d0&&"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:"