From 108857f69ac611f970ded65ba5c1207b8a6964d0 Mon Sep 17 00:00:00 2001 From: SimeonC Date: Mon, 25 May 2015 11:01:20 +1200 Subject: [PATCH] fix(taPaste): Fix the taPaste order s.t. sanitizer is called after paste handler. Fixes #686 BREAKING CHANGES: This changes the structure of the files - all production files are now in the dist folder, this makes where PR's should be done a little more clear. If you were referencing the src/*.js files they will need to be updated to dist/*js. --- Gruntfile.js | 69 ++-- README.md | 8 +- bower.json | 8 +- dist/textAngular-sanitize.js | 763 +++++++++++++++++++++++++++++++++++ dist/textAngular.css | 202 ++++++++++ {src => dist}/textAngular.js | 3 +- dist/textAngular.min.js | 2 +- dist/textAngularSetup.js | 704 ++++++++++++++++++++++++++++++++ karma-jqlite.conf.js | 10 +- karma-jquery.conf.js | 10 +- package.json | 1 + {lib => src}/DOM.js | 0 {lib => src}/factories.js | 0 {lib => src}/globals.js | 0 {lib => src}/main.js | 0 {lib => src}/taBind.js | 3 +- {lib => src}/validators.js | 0 17 files changed, 1732 insertions(+), 51 deletions(-) create mode 100644 dist/textAngular-sanitize.js create mode 100644 dist/textAngular.css rename {src => dist}/textAngular.js (99%) create mode 100644 dist/textAngularSetup.js rename {lib => src}/DOM.js (100%) rename {lib => src}/factories.js (100%) rename {lib => src}/globals.js (100%) rename {lib => src}/main.js (100%) rename {lib => src}/taBind.js (99%) rename {lib => src}/validators.js (100%) diff --git a/Gruntfile.js b/Gruntfile.js index 2f07552c..ae567c82 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,6 +4,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-istanbul-coverage'); @@ -14,10 +15,10 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-git'); grunt.loadNpmTasks('grunt-shell'); - grunt.registerTask('compile', ['concat', 'jshint', 'uglify']); + grunt.registerTask('compile', ['concat', 'copy:setupFiles', 'jshint', 'uglify']); grunt.registerTask('default', ['compile', 'test']); grunt.registerTask('test', ['clean', 'jshint', 'karma', 'coverage']); - grunt.registerTask('travis-test', ['concat', 'jshint', 'karma', 'coverage', 'coveralls']); + grunt.registerTask('travis-test', ['concat', 'copy:setupFiles', 'jshint', 'karma', 'coverage', 'coveralls']); grunt.registerTask('release', ['bump-only','compile','changelog','gitcommit','bump-commit', 'shell:publish']); grunt.registerTask('release:patch', ['bump-only:patch','compile','changelog','gitcommit','bump-commit', 'shell:publish']); @@ -61,15 +62,15 @@ module.exports = function (grunt) { }, clean: ["coverage"], coverage: { - options: { - thresholds: { - 'statements': 100, - 'branches': 100, - 'lines': 100, - 'functions': 100 + options: { + thresholds: { + 'statements': 100, + 'branches': 100, + 'lines': 100, + 'functions': 100 }, dir: 'coverage' - } + } }, coveralls: { options: { @@ -79,26 +80,34 @@ module.exports = function (grunt) { } }, karma: { - jquery: { - options: testConfig('karma-jquery.conf.js') - }, - jqlite: { - options: testConfig('karma-jqlite.conf.js') - } + jquery: { + options: testConfig('karma-jquery.conf.js') + }, + jqlite: { + options: testConfig('karma-jqlite.conf.js') + } }, jshint: { - files: ['lib/*.js', 'src/textAngularSetup.js', 'test/*.spec.js', 'test/taBind/*.spec.js'],// don't hint the textAngularSanitize as they will fail - options: { - eqeqeq: true, - immed: true, - latedef: true, - newcap: true, - noarg: true, - sub: true, - boss: true, - eqnull: true, - globals: {} - } + files: ['src/*.js', 'test/*.spec.js', 'test/taBind/*.spec.js', '!src/textAngular-sanitize.js'],// don't hint the textAngularSanitize as they will fail + options: { + eqeqeq: true, + immed: true, + latedef: true, + newcap: true, + noarg: true, + sub: true, + boss: true, + eqnull: true, + globals: {} + } + }, + copy: { + setupFiles: { + expand: true, + cwd: 'src/', + src: ['textAngularSetup.js', 'textAngular.css', 'textAngular-sanitize.js'], + dest: 'dist/' + } }, concat: { options: { @@ -106,8 +115,8 @@ module.exports = function (grunt) { footer: "})();" }, dist: { - src: ['lib/globals.js','lib/factories.js','lib/DOM.js','lib/validators.js','lib/taBind.js','lib/main.js'], - dest: 'src/textAngular.js' + src: ['src/globals.js','src/factories.js','src/DOM.js','src/validators.js','src/taBind.js','src/main.js'], + dest: 'dist/textAngular.js' } }, uglify: { @@ -120,7 +129,7 @@ module.exports = function (grunt) { my_target: { files: { 'dist/textAngular-rangy.min.js': ['bower_components/rangy/rangy-core.js', 'bower_components/rangy/rangy-selectionsaverestore.js'], - 'dist/textAngular.min.js': ['src/textAngularSetup.js','src/textAngular.js'], + 'dist/textAngular.min.js': ['dist/textAngularSetup.js','dist/textAngular.js'], 'dist/textAngular-sanitize.min.js': ['src/textAngular-sanitize.js'] } } diff --git a/README.md b/README.md index 438cc124..9af41d15 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Demo is available at: http://www.textangular.com (Or editable [Plunkr Demo](http To upgrade from version 1.2.2 or earlier you need to follow these steps: -1. The styling for textAngular is now in the `src/textAngular.css` file, you will need to include this or a copy of it with your own modifications. +1. The styling for textAngular is now in the `dist/textAngular.css` file, you will need to include this or a copy of it with your own modifications. 2. The rangy library is now required, you will need both the `rangy-core` and `rangy-saveselection` modules, alternatively you can include the compressed version (`textAngular-rangy.min.js`) in the dist folder ## Requirements @@ -29,7 +29,7 @@ To upgrade from version 1.2.2 or earlier you need to follow these steps: Run `bower install textAngular` from the command line. Include script tags similar to the following: ```html - + @@ -40,7 +40,7 @@ Include script tags similar to the following: Run `npm install textangular` from the command line. Include script tags similar to the following: ```html - + @@ -65,7 +65,7 @@ Include script tag similar to the following: (For details on how this works see: 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: ```html - + diff --git a/bower.json b/bower.json index 030e838a..bb93918f 100644 --- a/bower.json +++ b/bower.json @@ -2,10 +2,10 @@ "name": "textAngular", "version": "1.4.0", "main": [ - "./src/textAngular.js", - "./src/textAngular-sanitize.js", - "./src/textAngularSetup.js", - "./src/textAngular.css" + "./dist/textAngular.js", + "./dist/textAngular-sanitize.js", + "./dist/textAngularSetup.js", + "./dist/textAngular.css" ], "description": "A radically powerful Text-Editor/Wysiwyg editor for Angular.js", "keywords": [ diff --git a/dist/textAngular-sanitize.js b/dist/textAngular-sanitize.js new file mode 100644 index 00000000..af4229f0 --- /dev/null +++ b/dist/textAngular-sanitize.js @@ -0,0 +1,763 @@ +/** + * @license AngularJS v1.3.10 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + *
+ * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/* + * HTML Parser By Misko Hevery (misko@hevery.com) + * based on: HTML Parser By John Resig (ejohn.org) + * Original code by Erik Arvidsson, Mozilla Public License + * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js + * + * // Use like so: + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + */ + + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. The input may also contain SVG markup. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var START_TAG_REGEXP = + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, + ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, + BEGIN_TAG_REGEXP = /^/g, + DOCTYPE_REGEXP = /]*?)>/i, + CDATA_REGEXP = //g, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = makeMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = makeMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("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")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("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")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + + "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + + "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + + "stop,svg,switch,text,title,tspan,use"); + +// Special Elements (can contain anything) +var specialElements = makeMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements, + svgElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); + +var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ + 'id,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ + 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + + 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + + 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + + 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + + 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + + 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + + 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + + 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + + 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + + 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + + 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + + 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + + 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + + 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + + 'zoomAndPan'); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function makeMap(str) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) obj[items[i]] = true; + return obj; +} + + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; + stack.last = function() { return stack[ stack.length - 1 ]; }; + + while (html) { + text = ''; + chars = true; + + // Make sure we're not in a script or style element + if (!stack.last() || !specialElements[ stack.last() ]) { + + // Comment + if (html.indexOf("", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); + chars = false; + } + // DOCTYPE + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); + + if (match) { + html = html.replace(match[0], ''); + chars = false; + } + // end tag + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); + + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); + chars = false; + } + + // start tag + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); + } + chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); + } + } + + if (chars) { + index = html.indexOf("<"); + + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); + + if (handler.chars) handler.chars(decodeEntities(text)); + } + + } else { + html = html.replace(new RegExp("([^]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text) { + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); + + if (handler.chars) handler.chars(decodeEntities(text)); + + return ""; + }); + + parseEndTag("", stack.last()); + } + + if (html == last) { + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); + } + last = html; + } + + // Clean up any remaining tags + parseEndTag(); + + function parseStartTag(tag, tagName, rest, unary) { + tagName = angular.lowercase(tagName); + if (blockElements[ tagName ]) { + while (stack.last() && inlineElements[ stack.last() ]) { + parseEndTag("", stack.last()); + } + } + + if (optionalEndTagElements[ tagName ] && stack.last() == tagName) { + parseEndTag("", tagName); + } + + unary = voidElements[ tagName ] || !!unary; + + if (!unary) + stack.push(tagName); + + var attrs = {}; + + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; + + attrs[name] = decodeEntities(value); + }); + if (handler.start) handler.start(tagName, attrs, unary); + } + + function parseEndTag(tag, tagName) { + var pos = 0, i; + tagName = angular.lowercase(tagName); + if (tagName) + // Find the closest opened tag of the same type + for (pos = stack.length - 1; pos >= 0; pos--) + if (stack[ pos ] == tagName) + break; + + if (pos >= 0) { + // Close all the open elements, up the stack + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[ i ]); + + // Remove the open elements from the stack + stack.length = pos; + } + } +} + +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; +/** + * decodes all entities into regular string + * @param value + * @returns {string} A string with decoded entities. + */ +function decodeEntities(value) { + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(/= 1536 && c <= 1540) || + c == 1807 || + c == 6068 || + c == 6069 || + (c >= 8204 && c <= 8207) || + (c >= 8232 && c <= 8239) || + (c >= 8288 && c <= 8303) || + c == 65279 || + (c >= 65520 && c <= 65535)) return '&#' + c + ';'; + return value; // avoids multilingual issues + }). + replace(//g, '>'); +} + +var trim = (function() { + // native trim is way faster: http://jsperf.com/angular-trim-test + // but IE doesn't have it... :-( + // TODO: we should move this into IE/ES5 polyfill + if (!String.prototype.trim) { + return function(value) { + return angular.isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; + }; + } + return function(value) { + return angular.isString(value) ? value.trim() : value; + }; +})(); + +// Custom logic for accepting certain style options only - textAngular +// Currently allows only the color, background-color, text-align, float, width and height attributes +// all other attributes should be easily done through classes. +function validStyles(styleAttr){ + var result = ''; + var styleArray = styleAttr.split(';'); + angular.forEach(styleArray, function(value){ + var v = value.split(':'); + if(v.length == 2){ + var key = trim(angular.lowercase(v[0])); + var value = trim(angular.lowercase(v[1])); + if( + (key === 'color' || key === 'background-color') && ( + value.match(/^rgb\([0-9%,\. ]*\)$/i) + || value.match(/^rgba\([0-9%,\. ]*\)$/i) + || value.match(/^hsl\([0-9%,\. ]*\)$/i) + || value.match(/^hsla\([0-9%,\. ]*\)$/i) + || value.match(/^#[0-9a-f]{3,6}$/i) + || value.match(/^[a-z]*$/i) + ) + || + key === 'text-align' && ( + value === 'left' + || value === 'right' + || value === 'center' + || value === 'justify' + ) + || + key === 'float' && ( + value === 'left' + || value === 'right' + || value === 'none' + ) + || + (key === 'width' || key === 'height') && ( + value.match(/[0-9\.]*(px|em|rem|%)/) + ) + || // Reference #520 + (key === 'direction' && value.match(/^ltr|rtl|initial|inherit$/)) + ) result += key + ': ' + value + ';'; + } + }); + return result; +} + +// this function is used to manually allow specific attributes on specific tags with certain prerequisites +function validCustomTag(tag, attrs, lkey, value){ + // catch the div placeholder for the iframe replacement + if (tag === 'img' && attrs['ta-insert-video']){ + if(lkey === 'ta-insert-video' || lkey === 'allowfullscreen' || lkey === 'frameborder' || (lkey === 'contenteditable' && value === 'false')) return true; + } + return false; +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.jain('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs, unary) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignore = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs, unary) { + tag = angular.lowercase(tag); + if (!ignore && specialElements[tag]) { + ignore = tag; + } + if (!ignore && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage=(tag === 'img' && lkey === 'src') || (lkey === 'background'); + if ((lkey === 'style' && (value = validStyles(value)) !== '') || validCustomTag(tag, attrs, lkey, value) || validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out(unary ? '/>' : '>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignore && validElements[tag] === true) { + out(''); + } + if (tag == ignore) { + ignore = false; + } + }, + chars: function(chars) { + if (!ignore) { + out(encodeEntities(chars)); + } + } + }; +} + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. + * @returns {string} Html-linkified text. + * + * @usage + + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, + MAILTO_REGEXP = /^mailto:/; + + return function(text, target) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/dist/textAngular.css b/dist/textAngular.css new file mode 100644 index 00000000..906c2674 --- /dev/null +++ b/dist/textAngular.css @@ -0,0 +1,202 @@ +/* +@license textAngular +Author : Austin Anderson +License : 2013 MIT +Version 1.3.7 + +See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use. +*/ + +.ta-hidden-input { + width: 1px; + height: 1px; + border: none; + margin: 0; + padding: 0; + position: absolute; + top: -10000px; + left: -10000px; + opacity: 0; + overflow: hidden; +} + +/* add generic styling for the editor */ +.ta-root.focussed > .ta-scroll-window.form-control { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.ta-editor.ta-html, .ta-scroll-window.form-control { + min-height: 300px; + height: auto; + overflow: auto; + font-family: inherit; + font-size: 100%; +} + +.ta-scroll-window.form-control { + position: relative; + padding: 0; +} + +.ta-scroll-window > .ta-bind { + height: auto; + min-height: 300px; + padding: 6px 12px; +} + +.ta-editor:focus { + user-select: text; +} + +/* add the styling for the awesomness of the resizer */ +.ta-resizer-handle-overlay { + z-index: 100; + position: absolute; + display: none; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-info { + position: absolute; + bottom: 16px; + right: 16px; + border: 1px solid black; + background-color: #FFF; + padding: 0 4px; + opacity: 0.7; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-background { + position: absolute; + bottom: 5px; + right: 5px; + left: 5px; + top: 5px; + border: 1px solid black; + background-color: rgba(0, 0, 0, 0.2); +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner { + width: 10px; + height: 10px; + position: absolute; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl{ + top: 0; + left: 0; + border-left: 1px solid black; + border-top: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr{ + top: 0; + right: 0; + border-right: 1px solid black; + border-top: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl{ + bottom: 0; + left: 0; + border-left: 1px solid black; + border-bottom: 1px solid black; +} + +.ta-resizer-handle-overlay > .ta-resizer-handle-corner-br{ + bottom: 0; + right: 0; + border: 1px solid black; + cursor: se-resize; + background-color: white; +} + +/* copy the popover code from bootstrap so this will work even without it */ +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} \ No newline at end of file diff --git a/src/textAngular.js b/dist/textAngular.js similarity index 99% rename from src/textAngular.js rename to dist/textAngular.js index 95b0e201..d4954417 100644 --- a/src/textAngular.js +++ b/dist/textAngular.js @@ -1432,7 +1432,6 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM' text = text.replace(/
]*?>/ig, '').replace(/( | )<\/span>/ig, ' '); } - text = taSanitize(text, '', _disableSanitizer); if (//i.test(text) && /(|).*/i.test(text) === false) { // insert missing parent of li element text = text.replace(/.*<\/li(\s.*)?>/i, '
    $&
'); @@ -1440,6 +1439,8 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM' if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text; + text = taSanitize(text, '', _disableSanitizer); + taSelection.insertHtml(text, element[0]); $timeout(function(){ ngModel.$setViewValue(_compileHtml()); diff --git a/dist/textAngular.min.js b/dist/textAngular.min.js index bd9c18bd..62a675dc 100644 --- a/dist/textAngular.min.js +++ b/dist/textAngular.min.js @@ -14,5 +14,5 @@ Version 1.4.0 See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use. */ -function(){"use strict";function a(a){try{return 0!==angular.element(a).length}catch(b){return!1}}function b(b,c){if(!b||""===b||r.hasOwnProperty(b))throw"textAngular Error: A unique name is required for a Tool Definition";if(c.display&&(""===c.display||!a(c.display))||!c.display&&!c.buttontext&&!c.iconclass)throw'textAngular Error: Tool Definition for "'+b+'" does not have a valid display/iconclass/buttontext value';r[b]=c}var c={ie:function(){for(var a,b=3,c=document.createElement("div"),d=c.getElementsByTagName("i");c.innerHTML="",d[0];);return b>4?b:a}(),webkit:/AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)},d=!1;c.webkit&&(document.addEventListener("mousedown",function(a){var b=a||window.event,c=b.target;if(d&&null!==c){for(var e=!1,f=c;null!==f&&"html"!==f.tagName.toLowerCase()&&!e;)e="true"===f.contentEditable,f=f.parentNode;e||(document.getElementById("textAngular-editableFix-010203040506070809").setSelectionRange(0,0),c.focus(),c.select&&c.select())}d=!1},!1),angular.element(document).ready(function(){angular.element(document.body).append(angular.element(''))}));var e=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i,f=/^(ul|li|ol)$/i,g=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});var h,i,j,k,l,m;if(c.ie>8||void 0===c.ie){for(var n=document.styleSheets,o=0;o
");return d[0].innerHTML=c,angular.forEach(a,function(a){var c=[];a.selector&&""!==a.selector?c=d.find(a.selector):a.customAttribute&&""!==a.customAttribute&&(c=b.getByAttribute(d,a.customAttribute)),angular.forEach(c,function(b){b=angular.element(b),a.selector&&""!==a.selector&&a.customAttribute&&""!==a.customAttribute?void 0!==b.attr(a.customAttribute)&&a.renderLogic(b):a.renderLogic(b)})}),d[0].innerHTML}}]).factory("taFixChrome",function(){var a=function(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c,d,e=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,f="",g=0;b=e.exec(a);)c=b[3]||b[4],c&&c.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)&&(c=c.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/gi,""),d="<"+b[1].trim(),c.trim().length>0&&(d+=" style="+b[2].substring(0,1)+c+b[2].substring(0,1)),d+=b[5].trim()+">",f+=a.substring(g,b.index)+d,g=b.index+b[0].length);return f+=a.substring(g),g>0?f.replace(/(.*?)<\/span>(|)/gi,"$1"):a};return a}).factory("taSanitize",["$sanitize",function(a){function b(a,b){for(var c,d=0,e=0,f=/<[^>]*>/gi;c=f.exec(a);)if(e=c.index,"/"===c[0].substr(1,1)){if(0===d)break;d--}else d++;return b+a.substring(0,e)+angular.element(b)[0].outerHTML.substring(b.length)+a.substring(e)}function c(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var d,f,g,h,i,k,l=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,m="",n="",o=0;f=l.exec(a);){h=f[3]||f[4];var p=new RegExp(j,"i");if(angular.isString(h)&&p.test(h)){i="";for(var q=new RegExp(j,"ig");g=q.exec(h);)for(d=0;d");k=c(a.substring(o,f.index)),n+=m.length>0?b(k,m):k,h=h.replace(new RegExp(j,"ig"),""),n+="<"+f[1].trim(),h.length>0&&(n+=' style="'+h+'"'),n+=f[5]+">",o=f.index+f[0].length,m=i}}return n+=m.length>0?b(a.substring(o),m):a.substring(o)}function d(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c=/<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/gi,d="",e=0;b=c.exec(a);){d+=a.substring(e,b.index),e=b.index+b[0].length;var f="<"+b[1]+b[5];/style=("([^"]+)"|'([^']+)')/gi.test(f)?f=f.replace(/style=("([^"]+)"|'([^']+)')/i,'style="$2$3 text-align:'+(b[3]||b[4])+';"'):f+=' style="text-align:'+(b[3]||b[4])+';"',f+=">",d+=f}return d+a.substring(e)}for(var e=[{property:"font-weight",values:["bold"],tag:"b"},{property:"font-style",values:["italic"],tag:"i"}],f=[],g=0;g0&&(h+="|"),h+=e[g].values[i];h+=");)",f.push(h)}var j="("+f.join("|")+")";return function(b,e,f){if(!f)try{b=c(b)}catch(g){}b=d(b);var h;try{h=a(b),f&&(h=b)}catch(g){h=e||""}var i,j=h.match(/(]*>.*?<\/pre[^>]*>)/gi),k=h.replace(/(&#(9|10);)*/gi,""),l=/]*>.*?<\/pre[^>]*>/gi,m=0,n=0;for(h="";null!==(i=l.exec(k))&&m=0;e--)d=angular.element("<"+c+">"+f[e].innerHTML+""),b.after(d);b.remove(),a.setSelectionToElementEnd(d[0])},g=function(b){/()$/i.test(b.innerHTML.trim())?a.setSelectionBeforeElement(angular.element(b).find("br")[0]):a.setSelectionToElementEnd(b)},h=function(a,b){var c=angular.element("<"+b+">"+a[0].innerHTML+"");a.after(c),a.remove(),g(c.find("li")[0])},i=function(a,c,d){for(var e="",f=0;f"+a[f].innerHTML+"";var h=angular.element("<"+d+">"+e+"");c.after(h),c.remove(),g(h.find("li")[0])};return function(g,j){return g=b(g),function(k,l,m,n){var o,p,q,r,s,t,u,v=angular.element("<"+g+">");try{u=a.getSelectionElement()}catch(w){}var x=angular.element(u);if(void 0!==u){var y=u.tagName.toLowerCase();if("insertorderedlist"===k.toLowerCase()||"insertunorderedlist"===k.toLowerCase()){var z=b("insertorderedlist"===k.toLowerCase()?"ol":"ul");if(y===z)return d(x,g);if("li"===y&&x.parent()[0].tagName.toLowerCase()===z&&1===x.parent().children().length)return d(x.parent(),g);if("li"===y&&x.parent()[0].tagName.toLowerCase()!==z&&1===x.parent().children().length)return h(x.parent(),z);if(y.match(e)&&!x.hasClass("ta-bind")){if("ol"===y||"ul"===y)return h(x,z);var A=!1;return angular.forEach(x.children(),function(a){a.tagName.match(e)&&(A=!0)}),A?i(x.children(),x,z):i([angular.element("
"+u.innerHTML+"
")[0]],x,z)}if(y.match(e)){if(r=a.getOnlySelectedElements(),0===r.length)p=angular.element("<"+z+">
  • "+u.innerHTML+"
  • "),x.html(""),x.append(p);else{if(1===r.length&&("ol"===r[0].tagName.toLowerCase()||"ul"===r[0].tagName.toLowerCase()))return r[0].tagName.toLowerCase()===z?d(angular.element(r[0]),g):h(angular.element(r[0]),z);q="";var B=[];for(o=0;o"+C[0].innerHTML+"":C[0].childNodes[0].innerHTML,B.unshift(C)}p=angular.element("<"+z+">"+q+""),B.pop().replaceWith(p),angular.forEach(B,function(a){a.remove()})}return void a.setSelectionToElementEnd(p[0])}}else{if("formatblock"===k.toLowerCase()){for(t=m.toLowerCase().replace(/[<>]/gi,""),"default"===t.trim()&&(t=g,m="<"+g+">"),p="li"===y?x.parent():x;!p[0].tagName||!p[0].tagName.match(e)&&!p.parent().attr("contenteditable");)p=p.parent(),y=(p[0].tagName||"").toLowerCase();if(y===t){r=p.children();var D=!1;for(o=0;o=0;o--)r[o].parentNode&&r[o].parentNode.removeChild(r[o])}else for(o=0;o"),v[0].innerHTML=F[o].outerHTML,F[o]=v[0]),E.parent()[0].insertBefore(F[o],E[0]);E.remove()}return void a.setSelectionToElementEnd(p[0])}if("createlink"===k.toLowerCase()){var G='',H="",I=a.getSelection();if(I.collapsed)a.insertHtml(G+m+H,j);else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){var J=angular.element(G+H)[0];rangy.getSelection().getRangeAt(0).surroundContents(J)}return}if("inserthtml"===k.toLowerCase())return void a.insertHtml(m,j)}}try{c[0].execCommand(k,l,m)}catch(w){}}}}]).service("taSelection",["$window","$document","taDOM",function(a,b,c){var d=b[0],f=a.rangy,h=function(a,b){return a.tagName&&a.tagName.match(/^br$/i)&&0===b&&!a.previousSibling?{element:a.parentNode,offset:0}:{element:a,offset:b}},i={getSelection:function(){var a=f.getSelection().getRangeAt(0),b=a.commonAncestorContainer,c={start:h(a.startContainer,a.startOffset),end:h(a.endContainer,a.endOffset),collapsed:a.collapsed};return b=3===b.nodeType?b.parentNode:b,c.container=b.parentNode===c.start.element||b.parentNode===c.end.element?b.parentNode:b,c},getOnlySelectedElements:function(){var a=f.getSelection().getRangeAt(0),b=a.commonAncestorContainer;return b=3===b.nodeType?b.parentNode:b,a.getNodes([1],function(a){return a.parentNode===b})},getSelectionElement:function(){return i.getSelection().container},setSelection:function(a,b,c){var d=f.createRange();d.setStart(a,b),d.setEnd(a,c),f.getSelection().setSingleRange(d)},setSelectionBeforeElement:function(a){var b=f.createRange();b.selectNode(a),b.collapse(!0),f.getSelection().setSingleRange(b)},setSelectionAfterElement:function(a){var b=f.createRange();b.selectNode(a),b.collapse(!1),f.getSelection().setSingleRange(b)},setSelectionToElementStart:function(a){var b=f.createRange();b.selectNodeContents(a),b.collapse(!0),f.getSelection().setSingleRange(b)},setSelectionToElementEnd:function(a){var b=f.createRange();b.selectNodeContents(a),b.collapse(!1),a.childNodes&&a.childNodes[a.childNodes.length-1]&&"br"===a.childNodes[a.childNodes.length-1].nodeName&&(b.startOffset=b.endOffset=b.startOffset-1),f.getSelection().setSingleRange(b)},insertHtml:function(a,b){var h,j,k,l,m,n,o,p=angular.element("
    "+a+"
    "),q=f.getSelection().getRangeAt(0),r=d.createDocumentFragment(),s=p[0].childNodes,t=!0;if(s.length>0){for(l=[],k=0;k)$/i.test(q.startContainer.innerHTML)&&q.selectNode(q.startContainer)}else t=!0,n=r=d.createTextNode(a);if(t)q.deleteContents();else if(q.collapsed&&q.startContainer!==b)if(q.startContainer.innerHTML&&q.startContainer.innerHTML.match(/^<[^>]*>$/i))h=q.startContainer,1===q.startOffset?(q.setStartAfter(h),q.setEndAfter(h)):(q.setStartBefore(h),q.setEndBefore(h));else{if(3===q.startContainer.nodeType&&q.startContainer.parentNode!==b)for(h=q.startContainer.parentNode,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,q.startContainer,q.startOffset);!g.test(h.nodeName);){angular.element(h).after(j),h=h.parentNode;var v=j;j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,v)}else h=q.startContainer,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,void 0,void 0,q.startOffset);if(angular.element(h).after(j),q.setStartAfter(h),q.setEndAfter(h),/^(|)$/i.test(h.innerHTML.trim())&&(q.setStartBefore(h),q.setEndBefore(h),angular.element(h).remove()),/^(|)$/i.test(j.innerHTML.trim())&&angular.element(j).remove(),"li"===h.nodeName.toLowerCase()){for(o=d.createDocumentFragment(),m=0;m"),c.transferChildNodes(r.childNodes[m],p[0]),c.transferNodeAttributes(r.childNodes[m],p[0]),o.appendChild(p[0]);r=o,n&&(n=r.childNodes[r.childNodes.length-1],n=n.childNodes[n.childNodes.length-1])}}else q.deleteContents();q.insertNode(r),n&&i.setSelectionToElementEnd(n)}};return i}]).service("taDOM",function(){var a={getByAttribute:function(b,c){var d=[],e=b.children();return e.length&&angular.forEach(e,function(b){d=d.concat(a.getByAttribute(angular.element(b),c))}),void 0!==b.attr(c)&&d.push(b),d},transferChildNodes:function(a,b){for(b.innerHTML="";a.childNodes.length>0;)b.appendChild(a.childNodes[0]);return b},splitNodes:function(b,c,d,e,f,g){if(!e&&isNaN(g))throw new Error("taDOM.splitNodes requires a splitNode or splitIndex");for(var h=document.createDocumentFragment(),i=document.createDocumentFragment(),j=0;b.length>0&&(isNaN(g)||g!==j)&&b[0]!==e;)h.appendChild(b[0]),j++;for(!isNaN(f)&&f>=0&&b[0]&&(h.appendChild(document.createTextNode(b[0].nodeValue.substring(0,f))),b[0].nodeValue=b[0].nodeValue.substring(f));b.length>0;)i.appendChild(b[0]);a.transferChildNodes(h,c),a.transferChildNodes(i,d)},transferNodeAttributes:function(a,b){for(var c=0;c");return b.html(a),b.text().length<=e}}}}).directive("taMinText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e=parseInt(a.$eval(c.taMinText));if(isNaN(e))throw"Min text must be an integer";c.$observe("taMinText",function(a){if(e=parseInt(a),isNaN(e))throw"Min text must be an integer";d.$dirty&&d.$validate()}),d.$validators.taMinText=function(a){var b=angular.element("
    ");return b.html(a),!b.text().length||b.text().length>=e}}}}),angular.module("textAngular.taBind",["textAngular.factories","textAngular.DOM"]).service("_taBlankTest",[function(){var a=/<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;return function(b){return function(c){if(!c)return!0;var d,e=/(^[^<]|>)[^<]/i.exec(c);return e?d=e.index:(c=c.toString().replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,""),d=c.indexOf(">")),c=c.trim().substring(d,d+100),/^[^<>]+$/i.test(c)?!1:0===c.length||c===b||/^>(\s| )*<\/[^>]+>$/gi.test(c)?!0:/>\s*[^\s<]/i.test(c)||a.test(c)?!1:!0}}}]).directive("taButton",[function(){return{link:function(a,b){b.attr("unselectable","on"),b.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1})}}}]).directive("taBind",["taSanitize","$timeout","$window","$document","taFixChrome","taBrowserTag","taSelection","taSelectableElements","taApplyCustomRenderers","taOptions","_taBlankTest","$parse","taDOM",function(a,b,f,h,k,l,m,n,o,q,r,s,t){return{priority:2,require:["ngModel","?ngModelOptions"],link:function(l,u,v,w){var x,y,z,A,B=w[0],C=w[1]||{},D=void 0!==u.attr("contenteditable")&&u.attr("contenteditable"),E=D||"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase(),F=!1,G=!1,H=!1,I=v.taUnsafeSanitizer||q.disableSanitizer,J=/^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i,K=/^(8|13|32|46|59|61|107|109|186|187|188|189|190|191|192|219|220|221|222)$/i;void 0===v.taDefaultWrap&&(v.taDefaultWrap="p"),""===v.taDefaultWrap?(z="",A=void 0===c.ie?"

    ":c.ie>=11?"


    ":c.ie<=8?"

     

    ":"

     

    "):(z=void 0===c.ie||c.ie>=11?"<"+v.taDefaultWrap+">
    ":c.ie<=8?"<"+v.taDefaultWrap.toUpperCase()+">":"<"+v.taDefaultWrap+">",A=void 0===c.ie||c.ie>=11?"<"+v.taDefaultWrap+">
    ":c.ie<=8?"<"+v.taDefaultWrap.toUpperCase()+"> ":"<"+v.taDefaultWrap+"> "),C.$options||(C.$options={});var L=r(A),M=function(a){if(L(a))return a;var b=angular.element("
    "+a+"
    ");if(0===b.children().length)a="<"+v.taDefaultWrap+">"+a+"";else{var c,d=b[0].childNodes,f=!1;for(c=0;c"+g+"":g}else a="<"+v.taDefaultWrap+">"+a+""}return a};v.taPaste&&(y=s(v.taPaste)),u.addClass("ta-bind");var N;l["$undoManager"+(v.id||"")]=B.$undoManager={_stack:[],_index:0,_max:1e3,push:function(a){return"undefined"==typeof a||null===a||"undefined"!=typeof this.current()&&null!==this.current()&&a===this.current()?a:(this._indexthis._max&&this._stack.shift(),this._index=this._stack.length-1,a)},undo:function(){return this.setToIndex(this._index-1)},redo:function(){return this.setToIndex(this._index+1)},setToIndex:function(a){return 0>a||a>this._stack.length-1?void 0:(this._index=a,this.current())},current:function(){return this._stack[this._index]}};var O,P=l["$undoTaBind"+(v.id||"")]=function(){if(!F&&D){var a=B.$undoManager.undo();"undefined"!=typeof a&&null!==a&&(cb(a),S(a,!1),O&&b.cancel(O),O=b(function(){u[0].focus(),m.setSelectionToElementEnd(u[0])},1))}},Q=l["$redoTaBind"+(v.id||"")]=function(){if(!F&&D){var a=B.$undoManager.redo();"undefined"!=typeof a&&null!==a&&(cb(a),S(a,!1),O&&b.cancel(O),O=b(function(){u[0].focus(),m.setSelectionToElementEnd(u[0])},1))}},R=function(){if(D)return u[0].innerHTML;if(E)return u.val();throw"textAngular Error: attempting to update non-editable taBind"},S=function(a,b,c){H=c||!1,("undefined"==typeof b||null===b)&&(b=!0&&D),("undefined"==typeof a||null===a)&&(a=R()),L(a)?(""!==B.$viewValue&&B.$setViewValue(""),b&&""!==B.$undoManager.current()&&B.$undoManager.push("")):(bb(),B.$viewValue!==a&&(B.$setViewValue(a),b&&B.$undoManager.push(a))),B.$render()};l["updateTaBind"+(v.id||"")]=function(){F||S(void 0,void 0,!0)};var T=function(b){return B.$oldViewValue=a(k(b),B.$oldViewValue,I)};if(u.attr("required")&&(B.$validators.required=function(a,b){return!L(a||b)}),B.$parsers.push(T),B.$parsers.unshift(M),B.$formatters.push(T),B.$formatters.unshift(M),B.$formatters.unshift(function(a){return B.$undoManager.push(a||"")}),E)if(l.events={},D){var U=!1,V=function(c){if(c&&c.trim().length){if(c.match(/class=["']*Mso(Normal|List)/i)){var d=c.match(/([\s\S]*?)/i);d=d?d[1]:c,d=d.replace(/[\s\S]*?<\/o:p>/gi,"").replace(/class=(["']|)MsoNormal(["']|)/gi,"");var e=angular.element("
    "+d+"
    "),f=angular.element("
    "),g={element:null,lastIndent:[],lastLi:null,isUl:!1};g.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0};for(var h=function(a){g.isUl=a,g.element=angular.element(a?"
      ":"
        "),g.lastIndent=[],g.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0},g.lastLevelMatch=null},i=0;i<=e[0].childNodes.length;i++)if(e[0].childNodes[i]&&"#text"!==e[0].childNodes[i].nodeName&&"p"===e[0].childNodes[i].tagName.toLowerCase()){var j=angular.element(e[0].childNodes[i]),k=(j.attr("class")||"").match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);if(k){if(j[0].childNodes.length<2||j[0].childNodes[1].childNodes.length<1)continue;var n="bullet"===k[1].toLowerCase()||"number"!==k[1].toLowerCase()&&!(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]]":"
          "),g.lastLi.append(g.element);else if(null!=g.lastIndent.peek()&&g.lastIndent.peek()>p){for(;null!=g.lastIndent.peek()&&g.lastIndent.peek()>p;)if("li"!==g.element.parent()[0].tagName.toLowerCase()){if(!/[uo]l/i.test(g.element.parent()[0].tagName.toLowerCase()))break;g.element=g.element.parent(),g.lastIndent.pop()}else g.element=g.element.parent();g.isUl="ul"===g.element[0].tagName.toLowerCase(),n!==g.isUl&&(h(n),f.append(g.element))}g.lastLevelMatch=q,p!==g.lastIndent.peek()&&g.lastIndent.push(p),g.lastLi=angular.element("
        1. "),g.element.append(g.lastLi),g.lastLi.html(j.html().replace(/[\s\S]*?/gi,"")),j.remove()}else h(!1),f.append(j)}var r=function(a){a=angular.element(a);for(var b=a[0].childNodes.length-1;b>=0;b--)a.after(a[0].childNodes[b]);a.remove()};angular.forEach(f.find("span"),function(a){a.removeAttribute("lang"),a.attributes.length<=0&&r(a)}),angular.forEach(f.find("font"),r),c=f.html()}else{if(c=c.replace(/<(|\/)meta[^>]*?>/gi,""),c.match(/<[^>]*?(ta-bind)[^>]*?>/)){if(c.match(/<[^>]*?(text-angular)[^>]*?>/)){var s=angular.element("
          "+c+"
          ");s.find("textarea").remove();for(var v=t.getByAttribute(s,"ta-bind"),w=0;w',"")}}else c.match(/^]*?>/gi,""));c=c.replace(/
          ]*?>/gi,"").replace(/( | )<\/span>/gi," ")}c=a(c,"",I),//i.test(c)&&/(|).*/i.test(c)===!1&&(c=c.replace(/.*<\/li(\s.*)?>/i,"
            $&
          ")),y&&(c=y(l,{$html:c})||c),m.insertHtml(c,u[0]),b(function(){B.$setViewValue(R()),U=!1,u.removeClass("processing-paste")},0)}else U=!1,u.removeClass("processing-paste")};u.on("paste",l.events.paste=function(a,c){if(c&&angular.extend(a,c),F||U)return a.stopPropagation(),a.preventDefault(),!1;U=!0,u.addClass("processing-paste");var d,e=(a.originalEvent||a).clipboardData;if(e&&e.getData&&e.types.length>0){for(var g="",i=0;i
    ');h.find("body").append(k),k[0].focus(),b(function(){f.rangy.restoreSelection(j),V(k[0].innerHTML),u[0].focus(),k.remove()},0)}),u.on("cut",l.events.cut=function(a){F?a.preventDefault():b(function(){B.$setViewValue(R())},0)}),u.on("keydown",l.events.keydown=function(a,b){if(b&&angular.extend(a,b),!F)if(a.altKey||!a.metaKey&&!a.ctrlKey){if(13===a.keyCode&&!a.shiftKey){var c,d=m.getSelectionElement();if(!d.tagName.match(g))return;var e=angular.element(z);if(/^$/i.test(d.innerHTML.trim())&&"blockquote"===d.parentNode.tagName.toLowerCase()&&!d.nextSibling){c=angular.element(d);var f=c.parent();f.after(e),c.remove(),0===f.children().length&&f.remove(),m.setSelectionToElementStart(e[0]),a.preventDefault()}else/^<[^>]+><\/[^>]+>$/i.test(d.innerHTML.trim())&&"blockquote"===d.tagName.toLowerCase()&&(c=angular.element(d),c.after(e),c.remove(),m.setSelectionToElementStart(e[0]),a.preventDefault())}}else 90!==a.keyCode||a.shiftKey?(90===a.keyCode&&a.shiftKey||89===a.keyCode&&!a.shiftKey)&&(Q(),a.preventDefault()):(P(),a.preventDefault())});var W;if(u.on("keyup",l.events.keyup=function(a,c){if(c&&angular.extend(a,c),9===a.keyCode){var d=m.getSelection();return void(d.start.element===u[0]&&u.children().length&&m.setSelectionToElementStart(u.children()[0]))}if(N&&b.cancel(N),!F&&!J.test(a.keyCode)){if(""!==z&&13===a.keyCode&&!a.shiftKey){for(var e=m.getSelectionElement();!e.tagName.match(g)&&e!==u[0];)e=e.parentNode;if(e.tagName.toLowerCase()!==v.taDefaultWrap&&"li"!==e.tagName.toLowerCase()&&(""===e.innerHTML.trim()||"
    "===e.innerHTML.trim())){var h=angular.element(z);angular.element(e).replaceWith(h),m.setSelectionToElementStart(h[0])}}var i=R();if(""!==z&&""===i.trim())cb(z),m.setSelectionToElementStart(u.children()[0]);else if("<"!==i.substring(0,1)&&""!==v.taDefaultWrap){var j=f.rangy.saveSelection();i=R(),i="<"+v.taDefaultWrap+">"+i+"",cb(i),f.rangy.restoreSelection(j)}var k=x!==a.keyCode&&K.test(a.keyCode);W&&b.cancel(W),W=b(function(){S(i,k,!0)},C.$options.debounce||400),k||(N=b(function(){B.$undoManager.push(i)},250)),x=a.keyCode}}),u.on("blur",l.events.blur=function(){G=!1,F?(H=!0,B.$render()):S(void 0,void 0,!0)}),v.placeholder&&(c.ie>8||void 0===c.ie)){var X;if(!v.id)throw"textAngular Error: An unique ID is required for placeholders to work";X=i("#"+v.id+".placeholder-text:before",'content: "'+v.placeholder+'"'),l.$on("$destroy",function(){j(X)})}u.on("focus",l.events.focus=function(){G=!0,u.removeClass("placeholder-text")}),u.on("mouseup",l.events.mouseup=function(){var a=m.getSelection();a.start.element===u[0]&&u.children().length&&m.setSelectionToElementStart(u.children()[0])}),u.on("mousedown",l.events.mousedown=function(a,b){b&&angular.extend(a,b),a.stopPropagation()})}else{u.on("change blur",l.events.change=l.events.blur=function(){F||B.$setViewValue(R())}),u.on("keydown",l.events.keydown=function(a,b){if(b&&angular.extend(a,b),9===a.keyCode){var c=this.selectionStart,d=this.selectionEnd,e=u.val();if(a.shiftKey){var f=e.lastIndexOf("\n",c),g=e.lastIndexOf(" ",c);-1!==g&&g>=f&&(u.val(e.substring(0,g)+e.substring(g+1)),this.selectionStart=this.selectionEnd=c-1)}else u.val(e.substring(0,c)+" "+e.substring(d)),this.selectionStart=this.selectionEnd=c+1;a.preventDefault()}});var Y=function(a,b){for(var c="",d=0;b>d;d++)c+=a;return c},Z=function(a,b){var c="",d=a.childNodes;b++,c+=Y(" ",b-1)+a.outerHTML.substring(0,a.outerHTML.indexOf(""+a+"")[0].childNodes;if(b.length>0){a="";for(var c=0;c0&&(a+="\n"),a+="ul"===b[c].nodeName.toLowerCase()||"ol"===b[c].nodeName.toLowerCase()?""+Z(b[c],0):""+b[c].outerHTML)}return a})}var $,_=function(a){return l.$emit("ta-element-select",this),a.preventDefault(),!1},ab=function(a,c){if(c&&angular.extend(a,c),!p&&!F){p=!0;var d;d=a.originalEvent?a.originalEvent.dataTransfer:a.dataTransfer,l.$emit("ta-drop-event",this,a,d),b(function(){p=!1,S(void 0,void 0,!0)},100)}},bb=l["reApplyOnSelectorHandlers"+(v.id||"")]=function(){F||angular.forEach(n,function(a){u.find(a).off("click",_).on("click",_)})},cb=function(a){u[0].innerHTML=a},db=!1;B.$render=function(){if(!db){db=!0;var a=B.$viewValue||"";H||(D&&G&&(u.removeClass("placeholder-text"),$&&b.cancel($),$=b(function(){G||(u[0].focus(),m.setSelectionToElementEnd(u.children()[u.children().length-1])),$=void 0},1)),D?(cb(v.placeholder?""===a?z:a:""===a?z:a),F?u.off("drop",ab):(bb(),u.on("drop",ab))):"textarea"!==u[0].tagName.toLowerCase()&&"input"!==u[0].tagName.toLowerCase()?cb(o(a)):u.val(a)),D&&v.placeholder&&(""===a?G?u.removeClass("placeholder-text"):u.addClass("placeholder-text"):u.removeClass("placeholder-text")),db=H=!1}},v.taReadonly&&(F=l.$eval(v.taReadonly),F?(u.addClass("ta-readonly"),("textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase())&&u.attr("disabled","disabled"),void 0!==u.attr("contenteditable")&&u.attr("contenteditable")&&u.removeAttr("contenteditable")):(u.removeClass("ta-readonly"),"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase()?u.removeAttr("disabled"):D&&u.attr("contenteditable","true")),l.$watch(v.taReadonly,function(a,b){b!==a&&(a?(u.addClass("ta-readonly"),("textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase())&&u.attr("disabled","disabled"),void 0!==u.attr("contenteditable")&&u.attr("contenteditable")&&u.removeAttr("contenteditable"),angular.forEach(n,function(a){u.find(a).on("click",_)}),u.off("drop",ab)):(u.removeClass("ta-readonly"),"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase()?u.removeAttr("disabled"):D&&u.attr("contenteditable","true"),angular.forEach(n,function(a){u.find(a).off("click",_)}),u.on("drop",ab)),F=a)})),D&&!F&&(angular.forEach(n,function(a){u.find(a).on("click",_)}),u.on("drop",ab),u.on("blur",function(){c.webkit&&(d=!0)}))}}}]);var p=!1,q=angular.module("textAngular",["ngSanitize","textAngularSetup","textAngular.factories","textAngular.DOM","textAngular.validators","textAngular.taBind"]),r={};q.constant("taRegisterTool",b),q.value("taTools",r),q.config([function(){angular.forEach(r,function(a,b){delete r[b]})}]),q.run([function(){if(!window.rangy)throw"rangy-core.js and rangy-selectionsaverestore.js are required for textAngular to work correctly, rangy-core is not yet loaded.";if(window.rangy.init(),!window.rangy.saveSelection)throw"rangy-selectionsaverestore.js is required for textAngular to work correctly."}]),q.directive("textAngular",["$compile","$timeout","taOptions","taSelection","taExecCommand","textAngularManager","$window","$document","$animate","$log","$q","$parse",function(a,b,c,d,e,f,g,h,i,j,k,l){return{require:"?ngModel",scope:{},restrict:"EA",priority:2,link:function(m,n,o,p){var q,r,s,t,u,v,w,x,y,z,A,B=o.serial?o.serial:Math.floor(1e16*Math.random()); +function(){"use strict";function a(a){try{return 0!==angular.element(a).length}catch(b){return!1}}function b(b,c){if(!b||""===b||r.hasOwnProperty(b))throw"textAngular Error: A unique name is required for a Tool Definition";if(c.display&&(""===c.display||!a(c.display))||!c.display&&!c.buttontext&&!c.iconclass)throw'textAngular Error: Tool Definition for "'+b+'" does not have a valid display/iconclass/buttontext value';r[b]=c}var c={ie:function(){for(var a,b=3,c=document.createElement("div"),d=c.getElementsByTagName("i");c.innerHTML="",d[0];);return b>4?b:a}(),webkit:/AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)},d=!1;c.webkit&&(document.addEventListener("mousedown",function(a){var b=a||window.event,c=b.target;if(d&&null!==c){for(var e=!1,f=c;null!==f&&"html"!==f.tagName.toLowerCase()&&!e;)e="true"===f.contentEditable,f=f.parentNode;e||(document.getElementById("textAngular-editableFix-010203040506070809").setSelectionRange(0,0),c.focus(),c.select&&c.select())}d=!1},!1),angular.element(document).ready(function(){angular.element(document.body).append(angular.element(''))}));var e=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i,f=/^(ul|li|ol)$/i,g=/^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i;String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});var h,i,j,k,l,m;if(c.ie>8||void 0===c.ie){for(var n=document.styleSheets,o=0;o");return d[0].innerHTML=c,angular.forEach(a,function(a){var c=[];a.selector&&""!==a.selector?c=d.find(a.selector):a.customAttribute&&""!==a.customAttribute&&(c=b.getByAttribute(d,a.customAttribute)),angular.forEach(c,function(b){b=angular.element(b),a.selector&&""!==a.selector&&a.customAttribute&&""!==a.customAttribute?void 0!==b.attr(a.customAttribute)&&a.renderLogic(b):a.renderLogic(b)})}),d[0].innerHTML}}]).factory("taFixChrome",function(){var a=function(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c,d,e=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,f="",g=0;b=e.exec(a);)c=b[3]||b[4],c&&c.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)&&(c=c.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/gi,""),d="<"+b[1].trim(),c.trim().length>0&&(d+=" style="+b[2].substring(0,1)+c+b[2].substring(0,1)),d+=b[5].trim()+">",f+=a.substring(g,b.index)+d,g=b.index+b[0].length);return f+=a.substring(g),g>0?f.replace(/(.*?)<\/span>(|)/gi,"$1"):a};return a}).factory("taSanitize",["$sanitize",function(a){function b(a,b){for(var c,d=0,e=0,f=/<[^>]*>/gi;c=f.exec(a);)if(e=c.index,"/"===c[0].substr(1,1)){if(0===d)break;d--}else d++;return b+a.substring(0,e)+angular.element(b)[0].outerHTML.substring(b.length)+a.substring(e)}function c(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var d,f,g,h,i,k,l=/<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/gi,m="",n="",o=0;f=l.exec(a);){h=f[3]||f[4];var p=new RegExp(j,"i");if(angular.isString(h)&&p.test(h)){i="";for(var q=new RegExp(j,"ig");g=q.exec(h);)for(d=0;d");k=c(a.substring(o,f.index)),n+=m.length>0?b(k,m):k,h=h.replace(new RegExp(j,"ig"),""),n+="<"+f[1].trim(),h.length>0&&(n+=' style="'+h+'"'),n+=f[5]+">",o=f.index+f[0].length,m=i}}return n+=m.length>0?b(a.substring(o),m):a.substring(o)}function d(a){if(!a||!angular.isString(a)||a.length<=0)return a;for(var b,c=/<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/gi,d="",e=0;b=c.exec(a);){d+=a.substring(e,b.index),e=b.index+b[0].length;var f="<"+b[1]+b[5];/style=("([^"]+)"|'([^']+)')/gi.test(f)?f=f.replace(/style=("([^"]+)"|'([^']+)')/i,'style="$2$3 text-align:'+(b[3]||b[4])+';"'):f+=' style="text-align:'+(b[3]||b[4])+';"',f+=">",d+=f}return d+a.substring(e)}for(var e=[{property:"font-weight",values:["bold"],tag:"b"},{property:"font-style",values:["italic"],tag:"i"}],f=[],g=0;g0&&(h+="|"),h+=e[g].values[i];h+=");)",f.push(h)}var j="("+f.join("|")+")";return function(b,e,f){if(!f)try{b=c(b)}catch(g){}b=d(b);var h;try{h=a(b),f&&(h=b)}catch(g){h=e||""}var i,j=h.match(/(]*>.*?<\/pre[^>]*>)/gi),k=h.replace(/(&#(9|10);)*/gi,""),l=/]*>.*?<\/pre[^>]*>/gi,m=0,n=0;for(h="";null!==(i=l.exec(k))&&m=0;e--)d=angular.element("<"+c+">"+f[e].innerHTML+""),b.after(d);b.remove(),a.setSelectionToElementEnd(d[0])},g=function(b){/()$/i.test(b.innerHTML.trim())?a.setSelectionBeforeElement(angular.element(b).find("br")[0]):a.setSelectionToElementEnd(b)},h=function(a,b){var c=angular.element("<"+b+">"+a[0].innerHTML+"");a.after(c),a.remove(),g(c.find("li")[0])},i=function(a,c,d){for(var e="",f=0;f"+a[f].innerHTML+"";var h=angular.element("<"+d+">"+e+"");c.after(h),c.remove(),g(h.find("li")[0])};return function(g,j){return g=b(g),function(k,l,m,n){var o,p,q,r,s,t,u,v=angular.element("<"+g+">");try{u=a.getSelectionElement()}catch(w){}var x=angular.element(u);if(void 0!==u){var y=u.tagName.toLowerCase();if("insertorderedlist"===k.toLowerCase()||"insertunorderedlist"===k.toLowerCase()){var z=b("insertorderedlist"===k.toLowerCase()?"ol":"ul");if(y===z)return d(x,g);if("li"===y&&x.parent()[0].tagName.toLowerCase()===z&&1===x.parent().children().length)return d(x.parent(),g);if("li"===y&&x.parent()[0].tagName.toLowerCase()!==z&&1===x.parent().children().length)return h(x.parent(),z);if(y.match(e)&&!x.hasClass("ta-bind")){if("ol"===y||"ul"===y)return h(x,z);var A=!1;return angular.forEach(x.children(),function(a){a.tagName.match(e)&&(A=!0)}),A?i(x.children(),x,z):i([angular.element("
    "+u.innerHTML+"
    ")[0]],x,z)}if(y.match(e)){if(r=a.getOnlySelectedElements(),0===r.length)p=angular.element("<"+z+">
  • "+u.innerHTML+"
  • "),x.html(""),x.append(p);else{if(1===r.length&&("ol"===r[0].tagName.toLowerCase()||"ul"===r[0].tagName.toLowerCase()))return r[0].tagName.toLowerCase()===z?d(angular.element(r[0]),g):h(angular.element(r[0]),z);q="";var B=[];for(o=0;o"+C[0].innerHTML+"":C[0].childNodes[0].innerHTML,B.unshift(C)}p=angular.element("<"+z+">"+q+""),B.pop().replaceWith(p),angular.forEach(B,function(a){a.remove()})}return void a.setSelectionToElementEnd(p[0])}}else{if("formatblock"===k.toLowerCase()){for(t=m.toLowerCase().replace(/[<>]/gi,""),"default"===t.trim()&&(t=g,m="<"+g+">"),p="li"===y?x.parent():x;!p[0].tagName||!p[0].tagName.match(e)&&!p.parent().attr("contenteditable");)p=p.parent(),y=(p[0].tagName||"").toLowerCase();if(y===t){r=p.children();var D=!1;for(o=0;o=0;o--)r[o].parentNode&&r[o].parentNode.removeChild(r[o])}else for(o=0;o"),v[0].innerHTML=F[o].outerHTML,F[o]=v[0]),E.parent()[0].insertBefore(F[o],E[0]);E.remove()}return void a.setSelectionToElementEnd(p[0])}if("createlink"===k.toLowerCase()){var G='',H="",I=a.getSelection();if(I.collapsed)a.insertHtml(G+m+H,j);else if(rangy.getSelection().getRangeAt(0).canSurroundContents()){var J=angular.element(G+H)[0];rangy.getSelection().getRangeAt(0).surroundContents(J)}return}if("inserthtml"===k.toLowerCase())return void a.insertHtml(m,j)}}try{c[0].execCommand(k,l,m)}catch(w){}}}}]).service("taSelection",["$window","$document","taDOM",function(a,b,c){var d=b[0],f=a.rangy,h=function(a,b){return a.tagName&&a.tagName.match(/^br$/i)&&0===b&&!a.previousSibling?{element:a.parentNode,offset:0}:{element:a,offset:b}},i={getSelection:function(){var a=f.getSelection().getRangeAt(0),b=a.commonAncestorContainer,c={start:h(a.startContainer,a.startOffset),end:h(a.endContainer,a.endOffset),collapsed:a.collapsed};return b=3===b.nodeType?b.parentNode:b,c.container=b.parentNode===c.start.element||b.parentNode===c.end.element?b.parentNode:b,c},getOnlySelectedElements:function(){var a=f.getSelection().getRangeAt(0),b=a.commonAncestorContainer;return b=3===b.nodeType?b.parentNode:b,a.getNodes([1],function(a){return a.parentNode===b})},getSelectionElement:function(){return i.getSelection().container},setSelection:function(a,b,c){var d=f.createRange();d.setStart(a,b),d.setEnd(a,c),f.getSelection().setSingleRange(d)},setSelectionBeforeElement:function(a){var b=f.createRange();b.selectNode(a),b.collapse(!0),f.getSelection().setSingleRange(b)},setSelectionAfterElement:function(a){var b=f.createRange();b.selectNode(a),b.collapse(!1),f.getSelection().setSingleRange(b)},setSelectionToElementStart:function(a){var b=f.createRange();b.selectNodeContents(a),b.collapse(!0),f.getSelection().setSingleRange(b)},setSelectionToElementEnd:function(a){var b=f.createRange();b.selectNodeContents(a),b.collapse(!1),a.childNodes&&a.childNodes[a.childNodes.length-1]&&"br"===a.childNodes[a.childNodes.length-1].nodeName&&(b.startOffset=b.endOffset=b.startOffset-1),f.getSelection().setSingleRange(b)},insertHtml:function(a,b){var h,j,k,l,m,n,o,p=angular.element("
    "+a+"
    "),q=f.getSelection().getRangeAt(0),r=d.createDocumentFragment(),s=p[0].childNodes,t=!0;if(s.length>0){for(l=[],k=0;k)$/i.test(q.startContainer.innerHTML)&&q.selectNode(q.startContainer)}else t=!0,n=r=d.createTextNode(a);if(t)q.deleteContents();else if(q.collapsed&&q.startContainer!==b)if(q.startContainer.innerHTML&&q.startContainer.innerHTML.match(/^<[^>]*>$/i))h=q.startContainer,1===q.startOffset?(q.setStartAfter(h),q.setEndAfter(h)):(q.setStartBefore(h),q.setEndBefore(h));else{if(3===q.startContainer.nodeType&&q.startContainer.parentNode!==b)for(h=q.startContainer.parentNode,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,q.startContainer,q.startOffset);!g.test(h.nodeName);){angular.element(h).after(j),h=h.parentNode;var v=j;j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,v)}else h=q.startContainer,j=h.cloneNode(),c.splitNodes(h.childNodes,h,j,void 0,void 0,q.startOffset);if(angular.element(h).after(j),q.setStartAfter(h),q.setEndAfter(h),/^(|)$/i.test(h.innerHTML.trim())&&(q.setStartBefore(h),q.setEndBefore(h),angular.element(h).remove()),/^(|)$/i.test(j.innerHTML.trim())&&angular.element(j).remove(),"li"===h.nodeName.toLowerCase()){for(o=d.createDocumentFragment(),m=0;m"),c.transferChildNodes(r.childNodes[m],p[0]),c.transferNodeAttributes(r.childNodes[m],p[0]),o.appendChild(p[0]);r=o,n&&(n=r.childNodes[r.childNodes.length-1],n=n.childNodes[n.childNodes.length-1])}}else q.deleteContents();q.insertNode(r),n&&i.setSelectionToElementEnd(n)}};return i}]).service("taDOM",function(){var a={getByAttribute:function(b,c){var d=[],e=b.children();return e.length&&angular.forEach(e,function(b){d=d.concat(a.getByAttribute(angular.element(b),c))}),void 0!==b.attr(c)&&d.push(b),d},transferChildNodes:function(a,b){for(b.innerHTML="";a.childNodes.length>0;)b.appendChild(a.childNodes[0]);return b},splitNodes:function(b,c,d,e,f,g){if(!e&&isNaN(g))throw new Error("taDOM.splitNodes requires a splitNode or splitIndex");for(var h=document.createDocumentFragment(),i=document.createDocumentFragment(),j=0;b.length>0&&(isNaN(g)||g!==j)&&b[0]!==e;)h.appendChild(b[0]),j++;for(!isNaN(f)&&f>=0&&b[0]&&(h.appendChild(document.createTextNode(b[0].nodeValue.substring(0,f))),b[0].nodeValue=b[0].nodeValue.substring(f));b.length>0;)i.appendChild(b[0]);a.transferChildNodes(h,c),a.transferChildNodes(i,d)},transferNodeAttributes:function(a,b){for(var c=0;c");return b.html(a),b.text().length<=e}}}}).directive("taMinText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e=parseInt(a.$eval(c.taMinText));if(isNaN(e))throw"Min text must be an integer";c.$observe("taMinText",function(a){if(e=parseInt(a),isNaN(e))throw"Min text must be an integer";d.$dirty&&d.$validate()}),d.$validators.taMinText=function(a){var b=angular.element("
    ");return b.html(a),!b.text().length||b.text().length>=e}}}}),angular.module("textAngular.taBind",["textAngular.factories","textAngular.DOM"]).service("_taBlankTest",[function(){var a=/<(a|abbr|acronym|bdi|bdo|big|cite|code|del|dfn|img|ins|kbd|label|map|mark|q|ruby|rp|rt|s|samp|time|tt|var)[^>]*(>|$)/i;return function(b){return function(c){if(!c)return!0;var d,e=/(^[^<]|>)[^<]/i.exec(c);return e?d=e.index:(c=c.toString().replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,"").replace(/="[^"]*"/i,""),d=c.indexOf(">")),c=c.trim().substring(d,d+100),/^[^<>]+$/i.test(c)?!1:0===c.length||c===b||/^>(\s| )*<\/[^>]+>$/gi.test(c)?!0:/>\s*[^\s<]/i.test(c)||a.test(c)?!1:!0}}}]).directive("taButton",[function(){return{link:function(a,b){b.attr("unselectable","on"),b.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1})}}}]).directive("taBind",["taSanitize","$timeout","$window","$document","taFixChrome","taBrowserTag","taSelection","taSelectableElements","taApplyCustomRenderers","taOptions","_taBlankTest","$parse","taDOM",function(a,b,f,h,k,l,m,n,o,q,r,s,t){return{priority:2,require:["ngModel","?ngModelOptions"],link:function(l,u,v,w){var x,y,z,A,B=w[0],C=w[1]||{},D=void 0!==u.attr("contenteditable")&&u.attr("contenteditable"),E=D||"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase(),F=!1,G=!1,H=!1,I=v.taUnsafeSanitizer||q.disableSanitizer,J=/^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/i,K=/^(8|13|32|46|59|61|107|109|186|187|188|189|190|191|192|219|220|221|222)$/i;void 0===v.taDefaultWrap&&(v.taDefaultWrap="p"),""===v.taDefaultWrap?(z="",A=void 0===c.ie?"

    ":c.ie>=11?"


    ":c.ie<=8?"

     

    ":"

     

    "):(z=void 0===c.ie||c.ie>=11?"<"+v.taDefaultWrap+">
    ":c.ie<=8?"<"+v.taDefaultWrap.toUpperCase()+">":"<"+v.taDefaultWrap+">",A=void 0===c.ie||c.ie>=11?"<"+v.taDefaultWrap+">
    ":c.ie<=8?"<"+v.taDefaultWrap.toUpperCase()+"> ":"<"+v.taDefaultWrap+"> "),C.$options||(C.$options={});var L=r(A),M=function(a){if(L(a))return a;var b=angular.element("
    "+a+"
    ");if(0===b.children().length)a="<"+v.taDefaultWrap+">"+a+"";else{var c,d=b[0].childNodes,f=!1;for(c=0;c"+g+"":g}else a="<"+v.taDefaultWrap+">"+a+""}return a};v.taPaste&&(y=s(v.taPaste)),u.addClass("ta-bind");var N;l["$undoManager"+(v.id||"")]=B.$undoManager={_stack:[],_index:0,_max:1e3,push:function(a){return"undefined"==typeof a||null===a||"undefined"!=typeof this.current()&&null!==this.current()&&a===this.current()?a:(this._indexthis._max&&this._stack.shift(),this._index=this._stack.length-1,a)},undo:function(){return this.setToIndex(this._index-1)},redo:function(){return this.setToIndex(this._index+1)},setToIndex:function(a){return 0>a||a>this._stack.length-1?void 0:(this._index=a,this.current())},current:function(){return this._stack[this._index]}};var O,P=l["$undoTaBind"+(v.id||"")]=function(){if(!F&&D){var a=B.$undoManager.undo();"undefined"!=typeof a&&null!==a&&(cb(a),S(a,!1),O&&b.cancel(O),O=b(function(){u[0].focus(),m.setSelectionToElementEnd(u[0])},1))}},Q=l["$redoTaBind"+(v.id||"")]=function(){if(!F&&D){var a=B.$undoManager.redo();"undefined"!=typeof a&&null!==a&&(cb(a),S(a,!1),O&&b.cancel(O),O=b(function(){u[0].focus(),m.setSelectionToElementEnd(u[0])},1))}},R=function(){if(D)return u[0].innerHTML;if(E)return u.val();throw"textAngular Error: attempting to update non-editable taBind"},S=function(a,b,c){H=c||!1,("undefined"==typeof b||null===b)&&(b=!0&&D),("undefined"==typeof a||null===a)&&(a=R()),L(a)?(""!==B.$viewValue&&B.$setViewValue(""),b&&""!==B.$undoManager.current()&&B.$undoManager.push("")):(bb(),B.$viewValue!==a&&(B.$setViewValue(a),b&&B.$undoManager.push(a))),B.$render()};l["updateTaBind"+(v.id||"")]=function(){F||S(void 0,void 0,!0)};var T=function(b){return B.$oldViewValue=a(k(b),B.$oldViewValue,I)};if(u.attr("required")&&(B.$validators.required=function(a,b){return!L(a||b)}),B.$parsers.push(T),B.$parsers.unshift(M),B.$formatters.push(T),B.$formatters.unshift(M),B.$formatters.unshift(function(a){return B.$undoManager.push(a||"")}),E)if(l.events={},D){var U=!1,V=function(c){if(c&&c.trim().length){if(c.match(/class=["']*Mso(Normal|List)/i)){var d=c.match(/([\s\S]*?)/i);d=d?d[1]:c,d=d.replace(/[\s\S]*?<\/o:p>/gi,"").replace(/class=(["']|)MsoNormal(["']|)/gi,"");var e=angular.element("
    "+d+"
    "),f=angular.element("
    "),g={element:null,lastIndent:[],lastLi:null,isUl:!1};g.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0};for(var h=function(a){g.isUl=a,g.element=angular.element(a?"
      ":"
        "),g.lastIndent=[],g.lastIndent.peek=function(){var a=this.length;return a>0?this[a-1]:void 0},g.lastLevelMatch=null},i=0;i<=e[0].childNodes.length;i++)if(e[0].childNodes[i]&&"#text"!==e[0].childNodes[i].nodeName&&"p"===e[0].childNodes[i].tagName.toLowerCase()){var j=angular.element(e[0].childNodes[i]),k=(j.attr("class")||"").match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);if(k){if(j[0].childNodes.length<2||j[0].childNodes[1].childNodes.length<1)continue;var n="bullet"===k[1].toLowerCase()||"number"!==k[1].toLowerCase()&&!(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]]":"
          "),g.lastLi.append(g.element);else if(null!=g.lastIndent.peek()&&g.lastIndent.peek()>p){for(;null!=g.lastIndent.peek()&&g.lastIndent.peek()>p;)if("li"!==g.element.parent()[0].tagName.toLowerCase()){if(!/[uo]l/i.test(g.element.parent()[0].tagName.toLowerCase()))break;g.element=g.element.parent(),g.lastIndent.pop()}else g.element=g.element.parent();g.isUl="ul"===g.element[0].tagName.toLowerCase(),n!==g.isUl&&(h(n),f.append(g.element))}g.lastLevelMatch=q,p!==g.lastIndent.peek()&&g.lastIndent.push(p),g.lastLi=angular.element("
        1. "),g.element.append(g.lastLi),g.lastLi.html(j.html().replace(/[\s\S]*?/gi,"")),j.remove()}else h(!1),f.append(j)}var r=function(a){a=angular.element(a);for(var b=a[0].childNodes.length-1;b>=0;b--)a.after(a[0].childNodes[b]);a.remove()};angular.forEach(f.find("span"),function(a){a.removeAttribute("lang"),a.attributes.length<=0&&r(a)}),angular.forEach(f.find("font"),r),c=f.html()}else{if(c=c.replace(/<(|\/)meta[^>]*?>/gi,""),c.match(/<[^>]*?(ta-bind)[^>]*?>/)){if(c.match(/<[^>]*?(text-angular)[^>]*?>/)){var s=angular.element("
          "+c+"
          ");s.find("textarea").remove();for(var v=t.getByAttribute(s,"ta-bind"),w=0;w',"")}}else c.match(/^]*?>/gi,""));c=c.replace(/
          ]*?>/gi,"").replace(/( | )<\/span>/gi," ")}//i.test(c)&&/(|).*/i.test(c)===!1&&(c=c.replace(/.*<\/li(\s.*)?>/i,"
            $&
          ")),y&&(c=y(l,{$html:c})||c),c=a(c,"",I),m.insertHtml(c,u[0]),b(function(){B.$setViewValue(R()),U=!1,u.removeClass("processing-paste")},0)}else U=!1,u.removeClass("processing-paste")};u.on("paste",l.events.paste=function(a,c){if(c&&angular.extend(a,c),F||U)return a.stopPropagation(),a.preventDefault(),!1;U=!0,u.addClass("processing-paste");var d,e=(a.originalEvent||a).clipboardData;if(e&&e.getData&&e.types.length>0){for(var g="",i=0;i
    ');h.find("body").append(k),k[0].focus(),b(function(){f.rangy.restoreSelection(j),V(k[0].innerHTML),u[0].focus(),k.remove()},0)}),u.on("cut",l.events.cut=function(a){F?a.preventDefault():b(function(){B.$setViewValue(R())},0)}),u.on("keydown",l.events.keydown=function(a,b){if(b&&angular.extend(a,b),!F)if(a.altKey||!a.metaKey&&!a.ctrlKey){if(13===a.keyCode&&!a.shiftKey){var c,d=m.getSelectionElement();if(!d.tagName.match(g))return;var e=angular.element(z);if(/^$/i.test(d.innerHTML.trim())&&"blockquote"===d.parentNode.tagName.toLowerCase()&&!d.nextSibling){c=angular.element(d);var f=c.parent();f.after(e),c.remove(),0===f.children().length&&f.remove(),m.setSelectionToElementStart(e[0]),a.preventDefault()}else/^<[^>]+><\/[^>]+>$/i.test(d.innerHTML.trim())&&"blockquote"===d.tagName.toLowerCase()&&(c=angular.element(d),c.after(e),c.remove(),m.setSelectionToElementStart(e[0]),a.preventDefault())}}else 90!==a.keyCode||a.shiftKey?(90===a.keyCode&&a.shiftKey||89===a.keyCode&&!a.shiftKey)&&(Q(),a.preventDefault()):(P(),a.preventDefault())});var W;if(u.on("keyup",l.events.keyup=function(a,c){if(c&&angular.extend(a,c),9===a.keyCode){var d=m.getSelection();return void(d.start.element===u[0]&&u.children().length&&m.setSelectionToElementStart(u.children()[0]))}if(N&&b.cancel(N),!F&&!J.test(a.keyCode)){if(""!==z&&13===a.keyCode&&!a.shiftKey){for(var e=m.getSelectionElement();!e.tagName.match(g)&&e!==u[0];)e=e.parentNode;if(e.tagName.toLowerCase()!==v.taDefaultWrap&&"li"!==e.tagName.toLowerCase()&&(""===e.innerHTML.trim()||"
    "===e.innerHTML.trim())){var h=angular.element(z);angular.element(e).replaceWith(h),m.setSelectionToElementStart(h[0])}}var i=R();if(""!==z&&""===i.trim())cb(z),m.setSelectionToElementStart(u.children()[0]);else if("<"!==i.substring(0,1)&&""!==v.taDefaultWrap){var j=f.rangy.saveSelection();i=R(),i="<"+v.taDefaultWrap+">"+i+"",cb(i),f.rangy.restoreSelection(j)}var k=x!==a.keyCode&&K.test(a.keyCode);W&&b.cancel(W),W=b(function(){S(i,k,!0)},C.$options.debounce||400),k||(N=b(function(){B.$undoManager.push(i)},250)),x=a.keyCode}}),u.on("blur",l.events.blur=function(){G=!1,F?(H=!0,B.$render()):S(void 0,void 0,!0)}),v.placeholder&&(c.ie>8||void 0===c.ie)){var X;if(!v.id)throw"textAngular Error: An unique ID is required for placeholders to work";X=i("#"+v.id+".placeholder-text:before",'content: "'+v.placeholder+'"'),l.$on("$destroy",function(){j(X)})}u.on("focus",l.events.focus=function(){G=!0,u.removeClass("placeholder-text")}),u.on("mouseup",l.events.mouseup=function(){var a=m.getSelection();a.start.element===u[0]&&u.children().length&&m.setSelectionToElementStart(u.children()[0])}),u.on("mousedown",l.events.mousedown=function(a,b){b&&angular.extend(a,b),a.stopPropagation()})}else{u.on("change blur",l.events.change=l.events.blur=function(){F||B.$setViewValue(R())}),u.on("keydown",l.events.keydown=function(a,b){if(b&&angular.extend(a,b),9===a.keyCode){var c=this.selectionStart,d=this.selectionEnd,e=u.val();if(a.shiftKey){var f=e.lastIndexOf("\n",c),g=e.lastIndexOf(" ",c);-1!==g&&g>=f&&(u.val(e.substring(0,g)+e.substring(g+1)),this.selectionStart=this.selectionEnd=c-1)}else u.val(e.substring(0,c)+" "+e.substring(d)),this.selectionStart=this.selectionEnd=c+1;a.preventDefault()}});var Y=function(a,b){for(var c="",d=0;b>d;d++)c+=a;return c},Z=function(a,b){var c="",d=a.childNodes;b++,c+=Y(" ",b-1)+a.outerHTML.substring(0,a.outerHTML.indexOf(""+a+"")[0].childNodes;if(b.length>0){a="";for(var c=0;c0&&(a+="\n"),a+="ul"===b[c].nodeName.toLowerCase()||"ol"===b[c].nodeName.toLowerCase()?""+Z(b[c],0):""+b[c].outerHTML)}return a})}var $,_=function(a){return l.$emit("ta-element-select",this),a.preventDefault(),!1},ab=function(a,c){if(c&&angular.extend(a,c),!p&&!F){p=!0;var d;d=a.originalEvent?a.originalEvent.dataTransfer:a.dataTransfer,l.$emit("ta-drop-event",this,a,d),b(function(){p=!1,S(void 0,void 0,!0)},100)}},bb=l["reApplyOnSelectorHandlers"+(v.id||"")]=function(){F||angular.forEach(n,function(a){u.find(a).off("click",_).on("click",_)})},cb=function(a){u[0].innerHTML=a},db=!1;B.$render=function(){if(!db){db=!0;var a=B.$viewValue||"";H||(D&&G&&(u.removeClass("placeholder-text"),$&&b.cancel($),$=b(function(){G||(u[0].focus(),m.setSelectionToElementEnd(u.children()[u.children().length-1])),$=void 0},1)),D?(cb(v.placeholder?""===a?z:a:""===a?z:a),F?u.off("drop",ab):(bb(),u.on("drop",ab))):"textarea"!==u[0].tagName.toLowerCase()&&"input"!==u[0].tagName.toLowerCase()?cb(o(a)):u.val(a)),D&&v.placeholder&&(""===a?G?u.removeClass("placeholder-text"):u.addClass("placeholder-text"):u.removeClass("placeholder-text")),db=H=!1}},v.taReadonly&&(F=l.$eval(v.taReadonly),F?(u.addClass("ta-readonly"),("textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase())&&u.attr("disabled","disabled"),void 0!==u.attr("contenteditable")&&u.attr("contenteditable")&&u.removeAttr("contenteditable")):(u.removeClass("ta-readonly"),"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase()?u.removeAttr("disabled"):D&&u.attr("contenteditable","true")),l.$watch(v.taReadonly,function(a,b){b!==a&&(a?(u.addClass("ta-readonly"),("textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase())&&u.attr("disabled","disabled"),void 0!==u.attr("contenteditable")&&u.attr("contenteditable")&&u.removeAttr("contenteditable"),angular.forEach(n,function(a){u.find(a).on("click",_)}),u.off("drop",ab)):(u.removeClass("ta-readonly"),"textarea"===u[0].tagName.toLowerCase()||"input"===u[0].tagName.toLowerCase()?u.removeAttr("disabled"):D&&u.attr("contenteditable","true"),angular.forEach(n,function(a){u.find(a).off("click",_)}),u.on("drop",ab)),F=a)})),D&&!F&&(angular.forEach(n,function(a){u.find(a).on("click",_)}),u.on("drop",ab),u.on("blur",function(){c.webkit&&(d=!0)}))}}}]);var p=!1,q=angular.module("textAngular",["ngSanitize","textAngularSetup","textAngular.factories","textAngular.DOM","textAngular.validators","textAngular.taBind"]),r={};q.constant("taRegisterTool",b),q.value("taTools",r),q.config([function(){angular.forEach(r,function(a,b){delete r[b]})}]),q.run([function(){if(!window.rangy)throw"rangy-core.js and rangy-selectionsaverestore.js are required for textAngular to work correctly, rangy-core is not yet loaded.";if(window.rangy.init(),!window.rangy.saveSelection)throw"rangy-selectionsaverestore.js is required for textAngular to work correctly."}]),q.directive("textAngular",["$compile","$timeout","taOptions","taSelection","taExecCommand","textAngularManager","$window","$document","$animate","$log","$q","$parse",function(a,b,c,d,e,f,g,h,i,j,k,l){return{require:"?ngModel",scope:{},restrict:"EA",priority:2,link:function(m,n,o,p){var q,r,s,t,u,v,w,x,y,z,A,B=o.serial?o.serial:Math.floor(1e16*Math.random()); m._name=o.name?o.name:"textAngularEditor"+B;var C=function(a,c,d){b(function(){var b=function(){a.off(c,b),d.apply(this,arguments)};a.on(c,b)},100)};if(y=e(o.taDefaultWrap),angular.extend(m,angular.copy(c),{wrapSelection:function(a,b,c){"undo"===a.toLowerCase()?m["$undoTaBindtaTextElement"+B]():"redo"===a.toLowerCase()?m["$redoTaBindtaTextElement"+B]():(y(a,!1,b,m.defaultTagAttributes),c&&m["reApplyOnSelectorHandlerstaTextElement"+B](),m.displayElements.text[0].focus())},showHtml:m.$eval(o.taShowHtml)||!1}),o.taFocussedClass&&(m.classes.focussed=o.taFocussedClass),o.taTextEditorClass&&(m.classes.textEditor=o.taTextEditorClass),o.taHtmlEditorClass&&(m.classes.htmlEditor=o.taHtmlEditorClass),o.taDefaultTagAttributes)try{angular.extend(m.defaultTagAttributes,angular.fromJson(o.taDefaultTagAttributes))}catch(D){j.error(D)}o.taTextEditorSetup&&(m.setup.textEditorSetup=m.$parent.$eval(o.taTextEditorSetup)),o.taHtmlEditorSetup&&(m.setup.htmlEditorSetup=m.$parent.$eval(o.taHtmlEditorSetup)),m.fileDropHandler=o.taFileDrop?m.$parent.$eval(o.taFileDrop):m.defaultFileDropHandler,w=n[0].innerHTML,n[0].innerHTML="",m.displayElements={forminput:angular.element(""),html:angular.element(""),text:angular.element("
    "),scrollWindow:angular.element("
    "),popover:angular.element('
    '),popoverArrow:angular.element('
    '),popoverContainer:angular.element('
    '),resize:{overlay:angular.element('
    '),background:angular.element('
    '),anchors:[angular.element('
    '),angular.element('
    '),angular.element('
    '),angular.element('
    ')],info:angular.element('
    ')}},m.displayElements.popover.append(m.displayElements.popoverArrow),m.displayElements.popover.append(m.displayElements.popoverContainer),m.displayElements.scrollWindow.append(m.displayElements.popover),m.displayElements.popover.on("mousedown",function(a,b){return b&&angular.extend(a,b),a.preventDefault(),!1}),m.showPopover=function(a){m.displayElements.popover.css("display","block"),m.reflowPopover(a),i.addClass(m.displayElements.popover,"in"),C(h.find("body"),"click keyup",function(){m.hidePopover()})},m.reflowPopover=function(a){m.displayElements.text[0].offsetHeight-51>a[0].offsetTop?(m.displayElements.popover.css("top",a[0].offsetTop+a[0].offsetHeight+m.displayElements.scrollWindow[0].scrollTop+"px"),m.displayElements.popover.removeClass("top").addClass("bottom")):(m.displayElements.popover.css("top",a[0].offsetTop-54+m.displayElements.scrollWindow[0].scrollTop+"px"),m.displayElements.popover.removeClass("bottom").addClass("top"));var b=m.displayElements.text[0].offsetWidth-m.displayElements.popover[0].offsetWidth,c=a[0].offsetLeft+a[0].offsetWidth/2-m.displayElements.popover[0].offsetWidth/2;m.displayElements.popover.css("left",Math.max(0,Math.min(b,c))+"px"),m.displayElements.popoverArrow.css("margin-left",Math.min(c,Math.max(0,c-b))-11+"px")},m.hidePopover=function(){var a=function(){m.displayElements.popover.css("display",""),m.displayElements.popoverContainer.attr("style",""),m.displayElements.popoverContainer.attr("class","popover-content")};k.when(i.removeClass(m.displayElements.popover,"in",a)).then(a)},m.displayElements.resize.overlay.append(m.displayElements.resize.background),angular.forEach(m.displayElements.resize.anchors,function(a){m.displayElements.resize.overlay.append(a)}),m.displayElements.resize.overlay.append(m.displayElements.resize.info),m.displayElements.scrollWindow.append(m.displayElements.resize.overlay),m.reflowResizeOverlay=function(a){a=angular.element(a)[0],m.displayElements.resize.overlay.css({display:"block",left:a.offsetLeft-5+"px",top:a.offsetTop-5+"px",width:a.offsetWidth+10+"px",height:a.offsetHeight+10+"px"}),m.displayElements.resize.info.text(a.offsetWidth+" x "+a.offsetHeight)},m.showResizeOverlay=function(a){var b=h.find("body");z=function(c){var d={width:parseInt(a.attr("width")),height:parseInt(a.attr("height")),x:c.clientX,y:c.clientY};(void 0===d.width||isNaN(d.width))&&(d.width=a[0].offsetWidth),(void 0===d.height||isNaN(d.height))&&(d.height=a[0].offsetHeight),m.hidePopover();var e=d.height/d.width,f=function(b){var c={x:Math.max(0,d.width+(b.clientX-d.x)),y:Math.max(0,d.height+(b.clientY-d.y))};if(b.shiftKey){var f=c.y/c.x;c.x=e>f?c.x:c.y/e,c.y=e>f?c.x*e:c.y}var g=angular.element(a);g.attr("height",Math.max(0,c.y)),g.attr("width",Math.max(0,c.x)),m.reflowResizeOverlay(a)};b.on("mousemove",f),C(b,"mouseup",function(c){c.preventDefault(),c.stopPropagation(),b.off("mousemove",f),m.showPopover(a)}),c.stopPropagation(),c.preventDefault()},m.displayElements.resize.anchors[3].on("mousedown",z),m.reflowResizeOverlay(a),C(b,"click",function(){m.hideResizeOverlay()})},m.hideResizeOverlay=function(){m.displayElements.resize.anchors[3].off("mousedown",z),m.displayElements.resize.overlay.css("display","")},m.setup.htmlEditorSetup(m.displayElements.html),m.setup.textEditorSetup(m.displayElements.text),m.displayElements.html.attr({id:"taHtmlElement"+B,"ng-show":"showHtml","ta-bind":"ta-bind","ng-model":"html","ng-model-options":n.attr("ng-model-options")}),m.displayElements.text.attr({id:"taTextElement"+B,contentEditable:"true","ta-bind":"ta-bind","ng-model":"html","ng-model-options":n.attr("ng-model-options")}),m.displayElements.scrollWindow.attr({"ng-hide":"showHtml"}),o.taDefaultWrap&&m.displayElements.text.attr("ta-default-wrap",o.taDefaultWrap),o.taUnsafeSanitizer&&(m.displayElements.text.attr("ta-unsafe-sanitizer",o.taUnsafeSanitizer),m.displayElements.html.attr("ta-unsafe-sanitizer",o.taUnsafeSanitizer)),m.displayElements.scrollWindow.append(m.displayElements.text),n.append(m.displayElements.scrollWindow),n.append(m.displayElements.html),m.displayElements.forminput.attr("name",m._name),n.append(m.displayElements.forminput),o.tabindex&&(n.removeAttr("tabindex"),m.displayElements.text.attr("tabindex",o.tabindex),m.displayElements.html.attr("tabindex",o.tabindex)),o.placeholder&&(m.displayElements.text.attr("placeholder",o.placeholder),m.displayElements.html.attr("placeholder",o.placeholder)),o.taDisabled&&(m.displayElements.text.attr("ta-readonly","disabled"),m.displayElements.html.attr("ta-readonly","disabled"),m.disabled=m.$parent.$eval(o.taDisabled),m.$parent.$watch(o.taDisabled,function(a){m.disabled=a,m.disabled?n.addClass(m.classes.disabled):n.removeClass(m.classes.disabled)})),o.taPaste&&(m._pasteHandler=function(a){return l(o.taPaste)(m.$parent,{$html:a})},m.displayElements.text.attr("ta-paste","_pasteHandler($html)")),a(m.displayElements.scrollWindow)(m),a(m.displayElements.html)(m),m.updateTaBindtaTextElement=m["updateTaBindtaTextElement"+B],m.updateTaBindtaHtmlElement=m["updateTaBindtaHtmlElement"+B],n.addClass("ta-root"),m.displayElements.scrollWindow.addClass("ta-text ta-editor "+m.classes.textEditor),m.displayElements.html.addClass("ta-html ta-editor "+m.classes.htmlEditor),m._actionRunning=!1;var E=!1;if(m.startAction=function(){return m._actionRunning=!0,E=g.rangy.saveSelection(),function(){E&&g.rangy.restoreSelection(E)}},m.endAction=function(){m._actionRunning=!1,E&&(m.showHtml?m.displayElements.html[0].focus():m.displayElements.text[0].focus(),g.rangy.restoreSelection(E),g.rangy.removeMarkers(E)),E=!1,m.updateSelectedStyles(),m.showHtml||m["updateTaBindtaTextElement"+B]()},u=function(){m.focussed=!0,n.addClass(m.classes.focussed),x.focus(),n.triggerHandler("focus")},m.displayElements.html.on("focus",u),m.displayElements.text.on("focus",u),v=function(a){return m._actionRunning||h[0].activeElement===m.displayElements.html[0]||h[0].activeElement===m.displayElements.text[0]||(n.removeClass(m.classes.focussed),x.unfocus(),b(function(){m._bUpdateSelectedStyles=!1,n.triggerHandler("blur"),m.focussed=!1},0)),a.preventDefault(),!1},m.displayElements.html.on("blur",v),m.displayElements.text.on("blur",v),m.displayElements.text.on("paste",function(a){n.triggerHandler("paste",a)}),m.queryFormatBlockState=function(a){return!m.showHtml&&a.toLowerCase()===h[0].queryCommandValue("formatBlock").toLowerCase()},m.queryCommandState=function(a){return m.showHtml?"":h[0].queryCommandState(a)},m.switchView=function(){m.showHtml=!m.showHtml,i.enabled(!1,m.displayElements.html),i.enabled(!1,m.displayElements.text),m.showHtml?b(function(){return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.html[0].focus()},100):b(function(){return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.text[0].focus()},100)},o.ngModel){var F=!0;p.$render=function(){if(F){F=!1;var a=m.$parent.$eval(o.ngModel);void 0!==a&&null!==a||!w||""===w||p.$setViewValue(w)}m.displayElements.forminput.val(p.$viewValue),m.html=p.$viewValue||""},n.attr("required")&&(p.$validators.required=function(a,b){var c=a||b;return!(!c||""===c.trim())})}else m.displayElements.forminput.val(w),m.html=w;if(m.$watch("html",function(a,b){a!==b&&(o.ngModel&&p.$viewValue!==a&&p.$setViewValue(a),m.displayElements.forminput.val(a))}),o.taTargetToolbars)x=f.registerEditor(m._name,m,o.taTargetToolbars.split(","));else{var G=angular.element('
    ');o.taToolbar&&G.attr("ta-toolbar",o.taToolbar),o.taToolbarClass&&G.attr("ta-toolbar-class",o.taToolbarClass),o.taToolbarGroupClass&&G.attr("ta-toolbar-group-class",o.taToolbarGroupClass),o.taToolbarButtonClass&&G.attr("ta-toolbar-button-class",o.taToolbarButtonClass),o.taToolbarActiveButtonClass&&G.attr("ta-toolbar-active-button-class",o.taToolbarActiveButtonClass),o.taFocussedClass&&G.attr("ta-focussed-class",o.taFocussedClass),n.prepend(G),a(G)(m.$parent),x=f.registerEditor(m._name,m,["textAngularToolbar"+B])}m.$on("$destroy",function(){f.unregisterEditor(m._name)}),m.$on("ta-element-select",function(a,b){x.triggerElementSelect(a,b)&&m["reApplyOnSelectorHandlerstaTextElement"+B]()}),m.$on("ta-drop-event",function(a,c,d,e){m.displayElements.text[0].focus(),e&&e.files&&e.files.length>0?(angular.forEach(e.files,function(a){try{k.when(m.fileDropHandler(a,m.wrapSelection)||m.fileDropHandler!==m.defaultFileDropHandler&&k.when(m.defaultFileDropHandler(a,m.wrapSelection))).then(function(){m["updateTaBindtaTextElement"+B]()})}catch(b){j.error(b)}}),d.preventDefault(),d.stopPropagation()):b(function(){m["updateTaBindtaTextElement"+B]()},0)}),m._bUpdateSelectedStyles=!1,angular.element(window).on("blur",function(){m._bUpdateSelectedStyles=!1,m.focussed=!1}),m.updateSelectedStyles=function(){var a;A&&b.cancel(A),void 0!==(a=d.getSelectionElement())&&a.parentNode!==m.displayElements.text[0]?x.updateSelectedStyles(angular.element(a)):x.updateSelectedStyles(),m._bUpdateSelectedStyles&&(A=b(m.updateSelectedStyles,200))},q=function(){return m.focussed?void(m._bUpdateSelectedStyles||(m._bUpdateSelectedStyles=!0,m.$apply(function(){m.updateSelectedStyles()}))):void(m._bUpdateSelectedStyles=!1)},m.displayElements.html.on("keydown",q),m.displayElements.text.on("keydown",q),r=function(){m._bUpdateSelectedStyles=!1},m.displayElements.html.on("keyup",r),m.displayElements.text.on("keyup",r),s=function(a,b){b&&angular.extend(a,b),m.$apply(function(){return x.sendKeyCommand(a)?(m._bUpdateSelectedStyles||m.updateSelectedStyles(),a.preventDefault(),!1):void 0})},m.displayElements.html.on("keypress",s),m.displayElements.text.on("keypress",s),t=function(){m._bUpdateSelectedStyles=!1,m.$apply(function(){m.updateSelectedStyles()})},m.displayElements.html.on("mouseup",t),m.displayElements.text.on("mouseup",t)}}}]),q.service("textAngularManager",["taToolExecuteAction","taTools","taRegisterTool",function(a,b,c){var d={},e={};return{registerEditor:function(c,f,g){if(!c||""===c)throw"textAngular Error: An editor requires a name";if(!f)throw"textAngular Error: An editor requires a scope";if(e[c])throw'textAngular Error: An Editor with name "'+c+'" already exists';var h=[];return angular.forEach(g,function(a){d[a]&&h.push(d[a])}),e[c]={scope:f,toolbars:g,_registerToolbar:function(a){this.toolbars.indexOf(a.name)>=0&&h.push(a)},editorFunctions:{disable:function(){angular.forEach(h,function(a){a.disabled=!0})},enable:function(){angular.forEach(h,function(a){a.disabled=!1})},focus:function(){angular.forEach(h,function(a){a._parent=f,a.disabled=!1,a.focussed=!0,f.focussed=!0})},unfocus:function(){angular.forEach(h,function(a){a.disabled=!0,a.focussed=!1}),f.focussed=!1},updateSelectedStyles:function(a){angular.forEach(h,function(b){angular.forEach(b.tools,function(c){c.activeState&&(b._parent=f,c.active=c.activeState(a))})})},sendKeyCommand:function(c){var d=!1;return(c.ctrlKey||c.metaKey)&&angular.forEach(b,function(b,e){if(b.commandKeyCode&&b.commandKeyCode===c.which)for(var g=0;g0)for(var k=0;k"),d.addClass(b&&b["class"]?b["class"]:g.classes.toolbarButton),d.attr("name",c.name),d.attr("ta-button","ta-button"),d.attr("ng-disabled","isDisabled()"),d.attr("tabindex","-1"),d.attr("ng-click","executeAction()"),d.attr("ng-class","displayActiveToolClass(active)"),b&&b.tooltiptext&&d.attr("title",b.tooltiptext),b&&!b.display&&!c._display&&(d[0].innerHTML="",b.buttontext&&(d[0].innerHTML=b.buttontext),b.iconclass)){var e=angular.element(""),f=d[0].innerHTML;e.addClass(b.iconclass),d[0].innerHTML="",d.append(e),f&&""!==f&&d.append(" "+f)}return c._lastToolDefinition=angular.copy(b),a(d)(c)};g.tools={},g._parent={disabled:!0,showHtml:!1,queryFormatBlockState:function(){return!1},queryCommandState:function(){return!1}};var k={$window:f,$editor:function(){return g._parent},isDisabled:function(){return"function"!=typeof this.$eval("disabled")&&this.$eval("disabled")||this.$eval("disabled()")||"html"!==this.name&&this.$editor().showHtml||this.$parent.disabled||this.$editor().disabled},displayActiveToolClass:function(a){return a?g.classes.toolbarButtonActive:""},executeAction:e};angular.forEach(g.toolbar,function(a){var b=angular.element("
    ");b.addClass(g.classes.toolbarGroup),angular.forEach(a,function(a){g.tools[a]=angular.extend(g.$new(!0),d[a],k,{name:a}),g.tools[a].$element=j(d[a],g.tools[a]),b.append(g.tools[a].$element)}),h.append(b)}),g.updateToolDisplay=function(a,b,c){var d=g.tools[a];if(d){if(d._lastToolDefinition&&!c&&(b=angular.extend({},d._lastToolDefinition,b)),null===b.buttontext&&null===b.iconclass&&null===b.display)throw'textAngular Error: Tool Definition for updating "'+a+'" does not have a valid display/iconclass/buttontext value';null===b.buttontext&&delete b.buttontext,null===b.iconclass&&delete b.iconclass,null===b.display&&delete b.display;var e=j(b,d);d.$element.replaceWith(e),d.$element=e}},g.addTool=function(a,b,c,e){g.tools[a]=angular.extend(g.$new(!0),d[a],k,{name:a}),g.tools[a].$element=j(d[a],g.tools[a]);var f;void 0===c&&(c=g.toolbar.length-1),f=angular.element(h.children()[c]),void 0===e?(f.append(g.tools[a].$element),g.toolbar[c][g.toolbar[c].length-1]=a):(f.children().eq(e).after(g.tools[a].$element),g.toolbar[c][e]=a)},b.registerToolbar(g),g.$on("$destroy",function(){b.unregisterToolbar(g.name)})}}}])}()}({},function(){return this}()); \ No newline at end of file diff --git a/dist/textAngularSetup.js b/dist/textAngularSetup.js new file mode 100644 index 00000000..d97b372b --- /dev/null +++ b/dist/textAngularSetup.js @@ -0,0 +1,704 @@ +/* +@license textAngular +Author : Austin Anderson +License : 2013 MIT +Version 1.3.7 + +See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use. +*/ +angular.module('textAngularSetup', []) + +// Here we set up the global display defaults, to set your own use a angular $provider#decorator. +.value('taOptions', { + toolbar: [ + ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], + ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], + ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], + ['html', 'insertImage', 'insertLink', 'insertVideo', 'wordcount', 'charcount'] + ], + classes: { + focussed: "focussed", + toolbar: "btn-toolbar", + toolbarGroup: "btn-group", + toolbarButton: "btn btn-default", + toolbarButtonActive: "active", + disabled: "disabled", + textEditor: 'form-control', + htmlEditor: 'form-control' + }, + defaultTagAttributes : { + a: {target:""} + }, + setup: { + // wysiwyg mode + textEditorSetup: function($element){ /* Do some processing here */ }, + // raw html + htmlEditorSetup: function($element){ /* Do some processing here */ } + }, + defaultFileDropHandler: + /* istanbul ignore next: untestable image processing */ + function(file, insertAction){ + var reader = new FileReader(); + if(file.type.substring(0, 5) === 'image'){ + reader.onload = function() { + if(reader.result !== '') insertAction('insertImage', reader.result, true); + }; + + reader.readAsDataURL(file); + // NOTE: For async procedures return a promise and resolve it when the editor should update the model. + return true; + } + return false; + } +}) + +// This is the element selector string that is used to catch click events within a taBind, prevents the default and $emits a 'ta-element-select' event +// these are individually used in an angular.element().find() call. What can go here depends on whether you have full jQuery loaded or just jQLite with angularjs. +// div is only used as div.ta-insert-video caught in filter. +.value('taSelectableElements', ['a','img']) + +// This is an array of objects with the following options: +// selector: a jqLite or jQuery selector string +// customAttribute: an attribute to search for +// renderLogic: +// Both or one of selector and customAttribute must be defined. +.value('taCustomRenderers', [ + { + // Parse back out: '
    ' + // To correct video element. For now only support youtube + selector: 'img', + customAttribute: 'ta-insert-video', + renderLogic: function(element){ + var iframe = angular.element(''); + var attributes = element.prop("attributes"); + // loop through element attributes and apply them on iframe + angular.forEach(attributes, function(attr) { + iframe.attr(attr.name, attr.value); + }); + iframe.attr('src', iframe.attr('ta-insert-video')); + element.replaceWith(iframe); + } + } +]) + +.value('taTranslations', { + // moved to sub-elements + //toggleHTML: "Toggle HTML", + //insertImage: "Please enter a image URL to insert", + //insertLink: "Please enter a URL to insert", + //insertVideo: "Please enter a youtube URL to embed", + html: { + tooltip: 'Toggle html / Rich Text' + }, + // tooltip for heading - might be worth splitting + heading: { + tooltip: 'Heading ' + }, + p: { + tooltip: 'Paragraph' + }, + pre: { + tooltip: 'Preformatted text' + }, + ul: { + tooltip: 'Unordered List' + }, + ol: { + tooltip: 'Ordered List' + }, + quote: { + tooltip: 'Quote/unquote selection or paragraph' + }, + undo: { + tooltip: 'Undo' + }, + redo: { + tooltip: 'Redo' + }, + bold: { + tooltip: 'Bold' + }, + italic: { + tooltip: 'Italic' + }, + underline: { + tooltip: 'Underline' + }, + strikeThrough:{ + tooltip: 'Strikethrough' + }, + justifyLeft: { + tooltip: 'Align text left' + }, + justifyRight: { + tooltip: 'Align text right' + }, + justifyCenter: { + tooltip: 'Center' + }, + indent: { + tooltip: 'Increase indent' + }, + outdent: { + tooltip: 'Decrease indent' + }, + clear: { + tooltip: 'Clear formatting' + }, + insertImage: { + dialogPrompt: 'Please enter an image URL to insert', + tooltip: 'Insert image', + hotkey: 'the - possibly language dependent hotkey ... for some future implementation' + }, + insertVideo: { + tooltip: 'Insert video', + dialogPrompt: 'Please enter a youtube URL to embed' + }, + insertLink: { + tooltip: 'Insert / edit link', + dialogPrompt: "Please enter a URL to insert" + }, + editLink: { + reLinkButton: { + tooltip: "Relink" + }, + unLinkButton: { + tooltip: "Unlink" + }, + targetToggle: { + buttontext: "Open in New Window" + } + }, + wordcount: { + tooltip: 'Display words Count' + }, + charcount: { + tooltip: 'Display characters Count' + } +}) +.run(['taRegisterTool', '$window', 'taTranslations', 'taSelection', function(taRegisterTool, $window, taTranslations, taSelection){ + taRegisterTool("html", { + iconclass: 'fa fa-code', + tooltiptext: taTranslations.html.tooltip, + action: function(){ + this.$editor().switchView(); + }, + activeState: function(){ + return this.$editor().showHtml; + } + }); + // add the Header tools + // convenience functions so that the loop works correctly + var _retActiveStateFunction = function(q){ + return function(){ return this.$editor().queryFormatBlockState(q); }; + }; + var headerAction = function(){ + return this.$editor().wrapSelection("formatBlock", "<" + this.name.toUpperCase() +">"); + }; + angular.forEach(['h1','h2','h3','h4','h5','h6'], function(h){ + taRegisterTool(h.toLowerCase(), { + buttontext: h.toUpperCase(), + tooltiptext: taTranslations.heading.tooltip + h.charAt(1), + action: headerAction, + activeState: _retActiveStateFunction(h.toLowerCase()) + }); + }); + taRegisterTool('p', { + buttontext: 'P', + tooltiptext: taTranslations.p.tooltip, + action: function(){ + return this.$editor().wrapSelection("formatBlock", "

    "); + }, + activeState: function(){ return this.$editor().queryFormatBlockState('p'); } + }); + // key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext + taRegisterTool('pre', { + buttontext: 'pre', + tooltiptext: taTranslations.pre.tooltip, + action: function(){ + return this.$editor().wrapSelection("formatBlock", "

    ");
    +		},
    +		activeState: function(){ return this.$editor().queryFormatBlockState('pre'); }
    +	});
    +	taRegisterTool('ul', {
    +		iconclass: 'fa fa-list-ul',
    +		tooltiptext: taTranslations.ul.tooltip,
    +		action: function(){
    +			return this.$editor().wrapSelection("insertUnorderedList", null);
    +		},
    +		activeState: function(){ return this.$editor().queryCommandState('insertUnorderedList'); }
    +	});
    +	taRegisterTool('ol', {
    +		iconclass: 'fa fa-list-ol',
    +		tooltiptext: taTranslations.ol.tooltip,
    +		action: function(){
    +			return this.$editor().wrapSelection("insertOrderedList", null);
    +		},
    +		activeState: function(){ return this.$editor().queryCommandState('insertOrderedList'); }
    +	});
    +	taRegisterTool('quote', {
    +		iconclass: 'fa fa-quote-right',
    +		tooltiptext: taTranslations.quote.tooltip,
    +		action: function(){
    +			return this.$editor().wrapSelection("formatBlock", "
    "); + }, + activeState: function(){ return this.$editor().queryFormatBlockState('blockquote'); } + }); + taRegisterTool('undo', { + iconclass: 'fa fa-undo', + tooltiptext: taTranslations.undo.tooltip, + action: function(){ + return this.$editor().wrapSelection("undo", null); + } + }); + taRegisterTool('redo', { + iconclass: 'fa fa-repeat', + tooltiptext: taTranslations.redo.tooltip, + action: function(){ + return this.$editor().wrapSelection("redo", null); + } + }); + taRegisterTool('bold', { + iconclass: 'fa fa-bold', + tooltiptext: taTranslations.bold.tooltip, + action: function(){ + return this.$editor().wrapSelection("bold", null); + }, + activeState: function(){ + return this.$editor().queryCommandState('bold'); + }, + commandKeyCode: 98 + }); + taRegisterTool('justifyLeft', { + iconclass: 'fa fa-align-left', + tooltiptext: taTranslations.justifyLeft.tooltip, + action: function(){ + return this.$editor().wrapSelection("justifyLeft", null); + }, + activeState: function(commonElement){ + var result = false; + if(commonElement) result = + commonElement.css('text-align') === 'left' || + commonElement.attr('align') === 'left' || + ( + commonElement.css('text-align') !== 'right' && + commonElement.css('text-align') !== 'center' && + commonElement.css('text-align') !== 'justify' && + !this.$editor().queryCommandState('justifyRight') && + !this.$editor().queryCommandState('justifyCenter') + ) && !this.$editor().queryCommandState('justifyFull'); + result = result || this.$editor().queryCommandState('justifyLeft'); + return result; + } + }); + taRegisterTool('justifyRight', { + iconclass: 'fa fa-align-right', + tooltiptext: taTranslations.justifyRight.tooltip, + action: function(){ + return this.$editor().wrapSelection("justifyRight", null); + }, + activeState: function(commonElement){ + var result = false; + if(commonElement) result = commonElement.css('text-align') === 'right'; + result = result || this.$editor().queryCommandState('justifyRight'); + return result; + } + }); + taRegisterTool('justifyCenter', { + iconclass: 'fa fa-align-center', + tooltiptext: taTranslations.justifyCenter.tooltip, + action: function(){ + return this.$editor().wrapSelection("justifyCenter", null); + }, + activeState: function(commonElement){ + var result = false; + if(commonElement) result = commonElement.css('text-align') === 'center'; + result = result || this.$editor().queryCommandState('justifyCenter'); + return result; + } + }); + taRegisterTool('indent', { + iconclass: 'fa fa-indent', + tooltiptext: taTranslations.indent.tooltip, + action: function(){ + return this.$editor().wrapSelection("indent", null); + }, + activeState: function(){ + return this.$editor().queryFormatBlockState('blockquote'); + } + }); + taRegisterTool('outdent', { + iconclass: 'fa fa-outdent', + tooltiptext: taTranslations.outdent.tooltip, + action: function(){ + return this.$editor().wrapSelection("outdent", null); + }, + activeState: function(){ + return false; + } + }); + taRegisterTool('italics', { + iconclass: 'fa fa-italic', + tooltiptext: taTranslations.italic.tooltip, + action: function(){ + return this.$editor().wrapSelection("italic", null); + }, + activeState: function(){ + return this.$editor().queryCommandState('italic'); + }, + commandKeyCode: 105 + }); + taRegisterTool('underline', { + iconclass: 'fa fa-underline', + tooltiptext: taTranslations.underline.tooltip, + action: function(){ + return this.$editor().wrapSelection("underline", null); + }, + activeState: function(){ + return this.$editor().queryCommandState('underline'); + }, + commandKeyCode: 117 + }); + taRegisterTool('strikeThrough', { + iconclass: 'fa fa-strikethrough', + tooltiptext: taTranslations.strikeThrough.tooltip, + action: function(){ + return this.$editor().wrapSelection("strikeThrough", null); + }, + activeState: function(){ + return document.queryCommandState('strikeThrough'); + } + }); + taRegisterTool('clear', { + iconclass: 'fa fa-ban', + tooltiptext: taTranslations.clear.tooltip, + action: function(deferred, restoreSelection){ + var i; + this.$editor().wrapSelection("removeFormat", null); + var possibleNodes = angular.element(taSelection.getSelectionElement()); + // remove lists + var removeListElements = function(list){ + list = angular.element(list); + var prevElement = list; + angular.forEach(list.children(), function(liElem){ + var newElem = angular.element('

    '); + newElem.html(angular.element(liElem).html()); + prevElement.after(newElem); + prevElement = newElem; + }); + list.remove(); + }; + angular.forEach(possibleNodes.find("ul"), removeListElements); + angular.forEach(possibleNodes.find("ol"), removeListElements); + if(possibleNodes[0].tagName.toLowerCase() === 'li'){ + var _list = possibleNodes[0].parentNode.childNodes; + var _preLis = [], _postLis = [], _found = false; + for(i = 0; i < _list.length; i++){ + if(_list[i] === possibleNodes[0]){ + _found = true; + }else if(!_found) _preLis.push(_list[i]); + else _postLis.push(_list[i]); + } + var _parent = angular.element(possibleNodes[0].parentNode); + var newElem = angular.element('

    '); + newElem.html(angular.element(possibleNodes[0]).html()); + if(_preLis.length === 0 || _postLis.length === 0){ + if(_postLis.length === 0) _parent.after(newElem); + else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]); + + if(_preLis.length === 0 && _postLis.length === 0) _parent.remove(); + else angular.element(possibleNodes[0]).remove(); + }else{ + var _firstList = angular.element('<'+_parent[0].tagName+'>'); + var _secondList = angular.element('<'+_parent[0].tagName+'>'); + for(i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i])); + for(i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i])); + _parent.after(_secondList); + _parent.after(newElem); + _parent.after(_firstList); + _parent.remove(); + } + taSelection.setSelectionToElementEnd(newElem[0]); + } + // clear out all class attributes. These do not seem to be cleared via removeFormat + var $editor = this.$editor(); + var recursiveRemoveClass = function(node){ + node = angular.element(node); + if(node[0] !== $editor.displayElements.text[0]) node.removeAttr('class'); + angular.forEach(node.children(), recursiveRemoveClass); + }; + angular.forEach(possibleNodes, recursiveRemoveClass); + // check if in list. If not in list then use formatBlock option + if(possibleNodes[0].tagName.toLowerCase() !== 'li' && + possibleNodes[0].tagName.toLowerCase() !== 'ol' && + possibleNodes[0].tagName.toLowerCase() !== 'ul') this.$editor().wrapSelection("formatBlock", "default"); + restoreSelection(); + } + }); + + var imgOnSelectAction = function(event, $element, editorScope){ + // setup the editor toolbar + // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic/display + var finishEdit = function(){ + editorScope.updateTaBindtaTextElement(); + editorScope.hidePopover(); + }; + event.preventDefault(); + editorScope.displayElements.popover.css('width', '375px'); + var container = editorScope.displayElements.popoverContainer; + container.empty(); + var buttonGroup = angular.element('
    '); + var fullButton = angular.element(''); + fullButton.on('click', function(event){ + event.preventDefault(); + $element.css({ + 'width': '100%', + 'height': '' + }); + finishEdit(); + }); + var halfButton = angular.element(''); + halfButton.on('click', function(event){ + event.preventDefault(); + $element.css({ + 'width': '50%', + 'height': '' + }); + finishEdit(); + }); + var quartButton = angular.element(''); + quartButton.on('click', function(event){ + event.preventDefault(); + $element.css({ + 'width': '25%', + 'height': '' + }); + finishEdit(); + }); + var resetButton = angular.element(''); + resetButton.on('click', function(event){ + event.preventDefault(); + $element.css({ + width: '', + height: '' + }); + finishEdit(); + }); + buttonGroup.append(fullButton); + buttonGroup.append(halfButton); + buttonGroup.append(quartButton); + buttonGroup.append(resetButton); + container.append(buttonGroup); + + buttonGroup = angular.element('
    '); + var floatLeft = angular.element(''); + floatLeft.on('click', function(event){ + event.preventDefault(); + // webkit + $element.css('float', 'left'); + // firefox + $element.css('cssFloat', 'left'); + // IE < 8 + $element.css('styleFloat', 'left'); + finishEdit(); + }); + var floatRight = angular.element(''); + floatRight.on('click', function(event){ + event.preventDefault(); + // webkit + $element.css('float', 'right'); + // firefox + $element.css('cssFloat', 'right'); + // IE < 8 + $element.css('styleFloat', 'right'); + finishEdit(); + }); + var floatNone = angular.element(''); + floatNone.on('click', function(event){ + event.preventDefault(); + // webkit + $element.css('float', ''); + // firefox + $element.css('cssFloat', ''); + // IE < 8 + $element.css('styleFloat', ''); + finishEdit(); + }); + buttonGroup.append(floatLeft); + buttonGroup.append(floatNone); + buttonGroup.append(floatRight); + container.append(buttonGroup); + + buttonGroup = angular.element('
    '); + var remove = angular.element(''); + remove.on('click', function(event){ + event.preventDefault(); + $element.remove(); + finishEdit(); + }); + buttonGroup.append(remove); + container.append(buttonGroup); + + editorScope.showPopover($element); + editorScope.showResizeOverlay($element); + }; + + taRegisterTool('insertImage', { + iconclass: 'fa fa-picture-o', + tooltiptext: taTranslations.insertImage.tooltip, + action: function(){ + var imageLink; + imageLink = $window.prompt(taTranslations.insertImage.dialogPrompt, 'http://'); + if(imageLink && imageLink !== '' && imageLink !== 'http://'){ + return this.$editor().wrapSelection('insertImage', imageLink, true); + } + }, + onElementSelect: { + element: 'img', + action: imgOnSelectAction + } + }); + taRegisterTool('insertVideo', { + iconclass: 'fa fa-youtube-play', + tooltiptext: taTranslations.insertVideo.tooltip, + action: function(){ + var urlPrompt; + urlPrompt = $window.prompt(taTranslations.insertVideo.dialogPrompt, 'https://'); + if (urlPrompt && urlPrompt !== '' && urlPrompt !== 'https://') { + // get the video ID + var ids = urlPrompt.match(/(\?|&)v=[^&]*/); + /* istanbul ignore else: if it's invalid don't worry - though probably should show some kind of error message */ + if(ids && ids.length > 0){ + // create the embed link + var urlLink = "https://www.youtube.com/embed/" + ids[0].substring(3); + // create the HTML + // for all options see: http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api + // maxresdefault.jpg seems to be undefined on some. + var embed = ''; + // insert + return this.$editor().wrapSelection('insertHTML', embed, true); + } + } + }, + onElementSelect: { + element: 'img', + onlyWithAttrs: ['ta-insert-video'], + action: imgOnSelectAction + } + }); + taRegisterTool('insertLink', { + tooltiptext: taTranslations.insertLink.tooltip, + iconclass: 'fa fa-link', + action: function(){ + var urlLink; + urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, 'http://'); + if(urlLink && urlLink !== '' && urlLink !== 'http://'){ + return this.$editor().wrapSelection('createLink', urlLink, true); + } + }, + activeState: function(commonElement){ + if(commonElement) return commonElement[0].tagName === 'A'; + return false; + }, + onElementSelect: { + element: 'a', + action: function(event, $element, editorScope){ + // setup the editor toolbar + // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic + event.preventDefault(); + editorScope.displayElements.popover.css('width', '436px'); + var container = editorScope.displayElements.popoverContainer; + container.empty(); + container.css('line-height', '28px'); + var link = angular.element('' + $element.attr('href') + ''); + link.css({ + 'display': 'inline-block', + 'max-width': '200px', + 'overflow': 'hidden', + 'text-overflow': 'ellipsis', + 'white-space': 'nowrap', + 'vertical-align': 'middle' + }); + container.append(link); + var buttonGroup = angular.element('
    '); + var reLinkButton = angular.element(''); + reLinkButton.on('click', function(event){ + event.preventDefault(); + var urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, $element.attr('href')); + if(urlLink && urlLink !== '' && urlLink !== 'http://'){ + $element.attr('href', urlLink); + editorScope.updateTaBindtaTextElement(); + } + editorScope.hidePopover(); + }); + buttonGroup.append(reLinkButton); + var unLinkButton = angular.element(''); + // directly before this click event is fired a digest is fired off whereby the reference to $element is orphaned off + unLinkButton.on('click', function(event){ + event.preventDefault(); + $element.replaceWith($element.contents()); + editorScope.updateTaBindtaTextElement(); + editorScope.hidePopover(); + }); + buttonGroup.append(unLinkButton); + var targetToggle = angular.element(''); + if($element.attr('target') === '_blank'){ + targetToggle.addClass('active'); + } + targetToggle.on('click', function(event){ + event.preventDefault(); + $element.attr('target', ($element.attr('target') === '_blank') ? '' : '_blank'); + targetToggle.toggleClass('active'); + editorScope.updateTaBindtaTextElement(); + }); + buttonGroup.append(targetToggle); + container.append(buttonGroup); + editorScope.showPopover($element); + } + } + }); + taRegisterTool('wordcount', { + display: '
    Words:
    ', + disabled: true, + wordcount: 0, + activeState: function(){ // this fires on keyup + var textElement = this.$editor().displayElements.text; + /* istanbul ignore next: will default to '' when undefined */ + var workingHTML = textElement[0].innerHTML || ''; + var noOfWords = 0; + + /* istanbul ignore if: will default to '' when undefined */ + if (workingHTML.replace(/\s*<[^>]*?>\s*/g, '') !== '') { + noOfWords = workingHTML.replace(/<\/?(b|i|em|strong|span|u|strikethrough|a|img|small|sub|sup|label)( [^>*?])?>/gi, '') // remove inline tags without adding spaces + .replace(/(<[^>]*?>\s*<[^>]*?>)/ig, ' ') // replace adjacent tags with possible space between with a space + .replace(/(<[^>]*?>)/ig, '') // remove any singular tags + .replace(/\s+/ig, ' ') // condense spacing + .match(/\S+/g).length; // count remaining non-space strings + } + + //Set current scope + this.wordcount = noOfWords; + //Set editor scope + this.$editor().wordcount = noOfWords; + + return false; + } + }); + taRegisterTool('charcount', { + display: '
    Characters:
    ', + disabled: true, + charcount: 0, + activeState: function(){ // this fires on keyup + var textElement = this.$editor().displayElements.text; + var sourceText = textElement[0].innerText || textElement[0].textContent; // to cover the non-jquery use case. + + // Caculate number of chars + var noOfChars = sourceText.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g,' ').replace(/\s+$/g, ' ').length; + //Set current scope + this.charcount = noOfChars; + //Set editor scope + this.$editor().charcount = noOfChars; + return false; + } + }); +}]); diff --git a/karma-jqlite.conf.js b/karma-jqlite.conf.js index 25c28563..10d484ff 100644 --- a/karma-jqlite.conf.js +++ b/karma-jqlite.conf.js @@ -16,9 +16,9 @@ module.exports = function (config) { 'bower_components/rangy/rangy-selectionsaverestore.js', 'bower_components/angular/angular.min.js', 'bower_components/angular-mocks/angular-mocks.js', - 'src/textAngular-sanitize.js', - 'src/textAngularSetup.js', - 'src/textAngular.js', + 'dist/textAngular-sanitize.js', + 'dist/textAngularSetup.js', + 'dist/textAngular.js', 'test/helpers.js', 'bower_components/jquery/jquery.min.js', 'test/**/*.spec.js' @@ -30,8 +30,8 @@ module.exports = function (config) { ], preprocessors: { - 'src/textAngular.js': ['coverage'], - 'src/textAngularSetup.js': ['coverage'] + 'dist/textAngular.js': ['coverage'], + 'dist/textAngularSetup.js': ['coverage'] }, // test results reporter to use diff --git a/karma-jquery.conf.js b/karma-jquery.conf.js index ab7c596a..adb06a4f 100644 --- a/karma-jquery.conf.js +++ b/karma-jquery.conf.js @@ -17,9 +17,9 @@ module.exports = function (config) { 'bower_components/rangy/rangy-selectionsaverestore.js', 'bower_components/angular/angular.min.js', 'bower_components/angular-mocks/angular-mocks.js', - 'src/textAngular-sanitize.js', - 'src/textAngularSetup.js', - 'src/textAngular.js', + 'dist/textAngular-sanitize.js', + 'dist/textAngularSetup.js', + 'dist/textAngular.js', 'test/helpers.js', 'test/**/*.spec.js' ], @@ -30,8 +30,8 @@ module.exports = function (config) { ], preprocessors: { - 'src/textAngular.js': ['coverage'], - 'src/textAngularSetup.js': ['coverage'] + 'dist/textAngular.js': ['coverage'], + 'dist/textAngularSetup.js': ['coverage'] }, // test results reporter to use diff --git a/package.json b/package.json index f33a6c4b..3931122b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "grunt-cli": "~0.1.11", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "^0.5.0", + "grunt-contrib-copy": "^0.8.0", "grunt-contrib-jshint": "^0.8.0", "grunt-contrib-uglify": "^0.2.7", "grunt-contrib-watch": "^0.6.1", diff --git a/lib/DOM.js b/src/DOM.js similarity index 100% rename from lib/DOM.js rename to src/DOM.js diff --git a/lib/factories.js b/src/factories.js similarity index 100% rename from lib/factories.js rename to src/factories.js diff --git a/lib/globals.js b/src/globals.js similarity index 100% rename from lib/globals.js rename to src/globals.js diff --git a/lib/main.js b/src/main.js similarity index 100% rename from lib/main.js rename to src/main.js diff --git a/lib/taBind.js b/src/taBind.js similarity index 99% rename from lib/taBind.js rename to src/taBind.js index 03f44ab1..57fa318e 100644 --- a/lib/taBind.js +++ b/src/taBind.js @@ -457,7 +457,6 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM' text = text.replace(/
    ]*?>/ig, '').replace(/( | )<\/span>/ig, ' '); } - text = taSanitize(text, '', _disableSanitizer); if (//i.test(text) && /(|).*/i.test(text) === false) { // insert missing parent of li element text = text.replace(/.*<\/li(\s.*)?>/i, '
      $&
    '); @@ -465,6 +464,8 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM' if(_pasteHandler) text = _pasteHandler(scope, {$html: text}) || text; + text = taSanitize(text, '', _disableSanitizer); + taSelection.insertHtml(text, element[0]); $timeout(function(){ ngModel.$setViewValue(_compileHtml()); diff --git a/lib/validators.js b/src/validators.js similarity index 100% rename from lib/validators.js rename to src/validators.js