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.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/lib/text-angular-rails/version.rb +1 -1
- data/vendor/assets/javascripts/textAngular-rangy.min.js +478 -0
- data/vendor/assets/javascripts/textAngular-sanitize.min.js +322 -0
- data/vendor/assets/javascripts/textAngular.min.js +1481 -0
- data/vendor/assets/stylesheets/textAngular.css +193 -0
- metadata +7 -8
- data/vendor/assets/javascripts/text-angular-rangy.js +0 -2
- data/vendor/assets/javascripts/text-angular-sanitize.js +0 -6
- data/vendor/assets/javascripts/text-angular.js +0 -18
- data/vendor/assets/stylesheets/text-angular.css +0 -202
@@ -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><div ng-bind-html="snippet"><br/></div></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><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
|
87
|
+
</div></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><div ng-bind="snippet"><br/></div></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("<p style=\"color:blue\">an html\n" +
|
116
|
+
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
117
|
+
"snippet</p>");
|
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 <b onclick=\"alert(1)\">text</b>");
|
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,"<"),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,"&").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,"<").replace(/>/g,">")}
|
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><div ng-bind-html="snippet | linky"><br></div></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><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></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><div ng-bind="snippet"><br></div></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,"""),'">'),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(/ /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,"​")),/<br\/>/i.test(B)&&(
|
484
|
+
// Firefox adds <br/>'s and so we remove the <br/>
|
485
|
+
B=B.replace(/<br\/>/i,"​")),
|
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,"​")),/<br\/>/i.test(B)&&(
|
499
|
+
// Firefox adds <br/>'s and so we remove the <br/>
|
500
|
+
B=B.replace(/<br\/>/i,"​")),
|
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,"​"),// 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></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></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> </P>":"<p> </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()+"> </"+u.taDefaultWrap.toUpperCase()+">":"<"+u.taDefaultWrap+"> </"+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">( | )<\/span>/gi," ")}/<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+=" ";return b}).replace(/\n|\r\n|\r/g,"<br />").replace(/\t/g," "),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(" "+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});
|