text-angular-rails 1.4.2 → 1.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * @license AngularJS v1.3.10
3
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ !function(a,b,c){"use strict";/**
7
+ * @ngdoc module
8
+ * @name ngSanitize
9
+ * @description
10
+ *
11
+ * # ngSanitize
12
+ *
13
+ * The `ngSanitize` module provides functionality to sanitize HTML.
14
+ *
15
+ *
16
+ * <div doc-module-components="ngSanitize"></div>
17
+ *
18
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
19
+ */
20
+ /*
21
+ * HTML Parser By Misko Hevery (misko@hevery.com)
22
+ * based on: HTML Parser By John Resig (ejohn.org)
23
+ * Original code by Erik Arvidsson, Mozilla Public License
24
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
25
+ *
26
+ * // Use like so:
27
+ * htmlParser(htmlString, {
28
+ * start: function(tag, attrs, unary) {},
29
+ * end: function(tag) {},
30
+ * chars: function(text) {},
31
+ * comment: function(text) {}
32
+ * });
33
+ *
34
+ */
35
+ /**
36
+ * @ngdoc service
37
+ * @name $sanitize
38
+ * @kind function
39
+ *
40
+ * @description
41
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
42
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
43
+ * it into the returned string, however, since our parser is more strict than a typical browser
44
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
45
+ * browser, won't make it through the sanitizer. The input may also contain SVG markup.
46
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
47
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
48
+ *
49
+ * @param {string} html HTML input.
50
+ * @returns {string} Sanitized HTML.
51
+ *
52
+ * @example
53
+ <example module="sanitizeExample" deps="angular-sanitize.js">
54
+ <file name="index.html">
55
+ <script>
56
+ angular.module('sanitizeExample', ['ngSanitize'])
57
+ .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
58
+ $scope.snippet =
59
+ '<p style="color:blue">an html\n' +
60
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
61
+ 'snippet</p>';
62
+ $scope.deliberatelyTrustDangerousSnippet = function() {
63
+ return $sce.trustAsHtml($scope.snippet);
64
+ };
65
+ }]);
66
+ </script>
67
+ <div ng-controller="ExampleController">
68
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
69
+ <table>
70
+ <tr>
71
+ <td>Directive</td>
72
+ <td>How</td>
73
+ <td>Source</td>
74
+ <td>Rendered</td>
75
+ </tr>
76
+ <tr id="bind-html-with-sanitize">
77
+ <td>ng-bind-html</td>
78
+ <td>Automatically uses $sanitize</td>
79
+ <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
80
+ <td><div ng-bind-html="snippet"></div></td>
81
+ </tr>
82
+ <tr id="bind-html-with-trust">
83
+ <td>ng-bind-html</td>
84
+ <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
85
+ <td>
86
+ <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
87
+ &lt;/div&gt;</pre>
88
+ </td>
89
+ <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
90
+ </tr>
91
+ <tr id="bind-default">
92
+ <td>ng-bind</td>
93
+ <td>Automatically escapes</td>
94
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
95
+ <td><div ng-bind="snippet"></div></td>
96
+ </tr>
97
+ </table>
98
+ </div>
99
+ </file>
100
+ <file name="protractor.js" type="protractor">
101
+ it('should sanitize the html snippet by default', function() {
102
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
103
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
104
+ });
105
+
106
+ it('should inline raw snippet if bound to a trusted value', function() {
107
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
108
+ toBe("<p style=\"color:blue\">an html\n" +
109
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
110
+ "snippet</p>");
111
+ });
112
+
113
+ it('should escape snippet without any filter', function() {
114
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
115
+ toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
116
+ "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
117
+ "snippet&lt;/p&gt;");
118
+ });
119
+
120
+ it('should update', function() {
121
+ element(by.model('snippet')).clear();
122
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
123
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
124
+ toBe('new <b>text</b>');
125
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
126
+ 'new <b onclick="alert(1)">text</b>');
127
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
128
+ "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
129
+ });
130
+ </file>
131
+ </example>
132
+ */
133
+ function d(){this.$get=["$$sanitizeUri",function(a){return function(b){"undefined"!=typeof arguments[1]&&(arguments[1].version="taSanitize");var c=[];return g(b,l(c,function(b,c){return!/^unsafe/.test(a(b,c))})),c.join("")}}]}function e(a){var c=[],d=l(c,b.noop);return d.chars(a),c.join("")}function f(a){var b,c={},d=a.split(",");for(b=0;b<d.length;b++)c[d[b]]=!0;return c}/**
134
+ * @example
135
+ * htmlParser(htmlString, {
136
+ * start: function(tag, attrs, unary) {},
137
+ * end: function(tag) {},
138
+ * chars: function(text) {},
139
+ * comment: function(text) {}
140
+ * });
141
+ *
142
+ * @param {string} html string
143
+ * @param {object} handler
144
+ */
145
+ function g(a,c){function d(a,d,f,g){if(d=b.lowercase(d),D[d])for(;k.last()&&E[k.last()];)e("",k.last());C[d]&&k.last()==d&&e("",d),g=z[d]||!!g,g||k.push(d);var i={};f.replace(p,function(a,b,c,d,e){var f=c||d||e||"";i[b]=h(f)}),c.start&&c.start(d,i,g)}function e(a,d){var e,f=0;if(d=b.lowercase(d))
146
+ // Find the closest opened tag of the same type
147
+ for(f=k.length-1;f>=0&&k[f]!=d;f--);if(f>=0){
148
+ // Close all the open elements, up the stack
149
+ for(e=k.length-1;e>=f;e--)c.end&&c.end(k[e]);
150
+ // Remove the open elements from the stack
151
+ k.length=f}}"string"!=typeof a&&(a=null===a||"undefined"==typeof a?"":""+a);var f,g,i,j,k=[],l=a;for(k.last=function(){return k[k.length-1]};a;){
152
+ // Make sure we're not in a script or style element
153
+ if(j="",g=!0,k.last()&&G[k.last()])a=a.replace(new RegExp("([^]*)<\\s*\\/\\s*"+k.last()+"[^>]*>","i"),function(a,b){return b=b.replace(s,"$1").replace(v,"$1"),c.chars&&c.chars(h(b)),""}),e("",k.last());else{
154
+ // White space
155
+ if(y.test(a)){if(i=a.match(y)){i[0];c.whitespace&&c.whitespace(i[0]),a=a.replace(i[0],""),g=!1}}else t.test(a)?(i=a.match(t),i&&(c.comment&&c.comment(i[1]),a=a.replace(i[0],""),g=!1)):u.test(a)?(i=a.match(u),i&&(a=a.replace(i[0],""),g=!1)):r.test(a)?(i=a.match(o),i&&(a=a.substring(i[0].length),i[0].replace(o,e),g=!1)):q.test(a)&&(i=a.match(n),i?(
156
+ // We only have a valid start-tag if there is a '>'.
157
+ i[4]&&(a=a.substring(i[0].length),i[0].replace(n,d)),g=!1):(
158
+ // no ending tag found --- this piece should be encoded as an entity.
159
+ j+="<",a=a.substring(1)));g&&(f=a.indexOf("<"),j+=f<0?a:a.substring(0,f),a=f<0?"":a.substring(f),c.chars&&c.chars(h(j)))}if(a==l)throw m("badparse","The sanitizer was unable to parse the following block of html: {0}",a);l=a}
160
+ // Clean up any remaining tags
161
+ e()}/**
162
+ * decodes all entities into regular string
163
+ * @param value
164
+ * @returns {string} A string with decoded entities.
165
+ */
166
+ function h(a){if(!a)return"";
167
+ // Note: IE8 does not preserve spaces at the start/end of innerHTML
168
+ // so we must capture them and reattach them afterward
169
+ var b=N.exec(a),c=b[1],d=b[3],e=b[2];
170
+ // innerText depends on styling as it doesn't display hidden elements.
171
+ // Therefore, it's better to use textContent not to cause unnecessary
172
+ // reflows. However, IE<9 don't support textContent so the innerText
173
+ // fallback is necessary.
174
+ return e&&(M.innerHTML=e.replace(/</g,"&lt;"),e="textContent"in M?M.textContent:M.innerText),c+e+d}/**
175
+ * Escapes all potentially dangerous characters, so that the
176
+ * resulting string can be safely inserted into attribute or
177
+ * element text.
178
+ * @param value
179
+ * @returns {string} escaped text
180
+ */
181
+ function i(a){return a.replace(/&/g,"&amp;").replace(w,function(a){var b=a.charCodeAt(0),c=a.charCodeAt(1);return"&#"+(1024*(b-55296)+(c-56320)+65536)+";"}).replace(x,function(a){
182
+ // unsafe chars are: \u0000-\u001f \u007f-\u009f \u00ad \u0600-\u0604 \u070f \u17b4 \u17b5 \u200c-\u200f \u2028-\u202f \u2060-\u206f \ufeff \ufff0-\uffff from jslint.com/lint.html
183
+ // decimal values are: 0-31, 127-159, 173, 1536-1540, 1807, 6068, 6069, 8204-8207, 8232-8239, 8288-8303, 65279, 65520-65535
184
+ var b=a.charCodeAt(0);
185
+ // if unsafe character encode
186
+ // if unsafe character encode
187
+ return b<=159||173==b||b>=1536&&b<=1540||1807==b||6068==b||6069==b||b>=8204&&b<=8207||b>=8232&&b<=8239||b>=8288&&b<=8303||65279==b||b>=65520&&b<=65535?"&#"+b+";":a}).replace(/</g,"&lt;").replace(/>/g,"&gt;")}
188
+ // Custom logic for accepting certain style options only - textAngular
189
+ // Currently allows only the color, background-color, text-align, float, width and height attributes
190
+ // all other attributes should be easily done through classes.
191
+ function j(a){var c="",d=a.split(";");return b.forEach(d,function(a){var d=a.split(":");if(2==d.length){var e=O(b.lowercase(d[0])),a=O(b.lowercase(d[1]));(("color"===e||"background-color"===e)&&(a.match(/^rgb\([0-9%,\. ]*\)$/i)||a.match(/^rgba\([0-9%,\. ]*\)$/i)||a.match(/^hsl\([0-9%,\. ]*\)$/i)||a.match(/^hsla\([0-9%,\. ]*\)$/i)||a.match(/^#[0-9a-f]{3,6}$/i)||a.match(/^[a-z]*$/i))||"text-align"===e&&("left"===a||"right"===a||"center"===a||"justify"===a)||"text-decoration"===e&&("underline"===a||"line-through"===a)||"font-weight"===e&&"bold"===a||"font-style"===e&&"italic"===a||"float"===e&&("left"===a||"right"===a||"none"===a)||"vertical-align"===e&&("baseline"===a||"sub"===a||"super"===a||"test-top"===a||"text-bottom"===a||"middle"===a||"top"===a||"bottom"===a||a.match(/[0-9]*(px|em)/)||a.match(/[0-9]+?%/))||"font-size"===e&&("xx-small"===a||"x-small"===a||"small"===a||"medium"===a||"large"===a||"x-large"===a||"xx-large"===a||"larger"===a||"smaller"===a||a.match(/[0-9]*\.?[0-9]*(px|em|rem|mm|q|cm|in|pt|pc|%)/))||("width"===e||"height"===e)&&a.match(/[0-9\.]*(px|em|rem|%)/)||// Reference #520
192
+ "direction"===e&&a.match(/^ltr|rtl|initial|inherit$/))&&(c+=e+": "+a+";")}}),c}
193
+ // this function is used to manually allow specific attributes on specific tags with certain prerequisites
194
+ function k(a,b,c,d){
195
+ // catch the div placeholder for the iframe replacement
196
+ return!("img"!==a||!b["ta-insert-video"]||"ta-insert-video"!==c&&"allowfullscreen"!==c&&"frameborder"!==c&&("contenteditable"!==c||"false"!==d))}/**
197
+ * create an HTML/XML writer which writes to buffer
198
+ * @param {Array} buf use buf.jain('') to get out sanitized html string
199
+ * @returns {object} in the form of {
200
+ * start: function(tag, attrs, unary) {},
201
+ * end: function(tag) {},
202
+ * chars: function(text) {},
203
+ * comment: function(text) {}
204
+ * }
205
+ */
206
+ function l(a,c){var d=!1,e=b.bind(a,a.push);return{start:function(a,f,g){a=b.lowercase(a),!d&&G[a]&&(d=a),d||H[a]!==!0||(e("<"),e(a),b.forEach(f,function(d,g){var h=b.lowercase(g),l="img"===a&&"src"===h||"background"===h;("style"===h&&""!==(d=j(d))||k(a,f,h,d)||L[h]===!0&&(I[h]!==!0||c(d,l)))&&(e(" "),e(g),e('="'),e(i(d)),e('"'))}),e(g?"/>":">"))},comment:function(a){e(a)},whitespace:function(a){e(i(a))},end:function(a){a=b.lowercase(a),d||H[a]!==!0||(e("</"),e(a),e(">")),a==d&&(d=!1)},chars:function(a){d||e(i(a))}}}var m=b.$$minErr("$sanitize"),n=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,o=/^<\/\s*([\w:-]+)[^>]*>/,p=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,q=/^</,r=/^<\//,s=/<!--(.*?)-->/g,t=/(^<!--.*?-->)/,u=/<!DOCTYPE([^>]*?)>/i,v=/<!\[CDATA\[(.*?)]]>/g,w=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
207
+ // Match everything outside of normal chars and " (quote character)
208
+ x=/([^\#-~| |!])/g,y=/^(\s+)/,z=f("area,br,col,hr,img,wbr,input"),A=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),B=f("rp,rt"),C=b.extend({},B,A),D=b.extend({},A,f("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")),E=b.extend({},B,f("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")),F=f("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"),G=f("script,style"),H=b.extend({},z,D,E,C,F),I=f("background,cite,href,longdesc,src,usemap,xlink:href"),J=f("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"),K=f("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"),L=b.extend({},I,K,J),M=document.createElement("pre"),N=/^(\s*)([\s\S]*?)(\s*)$/,O=function(){
209
+ // native trim is way faster: http://jsperf.com/angular-trim-test
210
+ // but IE doesn't have it... :-(
211
+ // TODO: we should move this into IE/ES5 polyfill
212
+ // native trim is way faster: http://jsperf.com/angular-trim-test
213
+ // but IE doesn't have it... :-(
214
+ // TODO: we should move this into IE/ES5 polyfill
215
+ return String.prototype.trim?function(a){return b.isString(a)?a.trim():a}:function(a){return b.isString(a)?a.replace(/^\s\s*/,"").replace(/\s\s*$/,""):a}}();
216
+ // define ngSanitize module and register $sanitize service
217
+ b.module("ngSanitize",[]).provider("$sanitize",d),/* global sanitizeText: false */
218
+ /**
219
+ * @ngdoc filter
220
+ * @name linky
221
+ * @kind function
222
+ *
223
+ * @description
224
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
225
+ * plain email address links.
226
+ *
227
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
228
+ *
229
+ * @param {string} text Input text.
230
+ * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
231
+ * @returns {string} Html-linkified text.
232
+ *
233
+ * @usage
234
+ <span ng-bind-html="linky_expression | linky"></span>
235
+ *
236
+ * @example
237
+ <example module="linkyExample" deps="angular-sanitize.js">
238
+ <file name="index.html">
239
+ <script>
240
+ angular.module('linkyExample', ['ngSanitize'])
241
+ .controller('ExampleController', ['$scope', function($scope) {
242
+ $scope.snippet =
243
+ 'Pretty text with some links:\n'+
244
+ 'http://angularjs.org/,\n'+
245
+ 'mailto:us@somewhere.org,\n'+
246
+ 'another@somewhere.org,\n'+
247
+ 'and one more: ftp://127.0.0.1/.';
248
+ $scope.snippetWithTarget = 'http://angularjs.org/';
249
+ }]);
250
+ </script>
251
+ <div ng-controller="ExampleController">
252
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
253
+ <table>
254
+ <tr>
255
+ <td>Filter</td>
256
+ <td>Source</td>
257
+ <td>Rendered</td>
258
+ </tr>
259
+ <tr id="linky-filter">
260
+ <td>linky filter</td>
261
+ <td>
262
+ <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
263
+ </td>
264
+ <td>
265
+ <div ng-bind-html="snippet | linky"></div>
266
+ </td>
267
+ </tr>
268
+ <tr id="linky-target">
269
+ <td>linky target</td>
270
+ <td>
271
+ <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
272
+ </td>
273
+ <td>
274
+ <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
275
+ </td>
276
+ </tr>
277
+ <tr id="escaped-html">
278
+ <td>no filter</td>
279
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
280
+ <td><div ng-bind="snippet"></div></td>
281
+ </tr>
282
+ </table>
283
+ </file>
284
+ <file name="protractor.js" type="protractor">
285
+ it('should linkify the snippet with urls', function() {
286
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
287
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
288
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
289
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
290
+ });
291
+
292
+ it('should not linkify snippet without the linky filter', function() {
293
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
294
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
295
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
296
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
297
+ });
298
+
299
+ it('should update', function() {
300
+ element(by.model('snippet')).clear();
301
+ element(by.model('snippet')).sendKeys('new http://link.');
302
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
303
+ toBe('new http://link.');
304
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
305
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
306
+ .toBe('new http://link.');
307
+ });
308
+
309
+ it('should work with the target property', function() {
310
+ expect(element(by.id('linky-target')).
311
+ element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
312
+ toBe('http://angularjs.org/');
313
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
314
+ });
315
+ </file>
316
+ </example>
317
+ */
318
+ b.module("ngSanitize").filter("linky",["$sanitize",function(a){var c=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,d=/^mailto:/;return function(f,g){function h(a){a&&n.push(e(a))}function i(a,c){n.push("<a "),b.isDefined(g)&&n.push('target="',g,'" '),n.push('href="',a.replace(/"/g,"&quot;"),'">'),h(c),n.push("</a>")}if(!f)return f;for(var j,k,l,m=f,n=[];j=m.match(c);)
319
+ // We can not end in these as they are sometimes found at the end of the sentence
320
+ k=j[0],
321
+ // if we did not match ftp/http/www/mailto then assume mailto
322
+ j[2]||j[4]||(k=(j[3]?"http://":"mailto:")+k),l=j.index,h(m.substr(0,l)),i(k,j[0].replace(d,"")),m=m.substring(l+j[0].length);return h(m),a(n.join(""))}}])}(window,window.angular);
@@ -0,0 +1,1481 @@
1
+ !function(a,b){"function"==typeof define&&define.amd?
2
+ // AMD. Register as an anonymous module unless amdModuleId is set
3
+ define("textAngular",["rangy","rangy/lib/rangy-selectionsaverestore"],function(c,d){return a["textAngular.name"]=b(c,d)}):"object"==typeof exports?
4
+ // Node. Does not work with strict CommonJS, but
5
+ // only CommonJS-like environments that support module.exports,
6
+ // like Node.
7
+ module.exports=b(require("rangy"),require("rangy/lib/rangy-selectionsaverestore")):a.textAngular=b(rangy)}(this,function(a){
8
+ // tests against the current jqLite/jquery implementation if this can be an element
9
+ function b(a){try{return 0!==angular.element(a).length}catch(a){return!1}}/*
10
+ A tool definition is an object with the following key/value parameters:
11
+ action: [function(deferred, restoreSelection)]
12
+ a function that is executed on clicking on the button - this will allways be executed using ng-click and will
13
+ overwrite any ng-click value in the display attribute.
14
+ The function is passed a deferred object ($q.defer()), if this is wanted to be used `return false;` from the action and
15
+ manually call `deferred.resolve();` elsewhere to notify the editor that the action has finished.
16
+ restoreSelection is only defined if the rangy library is included and it can be called as `restoreSelection()` to restore the users
17
+ selection in the WYSIWYG editor.
18
+ display: [string]?
19
+ Optional, an HTML element to be displayed as the button. The `scope` of the button is the tool definition object with some additional functions
20
+ If set this will cause buttontext and iconclass to be ignored
21
+ class: [string]?
22
+ Optional, if set will override the taOptions.classes.toolbarButton class.
23
+ buttontext: [string]?
24
+ if this is defined it will replace the contents of the element contained in the `display` element
25
+ iconclass: [string]?
26
+ if this is defined an icon (<i>) will be appended to the `display` element with this string as it's class
27
+ tooltiptext: [string]?
28
+ Optional, a plain text description of the action, used for the title attribute of the action button in the toolbar by default.
29
+ activestate: [function(commonElement)]?
30
+ this function is called on every caret movement, if it returns true then the class taOptions.classes.toolbarButtonActive
31
+ will be applied to the `display` element, else the class will be removed
32
+ disabled: [function()]?
33
+ if this function returns true then the tool will have the class taOptions.classes.disabled applied to it, else it will be removed
34
+ Other functions available on the scope are:
35
+ name: [string]
36
+ the name of the tool, this is the first parameter passed into taRegisterTool
37
+ isDisabled: [function()]
38
+ returns true if the tool is disabled, false if it isn't
39
+ displayActiveToolClass: [function(boolean)]
40
+ returns true if the tool is 'active' in the currently focussed toolbar
41
+ onElementSelect: [Object]
42
+ This object contains the following key/value pairs and is used to trigger the ta-element-select event
43
+ element: [String]
44
+ an element name, will only trigger the onElementSelect action if the tagName of the element matches this string
45
+ filter: [function(element)]?
46
+ an optional filter that returns a boolean, if true it will trigger the onElementSelect.
47
+ action: [function(event, element, editorScope)]
48
+ the action that should be executed if the onElementSelect function runs
49
+ */
50
+ // name and toolDefinition to add into the tools available to be added on the toolbar
51
+ function c(a,c){if(!a||""===a||e.hasOwnProperty(a))throw"textAngular Error: A unique name is required for a Tool Definition";if(c.display&&(""===c.display||!b(c.display))||!c.display&&!c.buttontext&&!c.iconclass)throw'textAngular Error: Tool Definition for "'+a+'" does not have a valid display/iconclass/buttontext value';e[a]=c}
52
+ // usage is:
53
+ // var t0 = performance.now();
54
+ // doSomething();
55
+ // var t1 = performance.now();
56
+ // console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to do something!');
57
+ //
58
+ // turn html into pure text that shows visiblity
59
+ function d(a){var b=document.createElement("DIV");b.innerHTML=a;var c=b.textContent||b.innerText||"";// zero width space
60
+ return c.replace("​",""),c=c.trim()}
61
+ // setup the global contstant functions for setting up the toolbar
62
+ // all tool definitions
63
+ var e={};angular.module("textAngularSetup",[]).constant("taRegisterTool",c).value("taTools",e).value("taOptions",{
64
+ //////////////////////////////////////////////////////////////////////////////////////
65
+ // forceTextAngularSanitize
66
+ // set false to allow the textAngular-sanitize provider to be replaced
67
+ // with angular-sanitize or a custom provider.
68
+ forceTextAngularSanitize:!0,
69
+ ///////////////////////////////////////////////////////////////////////////////////////
70
+ // keyMappings
71
+ // allow customizable keyMappings for specialized key boards or languages
72
+ //
73
+ // keyMappings provides key mappings that are attached to a given commandKeyCode.
74
+ // To modify a specific keyboard binding, simply provide function which returns true
75
+ // for the event you wish to map to.
76
+ // Or to disable a specific keyboard binding, provide a function which returns false.
77
+ // Note: 'RedoKey' and 'UndoKey' are internally bound to the redo and undo functionality.
78
+ // At present, the following commandKeyCodes are in use:
79
+ // 98, 'TabKey', 'ShiftTabKey', 105, 117, 'UndoKey', 'RedoKey'
80
+ //
81
+ // To map to an new commandKeyCode, add a new key mapping such as:
82
+ // {commandKeyCode: 'CustomKey', testForKey: function (event) {
83
+ // if (event.keyCode=57 && event.ctrlKey && !event.shiftKey && !event.altKey) return true;
84
+ // } }
85
+ // to the keyMappings. This example maps ctrl+9 to 'CustomKey'
86
+ // Then where taRegisterTool(...) is called, add a commandKeyCode: 'CustomKey' and your
87
+ // tool will be bound to ctrl+9.
88
+ //
89
+ // To disble one of the already bound commandKeyCodes such as 'RedoKey' or 'UndoKey' add:
90
+ // {commandKeyCode: 'RedoKey', testForKey: function (event) { return false; } },
91
+ // {commandKeyCode: 'UndoKey', testForKey: function (event) { return false; } },
92
+ // to disable them.
93
+ //
94
+ keyMappings:[],toolbar:[["h1","h2","h3","h4","h5","h6","p","pre","quote"],["bold","italics","underline","strikeThrough","ul","ol","redo","undo","clear"],["justifyLeft","justifyCenter","justifyRight","justifyFull","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:{
95
+ // wysiwyg mode
96
+ textEditorSetup:function(a){},
97
+ // raw html
98
+ htmlEditorSetup:function(a){}},defaultFileDropHandler:/* istanbul ignore next: untestable image processing */
99
+ function(a,b){var c=new FileReader;return"image"===a.type.substring(0,5)&&(c.onload=function(){""!==c.result&&b("insertImage",c.result,!0)},c.readAsDataURL(a),!0)}}).value("taSelectableElements",["a","img"]).value("taCustomRenderers",[{
100
+ // Parse back out: '<div class="ta-insert-video" ta-insert-video src="' + urlLink + '" allowfullscreen="true" width="300" frameborder="0" height="250"></div>'
101
+ // To correct video element. For now only support youtube
102
+ selector:"img",customAttribute:"ta-insert-video",renderLogic:function(a){var b=angular.element("<iframe></iframe>"),c=a.prop("attributes");
103
+ // loop through element attributes and apply them on iframe
104
+ angular.forEach(c,function(a){b.attr(a.name,a.value)}),b.attr("src",b.attr("ta-insert-video")),a.replaceWith(b)}}]).value("taTranslations",{
105
+ // moved to sub-elements
106
+ //toggleHTML: "Toggle HTML",
107
+ //insertImage: "Please enter a image URL to insert",
108
+ //insertLink: "Please enter a URL to insert",
109
+ //insertVideo: "Please enter a youtube URL to embed",
110
+ html:{tooltip:"Toggle html / Rich Text"},
111
+ // tooltip for heading - might be worth splitting
112
+ 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"},justifyFull:{tooltip:"Justify text"},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"}}).factory("taToolFunctions",["$window","taTranslations",function(a,b){return{imgOnSelectAction:function(a,b,c){
113
+ // setup the editor toolbar
114
+ // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic/display
115
+ var d=function(){c.updateTaBindtaTextElement(),c.hidePopover()};a.preventDefault(),c.displayElements.popover.css("width","375px");var e=c.displayElements.popoverContainer;e.empty();var f=angular.element('<div class="btn-group" style="padding-right: 6px;">'),g=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">100% </button>');g.on("click",function(a){a.preventDefault(),b.css({width:"100%",height:""}),d()});var h=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">50% </button>');h.on("click",function(a){a.preventDefault(),b.css({width:"50%",height:""}),d()});var i=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">25% </button>');i.on("click",function(a){a.preventDefault(),b.css({width:"25%",height:""}),d()});var j=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">Reset</button>');j.on("click",function(a){a.preventDefault(),b.css({width:"",height:""}),d()}),f.append(g),f.append(h),f.append(i),f.append(j),e.append(f),f=angular.element('<div class="btn-group" style="padding-right: 6px;">');var k=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-left"></i></button>');k.on("click",function(a){a.preventDefault(),
116
+ // webkit
117
+ b.css("float","left"),
118
+ // firefox
119
+ b.css("cssFloat","left"),
120
+ // IE < 8
121
+ b.css("styleFloat","left"),d()});var l=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-right"></i></button>');l.on("click",function(a){a.preventDefault(),
122
+ // webkit
123
+ b.css("float","right"),
124
+ // firefox
125
+ b.css("cssFloat","right"),
126
+ // IE < 8
127
+ b.css("styleFloat","right"),d()});var m=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-justify"></i></button>');m.on("click",function(a){a.preventDefault(),
128
+ // webkit
129
+ b.css("float",""),
130
+ // firefox
131
+ b.css("cssFloat",""),
132
+ // IE < 8
133
+ b.css("styleFloat",""),d()}),f.append(k),f.append(m),f.append(l),e.append(f),f=angular.element('<div class="btn-group">');var n=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-trash-o"></i></button>');n.on("click",function(a){a.preventDefault(),b.remove(),d()}),f.append(n),e.append(f),c.showPopover(b),c.showResizeOverlay(b)},aOnSelectAction:function(c,d,e){
134
+ // setup the editor toolbar
135
+ // Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic
136
+ c.preventDefault(),e.displayElements.popover.css("width","436px");var f=e.displayElements.popoverContainer;f.empty(),f.css("line-height","28px");var g=angular.element('<a href="'+d.attr("href")+'" target="_blank">'+d.attr("href")+"</a>");g.css({display:"inline-block","max-width":"200px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap","vertical-align":"middle"}),f.append(g);var h=angular.element('<div class="btn-group pull-right">'),i=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on" title="'+b.editLink.reLinkButton.tooltip+'"><i class="fa fa-edit icon-edit"></i></button>');i.on("click",function(c){c.preventDefault();var f=a.prompt(b.insertLink.dialogPrompt,d.attr("href"));f&&""!==f&&"http://"!==f&&(d.attr("href",f),e.updateTaBindtaTextElement()),e.hidePopover()}),h.append(i);var j=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on" title="'+b.editLink.unLinkButton.tooltip+'"><i class="fa fa-unlink icon-unlink"></i></button>');
137
+ // directly before this click event is fired a digest is fired off whereby the reference to $element is orphaned off
138
+ j.on("click",function(a){a.preventDefault(),d.replaceWith(d.contents()),e.updateTaBindtaTextElement(),e.hidePopover()}),h.append(j);var k=angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on">'+b.editLink.targetToggle.buttontext+"</button>");"_blank"===d.attr("target")&&k.addClass("active"),k.on("click",function(a){a.preventDefault(),d.attr("target","_blank"===d.attr("target")?"":"_blank"),k.toggleClass("active"),e.updateTaBindtaTextElement()}),h.append(k),f.append(h),e.showPopover(d)},extractYoutubeVideoId:function(a){var b=/(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i,c=a.match(b);return c&&c[1]||null}}}]).run(["taRegisterTool","$window","taTranslations","taSelection","taToolFunctions","$sanitize","taOptions","$log",function(a,b,c,d,e,f,g,h){
139
+ // test for the version of $sanitize that is in use
140
+ // You can disable this check by setting taOptions.textAngularSanitize == false
141
+ var i={};/* istanbul ignore next, throws error */
142
+ if(f("",i),g.forceTextAngularSanitize===!0&&"taSanitize"!==i.version)throw angular.$$minErr("textAngular")("textAngularSetup","The textAngular-sanitize provider has been replaced by another -- have you included angular-sanitize by mistake?");a("html",{iconclass:"fa fa-code",tooltiptext:c.html.tooltip,action:function(){this.$editor().switchView()},activeState:function(){return this.$editor().showHtml}});
143
+ // add the Header tools
144
+ // convenience functions so that the loop works correctly
145
+ var j=function(a){return function(){return this.$editor().queryFormatBlockState(a)}},k=function(){return this.$editor().wrapSelection("formatBlock","<"+this.name.toUpperCase()+">")};angular.forEach(["h1","h2","h3","h4","h5","h6"],function(b){a(b.toLowerCase(),{buttontext:b.toUpperCase(),tooltiptext:c.heading.tooltip+b.charAt(1),action:k,activeState:j(b.toLowerCase())})}),a("p",{buttontext:"P",tooltiptext:c.p.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","<P>")},activeState:function(){return this.$editor().queryFormatBlockState("p")}}),
146
+ // key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext
147
+ a("pre",{buttontext:"pre",tooltiptext:c.pre.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","<PRE>")},activeState:function(){return this.$editor().queryFormatBlockState("pre")}}),a("ul",{iconclass:"fa fa-list-ul",tooltiptext:c.ul.tooltip,action:function(){return this.$editor().wrapSelection("insertUnorderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertUnorderedList")}}),a("ol",{iconclass:"fa fa-list-ol",tooltiptext:c.ol.tooltip,action:function(){return this.$editor().wrapSelection("insertOrderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertOrderedList")}}),a("quote",{iconclass:"fa fa-quote-right",tooltiptext:c.quote.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","<BLOCKQUOTE>")},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("undo",{iconclass:"fa fa-undo",tooltiptext:c.undo.tooltip,action:function(){return this.$editor().wrapSelection("undo",null)}}),a("redo",{iconclass:"fa fa-repeat",tooltiptext:c.redo.tooltip,action:function(){return this.$editor().wrapSelection("redo",null)}}),a("bold",{iconclass:"fa fa-bold",tooltiptext:c.bold.tooltip,action:function(){return this.$editor().wrapSelection("bold",null)},activeState:function(){return this.$editor().queryCommandState("bold")},commandKeyCode:98}),a("justifyLeft",{iconclass:"fa fa-align-left",tooltiptext:c.justifyLeft.tooltip,action:function(){return this.$editor().wrapSelection("justifyLeft",null)},activeState:function(a){/* istanbul ignore next: */
148
+ if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a)
149
+ // commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions
150
+ // so we do try catch here...
151
+ try{b="left"===a.css("text-align")||"left"===a.attr("align")||"right"!==a.css("text-align")&&"center"!==a.css("text-align")&&"justify"!==a.css("text-align")&&!this.$editor().queryCommandState("justifyRight")&&!this.$editor().queryCommandState("justifyCenter")&&!this.$editor().queryCommandState("justifyFull")}catch(a){/* istanbul ignore next: error handler */
152
+ //console.log(e);
153
+ b=!1}return b=b||this.$editor().queryCommandState("justifyLeft")}}),a("justifyRight",{iconclass:"fa fa-align-right",tooltiptext:c.justifyRight.tooltip,action:function(){return this.$editor().wrapSelection("justifyRight",null)},activeState:function(a){/* istanbul ignore next: */
154
+ if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a)
155
+ // commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions
156
+ // so we do try catch here...
157
+ try{b="right"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */
158
+ //console.log(e);
159
+ b=!1}return b=b||this.$editor().queryCommandState("justifyRight")}}),a("justifyFull",{iconclass:"fa fa-align-justify",tooltiptext:c.justifyFull.tooltip,action:function(){return this.$editor().wrapSelection("justifyFull",null)},activeState:function(a){var b=!1;if(a)
160
+ // commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions
161
+ // so we do try catch here...
162
+ try{b="justify"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */
163
+ //console.log(e);
164
+ b=!1}return b=b||this.$editor().queryCommandState("justifyFull")}}),a("justifyCenter",{iconclass:"fa fa-align-center",tooltiptext:c.justifyCenter.tooltip,action:function(){return this.$editor().wrapSelection("justifyCenter",null)},activeState:function(a){/* istanbul ignore next: */
165
+ if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a)
166
+ // commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions
167
+ // so we do try catch here...
168
+ try{b="center"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */
169
+ //console.log(e);
170
+ b=!1}return b=b||this.$editor().queryCommandState("justifyCenter")}}),a("indent",{iconclass:"fa fa-indent",tooltiptext:c.indent.tooltip,action:function(){return this.$editor().wrapSelection("indent",null)},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")},commandKeyCode:"TabKey"}),a("outdent",{iconclass:"fa fa-outdent",tooltiptext:c.outdent.tooltip,action:function(){return this.$editor().wrapSelection("outdent",null)},activeState:function(){return!1},commandKeyCode:"ShiftTabKey"}),a("italics",{iconclass:"fa fa-italic",tooltiptext:c.italic.tooltip,action:function(){return this.$editor().wrapSelection("italic",null)},activeState:function(){return this.$editor().queryCommandState("italic")},commandKeyCode:105}),a("underline",{iconclass:"fa fa-underline",tooltiptext:c.underline.tooltip,action:function(){return this.$editor().wrapSelection("underline",null)},activeState:function(){return this.$editor().queryCommandState("underline")},commandKeyCode:117}),a("strikeThrough",{iconclass:"fa fa-strikethrough",tooltiptext:c.strikeThrough.tooltip,action:function(){return this.$editor().wrapSelection("strikeThrough",null)},activeState:function(){return document.queryCommandState("strikeThrough")}}),a("clear",{iconclass:"fa fa-ban",tooltiptext:c.clear.tooltip,action:function(a,b){var c;this.$editor().wrapSelection("removeFormat",null);var e=angular.element(d.getSelectionElement());c=d.getAllSelectedElements();
171
+ //$log.log('selectedElements:', selectedElements);
172
+ // remove lists
173
+ var f=function(a,b){a=angular.element(a);var c=b;return b||(c=a),angular.forEach(a.children(),function(a){if("ul"===a.tagName.toLowerCase()||"ol"===a.tagName.toLowerCase())c=f(a,c);else{var b=angular.element("<p></p>");b.html(angular.element(a).html()),c.after(b),c=b}}),a.remove(),c};angular.forEach(c,function(a){"ul"!==a.nodeName.toLowerCase()&&"ol"!==a.nodeName.toLowerCase()||
174
+ //console.log('removeListElements', element);
175
+ f(a)}),angular.forEach(e.find("ul"),f),angular.forEach(e.find("ol"),f);
176
+ // clear out all class attributes. These do not seem to be cleared via removeFormat
177
+ var g=this.$editor(),h=function(a){a=angular.element(a),/* istanbul ignore next: this is not triggered in tests any longer since we now never select the whole displayELement */
178
+ a[0]!==g.displayElements.text[0]&&a.removeAttr("class"),angular.forEach(a.children(),h)};angular.forEach(e,h),
179
+ // check if in list. If not in list then use formatBlock option
180
+ e[0]&&"li"!==e[0].tagName.toLowerCase()&&"ol"!==e[0].tagName.toLowerCase()&&"ul"!==e[0].tagName.toLowerCase()&&"true"!==e[0].getAttribute("contenteditable")&&this.$editor().wrapSelection("formatBlock","default"),b()}});/* jshint -W099 */
181
+ /****************************
182
+ // we don't use this code - since the previous way CLEAR is expected to work does not clear partially selected <li>
183
+
184
+ var removeListElement = function(listE){
185
+ console.log(listE);
186
+ var _list = listE.parentNode.childNodes;
187
+ console.log('_list', _list);
188
+ var _preLis = [], _postLis = [], _found = false;
189
+ for (i = 0; i < _list.length; i++) {
190
+ if (_list[i] === listE) {
191
+ _found = true;
192
+ } else if (!_found) _preLis.push(_list[i]);
193
+ else _postLis.push(_list[i]);
194
+ }
195
+ var _parent = angular.element(listE.parentNode);
196
+ var newElem = angular.element('<p></p>');
197
+ newElem.html(angular.element(listE).html());
198
+ if (_preLis.length === 0 || _postLis.length === 0) {
199
+ if (_postLis.length === 0) _parent.after(newElem);
200
+ else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]);
201
+
202
+ if (_preLis.length === 0 && _postLis.length === 0) _parent.remove();
203
+ else angular.element(listE).remove();
204
+ } else {
205
+ var _firstList = angular.element('<' + _parent[0].tagName + '></' + _parent[0].tagName + '>');
206
+ var _secondList = angular.element('<' + _parent[0].tagName + '></' + _parent[0].tagName + '>');
207
+ for (i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i]));
208
+ for (i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i]));
209
+ _parent.after(_secondList);
210
+ _parent.after(newElem);
211
+ _parent.after(_firstList);
212
+ _parent.remove();
213
+ }
214
+ taSelection.setSelectionToElementEnd(newElem[0]);
215
+ };
216
+
217
+ elementsSeen = [];
218
+ if (selectedElements.length !==0) console.log(selectedElements);
219
+ angular.forEach(selectedElements, function (element) {
220
+ if (elementsSeen.indexOf(element) !== -1 || elementsSeen.indexOf(element.parentElement) !== -1) {
221
+ return;
222
+ }
223
+ elementsSeen.push(element);
224
+ if (element.nodeName.toLowerCase() === 'li') {
225
+ console.log('removeListElement', element);
226
+ removeListElement(element);
227
+ }
228
+ else if (element.parentElement && element.parentElement.nodeName.toLowerCase() === 'li') {
229
+ console.log('removeListElement', element.parentElement);
230
+ elementsSeen.push(element.parentElement);
231
+ removeListElement(element.parentElement);
232
+ }
233
+ });
234
+ **********************/
235
+ /**********************
236
+ if(possibleNodes[0].tagName.toLowerCase() === 'li'){
237
+ var _list = possibleNodes[0].parentNode.childNodes;
238
+ var _preLis = [], _postLis = [], _found = false;
239
+ for(i = 0; i < _list.length; i++){
240
+ if(_list[i] === possibleNodes[0]){
241
+ _found = true;
242
+ }else if(!_found) _preLis.push(_list[i]);
243
+ else _postLis.push(_list[i]);
244
+ }
245
+ var _parent = angular.element(possibleNodes[0].parentNode);
246
+ var newElem = angular.element('<p></p>');
247
+ newElem.html(angular.element(possibleNodes[0]).html());
248
+ if(_preLis.length === 0 || _postLis.length === 0){
249
+ if(_postLis.length === 0) _parent.after(newElem);
250
+ else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]);
251
+
252
+ if(_preLis.length === 0 && _postLis.length === 0) _parent.remove();
253
+ else angular.element(possibleNodes[0]).remove();
254
+ }else{
255
+ var _firstList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
256
+ var _secondList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
257
+ for(i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i]));
258
+ for(i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i]));
259
+ _parent.after(_secondList);
260
+ _parent.after(newElem);
261
+ _parent.after(_firstList);
262
+ _parent.remove();
263
+ }
264
+ taSelection.setSelectionToElementEnd(newElem[0]);
265
+ }
266
+ *******************/
267
+ /* istanbul ignore next: if it's javascript don't worry - though probably should show some kind of error message */
268
+ var l=function(a){return a.toLowerCase().indexOf("javascript")!==-1};a("insertImage",{iconclass:"fa fa-picture-o",tooltiptext:c.insertImage.tooltip,action:function(){var a;if(a=b.prompt(c.insertImage.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a&&!l(a)){d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()&&
269
+ // due to differences in implementation between FireFox and Chrome, we must move the
270
+ // insertion point past the <a> element, otherwise FireFox inserts inside the <a>
271
+ // With this change, both FireFox and Chrome behave the same way!
272
+ d.setSelectionAfterElement(d.getSelectionElement());
273
+ // In the past we used the simple statement:
274
+ //return this.$editor().wrapSelection('insertImage', imageLink, true);
275
+ //
276
+ // However on Firefox only, when the content is empty this is a problem
277
+ // See Issue #1201
278
+ // Investigation reveals that Firefox only inserts a <p> only!!!!
279
+ // So now we use insertHTML here and all is fine.
280
+ // NOTE: this is what 'insertImage' is supposed to do anyway!
281
+ var e='<img src="'+a+'">';return this.$editor().wrapSelection("insertHTML",e,!0)}},onElementSelect:{element:"img",action:e.imgOnSelectAction}}),a("insertVideo",{iconclass:"fa fa-youtube-play",tooltiptext:c.insertVideo.tooltip,action:function(){var a;
282
+ // block javascript here
283
+ /* istanbul ignore else: if it's javascript don't worry - though probably should show some kind of error message */
284
+ if(a=b.prompt(c.insertVideo.dialogPrompt,"https://"),!l(a)&&a&&""!==a&&"https://"!==a&&(videoId=e.extractYoutubeVideoId(a),videoId)){
285
+ // create the embed link
286
+ var f="https://www.youtube.com/embed/"+videoId,g='<img class="ta-insert-video" src="https://img.youtube.com/vi/'+videoId+'/hqdefault.jpg" ta-insert-video="'+f+'" contenteditable="false" allowfullscreen="true" frameborder="0" />';
287
+ // insert
288
+ /* istanbul ignore next: don't know how to test this... since it needs a dialogPrompt */
289
+ // due to differences in implementation between FireFox and Chrome, we must move the
290
+ // insertion point past the <a> element, otherwise FireFox inserts inside the <a>
291
+ // With this change, both FireFox and Chrome behave the same way!
292
+ return d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()&&d.setSelectionAfterElement(d.getSelectionElement()),this.$editor().wrapSelection("insertHTML",g,!0)}},onElementSelect:{element:"img",onlyWithAttrs:["ta-insert-video"],action:e.imgOnSelectAction}}),a("insertLink",{tooltiptext:c.insertLink.tooltip,iconclass:"fa fa-link",action:function(){var a;if(
293
+ // if this link has already been set, we need to just edit the existing link
294
+ /* istanbul ignore if: we do not test this */
295
+ a=d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()?b.prompt(c.insertLink.dialogPrompt,d.getSelectionElement().href):b.prompt(c.insertLink.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a&&!l(a))return this.$editor().wrapSelection("createLink",a,!0)},activeState:function(a){return!!a&&"A"===a[0].tagName},onElementSelect:{element:"a",action:e.aOnSelectAction}}),a("wordcount",{display:'<div id="toolbarWC" style="display:block; min-width:100px;">Words: <span ng-bind="wordcount"></span></div>',disabled:!0,wordcount:0,activeState:function(){// this fires on keyup
296
+ var a=this.$editor().displayElements.text,b=a[0].innerHTML||"",c=0;/* istanbul ignore if: will default to '' when undefined */
297
+ //Set current scope
298
+ //Set editor scope
299
+ return""!==b.replace(/\s*<[^>]*?>\s*/g,"")&&""!==b.trim()&&(c=b.replace(/<\/?(b|i|em|strong|span|u|strikethrough|a|img|small|sub|sup|label)( [^>*?])?>/gi,"").replace(/(<[^>]*?>\s*<[^>]*?>)/gi," ").replace(/(<[^>]*?>)/gi,"").replace(/\s+/gi," ").match(/\S+/g).length),this.wordcount=c,this.$editor().wordcount=c,!1}}),a("charcount",{display:'<div id="toolbarCC" style="display:block; min-width:120px;">Characters: <span ng-bind="charcount"></span></div>',disabled:!0,charcount:0,activeState:function(){// this fires on keyup
300
+ var a=this.$editor().displayElements.text,b=a[0].innerText||a[0].textContent,c=b.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g," ").replace(/\s+$/g," ").length;
301
+ //Set current scope
302
+ //Set editor scope
303
+ return this.charcount=c,this.$editor().charcount=c,!1}})}]);// NOTE: textAngularVersion must match the Gruntfile.js 'setVersion' task.... and have format v/d+./d+./d+
304
+ var f="v1.5.16",g={ie:function(){for(var a,b=3,c=document.createElement("div"),d=c.getElementsByTagName("i");c.innerHTML="<!--[if gt IE "+ ++b+"]><i></i><![endif]-->",d[0];);return b>4?b:a}(),webkit:/AppleWebKit\/([\d.]+)/i.test(navigator.userAgent),isFirefox:navigator.userAgent.toLowerCase().indexOf("firefox")>-1},h=h||{};/* istanbul ignore next: untestable browser check */
305
+ h.now=function(){return h.now||h.mozNow||h.msNow||h.oNow||h.webkitNow||function(){return(new Date).getTime()}}();
306
+ // Global to textAngular REGEXP vars for block and list elements.
307
+ var i=/^(address|article|aside|audio|blockquote|canvas|center|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,j=/^(ul|li|ol)$/i,k=/^(#text|span|address|article|aside|audio|blockquote|canvas|center|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;
308
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
309
+ /* istanbul ignore next: trim shim for older browsers */
310
+ String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});/*
311
+ Custom stylesheet for the placeholders rules.
312
+ Credit to: http://davidwalsh.name/add-rules-stylesheets
313
+ */
314
+ var l,m,n,o,p,q;/* istanbul ignore else: IE <8 test*/
315
+ if(g.ie>8||void 0===g.ie){/* istanbul ignore next: preference for stylesheet loaded externally */
316
+ for(var r=document.styleSheets,s=0;s<r.length;s++)if((0===r[s].media.length||r[s].media.mediaText.match(/(all|screen)/gi))&&r[s].href&&r[s].href.match(/textangular\.(min\.|)css/gi)){l=r[s];break}/* istanbul ignore next: preference for stylesheet loaded externally */
317
+ l||(
318
+ // this sheet is used for the placeholders later on.
319
+ l=function(){
320
+ // Create the <style> tag
321
+ var a=document.createElement("style");/* istanbul ignore else : WebKit hack :( */
322
+ // Add the <style> element to the page, add as first so the styles can be overridden by custom stylesheets
323
+ return g.webkit&&a.appendChild(document.createTextNode("")),document.getElementsByTagName("head")[0].appendChild(a),a.sheet}()),
324
+ // use as: addCSSRule("header", "float: left");
325
+ m=function(a,b){return o(l,a,b)},o=function(a,b,c){var d,e;
326
+ // return the inserted stylesheet rule
327
+ // This order is important as IE 11 has both cssRules and rules but they have different lengths - cssRules is correct, rules gives an error in IE 11
328
+ /* istanbul ignore next: browser catches */
329
+ /* istanbul ignore else: untestable IE option */
330
+ /* istanbul ignore next: browser catches */
331
+ return a.cssRules?d=Math.max(a.cssRules.length-1,0):a.rules&&(d=Math.max(a.rules.length-1,0)),a.insertRule?a.insertRule(b+"{"+c+"}",d):a.addRule(b,c,d),l.rules?e=l.rules[d]:l.cssRules&&(e=l.cssRules[d]),e},q=function(a,b){var c,d;for(c=0;c<b.length;c++)/* istanbul ignore else: check for correct rule */
332
+ if(b[c].cssText===a.cssText){d=c;break}return d},n=function(a){p(l,a)},/* istanbul ignore next: tests are browser specific */
333
+ p=function(a,b){var c=a.cssRules||a.rules;if(c&&0!==c.length){var d=q(b,c);a.removeRule?a.removeRule(d):a.deleteRule(d)}}}angular.module("textAngular.factories",[]).factory("taBrowserTag",[function(){return function(a){/* istanbul ignore next: ie specific test */
334
+ /* istanbul ignore next: ie specific test */
335
+ return a?""===a?void 0===g.ie?"div":g.ie<=8?"P":"p":g.ie<=8?a.toUpperCase():a:g.ie<=8?"P":"p"}}]).factory("taApplyCustomRenderers",["taCustomRenderers","taDOM",function(a,b){return function(c){var d=angular.element("<div></div>");return d[0].innerHTML=c,angular.forEach(a,function(a){var c=[];
336
+ // get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
337
+ a.selector&&""!==a.selector?c=d.find(a.selector):a.customAttribute&&""!==a.customAttribute&&(c=b.getByAttribute(d,a.customAttribute)),
338
+ // process elements if any found
339
+ 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(){
340
+ // get whaterever rubbish is inserted in chrome
341
+ // should be passed an html string, returns an html string
342
+ var a=function(a,b){if(!a||!angular.isString(a)||a.length<=0)return a;
343
+ // remove all the Apple-converted-space spans and replace with the content of the span
344
+ //console.log('before:', html);
345
+ /* istanbul ignore next: apple-contereted-space span match */
346
+ for(
347
+ // grab all elements with a style attibute
348
+ // a betterSpanMatch matches only a style=... with matching quotes
349
+ // this captures the whole:
350
+ // 'style="background-color: rgb(255, 255, 255);"'
351
+ var c,d,e,f=/style\s?=\s?(["'])(?:(?=(\\?))\2.)*?\1/gi,g=/<span class="Apple-converted-space">([^<]+)<\/span>/gi,h="",i=0;c=g.exec(a);)e=c[1],e=e.replace(/&nbsp;/gi," "),h+=a.substring(i,c.index)+e,i=c.index+c[0].length;
352
+ /////////////////////////////////////////////////////////////
353
+ //
354
+ // Allow control of this modification
355
+ // taKeepStyles: False - removes these modification
356
+ //
357
+ // taFixChrome removes the following styles:
358
+ // font-family: inherit;
359
+ // line-height: <number>
360
+ // color: inherit;
361
+ // color: rgb( <rgb-component>#{3} )
362
+ // background-color: rgb( <rgb-component>#{3} )
363
+ //
364
+ /////////////////////////////////////////////////////////////
365
+ if(/* istanbul ignore next: apple-contereted-space span has matched */
366
+ i&&(
367
+ // modified....
368
+ h+=a.substring(i),a=h,h="",i=0),!b){for(;c=f.exec(a);)h+=a.substring(i,c.index-1),d=c[0],
369
+ // test for chrome inserted junk
370
+ c=/font-family: inherit;|line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;|color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);|background-color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);/gi.exec(d),c?(d=d.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;|( |)color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);|( |)background-color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);/gi,""),
371
+ //console.log(styleVal, styleVal.length);
372
+ d.length>8&&(h+=" "+d)):h+=" "+d,i=f.lastIndex;h+=a.substring(i)}
373
+ //console.log('final:', finalHtml);
374
+ // only replace when something has changed, else we get focus problems on inserting lists
375
+ if(i>0){
376
+ // replace all empty strings
377
+ var j=h.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/gi,"$1");return j}return 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++;
378
+ // get the start tags reversed - this is safe as we construct the strings with no content except the tags
379
+ 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);){
380
+ // one of the quoted values ' or "
381
+ /* istanbul ignore next: quotations match */
382
+ h=f[3]||f[4];var p=new RegExp(j,"i");
383
+ // test for style values to change
384
+ if(angular.isString(h)&&p.test(h)){
385
+ // remove build tag list
386
+ i="";
387
+ // find relevand tags and build a string of them
388
+ for(
389
+ // init regex here for exec
390
+ var q=new RegExp(j,"ig");g=q.exec(h);)for(d=0;d<e.length;d++)g[2*d+2]&&(i+="<"+e[d].tag+">");
391
+ // recursively find more legacy styles in html before this tag and after the previous match (if any)
392
+ k=c(a.substring(o,f.index)),
393
+ // build up html
394
+ n+=m.length>0?b(k,m):k,
395
+ // grab the style val without the transformed values
396
+ h=h.replace(new RegExp(j,"ig"),""),
397
+ // build the html tag
398
+ n+="<"+f[1].trim(),h.length>0&&(n+=' style="'+h+'"'),n+=f[5]+">",
399
+ // update the start index to after this tag
400
+ 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;
401
+ // match all attr tags
402
+ for(
403
+ // replace all align='...' tags with text-align attributes
404
+ var b,c=/<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/gi,d="",e=0;b=c.exec(a);){
405
+ // add all html before this tag
406
+ d+=a.substring(e,b.index),
407
+ // record last index after this tag
408
+ e=b.index+b[0].length;
409
+ // construct tag without the align attribute
410
+ var f="<"+b[1]+b[5];
411
+ // add the style attribute
412
+ /style=("([^"]+)"|'([^']+)')/gi.test(f)?/* istanbul ignore next: quotations match */
413
+ f=f.replace(/style=("([^"]+)"|'([^']+)')/i,'style="$2$3 text-align:'+(b[3]||b[4])+';"'):/* istanbul ignore next: quotations match */
414
+ f+=' style="text-align:'+(b[3]||b[4])+';"',f+=">",
415
+ // add to html
416
+ d+=f}
417
+ // return with remaining html
418
+ 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;g<e.length;g++){for(var h="("+e[g].property+":\\s*(",i=0;i<e[g].values.length;i++)/* istanbul ignore next: not needed to be tested yet */
419
+ i>0&&(h+="|"),h+=e[g].values[i];h+=");)",f.push(h)}var j="("+f.join("|")+")",k=new RegExp(/<span id="selectionBoundary_\d+_\d+" class="rangySelectionBoundary">[^<>]+?<\/span>/gi),l=new RegExp(/<span class="rangySelectionBoundary" id="selectionBoundary_\d+_\d+">[^<>]+?<\/span>/gi),m=new RegExp(/<span id="selectionBoundary_\d+_\d+" class="rangySelectionBoundary">[^<>]+?<\/span>/gi);return function(b,e,f){
420
+ // unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
421
+ if(!f)try{b=c(b)}catch(a){}
422
+ // we had an issue in the past, where we dumped a whole bunch of <span>'s into the content...
423
+ // so we remove them here
424
+ // IN A FUTURE release this can be removed after all have updated through release 1.5.9
425
+ if(
426
+ // unsafe and oldsafe should be valid HTML strings
427
+ // any exceptions (lets say, color for example) should be made here but with great care
428
+ // setup unsafe element for modification
429
+ b=d(b))try{b=b.replace(k,""),b=b.replace(l,""),b=b.replace(k,""),b=b.replace(m,"")}catch(a){}var g;try{g=a(b),
430
+ // do this afterwards, then the $sanitizer should still throw for bad markup
431
+ f&&(g=b)}catch(a){g=e||""}
432
+ // Do processing for <pre> tags, removing tabs and return carriages outside of them
433
+ var h,i=g.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/gi),j=g.replace(/(&#(9|10);)*/gi,""),n=/<pre[^>]*>.*?<\/pre[^>]*>/gi,o=0,p=0;for(g="";null!==(h=n.exec(j))&&o<i.length;)g+=j.substring(p,h.index)+i[o],p=h.index+h[0].length,o++;return g+j.substring(p)}}]).factory("taToolExecuteAction",["$q","$log",function(a,b){
434
+ // this must be called on a toolScope or instance
435
+ return function(c){void 0!==c&&(this.$editor=function(){return c});var d,e=a.defer(),f=e.promise,g=this.$editor();try{d=this.action(e,g.startAction()),
436
+ // We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
437
+ f.finally(function(){g.endAction.call(g)})}catch(a){b.error(a)}(d||void 0===d)&&
438
+ // if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
439
+ e.resolve()}}]),angular.module("textAngular.DOM",["textAngular.factories"]).factory("taExecCommand",["taSelection","taBrowserTag","$document",function(b,c,d){var e=function(a,c){var d,e,f=a.find("li");for(e=f.length-1;e>=0;e--)d=angular.element("<"+c+">"+f[e].innerHTML+"</"+c+">"),a.after(d);a.remove(),b.setSelectionToElementEnd(d[0])},f=function(a,d,e,f,g){var h,i,j,k,l,m=a.find("li");for(i=0;i<m.length;i++)if(m[i].outerHTML===d[0].outerHTML){
440
+ // found it...
441
+ l=i,i>0&&(j=m[i-1]),i+1<m.length&&(k=m[i+1]);break}
442
+ //console.log('listElementToSelfTag', list, listElement, selfTag, bDefault, priorElement, nextElement);
443
+ // un-list the listElement
444
+ var n="";
445
+ //console.log('$target', $target[0]);
446
+ if(f?n+="<"+g+">"+d[0].innerHTML+"</"+g+">":(n+="<"+c(e)+">",n+="<li>"+d[0].innerHTML+"</li>",n+="</"+c(e)+">"),h=angular.element(n),!j)
447
+ // this is the first the list, so we just remove it...
448
+ return d.remove(),a.after(angular.element(a[0].outerHTML)),a.after(h),a.remove(),void b.setSelectionToElementEnd(h[0]);if(k){var o=(a.parent(),""),p=a[0].nodeName.toLowerCase();for(o+="<"+p+">",i=0;i<l;i++)o+="<li>"+m[i].innerHTML+"</li>";o+="</"+p+">";var q="";for(q+="<"+p+">",i=l+1;i<m.length;i++)q+="<li>"+m[i].innerHTML+"</li>";q+="</"+p+">",
449
+ //console.log(html1, $target[0], html2);
450
+ a.after(angular.element(q)),a.after(h),a.after(angular.element(o)),a.remove(),
451
+ //console.log('parent ******XXX*****', p[0]);
452
+ b.setSelectionToElementEnd(h[0])}else
453
+ // this is the last in the list, so we just remove it..
454
+ d.remove(),a.after(h),b.setSelectionToElementEnd(h[0])},g=function(a,d,e,f,g){var h,i,j,k,l,m=a.find("li"),n=[];for(i=0;i<m.length;i++)for(j=0;j<d.length;j++)m[i].isEqualNode(d[j])&&(
455
+ // found it...
456
+ n[j]=i);n[0]>0&&(k=m[n[0]-1]),n[d.length-1]+1<m.length&&(l=m[n[d.length-1]+1]);
457
+ //console.log('listElementsToSelfTag', list, listElements, selfTag, bDefault, !priorElement, !afterElement, foundIndexes[listElements.length-1], children.length);
458
+ // un-list the listElements
459
+ var o="";if(f)for(j=0;j<d.length;j++)o+="<"+g+">"+d[j].innerHTML+"</"+g+">",d[j].remove();else{for(o+="<"+c(e)+">",j=0;j<d.length;j++)o+=d[j].outerHTML,d[j].remove();o+="</"+c(e)+">"}if(h=angular.element(o),!k)
460
+ // this is the first the list, so we just remove it...
461
+ return a.after(angular.element(a[0].outerHTML)),a.after(h),a.remove(),void b.setSelectionToElementEnd(h[0]);if(!l)
462
+ // this is the last in the list, so we just remove it..
463
+ return a.after(h),void b.setSelectionToElementEnd(h[0]);
464
+ // okay it was some where in the middle... so we need to break apart the list...
465
+ var p="",q=a[0].nodeName.toLowerCase();for(p+="<"+q+">",i=0;i<n[0];i++)p+="<li>"+m[i].innerHTML+"</li>";p+="</"+q+">";var r="";for(r+="<"+q+">",i=n[d.length-1]+1;i<m.length;i++)r+="<li>"+m[i].innerHTML+"</li>";r+="</"+q+">",a.after(angular.element(r)),a.after(h),a.after(angular.element(p)),a.remove(),
466
+ //console.log('parent ******YYY*****', list.parent()[0]);
467
+ b.setSelectionToElementEnd(h[0])},h=function(a){/(<br(|\/)>)$/i.test(a.innerHTML.trim())?b.setSelectionBeforeElement(angular.element(a).find("br")[0]):b.setSelectionToElementEnd(a)},k=function(a,b){var c=angular.element("<"+b+">"+a[0].innerHTML+"</"+b+">");a.after(c),a.remove(),h(c.find("li")[0])},l=function(a,b,d){for(var e="",f=0;f<a.length;f++)e+="<"+c("li")+">"+a[f].innerHTML+"</"+c("li")+">";var g=angular.element("<"+d+">"+e+"</"+d+">");b.after(g),b.remove(),h(g.find("li")[0])},m=function(a,b){for(var c=0;c<a.childNodes.length;c++){var d=a.childNodes[c];/* istanbul ignore next - more complex testing*/
468
+ d.tagName&&d.tagName.match(i)&&m(d,b)}/* istanbul ignore next - very rare condition that we do not test*/
469
+ if(null===a.parentNode)
470
+ // nothing left to do..
471
+ return a;/* istanbul ignore next - not sure have to test this */
472
+ if("<br>"===b)return a;var e=angular.element(b);return e[0].innerHTML=a.innerHTML,a.parentNode.insertBefore(e[0],a),a.parentNode.removeChild(a),e};return function(h,n){
473
+ // NOTE: here we are dealing with the html directly from the browser and not the html the user sees.
474
+ // IF you want to modify the html the user sees, do it when the user does a switchView
475
+ return h=c(h),function(o,p,q,r){var s,t,u,v,w,x,y,z,A=angular.element("<"+h+">");try{b.getSelection&&(z=b.getSelection()),y=b.getSelectionElement();
476
+ // special checks and fixes when we are selecting the whole container
477
+ var B,C;/* istanbul ignore next */
478
+ void 0!==y.tagName&&("div"===y.tagName.toLowerCase()&&/taTextElement.+/.test(y.id)&&z&&z.start&&1===z.start.offset&&1===z.end.offset?(
479
+ // opps we are actually selecting the whole container!
480
+ //console.log('selecting whole container!');
481
+ B=y.innerHTML,/<br>/i.test(B)&&(
482
+ // Firefox adds <br>'s and so we remove the <br>
483
+ B=B.replace(/<br>/i,"&#8203;")),/<br\/>/i.test(B)&&(
484
+ // Firefox adds <br/>'s and so we remove the <br/>
485
+ B=B.replace(/<br\/>/i,"&#8203;")),
486
+ // remove stacked up <span>'s
487
+ /<span>(<span>)+/i.test(B)&&(B=__.replace(/<span>(<span>)+/i,"<span>")),
488
+ // remove stacked up </span>'s
489
+ /<\/span>(<\/span>)+/i.test(B)&&(B=__.replace(/<\/span>(<\/span>)+/i,"</span>")),/<span><\/span>/i.test(B)&&(
490
+ // if we end up with a <span></span> here we remove it...
491
+ B=B.replace(/<span><\/span>/i,"")),
492
+ //console.log('inner whole container', selectedElement.childNodes);
493
+ C="<div>"+B+"</div>",y.innerHTML=C,b.setSelectionToElementEnd(y.childNodes[0]),y=b.getSelectionElement()):"span"===y.tagName.toLowerCase()&&z&&z.start&&1===z.start.offset&&1===z.end.offset?(
494
+ // just a span -- this is a problem...
495
+ //console.log('selecting span!');
496
+ B=y.innerHTML,/<br>/i.test(B)&&(
497
+ // Firefox adds <br>'s and so we remove the <br>
498
+ B=B.replace(/<br>/i,"&#8203;")),/<br\/>/i.test(B)&&(
499
+ // Firefox adds <br/>'s and so we remove the <br/>
500
+ B=B.replace(/<br\/>/i,"&#8203;")),
501
+ // remove stacked up <span>'s
502
+ /<span>(<span>)+/i.test(B)&&(B=__.replace(/<span>(<span>)+/i,"<span>")),
503
+ // remove stacked up </span>'s
504
+ /<\/span>(<\/span>)+/i.test(B)&&(B=__.replace(/<\/span>(<\/span>)+/i,"</span>")),/<span><\/span>/i.test(B)&&(
505
+ // if we end up with a <span></span> here we remove it...
506
+ B=B.replace(/<span><\/span>/i,"")),
507
+ //console.log('inner span', selectedElement.childNodes);
508
+ // we wrap this in a <div> because otherwise the browser get confused when we attempt to select the whole node
509
+ // and the focus is not set correctly no matter what we do
510
+ C="<div>"+B+"</div>",y.innerHTML=C,b.setSelectionToElementEnd(y.childNodes[0]),y=b.getSelectionElement()):"p"===y.tagName.toLowerCase()&&z&&z.start&&1===z.start.offset&&1===z.end.offset?(
511
+ //console.log('p special');
512
+ // we need to remove the </br> that firefox adds!
513
+ B=y.innerHTML,/<br>/i.test(B)&&(
514
+ // Firefox adds <br>'s and so we remove the <br>
515
+ B=B.replace(/<br>/i,"&#8203;"),// no space-space
516
+ y.innerHTML=B)):"li"===y.tagName.toLowerCase()&&z&&z.start&&z.start.offset===z.end.offset&&(
517
+ // we need to remove the </br> that firefox adds!
518
+ B=y.innerHTML,/<br>/i.test(B)&&(
519
+ // Firefox adds <br>'s and so we remove the <br>
520
+ B=B.replace(/<br>/i,""),// nothing
521
+ y.innerHTML=B)))}catch(a){}
522
+ //console.log('************** selectedElement:', selectedElement);
523
+ /* istanbul ignore if: */
524
+ if(y){var D=angular.element(y),E=y&&y.tagName&&y.tagName.toLowerCase()||/* istanbul ignore next: */
525
+ "";if("insertorderedlist"===o.toLowerCase()||"insertunorderedlist"===o.toLowerCase()){var F=c("insertorderedlist"===o.toLowerCase()?"ol":"ul"),G=b.getOnlySelectedElements();
526
+ //console.log('PPPPPPPPPPPPP', tagName, selfTag, selectedElements, tagName.match(BLOCKELEMENTS), $selected.hasClass('ta-bind'), $selected.parent()[0].tagName);
527
+ if(G.length>1&&("ol"===E||"ul"===E))return g(D,G,F,F===E,h);if(E===F)
528
+ // if all selected then we should remove the list
529
+ // grab all li elements and convert to taDefaultWrap tags
530
+ //console.log('tagName===selfTag');
531
+ // if all selected then we should remove the list
532
+ // grab all li elements and convert to taDefaultWrap tags
533
+ //console.log('tagName===selfTag');
534
+ return D[0].childNodes.length!==G.length&&1===G.length?(D=angular.element(G[0]),f(D.parent(),D,F,!0,h)):e(D,h);if("li"===E&&D.parent()[0].tagName.toLowerCase()===F&&1===D.parent().children().length)
535
+ // catch for the previous statement if only one li exists
536
+ return e(D.parent(),h);if("li"===E&&D.parent()[0].tagName.toLowerCase()!==F&&1===D.parent().children().length)
537
+ // catch for the previous statement if only one li exists
538
+ return k(D.parent(),F);if(E.match(i)&&!D.hasClass("ta-bind")){
539
+ // if it's one of those block elements we have to change the contents
540
+ // if it's a ol/ul we are changing from one to the other
541
+ if(G.length&&D[0].childNodes.length!==G.length&&1===G.length)
542
+ //console.log('&&&&&&&&&&&&&&& --------- &&&&&&&&&&&&&&&&', selectedElements[0], $selected[0].childNodes);
543
+ return D=angular.element(G[0]),f(D.parent(),D,F,F===E,h);if("ol"===E||"ul"===E)
544
+ // now if this is a set of selected elements... behave diferently
545
+ return k(D,F);var H=!1;return angular.forEach(D.children(),function(a){a.tagName.match(i)&&(H=!0)}),H?l(D.children(),D,F):l([angular.element("<div>"+y.innerHTML+"</div>")[0]],D,F)}if(E.match(i)){
546
+ //console.log('_nodes', _nodes, tagName);
547
+ if(
548
+ // if we get here then the contents of the ta-bind are selected
549
+ v=b.getOnlySelectedElements(),0===v.length)
550
+ // here is if there is only text in ta-bind ie <div ta-bind>test content</div>
551
+ t=angular.element("<"+F+"><li>"+y.innerHTML+"</li></"+F+">"),D.html(""),D.append(t);else{if(1===v.length&&("ol"===v[0].tagName.toLowerCase()||"ul"===v[0].tagName.toLowerCase()))return v[0].tagName.toLowerCase()===F?e(angular.element(v[0]),h):k(angular.element(v[0]),F);u="";var I=[];for(s=0;s<v.length;s++)/* istanbul ignore else: catch for real-world can't make it occur in testing */
552
+ if(3!==v[s].nodeType){var J=angular.element(v[s]);/* istanbul ignore if: browser check only, phantomjs doesn't return children nodes but chrome at least does */
553
+ if("li"===v[s].tagName.toLowerCase())continue;u+="ol"===v[s].tagName.toLowerCase()||"ul"===v[s].tagName.toLowerCase()?J[0].innerHTML:"span"!==v[s].tagName.toLowerCase()||"ol"!==v[s].childNodes[0].tagName.toLowerCase()&&"ul"!==v[s].childNodes[0].tagName.toLowerCase()?"<"+c("li")+">"+J[0].innerHTML+"</"+c("li")+">":J[0].childNodes[0].innerHTML,I.unshift(J)}
554
+ //console.log('$nodes', $nodes);
555
+ t=angular.element("<"+F+">"+u+"</"+F+">"),I.pop().replaceWith(t),angular.forEach(I,function(a){a.remove()})}return void b.setSelectionToElementEnd(t[0])}}else{if("formatblock"===o.toLowerCase()){
556
+ // find the first blockElement
557
+ for(x=q.toLowerCase().replace(/[<>]/gi,""),"default"===x.trim()&&(x=h,q="<"+h+">"),t="li"===E?D.parent():D;!t[0].tagName||!t[0].tagName.match(i)&&!t.parent().attr("contenteditable");)t=t.parent(),/* istanbul ignore next */
558
+ E=(t[0].tagName||"").toLowerCase();if(E===x){
559
+ // $target is wrap element
560
+ v=t.children();var K=!1;for(s=0;s<v.length;s++)K=K||v[s].tagName.match(i);K?(t.after(v),w=t.next(),t.remove(),t=w):(A.append(t[0].childNodes),t.after(A),t.remove(),t=A)}else if(t.parent()[0].tagName.toLowerCase()!==x||t.parent().hasClass("ta-bind"))if(E.match(j))
561
+ // wrapping a list element
562
+ t.wrap(q);else{
563
+ // find the parent block element if any of the nodes are inline or text
564
+ for(
565
+ // default wrap behaviour
566
+ v=b.getOnlySelectedElements(),0===v.length&&(
567
+ // no nodes at all....
568
+ v=[t[0]]),s=0;s<v.length;s++)if(3===v[s].nodeType||!v[s].tagName.match(i))for(;3===v[s].nodeType||!v[s].tagName||!v[s].tagName.match(i);)v[s]=v[s].parentNode;if(
569
+ // remove any duplicates from the array of _nodes!
570
+ v=v.filter(function(a,b,c){return c.indexOf(a)===b}),
571
+ // remove all whole taTextElement if it is here... unless it is the only element!
572
+ v.length>1&&(v=v.filter(function(a,b,c){return!("div"===a.nodeName.toLowerCase()&&/^taTextElement/.test(a.id))})),angular.element(v[0]).hasClass("ta-bind"))t=angular.element(q),t[0].innerHTML=v[0].innerHTML,v[0].innerHTML=t[0].outerHTML;else if("blockquote"===x){for(
573
+ // blockquotes wrap other block elements
574
+ u="",s=0;s<v.length;s++)u+=v[s].outerHTML;for(t=angular.element(q),t[0].innerHTML=u,v[0].parentNode.insertBefore(t[0],v[0]),s=v.length-1;s>=0;s--)/* istanbul ignore else: */
575
+ v[s].parentNode&&v[s].parentNode.removeChild(v[s])}else/* istanbul ignore next: not tested since identical to blockquote */
576
+ if("pre"===x&&b.getStateShiftKey()){for(
577
+ //console.log('shift pre', _nodes);
578
+ // pre wrap other block elements
579
+ u="",s=0;s<v.length;s++)u+=v[s].outerHTML;for(t=angular.element(q),t[0].innerHTML=u,v[0].parentNode.insertBefore(t[0],v[0]),s=v.length-1;s>=0;s--)/* istanbul ignore else: */
580
+ v[s].parentNode&&v[s].parentNode.removeChild(v[s])}else
581
+ //console.log(optionsTagName, _nodes);
582
+ // regular block elements replace other block elements
583
+ for(s=0;s<v.length;s++){var L=m(v[s],q);v[s]===t[0]&&(t=angular.element(L))}}else{
584
+ //unwrap logic for parent
585
+ var M=t.parent(),N=M.contents();for(s=0;s<N.length;s++)/* istanbul ignore next: can't test - some wierd thing with how phantomjs works */
586
+ M.parent().hasClass("ta-bind")&&3===N[s].nodeType&&(A=angular.element("<"+h+">"),A[0].innerHTML=N[s].outerHTML,N[s]=A[0]),M.parent()[0].insertBefore(N[s],M[0]);M.remove()}
587
+ // looses focus when we have the whole container selected and no text!
588
+ // refocus on the shown display element, this fixes a bug when using firefox
589
+ return b.setSelectionToElementEnd(t[0]),void t[0].focus()}if("createlink"===o.toLowerCase()){/* istanbul ignore next: firefox specific fix */
590
+ if("a"===E)
591
+ // already a link!!! we are just replacing it...
592
+ return void(b.getSelectionElement().href=q);var O='<a href="'+q+'" target="'+(r.a.target?r.a.target:"")+'">',P="</a>",Q=b.getSelection();if(Q.collapsed)
593
+ //console.log('collapsed');
594
+ // insert text at selection, then select then just let normal exec-command run
595
+ b.insertHtml(O+q+P,n);else if(a.getSelection().getRangeAt(0).canSurroundContents()){var R=angular.element(O+P)[0];a.getSelection().getRangeAt(0).surroundContents(R)}return}if("inserthtml"===o.toLowerCase())
596
+ //console.log('inserthtml');
597
+ return void b.insertHtml(q,n)}try{d[0].execCommand(o,p,q)}catch(a){}}}}}]).service("taSelection",["$document","taDOM","$log",/* istanbul ignore next: all browser specifics and PhantomJS dosen't seem to support half of it */
598
+ function(b,c,d){
599
+ // need to dereference the document else the calls don't work correctly
600
+ var e,f=b[0],g=function(a,b){/* check if selection is a BR element at the beginning of a container. If so, get
601
+ * the parentNode instead.
602
+ * offset should be zero in this case. Otherwise, return the original
603
+ * element.
604
+ */
605
+ /* check if selection is a BR element at the beginning of a container. If so, get
606
+ * the parentNode instead.
607
+ * offset should be zero in this case. Otherwise, return the original
608
+ * element.
609
+ */
610
+ return a.tagName&&a.tagName.match(/^br$/i)&&0===b&&!a.previousSibling?{element:a.parentNode,offset:0}:{element:a,offset:b}},h={getSelection:function(){var b;try{
611
+ // catch any errors from rangy and ignore the issue
612
+ b=a.getSelection().getRangeAt(0)}catch(a){
613
+ //console.info(e);
614
+ return}var c=b.commonAncestorContainer,d={start:g(b.startContainer,b.startOffset),end:g(b.endContainer,b.endOffset),collapsed:b.collapsed};
615
+ //console.log('***selection container:', selection.container.nodeName, selection.start.offset, selection.container);
616
+ // This has problems under Firefox.
617
+ // On Firefox with
618
+ // <p>Try me !</p>
619
+ // <ul>
620
+ // <li>line 1</li>
621
+ // <li>line 2</li>
622
+ // </ul>
623
+ // <p>line 3</p>
624
+ // <ul>
625
+ // <li>line 4</li>
626
+ // <li>line 5</li>
627
+ // </ul>
628
+ // <p>Hello textAngular</p>
629
+ // WITH the cursor after the 3 on line 3, it gets the commonAncestorContainer as:
630
+ // <TextNode textContent='line 3'>
631
+ // AND Chrome gets the commonAncestorContainer as:
632
+ // <p>line 3</p>
633
+ //
634
+ // Check if the container is a text node and return its parent if so
635
+ // unless this is the whole taTextElement. If so we return the textNode
636
+ //console.log('*********taTextElement************');
637
+ //console.log('commonAncestorContainer:', container);
638
+ return 3===c.nodeType&&("div"===c.parentNode.nodeName.toLowerCase()&&/^taTextElement/.test(c.parentNode.id)||(c=c.parentNode)),"div"===c.nodeName.toLowerCase()&&/^taTextElement/.test(c.id)?(d.start.element=c.childNodes[d.start.offset],d.end.element=c.childNodes[d.end.offset],d.container=c):c.parentNode===d.start.element||c.parentNode===d.end.element?d.container=c.parentNode:d.container=c,d},
639
+ // if we use the LEFT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one...
640
+ // Chrome and Firefox behave differently so so fix this for Firefox here. No adjustment needed for Chrome.
641
+ updateLeftArrowKey:function(b){var c=a.getSelection().getRangeAt(0);if(c&&c.collapsed){var d=h.getFlattenedDom(c);if(!d.findIndex)return;var e,f,g=c.startContainer,i=d.findIndex(function(a,b){if(a.node===g)return!0;var c=a.parents.indexOf(g);return c!==-1});
642
+ //console.log('updateLeftArrowKey', range.startOffset, range.startContainer.textContent);
643
+ // this first section handles the case for Chrome browser
644
+ // if the first character of the nextNode is a \ufeff we know that we are just before the special span...
645
+ // and so we most left by one character
646
+ if(
647
+ //console.log('indexStartContainer', indexStartContainer, _nodes.length, 'startContainer:', _node, _node === _nodes[indexStartContainer].node);
648
+ d.forEach(function(a,b){
649
+ //console.log(i, n.node);
650
+ a.parents.forEach(function(a,b){})}),i+1<d.length&&(
651
+ // we need the node just after this startContainer
652
+ // so we can check and see it this is a special place
653
+ f=d[i+1].node),f&&f.textContent&&(e=/^\ufeff([^\ufeff]*)$/.exec(f.textContent)))
654
+ // we are before the special node with begins with a \ufeff character
655
+ //console.log('LEFT ...found it...', 'startOffset:', range.startOffset, m[0].length, m[1].length);
656
+ // no need to change anything in this case
657
+ return;var j;if(i>0&&(
658
+ // we need the node just after this startContainer
659
+ // so we can check and see it this is a special place
660
+ j=d[i-1].node),0===c.startOffset&&j&&(
661
+ //console.log(nextNodeToLeft, range.startOffset, nextNodeToLeft.textContent);
662
+ e=/^\ufeff([^\ufeff]*)$/.exec(j.textContent)))
663
+ //console.log('LEFT &&&&&&&&&&&&&&&&&&&...found it...&&&&&&&&&&&', nextNodeToLeft, m[0].length, m[1].length);
664
+ // move over to the left my one -- Firefox triggers this case
665
+ return void h.setSelectionToElementEnd(j)}},
666
+ // if we use the RIGHT_ARROW and we are at the special place <span>&#65279;</span> we move the cursor over by one...
667
+ updateRightArrowKey:function(a){},getFlattenedDom:function(a){function b(a){if(a.node.childNodes.length){var c=Array.prototype.slice.call(a.node.childNodes);// converts NodeList to Array
668
+ c.forEach(function(c){var d=a.parents.slice();d.slice(-1)[0]!==a.node&&d.push(a.node),b({parents:d,node:c})})}else d.push({parents:a.parents,node:a.node})}var c=a.commonAncestorContainer.parentNode;if(!c)return a.commonAncestorContainer.childNodes;var d=Array.prototype.slice.call(c.childNodes),e=d.indexOf(a.startContainer);
669
+ // make sure that we have a big enough set of nodes
670
+ // now walk the parent
671
+ return e+1<d.length&&e>0||c.parentNode&&(c=c.parentNode),d=[],b({parents:[c],node:c}),d},getOnlySelectedElements:function(){var b=a.getSelection().getRangeAt(0),c=b.commonAncestorContainer;
672
+ // get the nodes in the range that are ELEMENT_NODE and are children of the container
673
+ // in this range...
674
+ // Node.TEXT_NODE === 3
675
+ // Node.ELEMENT_NODE === 1
676
+ // Node.COMMENT_NODE === 8
677
+ // Check if the container is a text node and return its parent if so
678
+ return c=3===c.nodeType?c.parentNode:c,b.getNodes([1],function(a){return a.parentNode===c})},
679
+ // this includes the container element if all children are selected
680
+ getAllSelectedElements:function(){var b=a.getSelection().getRangeAt(0),c=b.commonAncestorContainer;
681
+ // Node.TEXT_NODE === 3
682
+ // Node.ELEMENT_NODE === 1
683
+ // Node.COMMENT_NODE === 8
684
+ // Check if the container is a text node and return its parent if so
685
+ c=3===c.nodeType?c.parentNode:c;
686
+ // get the nodes in the range that are ELEMENT_NODE and are children of the container
687
+ // in this range...
688
+ var d=b.getNodes([1],function(a){return a.parentNode===c}),e=c.innerHTML;
689
+ //console.log(innerHtml);
690
+ //console.log(range.toHtml());
691
+ //console.log(innerHtml === range.toHtml());
692
+ if(
693
+ // remove the junk that rangy has put down
694
+ e=e.replace(/<span id=.selectionBoundary[^>]+>\ufeff?<\/span>/gi,""),e===b.toHtml()&&("div"!==c.nodeName.toLowerCase()||!/^taTextElement/.test(c.id))){for(var f=[],g=d.length;g--;f.unshift(d[g]));d=f,d.push(c)}return d},
695
+ // Some basic selection functions
696
+ getSelectionElement:function(){var a=h.getSelection();return a?h.getSelection().container:void 0},setSelection:function(b,c,d,e){var f=a.createRange();f.setStart(b,d),f.setEnd(c,e),a.getSelection().setSingleRange(f)},setSelectionBeforeElement:function(b){var c=a.createRange();c.selectNode(b),c.collapse(!0),a.getSelection().setSingleRange(c)},setSelectionAfterElement:function(b){var c=a.createRange();c.selectNode(b),c.collapse(!1),a.getSelection().setSingleRange(c)},setSelectionToElementStart:function(b){var c=a.createRange();c.selectNodeContents(b),c.collapse(!0),a.getSelection().setSingleRange(c)},setSelectionToElementEnd:function(b){var c=a.createRange();c.selectNodeContents(b),c.collapse(!1),b.childNodes&&b.childNodes[b.childNodes.length-1]&&"br"===b.childNodes[b.childNodes.length-1].nodeName&&(c.startOffset=c.endOffset=c.startOffset-1),a.getSelection().setSingleRange(c)},setStateShiftKey:function(a){e=a},getStateShiftKey:function(){return e},
697
+ // from http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
698
+ // topNode is the contenteditable normally, all manipulation MUST be inside this.
699
+ insertHtml:function(b,d){var e,g,j,l,m,n,o,p=angular.element("<div>"+b+"</div>"),q=a.getSelection().getRangeAt(0),r=f.createDocumentFragment(),s=p[0].childNodes,t=!0;if(s.length>0){for(
700
+ // NOTE!! We need to do the following:
701
+ // check for blockelements - if they exist then we have to split the current element in half (and all others up to the closest block element) and insert all children in-between.
702
+ // If there are no block elements, or there is a mixture we need to create textNodes for the non wrapped text (we don't want them spans messing up the picture).
703
+ l=[],j=0;j<s.length;j++){var u=s[j];"p"===u.nodeName.toLowerCase()&&""===u.innerHTML.trim()||(/****************
704
+ * allow any text to be inserted...
705
+ if(( _cnode.nodeType === 3 &&
706
+ _cnode.nodeValue === '\ufeff'[0] &&
707
+ _cnode.nodeValue.trim() === '') // empty no-space space element
708
+ ) {
709
+ // no change to isInline
710
+ nodes.push(_cnode);
711
+ continue;
712
+ }
713
+ if(_cnode.nodeType === 3 &&
714
+ _cnode.nodeValue.trim() === '') { // empty text node
715
+ continue;
716
+ }
717
+ *****************/
718
+ t=t&&!i.test(u.nodeName),l.push(u))}for(var v=0;v<l.length;v++)n=r.appendChild(l[v]);!t&&q.collapsed&&/^(|<br(|\/)>)$/i.test(q.startContainer.innerHTML)&&q.selectNode(q.startContainer)}else t=!0,
719
+ // paste text of some sort
720
+ n=r=f.createTextNode(b);
721
+ // Other Edge case - selected data spans multiple blocks.
722
+ if(t)q.deleteContents();else// not inline insert
723
+ if(q.collapsed&&q.startContainer!==d)if(q.startContainer.innerHTML&&q.startContainer.innerHTML.match(/^<[^>]*>$/i))
724
+ // this log is to catch when innerHTML is something like `<img ...>`
725
+ e=q.startContainer,1===q.startOffset?(
726
+ // before single tag
727
+ q.setStartAfter(e),q.setEndAfter(e)):(
728
+ // after single tag
729
+ q.setStartBefore(e),q.setEndBefore(e));else{
730
+ // split element into 2 and insert block element in middle
731
+ if(3===q.startContainer.nodeType&&q.startContainer.parentNode!==d)
732
+ // Escape out of the inline tags like b
733
+ for(// if text node
734
+ e=q.startContainer.parentNode,g=e.cloneNode(),
735
+ // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
736
+ c.splitNodes(e.childNodes,e,g,q.startContainer,q.startOffset);!k.test(e.nodeName);){angular.element(e).after(g),e=e.parentNode;var w=g;g=e.cloneNode(),
737
+ // split the nodes into two lists - before and after, splitting the node with the selection into 2 text nodes.
738
+ c.splitNodes(e.childNodes,e,g,w)}else e=q.startContainer,g=e.cloneNode(),c.splitNodes(e.childNodes,e,g,void 0,void 0,q.startOffset);if(angular.element(e).after(g),
739
+ // put cursor to end of inserted content
740
+ //console.log('setStartAfter', parent);
741
+ q.setStartAfter(e),q.setEndAfter(e),/^(|<br(|\/)>)$/i.test(e.innerHTML.trim())&&(q.setStartBefore(e),q.setEndBefore(e),angular.element(e).remove()),/^(|<br(|\/)>)$/i.test(g.innerHTML.trim())&&angular.element(g).remove(),"li"===e.nodeName.toLowerCase()){for(o=f.createDocumentFragment(),m=0;m<r.childNodes.length;m++)p=angular.element("<li>"),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&&h.setSelectionToElementEnd(n)}};return h}]).service("taDOM",function(){var a={
742
+ // recursive function that returns an array of angular.elements that have the passed attribute set on them
743
+ 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(
744
+ // clear out target
745
+ 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]),// this removes from the nodes array (if proper childNodes object.
746
+ 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<a.attributes.length;c++)b.setAttribute(a.attributes[c].name,a.attributes[c].value);return b}};return a}),angular.module("textAngular.validators",[]).directive("taMaxText",function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e=parseInt(a.$eval(c.taMaxText));if(isNaN(e))throw"Max text must be an integer";c.$observe("taMaxText",function(a){if(e=parseInt(a),isNaN(e))throw"Max text must be an integer";d.$dirty&&d.$validate()}),d.$validators.taMaxText=function(a){var b=angular.element("<div/>");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("<div/>");return b.html(a),!b.text().length||b.text().length>=e}}}}),angular.module("textAngular.taBind",["textAngular.factories","textAngular.DOM"]).service("_taBlankTest",[function(){return function(a){
747
+ // we radically restructure this code.
748
+ // what was here before was incredibly fragile.
749
+ // What we do now is to check that the html is non-blank visually
750
+ // which we check by looking at html->text
751
+ if(!a)return!0;
752
+ // find first non-tag match - ie start of string or after tag that is not whitespace
753
+ // var t0 = performance.now();
754
+ // Takes a small fraction of a mSec to do this...
755
+ var b=d(a);
756
+ // var t1 = performance.now();
757
+ // console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:');
758
+ // var t1 = performance.now();
759
+ // console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:');
760
+ return""===b&&!/<img[^>]+>/.test(a)}}]).directive("taButton",[function(){return{link:function(a,b,c){b.attr("unselectable","on"),b.on("mousedown",function(a,b){/* istanbul ignore else: this is for catching the jqLite testing*/
761
+ // this prevents focusout from firing on the editor when clicking toolbar buttons
762
+ return b&&angular.extend(a,b),a.preventDefault(),!1})}}}]).directive("taBind",["taSanitize","$timeout","$document","taFixChrome","taBrowserTag","taSelection","taSelectableElements","taApplyCustomRenderers","taOptions","_taBlankTest","$parse","taDOM","textAngularManager",function(b,c,d,e,f,h,j,l,o,p,q,r,s){
763
+ // Uses for this are textarea or input with ng-model and ta-bind='text'
764
+ // OR any non-form element with contenteditable="contenteditable" ta-bind="html|text" ng-model
765
+ return{priority:2,// So we override validators correctly
766
+ require:["ngModel","?ngModelOptions"],link:function(f,r,u,v){function w(a){var b;return V.forEach(function(c){if(c.keyCode===a.keyCode){var d=(a.metaKey?N:0)+(a.ctrlKey?M:0)+(a.shiftKey?P:0)+(a.altKey?O:0);if(c.forbiddenModifiers&d)return;c.mustHaveModifiers.every(function(a){return d&a})&&(b=c.specialKey)}}),b}var x,y,z,A,B=v[0],C=v[1]||{},D=void 0!==r.attr("contenteditable")&&r.attr("contenteditable"),E=D||"textarea"===r[0].tagName.toLowerCase()||"input"===r[0].tagName.toLowerCase(),F=!1,G=!1,H=!1,I=u.taUnsafeSanitizer||o.disableSanitizer,J=u.taKeepStyles||o.keepStyles,K=/^(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,L=/^(8|13|32|46|59|61|107|109|173|186|187|188|189|190|191|192|219|220|221|222)$/i,M=1,N=2,O=4,P=8,Q=13,R=16,S=9,T=37,U=39,V=[
767
+ // ctrl/command + z
768
+ {specialKey:"UndoKey",forbiddenModifiers:O+P,mustHaveModifiers:[N+M],keyCode:90},
769
+ // ctrl/command + shift + z
770
+ {specialKey:"RedoKey",forbiddenModifiers:O,mustHaveModifiers:[N+M,P],keyCode:90},
771
+ // ctrl/command + y
772
+ {specialKey:"RedoKey",forbiddenModifiers:O+P,mustHaveModifiers:[N+M],keyCode:89},
773
+ // TabKey
774
+ {specialKey:"TabKey",forbiddenModifiers:N+P+O+M,mustHaveModifiers:[],keyCode:S},
775
+ // shift + TabKey
776
+ {specialKey:"ShiftTabKey",forbiddenModifiers:N+O+M,mustHaveModifiers:[P],keyCode:S}];
777
+ // set the default to be a paragraph value
778
+ void 0===u.taDefaultWrap&&(u.taDefaultWrap="p"),/* istanbul ignore next: ie specific test */
779
+ ""===u.taDefaultWrap?(z="",A=void 0===g.ie?"<div><br></div>":g.ie>=11?"<p><br></p>":g.ie<=8?"<P>&nbsp;</P>":"<p>&nbsp;</p>"):(z=void 0===g.ie||g.ie>=11?"br"===u.taDefaultWrap.toLowerCase()?"<BR><BR>":"<"+u.taDefaultWrap+"><br></"+u.taDefaultWrap+">":g.ie<=8?"<"+u.taDefaultWrap.toUpperCase()+"></"+u.taDefaultWrap.toUpperCase()+">":"<"+u.taDefaultWrap+"></"+u.taDefaultWrap+">",A=void 0===g.ie||g.ie>=11?"br"===u.taDefaultWrap.toLowerCase()?"<br><br>":"<"+u.taDefaultWrap+"><br></"+u.taDefaultWrap+">":g.ie<=8?"<"+u.taDefaultWrap.toUpperCase()+">&nbsp;</"+u.taDefaultWrap.toUpperCase()+">":"<"+u.taDefaultWrap+">&nbsp;</"+u.taDefaultWrap+">"),/* istanbul ignore else */
780
+ C.$options||(C.$options={});// ng-model-options support
781
+ var W=function(a){if(p(a))return a;var b=angular.element("<div>"+a+"</div>");
782
+ //console.log('domTest.children().length():', domTest.children().length);
783
+ //console.log('_ensureContentWrapped', domTest.children());
784
+ //console.log(value, attrs.taDefaultWrap);
785
+ if(0===b.children().length)
786
+ // if we have a <br> and the attrs.taDefaultWrap is a <p> we need to remove the <br>
787
+ //value = value.replace(/<br>/i, '');
788
+ a="<"+u.taDefaultWrap+">"+a+"</"+u.taDefaultWrap+">";else{var c,d=b[0].childNodes,e=!1;for(c=0;c<d.length&&!(e=d[c].nodeName.toLowerCase().match(i));c++);if(e)for(a="",c=0;c<d.length;c++){var f=d[c],g=f.nodeName.toLowerCase();
789
+ //console.log('node#:', i, 'name:', nodeName);
790
+ if("#comment"===g)a+="<!--"+f.nodeValue+"-->";else if("#text"===g){
791
+ // determine if this is all whitespace, if so, we will leave it as it is.
792
+ // otherwise, we will wrap it as it is
793
+ var h=f.textContent;
794
+ // not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
795
+ a+=h.trim()?"<"+u.taDefaultWrap+">"+h+"</"+u.taDefaultWrap+">":h}else if(g.match(i))a+=f.outerHTML;else{/* istanbul ignore next: Doesn't seem to trigger on tests */
796
+ var j=f.outerHTML||f.nodeValue;/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
797
+ a+=""!==j.trim()?"<"+u.taDefaultWrap+">"+j+"</"+u.taDefaultWrap+">":j}}else a="<"+u.taDefaultWrap+">"+a+"</"+u.taDefaultWrap+">"}
798
+ //console.log(value);
799
+ return a};u.taPaste&&(y=q(u.taPaste)),r.addClass("ta-bind");var X;f["$undoManager"+(u.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._index<this._stack.length-1&&(this._stack=this._stack.slice(0,this._index+1)),this._stack.push(a),X&&c.cancel(X),this._stack.length>this._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){if(!(a<0||a>this._stack.length-1))return this._index=a,this.current()},current:function(){return this._stack[this._index]}};
800
+ // in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
801
+ var Y,Z=function(){if(D)return r[0].innerHTML;if(E)return r.val();throw"textAngular Error: attempting to update non-editable taBind"},$=function(a){
802
+ // emit the element-select event, pass the element
803
+ return f.$emit("ta-element-select",this),a.preventDefault(),!1},_=f["reApplyOnSelectorHandlers"+(u.id||"")]=function(){/* istanbul ignore else */
804
+ F||angular.forEach(j,function(a){
805
+ // check we don't apply the handler twice
806
+ r.find(a).off("click",$).on("click",$)})},aa=function(a,b,c){H=c||!1,"undefined"!=typeof b&&null!==b||(b=D),// if not contentEditable then the native undo/redo is fine
807
+ "undefined"!=typeof a&&null!==a||(a=Z()),p(a)?(
808
+ // this avoids us from tripping the ng-pristine flag if we click in and out with out typing
809
+ ""!==B.$viewValue&&B.$setViewValue(""),b&&""!==B.$undoManager.current()&&B.$undoManager.push("")):(_(),B.$viewValue!==a&&(B.$setViewValue(a),b&&B.$undoManager.push(a))),B.$render()},ba=function(a){r[0].innerHTML=a},ca=f["$undoTaBind"+(u.id||"")]=function(){/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
810
+ if(!F&&D){var a=B.$undoManager.undo();"undefined"!=typeof a&&null!==a&&(ba(a),aa(a,!1),Y&&c.cancel(Y),Y=c(function(){r[0].focus(),h.setSelectionToElementEnd(r[0])},1))}},da=f["$redoTaBind"+(u.id||"")]=function(){/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
811
+ if(!F&&D){var a=B.$undoManager.redo();"undefined"!=typeof a&&null!==a&&(ba(a),aa(a,!1),/* istanbul ignore next */
812
+ Y&&c.cancel(Y),Y=c(function(){r[0].focus(),h.setSelectionToElementEnd(r[0])},1))}};
813
+ //used for updating when inserting wrapped elements
814
+ f["updateTaBind"+(u.id||"")]=function(){F||aa(void 0,void 0,!0)};
815
+ // catch DOM XSS via taSanitize
816
+ // Sanitizing both ways is identical
817
+ var ea=function(a){return B.$oldViewValue=b(e(a,J),B.$oldViewValue,I)};
818
+ //this code is used to update the models when data is entered/deleted
819
+ if(
820
+ // trigger the validation calls
821
+ r.attr("required")&&(B.$validators.required=function(a,b){return!p(a||b)}),
822
+ // parsers trigger from the above keyup function or any other time that the viewValue is updated and parses it for storage in the ngModel
823
+ B.$parsers.push(ea),B.$parsers.unshift(W),
824
+ // because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
825
+ B.$formatters.push(ea),B.$formatters.unshift(W),B.$formatters.unshift(function(a){return B.$undoManager.push(a||"")}),E)if(f.events={},D){
826
+ // all the code specific to contenteditable divs
827
+ var fa=!1,ga=function(a){var d=void 0!==a&&a.match(/content=["']*OneNote.File/i);/* istanbul ignore else: don't care if nothing pasted */
828
+ //console.log(text);
829
+ if(a&&a.trim().length){
830
+ // test paste from word/microsoft product
831
+ if(a.match(/class=["']*Mso(Normal|List)/i)||a.match(/content=["']*Word.Document/i)||a.match(/content=["']*OneNote.File/i)){var e=a.match(/<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/i);e=e?e[1]:a,e=e.replace(/<o:p>[\s\S]*?<\/o:p>/gi,"").replace(/class=(["']|)MsoNormal(["']|)/gi,"");var g=angular.element("<div>"+e+"</div>"),i=angular.element("<div></div>"),j={element:null,lastIndent:[],lastLi:null,isUl:!1};j.lastIndent.peek=function(){var a=this.length;if(a>0)return this[a-1]};for(var k=function(a){j.isUl=a,j.element=angular.element(a?"<ul>":"<ol>"),j.lastIndent=[],j.lastIndent.peek=function(){var a=this.length;if(a>0)return this[a-1]},j.lastLevelMatch=null},l=0;l<=g[0].childNodes.length;l++)if(g[0].childNodes[l]&&"#text"!==g[0].childNodes[l].nodeName){var m=g[0].childNodes[l].tagName.toLowerCase();if("p"===m||"ul"===m||"h1"===m||"h2"===m||"h3"===m||"h4"===m||"h5"===m||"h6"===m||"table"===m){var n=angular.element(g[0].childNodes[l]),o=(n.attr("class")||"").match(/MsoList(Bullet|Number|Paragraph)(CxSp(First|Middle|Last)|)/i);if(o){if(n[0].childNodes.length<2||n[0].childNodes[1].childNodes.length<1)continue;var p="bullet"===o[1].toLowerCase()||"number"!==o[1].toLowerCase()&&!(/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(n[0].childNodes[1].innerHTML)||/^[^0-9a-z<]*[0-9a-z]+[^0-9a-z<>]</i.test(n[0].childNodes[1].childNodes[0].innerHTML)),q=(n.attr("style")||"").match(/margin-left:([\-\.0-9]*)/i),s=parseFloat(q?q[1]:0),t=(n.attr("style")||"").match(/mso-list:l([0-9]+) level([0-9]+) lfo[0-9+]($|;)/i);if(
832
+ // prefers the mso-list syntax
833
+ t&&t[2]&&(s=parseInt(t[2])),t&&(!j.lastLevelMatch||t[1]!==j.lastLevelMatch[1])||!o[3]||"first"===o[3].toLowerCase()||null===j.lastIndent.peek()||j.isUl!==p&&j.lastIndent.peek()===s)k(p),i.append(j.element);else if(null!=j.lastIndent.peek()&&j.lastIndent.peek()<s)j.element=angular.element(p?"<ul>":"<ol>"),j.lastLi.append(j.element);else if(null!=j.lastIndent.peek()&&j.lastIndent.peek()>s){for(;null!=j.lastIndent.peek()&&j.lastIndent.peek()>s;)if("li"!==j.element.parent()[0].tagName.toLowerCase()){if(!/[uo]l/i.test(j.element.parent()[0].tagName.toLowerCase()))// else it's it should be a sibling
834
+ break;j.element=j.element.parent(),j.lastIndent.pop()}else j.element=j.element.parent();j.isUl="ul"===j.element[0].tagName.toLowerCase(),p!==j.isUl&&(k(p),i.append(j.element))}j.lastLevelMatch=t,s!==j.lastIndent.peek()&&j.lastIndent.push(s),j.lastLi=angular.element("<li>"),j.element.append(j.lastLi),j.lastLi.html(n.html().replace(/<!(--|)\[if !supportLists\](--|)>[\s\S]*?<!(--|)\[endif\](--|)>/gi,"")),n.remove()}else k(!1),i.append(n)}}var u=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(i.find("span"),function(a){a.removeAttribute("lang"),a.attributes.length<=0&&u(a)}),angular.forEach(i.find("font"),u),a=i.html(),d&&(a=i.html()||g.html()),
835
+ // LF characters instead of spaces in some spots and they are replaced by '/n', so we need to just swap them to spaces
836
+ a=a.replace(/\n/g," ")}else{if(
837
+ // remove unnecessary chrome insert
838
+ a=a.replace(/<(|\/)meta[^>]*?>/gi,""),a.match(/<[^>]*?(ta-bind)[^>]*?>/)){
839
+ // entire text-angular or ta-bind has been pasted, REMOVE AT ONCE!!
840
+ if(a.match(/<[^>]*?(text-angular)[^>]*?>/)){var v=angular.element("<div>"+a+"</div>");v.find("textarea").remove();for(var w=0;w<binds.length;w++){for(var x=binds[w][0].parentNode.parentNode,z=0;z<binds[w][0].childNodes.length;z++)x.parentNode.insertBefore(binds[w][0].childNodes[z],x);x.parentNode.removeChild(x)}a=v.html().replace('<br class="Apple-interchange-newline">',"")}}else a.match(/^<span/)&&(
841
+ // in case of pasting only a span - chrome paste, remove them. THis is just some wierd formatting
842
+ // if we remove the '<span class="Apple-converted-space"> </span>' here we destroy the spacing
843
+ // on paste from even ourselves!
844
+ a.match(/<span class=(\"Apple-converted-space\"|\'Apple-converted-space\')>.<\/span>/gi)||(a=a.replace(/<(|\/)span[^>]*?>/gi,"")));
845
+ // Webkit on Apple tags
846
+ a=a.replace(/<br class="Apple-interchange-newline"[^>]*?>/gi,"").replace(/<span class="Apple-converted-space">( |&nbsp;)<\/span>/gi,"&nbsp;")}/<li(\s.*)?>/i.test(a)&&/(<ul(\s.*)?>|<ol(\s.*)?>).*<li(\s.*)?>/i.test(a)===!1&&(
847
+ // insert missing parent of li element
848
+ a=a.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i,"<ul>$&</ul>")),
849
+ // parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
850
+ a=a.replace(/^[ |\u00A0]+/gm,function(a){for(var b="",c=0;c<a.length;c++)b+="&nbsp;";return b}).replace(/\n|\r\n|\r/g,"<br />").replace(/\t/g,"&nbsp;&nbsp;&nbsp;&nbsp;"),y&&(a=y(f,{$html:a})||a),
851
+ // turn span vertical-align:super into <sup></sup>
852
+ a=a.replace(/<span style=("|')([^<]*?)vertical-align\s*:\s*super;?([^>]*?)("|')>([^<]+?)<\/span>/g,"<sup style='$2$3'>$5</sup>"),a=b(a,"",I),
853
+ //console.log('DONE\n', text);
854
+ h.insertHtml(a,r[0]),c(function(){B.$setViewValue(Z()),fa=!1,r.removeClass("processing-paste")},0)}else fa=!1,r.removeClass("processing-paste")};r.on("paste",f.events.paste=function(b,e){if(/* istanbul ignore else: this is for catching the jqLite testing*/
855
+ e&&angular.extend(b,e),F||fa)return b.stopPropagation(),b.preventDefault(),!1;
856
+ // Code adapted from http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718
857
+ fa=!0,r.addClass("processing-paste");var f,g=(b.originalEvent||b).clipboardData;/* istanbul ignore next: Handle legacy IE paste */
858
+ if(!g&&window.clipboardData&&window.clipboardData.getData)return f=window.clipboardData.getData("Text"),ga(f),b.stopPropagation(),b.preventDefault(),!1;if(g&&g.getData&&g.types.length>0){for(var h="",i=0;i<g.types.length;i++)h+=" "+g.types[i];/* istanbul ignore next: browser tests */
859
+ return/text\/html/i.test(h)?f=g.getData("text/html"):/text\/plain/i.test(h)&&(f=g.getData("text/plain")),ga(f),b.stopPropagation(),b.preventDefault(),!1}// Everything else - empty editdiv and allow browser to paste content into it, then cleanup
860
+ var j=a.saveSelection(),k=angular.element('<div class="ta-hidden-input" contenteditable="true"></div>');d.find("body").append(k),k[0].focus(),c(function(){
861
+ // restore selection
862
+ a.restoreSelection(j),ga(k[0].innerHTML),r[0].focus(),k.remove()},0)}),r.on("cut",f.events.cut=function(a){
863
+ // timeout to next is needed as otherwise the paste/cut event has not finished actually changing the display
864
+ F?a.preventDefault():c(function(){B.$setViewValue(Z())},0)}),r.on("keydown",f.events.keydown=function(a,b){/* istanbul ignore else: this is for catching the jqLite testing*/
865
+ b&&angular.extend(a,b),a.keyCode===R?h.setStateShiftKey(!0):h.setStateShiftKey(!1),a.specialKey=w(a);var c;/* istanbul ignore else: readonly check */
866
+ if(/* istanbul ignore next: difficult to test */
867
+ o.keyMappings.forEach(function(b){a.specialKey===b.commandKeyCode&&(
868
+ // taOptions has remapped this binding... so
869
+ // we disable our own
870
+ a.specialKey=void 0),b.testForKey(a)&&(c=b.commandKeyCode),"UndoKey"!==b.commandKeyCode&&"RedoKey"!==b.commandKeyCode||b.enablePropagation||a.preventDefault()}),/* istanbul ignore next: difficult to test */
871
+ "undefined"!=typeof c&&(a.specialKey=c),/* istanbul ignore next: difficult to test as can't seem to select */
872
+ "undefined"==typeof a.specialKey||"UndoKey"===a.specialKey&&"RedoKey"===a.specialKey||(a.preventDefault(),s.sendKeyCommand(f,a)),!(F||("UndoKey"===a.specialKey&&(ca(),a.preventDefault()),"RedoKey"===a.specialKey&&(da(),a.preventDefault()),a.keyCode!==Q||a.shiftKey||a.ctrlKey||a.metaKey||a.altKey))){var d,e=function(a,b){for(var c=0;c<a.length;c++)if(a[c]===b)return!0;return!1},g=h.getSelectionElement();
873
+ // shifted to nodeName here from tagName since it is more widely supported see: http://stackoverflow.com/questions/4878484/difference-between-tagname-and-nodename
874
+ if(!g.nodeName.match(k))return;var i=angular.element(z),j=["blockquote","ul","ol"];if(e(j,g.parentNode.tagName.toLowerCase())){if(/^<br(|\/)>$/i.test(g.innerHTML.trim())&&!g.nextSibling){
875
+ // if last element is blank, pull element outside.
876
+ d=angular.element(g);var l=d.parent();l.after(i),d.remove(),0===l.children().length&&l.remove(),h.setSelectionToElementStart(i[0]),a.preventDefault()}/^<[^>]+><br(|\/)><\/[^>]+>$/i.test(g.innerHTML.trim())&&(d=angular.element(g),d.after(i),d.remove(),h.setSelectionToElementStart(i[0]),a.preventDefault())}}});var ha;r.on("keyup",f.events.keyup=function(a,b){// clear the ShiftKey state
877
+ /* istanbul ignore next: FF specific bug fix */
878
+ if(/* istanbul ignore else: this is for catching the jqLite testing*/
879
+ b&&angular.extend(a,b),h.setStateShiftKey(!1),a.keyCode===S){var d=h.getSelection();return void(d.start.element===r[0]&&r.children().length&&h.setSelectionToElementStart(r.children()[0]))}if(
880
+ // we do this here during the 'keyup' so that the browser has already moved the slection by one character...
881
+ a.keyCode!==T||a.shiftKey||h.updateLeftArrowKey(r),
882
+ // we do this here during the 'keyup' so that the browser has already moved the slection by one character...
883
+ a.keyCode!==U||a.shiftKey||h.updateRightArrowKey(r),X&&c.cancel(X),!F&&!K.test(a.keyCode))/* istanbul ignore next: Ignore any _ENTER_KEYCODE that has ctrlKey, metaKey or alKey */
884
+ if(a.keyCode===Q&&(a.ctrlKey||a.metaKey||a.altKey));else{
885
+ // if enter - insert new taDefaultWrap, if shift+enter insert <br/>
886
+ if(""!==z&&"<BR><BR>"!==z&&a.keyCode===Q&&!a.ctrlKey&&!a.metaKey&&!a.altKey){for(var e=h.getSelectionElement();!e.nodeName.match(k)&&e!==r[0];)e=e.parentNode;if(a.shiftKey){
887
+ // shift + Enter
888
+ var f=e.tagName.toLowerCase();
889
+ //console.log('Shift+Enter', selection.tagName, attrs.taDefaultWrap, selection.innerHTML.trim());
890
+ // For an LI: We see: LI p ....<br><br>
891
+ // For a P: We see: P p ....<br><br>
892
+ // on Safari, the browser ignores the Shift+Enter and acts just as an Enter Key
893
+ // For an LI: We see: LI p <br>
894
+ // For a P: We see: P p <br>
895
+ if((f===u.taDefaultWrap||"li"===f||"pre"===f||"div"===f)&&!/.+<br><br>/.test(e.innerHTML.trim())){var g=e.previousSibling;
896
+ //console.log('wrong....', ps);
897
+ // we need to remove this selection and fix the previousSibling up...
898
+ g&&(g.innerHTML=g.innerHTML+"<br><br>",angular.element(e).remove(),h.setSelectionToElementEnd(g))}}else
899
+ // new paragraph, br should be caught correctly
900
+ // shifted to nodeName here from tagName since it is more widely supported see: http://stackoverflow.com/questions/4878484/difference-between-tagname-and-nodename
901
+ //console.log('Enter', selection.nodeName, attrs.taDefaultWrap, selection.innerHTML.trim());
902
+ if(e.tagName.toLowerCase()!==u.taDefaultWrap&&"li"!==e.nodeName.toLowerCase()&&(""===e.innerHTML.trim()||"<br>"===e.innerHTML.trim())){
903
+ // Chrome starts with a <div><br></div> after an EnterKey
904
+ // so we replace this with the _defaultVal
905
+ var i=angular.element(z);angular.element(e).replaceWith(i),h.setSelectionToElementStart(i[0])}}var j=Z();""===z||""!==j.trim()&&"<br>"!==j.trim()?"<"!==j.substring(0,1)&&""!==u.taDefaultWrap:(ba(z),h.setSelectionToElementStart(r.children()[0]));var l=x!==a.keyCode&&L.test(a.keyCode);ha&&c.cancel(ha),ha=c(function(){aa(j,l,!0)},C.$options.debounce||400),l||(X=c(function(){B.$undoManager.push(j)},250)),x=a.keyCode}});
906
+ // when there is a change from a spelling correction in the browser, the only
907
+ // change that is seen is a 'input' and the $watch('html') sees nothing... So
908
+ // we added this element.on('input') to catch this change and call the _setViewValue()
909
+ // so the ngModel is updated and all works as it should.
910
+ var ia;
911
+ // Placeholders not supported on ie 8 and below
912
+ if(r.on("input",function(){Z()!==B.$viewValue&&(
913
+ // we wait a time now to allow the natural $watch('html') to handle this change
914
+ // and then after a 1 second delay, if there is still a difference we will do the
915
+ // _setViewValue() call.
916
+ /* istanbul ignore if: can't test */
917
+ ia&&c.cancel(ia),/* istanbul ignore next: cant' test? */
918
+ ia=c(function(){var b=a.saveSelection(),c=Z();c!==B.$viewValue&&
919
+ //console.log('_setViewValue');
920
+ //console.log('old:', ngModel.$viewValue);
921
+ //console.log('new:', _val);
922
+ aa(c,!0),
923
+ // if the savedSelection marker is gone at this point, we cannot restore the selection!!!
924
+ //console.log('rangy.restoreSelection', ngModel.$viewValue.length, _savedSelection);
925
+ 0!==B.$viewValue.length&&a.restoreSelection(b)},1e3))}),r.on("blur",f.events.blur=function(){G=!1,/* istanbul ignore else: if readonly don't update model */
926
+ F?(H=!0,// don't redo the whole thing, just check the placeholder logic
927
+ B.$render()):aa(void 0,void 0,!0)}),u.placeholder&&(g.ie>8||void 0===g.ie)){var ja;if(!u.id)throw"textAngular Error: An unique ID is required for placeholders to work";ja=m("#"+u.id+".placeholder-text:before",'content: "'+u.placeholder+'"'),f.$on("$destroy",function(){n(ja)})}r.on("focus",f.events.focus=function(){G=!0,r.removeClass("placeholder-text"),_()}),r.on("mouseup",f.events.mouseup=function(){var a=h.getSelection();a&&a.start.element===r[0]&&r.children().length&&h.setSelectionToElementStart(r.children()[0])}),
928
+ // prevent propagation on mousedown in editor, see #206
929
+ r.on("mousedown",f.events.mousedown=function(a,b){/* istanbul ignore else: this is for catching the jqLite testing*/
930
+ b&&angular.extend(a,b),a.stopPropagation()})}else{
931
+ // if a textarea or input just add in change and blur handlers, everything else is done by angulars input directive
932
+ r.on("change blur",f.events.change=f.events.blur=function(){F||B.$setViewValue(Z())}),r.on("keydown",f.events.keydown=function(a,b){
933
+ // Reference to http://stackoverflow.com/questions/6140632/how-to-handle-tab-in-textarea
934
+ /* istanbul ignore else: otherwise normal functionality */
935
+ if(/* istanbul ignore else: this is for catching the jqLite testing*/
936
+ b&&angular.extend(a,b),a.keyCode===S){// tab was pressed
937
+ // get caret position/selection
938
+ var c=this.selectionStart,d=this.selectionEnd,e=r.val();if(a.shiftKey){
939
+ // find \t
940
+ var f=e.lastIndexOf("\n",c),g=e.lastIndexOf("\t",c);g!==-1&&g>=f&&(
941
+ // set textarea value to: text before caret + tab + text after caret
942
+ r.val(e.substring(0,g)+e.substring(g+1)),
943
+ // put caret at right position again (add one for the tab)
944
+ this.selectionStart=this.selectionEnd=c-1)}else
945
+ // set textarea value to: text before caret + tab + text after caret
946
+ r.val(e.substring(0,c)+"\t"+e.substring(d)),
947
+ // put caret at right position again (add one for the tab)
948
+ this.selectionStart=this.selectionEnd=c+1;
949
+ // prevent the focus lose
950
+ a.preventDefault()}});var ka=function(a,b){for(var c="",d=0;d<b;d++)c+=a;return c},la=function(a,b,c){for(var d=0;d<a.length;d++)b.call(c,d,a[d])},ma=function(a,b){var c="",d=a.childNodes;
951
+ // tab out and add the <ul> or <ol> html piece
952
+ // now add on the </ol> or </ul> piece
953
+ return b++,c+=ka("\t",b-1)+a.outerHTML.substring(0,4),la(d,function(a,d){/* istanbul ignore next: browser catch */
954
+ var e=d.nodeName.toLowerCase();/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
955
+ return"#comment"===e?void(c+="<!--"+d.nodeValue+"-->"):"#text"===e?void(c+=d.textContent):void(d.outerHTML&&(c+="ul"===e||"ol"===e?"\n"+ma(d,b):"\n"+ka("\t",b)+d.outerHTML))}),c+="\n"+ka("\t",b-1)+a.outerHTML.substring(a.outerHTML.lastIndexOf("<"))};
956
+ // handle formating of something like:
957
+ // <ol><!--First comment-->
958
+ // <li>Test Line 1<!--comment test list 1--></li>
959
+ // <ul><!--comment ul-->
960
+ // <li>Nested Line 1</li>
961
+ // <!--comment between nested lines--><li>Nested Line 2</li>
962
+ // </ul>
963
+ // <li>Test Line 3</li>
964
+ // </ol>
965
+ B.$formatters.unshift(function(a){
966
+ // tabulate the HTML so it looks nicer
967
+ //
968
+ // first get a list of the nodes...
969
+ // we do this by using the element parser...
970
+ //
971
+ // doing this -- which is simpiler -- breaks our tests...
972
+ //var _nodes=angular.element(htmlValue);
973
+ var b=angular.element("<div>"+a+"</div>")[0].childNodes;
974
+ // do the reformatting of the layout...
975
+ return b.length>0&&(a="",la(b,function(b,c){var d=c.nodeName.toLowerCase();/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
976
+ // we aready have some content, so drop to a new line
977
+ // okay a set of list stuff we want to reformat in a nested way
978
+ return"#comment"===d?void(a+="<!--"+c.nodeValue+"-->"):"#text"===d?void(a+=c.textContent):void(c.outerHTML&&(a.length>0&&(a+="\n"),a+="ul"===d||"ol"===d?""+ma(c,0):""+c.outerHTML))})),a})}var na,oa=function(a,b){
979
+ // emit the drop event, pass the element, preventing should be done elsewhere
980
+ if(/* istanbul ignore else: this is for catching the jqLite testing*/
981
+ b&&angular.extend(a,b),!t&&!F){t=!0;var d;d=a.originalEvent?a.originalEvent.dataTransfer:a.dataTransfer,f.$emit("ta-drop-event",this,a,d),c(function(){t=!1,aa(void 0,void 0,!0)},100)}},pa=!1;
982
+ // changes to the model variable from outside the html/text inputs
983
+ B.$render=function(){/* istanbul ignore if: Catches rogue renders, hard to replicate in tests */
984
+ if(!pa){pa=!0;
985
+ // catch model being null or undefined
986
+ var a=B.$viewValue||"";
987
+ // if the editor isn't focused it needs to be updated, otherwise it's receiving user input
988
+ H||(/* istanbul ignore else: in other cases we don't care */
989
+ D&&G&&(
990
+ // update while focussed
991
+ r.removeClass("placeholder-text"),/* istanbul ignore next: don't know how to test this */
992
+ na&&c.cancel(na),na=c(function(){/* istanbul ignore if: Can't be bothered testing this... */
993
+ G||(r[0].focus(),h.setSelectionToElementEnd(r.children()[r.children().length-1])),na=void 0},1)),D?(
994
+ // blank
995
+ ba(
996
+ // WYSIWYG Mode
997
+ u.placeholder?""===a?z:a:""===a?z:a),
998
+ // if in WYSIWYG and readOnly we kill the use of links by clicking
999
+ F?r.off("drop",oa):(_(),r.on("drop",oa))):"textarea"!==r[0].tagName.toLowerCase()&&"input"!==r[0].tagName.toLowerCase()?
1000
+ // make sure the end user can SEE the html code as a display. This is a read-only display element
1001
+ ba(l(a)):
1002
+ // only for input and textarea inputs
1003
+ r.val(a)),D&&u.placeholder&&(""===a?G?r.removeClass("placeholder-text"):r.addClass("placeholder-text"):r.removeClass("placeholder-text")),pa=H=!1}},u.taReadonly&&(
1004
+ //set initial value
1005
+ F=f.$eval(u.taReadonly),F?(r.addClass("ta-readonly"),
1006
+ // we changed to readOnly mode (taReadonly='true')
1007
+ "textarea"!==r[0].tagName.toLowerCase()&&"input"!==r[0].tagName.toLowerCase()||r.attr("disabled","disabled"),void 0!==r.attr("contenteditable")&&r.attr("contenteditable")&&r.removeAttr("contenteditable")):(r.removeClass("ta-readonly"),
1008
+ // we changed to NOT readOnly mode (taReadonly='false')
1009
+ "textarea"===r[0].tagName.toLowerCase()||"input"===r[0].tagName.toLowerCase()?r.removeAttr("disabled"):D&&r.attr("contenteditable","true")),
1010
+ // taReadonly only has an effect if the taBind element is an input or textarea or has contenteditable='true' on it.
1011
+ // Otherwise it is readonly by default
1012
+ f.$watch(u.taReadonly,function(a,b){b!==a&&(a?(r.addClass("ta-readonly"),
1013
+ // we changed to readOnly mode (taReadonly='true')
1014
+ "textarea"!==r[0].tagName.toLowerCase()&&"input"!==r[0].tagName.toLowerCase()||r.attr("disabled","disabled"),void 0!==r.attr("contenteditable")&&r.attr("contenteditable")&&r.removeAttr("contenteditable"),
1015
+ // turn ON selector click handlers
1016
+ angular.forEach(j,function(a){r.find(a).on("click",$)}),r.off("drop",oa)):(r.removeClass("ta-readonly"),
1017
+ // we changed to NOT readOnly mode (taReadonly='false')
1018
+ "textarea"===r[0].tagName.toLowerCase()||"input"===r[0].tagName.toLowerCase()?r.removeAttr("disabled"):D&&r.attr("contenteditable","true"),
1019
+ // remove the selector click handlers
1020
+ angular.forEach(j,function(a){r.find(a).off("click",$)}),r.on("drop",oa)),F=a)})),
1021
+ // Initialise the selectableElements
1022
+ // if in WYSIWYG and readOnly we kill the use of links by clicking
1023
+ D&&!F&&(angular.forEach(j,function(a){r.find(a).on("click",$)}),r.on("drop",oa))}}}]);
1024
+ // this global var is used to prevent multiple fires of the drop event. Needs to be global to the textAngular file.
1025
+ var t=!1,u=angular.module("textAngular",["ngSanitize","textAngularSetup","textAngular.factories","textAngular.DOM","textAngular.validators","textAngular.taBind"]);//This makes ngSanitize required
1026
+ return u.config([function(){
1027
+ // clear taTools variable. Just catches testing and any other time that this config may run multiple times...
1028
+ angular.forEach(e,function(a,b){delete e[b]})}]),u.directive("textAngular",["$compile","$timeout","taOptions","taSelection","taExecCommand","textAngularManager","$document","$animate","$log","$q","$parse",function(b,c,d,e,f,g,h,i,j,k,l){return{require:"?ngModel",scope:{},restrict:"EA",priority:2,// So we override validators correctly
1029
+ link:function(m,n,o,p){
1030
+ // all these vars should not be accessable outside this directive
1031
+ var q,r,s,t,u,v,w,x,y,z,A,B,C=o.serial?o.serial:Math.floor(1e16*Math.random());m._name=o.name?o.name:"textAngularEditor"+C;var D=function(a,b,d){c(function(){a.one(b,d)},100)};if(y=f(o.taDefaultWrap),
1032
+ // get the settings from the defaults and add our specific functions that need to be on the scope
1033
+ angular.extend(m,angular.copy(d),{
1034
+ // wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
1035
+ wrapSelection:function(a,b,c){
1036
+ // we restore the saved selection that was saved when focus was lost
1037
+ /* NOT FUNCTIONAL YET */
1038
+ /* textAngularManager.restoreFocusSelection(scope._name, scope); */
1039
+ "undo"===a.toLowerCase()?m["$undoTaBindtaTextElement"+C]():"redo"===a.toLowerCase()?m["$redoTaBindtaTextElement"+C]():(
1040
+ // catch errors like FF erroring when you try to force an undo with nothing done
1041
+ y(a,!1,b,m.defaultTagAttributes),c&&
1042
+ // re-apply the selectable tool events
1043
+ m["reApplyOnSelectorHandlerstaTextElement"+C](),
1044
+ // refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
1045
+ // You still have focus on the text/html input it just doesn't show up
1046
+ m.displayElements.text[0].focus())},showHtml:m.$eval(o.taShowHtml)||!1}),
1047
+ // setup the options from the optional attributes
1048
+ o.taFocussedClass&&(m.classes.focussed=o.taFocussedClass),o.taTextEditorClass&&(m.classes.textEditor=o.taTextEditorClass),o.taHtmlEditorClass&&(m.classes.htmlEditor=o.taHtmlEditorClass),o.taDefaultTagAttributes)try{
1049
+ // TODO: This should use angular.merge to enhance functionality once angular 1.4 is required
1050
+ angular.extend(m.defaultTagAttributes,angular.fromJson(o.taDefaultTagAttributes))}catch(a){j.error(a)}
1051
+ // optional setup functions
1052
+ o.taTextEditorSetup&&(m.setup.textEditorSetup=m.$parent.$eval(o.taTextEditorSetup)),o.taHtmlEditorSetup&&(m.setup.htmlEditorSetup=m.$parent.$eval(o.taHtmlEditorSetup)),
1053
+ // optional fileDropHandler function
1054
+ o.taFileDrop?m.fileDropHandler=m.$parent.$eval(o.taFileDrop):m.fileDropHandler=m.defaultFileDropHandler,w=n[0].innerHTML,
1055
+ // clear the original content
1056
+ n[0].innerHTML="",
1057
+ // Setup the HTML elements as variable references for use later
1058
+ m.displayElements={
1059
+ // we still need the hidden input even with a textarea as the textarea may have invalid/old input in it,
1060
+ // wheras the input will ALLWAYS have the correct value.
1061
+ forminput:angular.element("<input type='hidden' tabindex='-1' style='display: none;'>"),html:angular.element("<textarea></textarea>"),text:angular.element("<div></div>"),
1062
+ // other toolbased elements
1063
+ scrollWindow:angular.element("<div class='ta-scroll-window'></div>"),popover:angular.element('<div class="popover fade bottom" style="max-width: none; width: 305px;"></div>'),popoverArrow:angular.element('<div class="arrow"></div>'),popoverContainer:angular.element('<div class="popover-content"></div>'),resize:{overlay:angular.element('<div class="ta-resizer-handle-overlay"></div>'),background:angular.element('<div class="ta-resizer-handle-background"></div>'),anchors:[angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tl"></div>'),angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-tr"></div>'),angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-bl"></div>'),angular.element('<div class="ta-resizer-handle-corner ta-resizer-handle-corner-br"></div>')],info:angular.element('<div class="ta-resizer-handle-info"></div>')}},
1064
+ // Setup the popover
1065
+ 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){/* istanbul ignore else: this is for catching the jqLite testing*/
1066
+ // this prevents focusout from firing on the editor when clicking anything in the popover
1067
+ return b&&angular.extend(a,b),a.preventDefault(),!1}),/* istanbul ignore next: popover resize and scroll events handled */
1068
+ m.handlePopoverEvents=function(){"block"===m.displayElements.popover.css("display")&&(B&&c.cancel(B),B=c(function(){
1069
+ //console.log('resize', scope.displayElements.popover.css('display'));
1070
+ m.reflowPopover(m.resizeElement),m.reflowResizeOverlay(m.resizeElement)},100))},/* istanbul ignore next: browser resize check */
1071
+ angular.element(window).on("resize",m.handlePopoverEvents),/* istanbul ignore next: browser scroll check */
1072
+ angular.element(window).on("scroll",m.handlePopoverEvents);
1073
+ // we want to know if a given node has a scrollbar!
1074
+ // credit to lotif on http://stackoverflow.com/questions/4880381/check-whether-html-element-has-scrollbars
1075
+ var E=function(a){var b,c={vertical:!1,horizontal:!1};try{if(b=window.getComputedStyle(a),null===b)return c}catch(a){/* istanbul ignore next: error handler */
1076
+ return c}var d=b["overflow-y"],e=b["overflow-x"];return{vertical:("scroll"===d||"auto"===d)&&/* istanbul ignore next: not tested */
1077
+ a.scrollHeight>a.clientHeight,horizontal:("scroll"===e||"auto"===e)&&/* istanbul ignore next: not tested */
1078
+ a.scrollWidth>a.clientWidth}};
1079
+ // getScrollTop
1080
+ //
1081
+ // we structure this so that it can climb the parents of the _el and when it finds
1082
+ // one with scrollbars, it adds an EventListener, so that no matter how the
1083
+ // DOM is structured in the user APP, if there is a scrollbar not as part of the
1084
+ // ta-scroll-window, we will still capture the 'scroll' events...
1085
+ // and handle the scroll event properly and do the resize, etc.
1086
+ //
1087
+ m.getScrollTop=function(a,b){var c=a.scrollTop;/* istanbul ignore next: triggered only if has scrollbar and scrolled */
1088
+ /* istanbul ignore next: triggered only if has scrollbar */
1089
+ // remove element eventListener
1090
+ /* istanbul ignore next: triggered only if has scrollbar and scrolled */
1091
+ /* istanbul ignore else: catches only if no scroll */
1092
+ return"undefined"==typeof c&&(c=0),b&&E(a).vertical&&(a.removeEventListener("scroll",m._scrollListener,!1),a.addEventListener("scroll",m._scrollListener,!1)),0!==c?{node:a.nodeName,top:c}:a.parentNode?m.getScrollTop(a.parentNode,b):{node:"<none>",top:0}},
1093
+ // define the popover show and hide functions
1094
+ m.showPopover=function(a){m.getScrollTop(m.displayElements.scrollWindow[0],!0),m.displayElements.popover.css("display","block"),
1095
+ // we must use a $timeout here, or the css change to the
1096
+ // displayElements.resize.overlay will not take!!!
1097
+ // WHY???
1098
+ c(function(){m.displayElements.resize.overlay.css("display","block")}),m.resizeElement=a,m.reflowPopover(a),i.addClass(m.displayElements.popover,"in"),D(h.find("body"),"click keyup",function(){m.hidePopover()})},/* istanbul ignore next: browser scroll event handler */
1099
+ m._scrollListener=function(a,b){m.handlePopoverEvents()},m.reflowPopover=function(a){var b=m.getScrollTop(m.displayElements.scrollWindow[0],!1),c=a[0].offsetTop-b.top;
1100
+ //var spaceBelowImage = scope.displayElements.text[0].offsetHeight - _el[0].offsetHeight - spaceAboveImage;
1101
+ //console.log(spaceAboveImage, spaceBelowImage);
1102
+ /* istanbul ignore if: catches only if near bottom of editor */
1103
+ c<51?(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 d=m.displayElements.text[0].offsetWidth-m.displayElements.popover[0].offsetWidth,e=a[0].offsetLeft+a[0].offsetWidth/2-m.displayElements.popover[0].offsetWidth/2,f=Math.max(0,Math.min(d,e)),g=Math.min(e,Math.max(0,e-d))-11;f+=window.scrollX,g-=window.scrollX,m.displayElements.popover.css("left",f+"px"),m.displayElements.popoverArrow.css("margin-left",g+"px")},m.hidePopover=function(){m.displayElements.popover.css("display","none"),m.displayElements.popoverContainer.attr("style",""),m.displayElements.popoverContainer.attr("class","popover-content"),m.displayElements.popover.removeClass("in"),m.displayElements.resize.overlay.css("display","none")},
1104
+ // setup the resize overlay
1105
+ 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),
1106
+ // A click event on the resize.background will now shift the focus to the editor
1107
+ /* istanbul ignore next: click on the resize.background to focus back to editor */
1108
+ m.displayElements.resize.background.on("click",function(a){m.displayElements.text[0].focus()}),
1109
+ // define the show and hide events
1110
+ 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)},/* istanbul ignore next: pretty sure phantomjs won't test this */
1111
+ 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){function c(a){return Math.round(Math.max(0,a))}
1112
+ // calculate new size
1113
+ var f={x:Math.max(0,d.width+(b.clientX-d.x)),y:Math.max(0,d.height+(b.clientY-d.y))},g=void 0!==o.taResizeForceAspectRatio,h=o.taResizeMaintainAspectRatio,i=g||h&&!b.shiftKey;if(i){var j=f.y/f.x;f.x=e>j?f.x:f.y/e,f.y=e>j?f.x*e:f.y}var k=angular.element(a);k.css("height",c(f.y)+"px"),k.css("width",c(f.x)+"px"),
1114
+ // reflow the popover tooltip
1115
+ m.reflowResizeOverlay(a)};b.on("mousemove",f),D(b,"mouseup",function(a){a.preventDefault(),a.stopPropagation(),b.off("mousemove",f),
1116
+ // at this point, we need to force the model to update! since the css has changed!
1117
+ // this fixes bug: #862 - we now hide the popover -- as this seems more consitent.
1118
+ // there are still issues under firefox, the window does not repaint. -- not sure
1119
+ // how best to resolve this, but clicking anywhere works.
1120
+ m.$apply(function(){m.hidePopover(),m.updateTaBindtaTextElement()},100)}),c.stopPropagation(),c.preventDefault()},m.displayElements.resize.anchors[3].off("mousedown"),m.displayElements.resize.anchors[3].on("mousedown",z),m.reflowResizeOverlay(a),D(b,"click",function(){m.hideResizeOverlay()})},/* istanbul ignore next: pretty sure phantomjs won't test this */
1121
+ m.hideResizeOverlay=function(){m.displayElements.resize.anchors[3].off("mousedown",z),m.displayElements.resize.overlay.css("display","none")},
1122
+ // allow for insertion of custom directives on the textarea and div
1123
+ m.setup.htmlEditorSetup(m.displayElements.html),m.setup.textEditorSetup(m.displayElements.text),m.displayElements.html.attr({id:"taHtmlElement"+C,"ng-show":"showHtml","ta-bind":"ta-bind","ng-model":"html","ng-model-options":n.attr("ng-model-options")}),m.displayElements.text.attr({id:"taTextElement"+C,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&&
1124
+ // taDefaultWrap is only applied to the text and not the html view
1125
+ 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)),o.taKeepStyles&&(m.displayElements.text.attr("ta-keep-styles",o.taKeepStyles),m.displayElements.html.attr("ta-keep-styles",o.taKeepStyles)),
1126
+ // add the main elements to the origional element
1127
+ 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)")),
1128
+ // compile the scope with the text and html elements only - if we do this with the main element it causes a compile loop
1129
+ b(m.displayElements.scrollWindow)(m),b(m.displayElements.html)(m),m.updateTaBindtaTextElement=m["updateTaBindtaTextElement"+C],m.updateTaBindtaHtmlElement=m["updateTaBindtaHtmlElement"+C],
1130
+ // add the classes manually last
1131
+ 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);var F=function(a,b){/* istanbul ignore next: this is only here because of a bug in rangy where rangy.saveSelection() has cleared the state */
1132
+ b!==h[0].queryCommandState(a)&&h[0].execCommand(a,!1,null)};
1133
+ // used in the toolbar actions
1134
+ m._actionRunning=!1;var G=!1;
1135
+ // changes to the model variable from outside the html/text inputs
1136
+ // if no ngModel, then the only input is from inside text-angular
1137
+ if(m.startAction=function(){var b=!1,c=!1,d=!1,e=!1;
1138
+ //console.log('B', $document[0].queryCommandState('bold'), 'I', $document[0].queryCommandState('italic'), '_', $document[0].queryCommandState('underline'), 'S', $document[0].queryCommandState('strikeThrough') );
1139
+ //console.log('B', _beforeStateBold, 'I', _beforeStateItalic, '_', _beforeStateUnderline, 'S', _beforeStateStrikethough);
1140
+ // if rangy library is loaded return a function to reload the current selection
1141
+ // rangy.saveSelection() clear the state of bold, italic, underline, strikethrough
1142
+ // so we reset them here....!!!
1143
+ // this fixes bugs #423, #1129, #1105, #693 which are actually rangy bugs!
1144
+ return m._actionRunning=!0,b=h[0].queryCommandState("bold"),c=h[0].queryCommandState("italic"),d=h[0].queryCommandState("underline"),e=h[0].queryCommandState("strikeThrough"),G=a.saveSelection(),F("bold",b),F("italic",c),F("underline",d),F("strikeThrough",e),function(){G&&a.restoreSelection(G)}},m.endAction=function(){m._actionRunning=!1,G&&(m.showHtml?m.displayElements.html[0].focus():m.displayElements.text[0].focus(),
1145
+ // rangy.restoreSelection(_savedSelection);
1146
+ a.removeMarkers(G)),G=!1,m.updateSelectedStyles(),
1147
+ // only update if in text or WYSIWYG mode
1148
+ m.showHtml||m["updateTaBindtaTextElement"+C]()},
1149
+ // note that focusout > focusin is called everytime we click a button - except bad support: http://www.quirksmode.org/dom/events/blurfocus.html
1150
+ // cascades to displayElements.text and displayElements.html automatically.
1151
+ u=function(a){m.focussed=!0,n.addClass(m.classes.focussed),/******* NOT FUNCTIONAL YET
1152
+ if (e.target.id === 'taTextElement' + _serial) {
1153
+ console.log('_focusin taTextElement');
1154
+ // we only do this if NOT focussed
1155
+ textAngularManager.restoreFocusSelection(scope._name);
1156
+ }
1157
+ *******/
1158
+ x.focus(),n.triggerHandler("focus"),
1159
+ // we call editorScope.updateSelectedStyles() here because we want the toolbar to be focussed
1160
+ // as soon as we have focus. Otherwise this only happens on mousedown or keydown etc...
1161
+ /* istanbul ignore else: don't run if already running */
1162
+ m.updateSelectedStyles&&!m._bUpdateSelectedStyles&&
1163
+ // we don't set editorScope._bUpdateSelectedStyles here, because we do not want the
1164
+ // updateSelectedStyles() to run twice which it will do after 200 msec if we have
1165
+ // set editorScope._bUpdateSelectedStyles
1166
+ //
1167
+ // WOW, normally I would do a scope.$apply here, but this causes ERRORs when doing tests!
1168
+ c(function(){m.updateSelectedStyles()},0)},m.displayElements.html.on("focus",u),m.displayElements.text.on("focus",u),v=function(a){/****************** NOT FUNCTIONAL YET
1169
+ try {
1170
+ var _s = rangy.getSelection();
1171
+ if (_s) {
1172
+ // we save the selection when we loose focus so that if do a wrapSelection, the
1173
+ // apropriate selection in the editor is restored before action.
1174
+ var _savedFocusRange = rangy.saveRange(_s.getRangeAt(0));
1175
+ textAngularManager.saveFocusSelection(scope._name, _savedFocusRange);
1176
+ }
1177
+ } catch(error) { }
1178
+ *****************/
1179
+ // if we are NOT runnig an action and have NOT focussed again on the text etc then fire the blur events
1180
+ // to prevent multiple apply error defer to next seems to work.
1181
+ return m._actionRunning||h[0].activeElement===m.displayElements.html[0]||h[0].activeElement===m.displayElements.text[0]||(n.removeClass(m.classes.focussed),x.unfocus(),c(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)}),
1182
+ // Setup the default toolbar tools, this way allows the user to add new tools like plugins.
1183
+ // This is on the editor for future proofing if we find a better way to do this.
1184
+ m.queryFormatBlockState=function(a){
1185
+ // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
1186
+ return!m.showHtml&&a.toLowerCase()===h[0].queryCommandValue("formatBlock").toLowerCase()},m.queryCommandState=function(a){
1187
+ // $document[0].queryCommandValue('formatBlock') errors in Firefox if we call this when focussed on the textarea
1188
+ 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),
1189
+ //Show the HTML view
1190
+ /* istanbul ignore next: ngModel exists check */
1191
+ /* THIS is not the correct thing to do, here....
1192
+ The ngModel is correct, but it is not formatted as the user as done it...
1193
+ var _model;
1194
+ if (ngModel) {
1195
+ _model = ngModel.$viewValue;
1196
+ } else {
1197
+ _model = scope.html;
1198
+ }
1199
+ var _html = scope.displayElements.html[0].value;
1200
+ if (getDomFromHtml(_html).childElementCount !== getDomFromHtml(_model).childElementCount) {
1201
+ // the model and the html do not agree
1202
+ // they can get out of sync and when they do, we correct that here...
1203
+ scope.displayElements.html.val(_model);
1204
+ }
1205
+ */
1206
+ m.showHtml?
1207
+ //defer until the element is visible
1208
+ c(function(){
1209
+ // [0] dereferences the DOM object from the angular.element
1210
+ return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.html[0].focus()},100):
1211
+ //Show the WYSIWYG view
1212
+ //defer until the element is visible
1213
+ c(function(){
1214
+ // [0] dereferences the DOM object from the angular.element
1215
+ return i.enabled(!0,m.displayElements.html),i.enabled(!0,m.displayElements.text),m.displayElements.text[0].focus()},100)},o.ngModel){var H=!0;p.$render=function(){if(H){
1216
+ // we need this firstRun to set the originalContents otherwise it gets overrided by the setting of ngModel to undefined from NaN
1217
+ H=!1;
1218
+ // if view value is null or undefined initially and there was original content, set to the original content
1219
+ var a=m.$parent.$eval(o.ngModel);void 0!==a&&null!==a||!w||""===w||
1220
+ // on passing through to taBind it will be sanitised
1221
+ p.$setViewValue(w)}m.displayElements.forminput.val(p.$viewValue),
1222
+ // if the editors aren't focused they need to be updated, otherwise they are doing the updating
1223
+ m.html=p.$viewValue||""},
1224
+ // trigger the validation calls
1225
+ n.attr("required")&&(p.$validators.required=function(a,b){var c=a||b;return!(!c||""===c.trim())})}else
1226
+ // if no ngModel then update from the contents of the origional html.
1227
+ m.displayElements.forminput.val(w),m.html=w;if(
1228
+ // changes from taBind back up to here
1229
+ m.$watch("html",function(a,b){a!==b&&(o.ngModel&&p.$viewValue!==a&&p.$setViewValue(a),m.displayElements.forminput.val(a))}),o.taTargetToolbars)x=g.registerEditor(m._name,m,o.taTargetToolbars.split(","));else{var I=angular.element('<div text-angular-toolbar name="textAngularToolbar'+C+'">');
1230
+ // passthrough init of toolbar options
1231
+ o.taToolbar&&I.attr("ta-toolbar",o.taToolbar),o.taToolbarClass&&I.attr("ta-toolbar-class",o.taToolbarClass),o.taToolbarGroupClass&&I.attr("ta-toolbar-group-class",o.taToolbarGroupClass),o.taToolbarButtonClass&&I.attr("ta-toolbar-button-class",o.taToolbarButtonClass),o.taToolbarActiveButtonClass&&I.attr("ta-toolbar-active-button-class",o.taToolbarActiveButtonClass),o.taFocussedClass&&I.attr("ta-focussed-class",o.taFocussedClass),n.prepend(I),b(I)(m.$parent),x=g.registerEditor(m._name,m,["textAngularToolbar"+C])}m.$on("$destroy",function(){g.unregisterEditor(m._name),angular.element(window).off("blur"),angular.element(window).off("resize",m.handlePopoverEvents),angular.element(window).off("scroll",m.handlePopoverEvents)}),
1232
+ // catch element select event and pass to toolbar tools
1233
+ m.$on("ta-element-select",function(a,b){x.triggerElementSelect(a,b)&&m["reApplyOnSelectorHandlerstaTextElement"+C]()}),/******************* no working fully
1234
+ var distanceFromPoint = function (px, py, x, y) {
1235
+ return Math.sqrt((px-x)*(px-x)+(py-y)*(py-y));
1236
+ };
1237
+ // because each object is a rectangle and we have a single point,
1238
+ // we need to give priority if the point is inside the rectangle
1239
+ var getPositionDistance = function(el, x, y) {
1240
+ var range = document.createRange();
1241
+ range.selectNode(el);
1242
+ var rect = range.getBoundingClientRect();
1243
+ console.log(el, rect);
1244
+ range.detach();
1245
+ var bcr = rect;
1246
+ // top left
1247
+ var d1 = distanceFromPoint(bcr.left, bcr.top, x, y);
1248
+ // bottom left
1249
+ var d2 = distanceFromPoint(bcr.left, bcr.bottom, x, y);
1250
+ // top right
1251
+ var d3 = distanceFromPoint(bcr.right, bcr.top, x, y);
1252
+ // bottom right
1253
+ var d4 = distanceFromPoint(bcr.right, bcr.bottom, x, y);
1254
+ return Math.min(d1, d2, d3, d4);
1255
+ };
1256
+ var findClosest = function(el, minElement, maxDistance, x, y) {
1257
+ var _d=0;
1258
+ for (var i = 0; i < el.childNodes.length; i++) {
1259
+ var _n = el.childNodes[i];
1260
+ if (!_n.childNodes.length) {
1261
+ _d = getPositionDistance(_n, x, y);
1262
+ //console.log(_n, _n.childNodes, _d);
1263
+ if (_d < maxDistance) {
1264
+ maxDistance = _d;
1265
+ minElement = _n;
1266
+ }
1267
+ }
1268
+ var res = findClosest(_n, minElement, maxDistance, x, y);
1269
+ if (res.max < maxDistance) {
1270
+ maxDistance = res.max;
1271
+ minElement = res.min;
1272
+ }
1273
+ }
1274
+ return { max: maxDistance, min: minElement };
1275
+ };
1276
+ var getClosestElement = function (el, x, y) {
1277
+ return findClosest(el, null, 12341234124, x, y);
1278
+ };
1279
+ ****************/
1280
+ m.$on("ta-drop-event",function(a,b,d,f){f&&f.files&&f.files.length>0?(m.displayElements.text[0].focus(),
1281
+ // we must set the location of the drop!
1282
+ //console.log(dropEvent.clientX, dropEvent.clientY, dropEvent.target);
1283
+ e.setSelectionToElementEnd(d.target),angular.forEach(f.files,function(a){
1284
+ // taking advantage of boolean execution, if the fileDropHandler returns true, nothing else after it is executed
1285
+ // If it is false then execute the defaultFileDropHandler if the fileDropHandler is NOT the default one
1286
+ // Once one of these has been executed wrap the result as a promise, if undefined or variable update the taBind, else we should wait for the promise
1287
+ try{k.when(m.fileDropHandler(a,m.wrapSelection)||m.fileDropHandler!==m.defaultFileDropHandler&&k.when(m.defaultFileDropHandler(a,m.wrapSelection))).then(function(){m["updateTaBindtaTextElement"+C]()})}catch(a){j.error(a)}}),d.preventDefault(),d.stopPropagation()):c(function(){m["updateTaBindtaTextElement"+C]()},0)}),
1288
+ // the following is for applying the active states to the tools that support it
1289
+ m._bUpdateSelectedStyles=!1,/* istanbul ignore next: browser window/tab leave check */
1290
+ angular.element(window).on("blur",function(){m._bUpdateSelectedStyles=!1,m.focussed=!1}),
1291
+ // loop through all the tools polling their activeState function if it exists
1292
+ m.updateSelectedStyles=function(){var a;/* istanbul ignore next: This check is to ensure multiple timeouts don't exist */
1293
+ A&&c.cancel(A),
1294
+ // test if the common element ISN'T the root ta-text node
1295
+ void 0!==(a=e.getSelectionElement())&&a.parentNode!==m.displayElements.text[0]?x.updateSelectedStyles(angular.element(a)):x.updateSelectedStyles(),
1296
+ // used to update the active state when a key is held down, ie the left arrow
1297
+ /* istanbul ignore else: browser only check */
1298
+ m._bUpdateSelectedStyles&&(A=c(m.updateSelectedStyles,200))},
1299
+ // start updating on keydown
1300
+ q=function(){/* istanbul ignore next: ie catch */
1301
+ /* istanbul ignore next: ie catch */
1302
+ /* istanbul ignore else: don't run if already running */
1303
+ 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),
1304
+ // stop updating on key up and update the display/model
1305
+ r=function(){m._bUpdateSelectedStyles=!1},m.displayElements.html.on("keyup",r),m.displayElements.text.on("keyup",r),
1306
+ // stop updating on key up and update the display/model
1307
+ s=function(a,b){
1308
+ // bug fix for Firefox. If we are selecting a <a> already, any characters will
1309
+ // be added within the <a> which is bad!
1310
+ /* istanbul ignore next: don't see how to test this... */
1311
+ if(e.getSelection){var c=e.getSelection();
1312
+ // in a weird case (can't reproduce) taSelection.getSelectionElement() can be undefined!!
1313
+ // this comes from range.commonAncestorContainer;
1314
+ // so I check for this here which fixes the error case
1315
+ e.getSelectionElement()&&"a"===e.getSelectionElement().nodeName.toLowerCase()&&(
1316
+ // check and see if we are at the edge of the <a>
1317
+ 3===c.start.element.nodeType&&c.start.element.textContent.length===c.end.offset&&
1318
+ // we are at the end of the <a>!!!
1319
+ // so move the selection to after the <a>!!
1320
+ e.setSelectionAfterElement(e.getSelectionElement()),3===c.start.element.nodeType&&0===c.start.offset&&
1321
+ // we are at the start of the <a>!!!
1322
+ // so move the selection before the <a>!!
1323
+ e.setSelectionBeforeElement(e.getSelectionElement()))}/* istanbul ignore else: this is for catching the jqLite testing*/
1324
+ b&&angular.extend(a,b),m.$apply(function(){if(x.sendKeyCommand(a))/* istanbul ignore else: don't run if already running */
1325
+ return m._bUpdateSelectedStyles||m.updateSelectedStyles(),a.preventDefault(),!1})},m.displayElements.html.on("keypress",s),m.displayElements.text.on("keypress",s),
1326
+ // update the toolbar active states when we click somewhere in the text/html boxed
1327
+ t=function(){
1328
+ // ensure only one execution of updateSelectedStyles()
1329
+ m._bUpdateSelectedStyles=!1,
1330
+ // for some reason, unless we do a $timeout here, after a _mouseup when the line is
1331
+ // highlighted, and instead use a scope.$apply(function(){ scope.updateSelectedStyles(); });
1332
+ // doesn't work properly, so we replaced this with:
1333
+ /* istanbul ignore next: not tested */
1334
+ c(function(){m.updateSelectedStyles()},0)},m.displayElements.html.on("mouseup",t),m.displayElements.text.on("mouseup",t)}}}]),u.service("textAngularManager",["taToolExecuteAction","taTools","taRegisterTool","$interval","$rootScope","$log",function(a,b,c,d,e,g){
1335
+ // this service is used to manage all textAngular editors and toolbars.
1336
+ // All publicly published functions that modify/need to access the toolbar or editor scopes should be in here
1337
+ // these contain references to all the editors and toolbars that have been initialised in this app
1338
+ var h,i={},j={},k=0,l=function(a){angular.forEach(j,function(b){b.editorFunctions.updateSelectedStyles(a)})},m=50,n=function(){k=Date.now(),/* istanbul ignore next: setup a one time updateStyles() */
1339
+ h=d(function(){l(),h=void 0},m,1)};/* istanbul ignore next: make sure clean up on destroy */
1340
+ e.$on("destroy",function(){h&&(d.cancel(h),h=void 0)});var o=function(){Math.abs(Date.now()-k)>m&&
1341
+ // we have already triggered the updateStyles a long time back... so setup it again...
1342
+ n()};
1343
+ // when we focus into a toolbar, we need to set the TOOLBAR's $parent to be the toolbars it's linked to.
1344
+ // We also need to set the tools to be updated to be the toolbars...
1345
+ return{
1346
+ // register an editor and the toolbars that it is affected by
1347
+ registerEditor:function(c,d,e){
1348
+ // NOTE: name === editorScope._name
1349
+ // targetToolbars is an [] of 'toolbar name's
1350
+ // targetToolbars are optional, we don't require a toolbar to function
1351
+ if(!c||""===c)throw"textAngular Error: An editor requires a name";if(!d)throw"textAngular Error: An editor requires a scope";if(j[c])throw'textAngular Error: An Editor with name "'+c+'" already exists';return j[c]={scope:d,toolbars:e,
1352
+ // toolbarScopes used by this editor
1353
+ toolbarScopes:[],_registerToolbarScope:function(a){
1354
+ // add to the list late
1355
+ this.toolbars.indexOf(a.name)>=0&&
1356
+ // if this toolbarScope is being used by this editor we add it as one of the scopes
1357
+ this.toolbarScopes.push(a)},
1358
+ // this is a suite of functions the editor should use to update all it's linked toolbars
1359
+ editorFunctions:{disable:function(){
1360
+ // disable all linked toolbars
1361
+ angular.forEach(j[c].toolbarScopes,function(a){a.disabled=!0})},enable:function(){
1362
+ // enable all linked toolbars
1363
+ angular.forEach(j[c].toolbarScopes,function(a){a.disabled=!1})},focus:function(){
1364
+ // this should be called when the editor is focussed
1365
+ angular.forEach(j[c].toolbarScopes,function(a){a._parent=d,a.disabled=!1,a.focussed=!0}),d.focussed=!0},unfocus:function(){
1366
+ // this should be called when the editor becomes unfocussed
1367
+ angular.forEach(j[c].toolbarScopes,function(a){a.disabled=!0,a.focussed=!1}),d.focussed=!1},updateSelectedStyles:function(a){
1368
+ // update the active state of all buttons on liked toolbars
1369
+ angular.forEach(j[c].toolbarScopes,function(b){angular.forEach(b.tools,function(c){c.activeState&&(b._parent=d,
1370
+ // selectedElement may be undefined if nothing selected
1371
+ c.active=c.activeState(a))})})},sendKeyCommand:function(e){
1372
+ // we return true if we applied an action, false otherwise
1373
+ var f=!1;return(e.ctrlKey||e.metaKey||e.specialKey)&&angular.forEach(b,function(b,g){if(b.commandKeyCode&&(b.commandKeyCode===e.which||b.commandKeyCode===e.specialKey))for(var h=0;h<j[c].toolbarScopes.length;h++)if(void 0!==j[c].toolbarScopes[h].tools[g]){a.call(j[c].toolbarScopes[h].tools[g],d),f=!0;break}}),f},triggerElementSelect:function(a,e){
1374
+ // search through the taTools to see if a match for the tag is made.
1375
+ // if there is, see if the tool is on a registered toolbar and not disabled.
1376
+ // NOTE: This can trigger on MULTIPLE tools simultaneously.
1377
+ var f=function(a,b){for(var c=!0,d=0;d<b.length;d++)c=c&&a.attr(b[d]);return c},g=[],h={},i=!1;e=angular.element(e);
1378
+ // get all valid tools by element name, keep track if one matches the
1379
+ var k=!1;
1380
+ // Run the actions on the first visible filtered tool only
1381
+ if(angular.forEach(b,function(a,b){a.onElementSelect&&a.onElementSelect.element&&a.onElementSelect.element.toLowerCase()===e[0].tagName.toLowerCase()&&(!a.onElementSelect.filter||a.onElementSelect.filter(e))&&(
1382
+ // this should only end up true if the element matches the only attributes
1383
+ k=k||angular.isArray(a.onElementSelect.onlyWithAttrs)&&f(e,a.onElementSelect.onlyWithAttrs),a.onElementSelect.onlyWithAttrs&&!f(e,a.onElementSelect.onlyWithAttrs)||(h[b]=a))}),
1384
+ // if we matched attributes to filter on, then filter, else continue
1385
+ k?(angular.forEach(h,function(a,b){a.onElementSelect.onlyWithAttrs&&f(e,a.onElementSelect.onlyWithAttrs)&&g.push({name:b,tool:a})}),
1386
+ // sort most specific (most attrs to find) first
1387
+ g.sort(function(a,b){return b.tool.onElementSelect.onlyWithAttrs.length-a.tool.onElementSelect.onlyWithAttrs.length})):angular.forEach(h,function(a,b){g.push({name:b,tool:a})}),g.length>0)for(var l=0;l<g.length;l++){for(var m=g[l].tool,n=g[l].name,o=0;o<j[c].toolbarScopes.length;o++)if(void 0!==j[c].toolbarScopes[o].tools[n]){m.onElementSelect.action.call(j[c].toolbarScopes[o].tools[n],a,e,d),i=!0;break}if(i)break}return i}}},angular.forEach(e,function(a){i[a]&&j[c].toolbarScopes.push(i[a])}),o(),j[c].editorFunctions},
1388
+ // retrieve editor by name, largely used by testing suites only
1389
+ retrieveEditor:function(a){return j[a]},unregisterEditor:function(a){delete j[a],o()},
1390
+ // registers a toolbar such that it can be linked to editors
1391
+ registerToolbar:function(a){if(!a)throw"textAngular Error: A toolbar requires a scope";if(!a.name||""===a.name)throw"textAngular Error: A toolbar requires a name";if(i[a.name])throw'textAngular Error: A toolbar with name "'+a.name+'" already exists';i[a.name]=a,
1392
+ // walk all the editors and connect this toolbarScope to the editors.... if we need to. This way, it does
1393
+ // not matter if we register the editors after the toolbars or not
1394
+ // Note the editor will ignore this toolbarScope if it is not connected to it...
1395
+ angular.forEach(j,function(b){b._registerToolbarScope(a)}),o()},
1396
+ // retrieve toolbar by name, largely used by testing suites only
1397
+ retrieveToolbar:function(a){return i[a]},
1398
+ // retrieve toolbars by editor name, largely used by testing suites only
1399
+ retrieveToolbarsViaEditor:function(a){var b=[],c=this;return angular.forEach(this.retrieveEditor(a).toolbars,function(a){b.push(c.retrieveToolbar(a))}),b},unregisterToolbar:function(a){delete i[a],o()},
1400
+ // functions for updating the toolbar buttons display
1401
+ updateToolsDisplay:function(a){
1402
+ // pass a partial struct of the taTools, this allows us to update the tools on the fly, will not change the defaults.
1403
+ var b=this;angular.forEach(a,function(a,c){b.updateToolDisplay(c,a)})},
1404
+ // this function resets all toolbars to their default tool definitions
1405
+ resetToolsDisplay:function(){var a=this;angular.forEach(b,function(b,c){a.resetToolDisplay(c)}),o()},
1406
+ // update a tool on all toolbars
1407
+ updateToolDisplay:function(a,b){var c=this;angular.forEach(i,function(d,e){c.updateToolbarToolDisplay(e,a,b)}),o()},
1408
+ // resets a tool to the default/starting state on all toolbars
1409
+ resetToolDisplay:function(a){var b=this;angular.forEach(i,function(c,d){b.resetToolbarToolDisplay(d,a)}),o()},
1410
+ // update a tool on a specific toolbar
1411
+ updateToolbarToolDisplay:function(a,b,c){if(!i[a])throw'textAngular Error: No Toolbar with name "'+a+'" exists';i[a].updateToolDisplay(b,c)},
1412
+ // reset a tool on a specific toolbar to it's default starting value
1413
+ resetToolbarToolDisplay:function(a,c){if(!i[a])throw'textAngular Error: No Toolbar with name "'+a+'" exists';i[a].updateToolDisplay(c,b[c],!0)},
1414
+ // removes a tool from all toolbars and it's definition
1415
+ removeTool:function(a){delete b[a],angular.forEach(i,function(b){delete b.tools[a];for(var c=0;c<b.toolbar.length;c++){for(var d,e=0;e<b.toolbar[c].length;e++){if(b.toolbar[c][e]===a){d={group:c,index:e};break}if(void 0!==d)break}void 0!==d&&(b.toolbar[d.group].slice(d.index,1),b._$element.children().eq(d.group).children().eq(d.index).remove())}}),o()},
1416
+ // toolkey, toolDefinition are required. If group is not specified will pick the last group, if index isnt defined will append to group
1417
+ addTool:function(a,b,d,e){c(a,b),angular.forEach(i,function(c){c.addTool(a,b,d,e)}),o()},
1418
+ // adds a Tool but only to one toolbar not all
1419
+ addToolToToolbar:function(a,b,d,e,f){c(a,b),i[d].addTool(a,b,e,f),o()},
1420
+ // this is used when externally the html of an editor has been changed and textAngular needs to be notified to update the model.
1421
+ // this will call a $digest if not already happening
1422
+ refreshEditor:function(a){if(!j[a])throw'textAngular Error: No Editor with name "'+a+'" exists';j[a].scope.updateTaBindtaTextElement(),/* istanbul ignore else: phase catch */
1423
+ j[a].scope.$$phase||j[a].scope.$digest(),o()},
1424
+ // this is used by taBind to send a key command in response to a special key event
1425
+ sendKeyCommand:function(a,b){var c=j[a._name];/* istanbul ignore else: if nothing to do, do nothing */
1426
+ if(c&&c.editorFunctions.sendKeyCommand(b))/* istanbul ignore else: don't run if already running */
1427
+ return a._bUpdateSelectedStyles||a.updateSelectedStyles(),b.preventDefault(),!1},
1428
+ //
1429
+ // When a toolbar and tools are created, it isn't until there is a key event or mouse event
1430
+ // that the updateSelectedStyles() is called behind the scenes.
1431
+ // This function forces an update through the existing editors to help the application make sure
1432
+ // the inital state is correct.
1433
+ //
1434
+ updateStyles:l,
1435
+ // return the current version of textAngular in use to the user
1436
+ getVersion:function(){return f},
1437
+ // for testing
1438
+ getToolbarScopes:function(){var a=[];return angular.forEach(j,function(b){a=a.concat(b.toolbarScopes)}),a}}}]),u.directive("textAngularToolbar",["$compile","textAngularManager","taOptions","taTools","taToolExecuteAction","$window",function(a,b,c,d,e,f){return{scope:{name:"@"},restrict:"EA",link:function(g,h,i){if(!g.name||""===g.name)throw"textAngular Error: A toolbar requires a name";angular.extend(g,angular.copy(c)),i.taToolbar&&(g.toolbar=g.$parent.$eval(i.taToolbar)),i.taToolbarClass&&(g.classes.toolbar=i.taToolbarClass),i.taToolbarGroupClass&&(g.classes.toolbarGroup=i.taToolbarGroupClass),i.taToolbarButtonClass&&(g.classes.toolbarButton=i.taToolbarButtonClass),i.taToolbarActiveButtonClass&&(g.classes.toolbarButtonActive=i.taToolbarActiveButtonClass),i.taFocussedClass&&(g.classes.focussed=i.taFocussedClass),g.disabled=!0,g.focussed=!1,g._$element=h,h[0].innerHTML="",h.addClass("ta-toolbar "+g.classes.toolbar),g.$watch("focussed",function(){g.focussed?h.addClass(g.classes.focussed):h.removeClass(g.classes.focussed)});var j=function(b,c){var d;if(d=b&&b.display?angular.element(b.display):angular.element("<button type='button'>"),b&&b.class?d.addClass(b.class):d.addClass(g.classes.toolbarButton),d.attr("name",c.name),
1439
+ // important to not take focus from the main text/html entry
1440
+ 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&&(
1441
+ // first clear out the current contents if any
1442
+ d[0].innerHTML="",
1443
+ // add the buttonText
1444
+ b.buttontext&&(d[0].innerHTML=b.buttontext),b.iconclass)){var e=angular.element("<i>"),f=d[0].innerHTML;e.addClass(b.iconclass),d[0].innerHTML="",d.append(e),f&&""!==f&&d.append("&nbsp;"+f)}return c._lastToolDefinition=angular.copy(b),a(d)(c)};
1445
+ // Keep a reference for updating the active states later
1446
+ g.tools={},
1447
+ // create the tools in the toolbar
1448
+ // default functions and values to prevent errors in testing and on init
1449
+ g._parent={disabled:!0,showHtml:!1,queryFormatBlockState:function(){return!1},queryCommandState:function(){return!1}};var k={$window:f,$editor:function(){
1450
+ // dynamically gets the editor as it is set
1451
+ return g._parent},isDisabled:function(){
1452
+ // view selection button is always enabled since it doesn not depend on a selction!
1453
+ // view selection button is always enabled since it doesn not depend on a selction!
1454
+ // this bracket is important as without it it just returns the first bracket and ignores the rest
1455
+ // when the button's disabled function/value evaluates to true
1456
+ // all buttons except the HTML Switch button should be disabled in the showHtml (RAW html) mode
1457
+ // if the toolbar is disabled
1458
+ // if the current editor is disabled
1459
+ return("html"!==this.name||!g._parent.startAction)&&("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){
1460
+ // setup the toolbar group
1461
+ var b=angular.element("<div>");b.addClass(g.classes.toolbarGroup),angular.forEach(a,function(a){
1462
+ // init and add the tools to the group
1463
+ // a tool name (key name from taTools struct)
1464
+ //creates a child scope of the main angularText scope and then extends the childScope with the functions of this particular tool
1465
+ // reference to the scope and element kept
1466
+ g.tools[a]=angular.extend(g.$new(!0),d[a],k,{name:a}),g.tools[a].$element=j(d[a],g.tools[a]),
1467
+ // append the tool compiled with the childScope to the group element
1468
+ b.append(g.tools[a].$element)}),
1469
+ // append the group to the toolbar
1470
+ h.append(b)}),
1471
+ // update a tool
1472
+ // if a value is set to null, remove from the display
1473
+ // when forceNew is set to true it will ignore all previous settings, used to reset to taTools definition
1474
+ // to reset to defaults pass in taTools[key] as _newTool and forceNew as true, ie `updateToolDisplay(key, taTools[key], true);`
1475
+ g.updateToolDisplay=function(a,b,c){var d=g.tools[a];if(d){if(
1476
+ // get the last toolDefinition, then override with the new definition
1477
+ 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';
1478
+ // if tool is defined on this toolbar, update/redo the tool
1479
+ 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}},
1480
+ // we assume here that all values passed are valid and correct
1481
+ 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)})}}}]),u.directive("textAngularVersion",["textAngularManager",function(a){var b=a.getVersion();return{restrict:"EA",link:function(a,c,d){c.html(b)}}}]),u.name});