odania 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{LICENSE.txt → MIT-LICENSE} +1 -3
- data/README.md +3 -3
- data/Rakefile +33 -3
- data/app/assets/config/odania_manifest.js +0 -0
- data/app/assets/javascripts/application.js +13 -0
- data/app/assets/javascripts/textAngular.js +15 -0
- data/app/assets/javascripts/textAngular/textAngular-rangy.min.js +478 -0
- data/app/assets/javascripts/textAngular/textAngular-sanitize.min.js +322 -0
- data/app/assets/javascripts/textAngular/textAngular.min.js +1481 -0
- data/app/assets/stylesheets/scaffold.css +80 -0
- data/app/assets/stylesheets/textAngular/application.css +15 -0
- data/app/assets/stylesheets/textAngular/textAngular.css +204 -0
- data/app/controllers/admin/home_controller.rb +2 -0
- data/app/controllers/admin/languages_controller.rb +74 -0
- data/app/controllers/admin_controller.rb +4 -0
- data/app/controllers/application_controller.rb +3 -0
- data/app/controllers/categories_controller.rb +34 -0
- data/app/controllers/home_controller.rb +11 -0
- data/app/controllers/protected/home_controller.rb +2 -0
- data/app/controllers/protected_controller.rb +22 -0
- data/app/controllers/registration_controller.rb +12 -0
- data/app/helpers/standard_form_builder.rb +71 -0
- data/app/helpers/standard_form_helper.rb +13 -0
- data/app/models/admin.rb +37 -0
- data/app/models/language.rb +5 -0
- data/app/models/user.rb +47 -0
- data/app/views/admin/home/index.html.erb +1 -0
- data/app/views/admin/languages/_form.html.erb +16 -0
- data/app/views/admin/languages/_language.json.jbuilder +2 -0
- data/app/views/admin/languages/edit.html.erb +6 -0
- data/app/views/admin/languages/index.html.erb +27 -0
- data/app/views/admin/languages/index.json.jbuilder +1 -0
- data/app/views/admin/languages/new.html.erb +5 -0
- data/app/views/admin/languages/show.html.erb +9 -0
- data/app/views/admin/languages/show.json.jbuilder +1 -0
- data/app/views/categories/index.html.erb +9 -0
- data/app/views/categories/show.html.erb +16 -0
- data/app/views/devise/confirmations/new.html.erb +11 -0
- data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/devise/mailer/email_changed.html.erb +7 -0
- data/app/views/devise/mailer/password_change.html.erb +4 -0
- data/app/views/devise/mailer/reset_password_instructions.html.erb +9 -0
- data/app/views/devise/mailer/unlock_instructions.html.erb +8 -0
- data/app/views/devise/passwords/edit.html.erb +13 -0
- data/app/views/devise/passwords/new.html.erb +11 -0
- data/app/views/devise/registrations/edit.html.erb +39 -0
- data/app/views/devise/registrations/new.html.erb +15 -0
- data/app/views/devise/sessions/new.html.erb +14 -0
- data/app/views/devise/shared/_links.html.erb +25 -0
- data/app/views/devise/unlocks/new.html.erb +11 -0
- data/app/views/home/index.html.erb +11 -0
- data/app/views/languages/_form.html.erb +20 -0
- data/app/views/languages/edit.html.erb +6 -0
- data/app/views/languages/index.html.erb +27 -0
- data/app/views/languages/new.html.erb +5 -0
- data/app/views/languages/show.html.erb +9 -0
- data/app/views/protected/home/index.html.erb +1 -0
- data/config/initializers/elasticsearch.rb +5 -0
- data/config/locales/devise.en.yml +64 -0
- data/config/routes.rb +23 -0
- data/db/seeds.rb +5 -0
- data/lib/odania.rb +7 -56
- data/lib/odania/engine.rb +14 -0
- data/lib/odania/version.rb +1 -1
- data/lib/tasks/odania_tasks.rake +4 -0
- data/lib/templates/erb/scaffold/_form.html.erb +27 -0
- data/lib/templates/erb/scaffold/edit.html.erb +6 -0
- data/lib/templates/erb/scaffold/index.html.erb +31 -0
- data/lib/templates/erb/scaffold/new.html.erb +5 -0
- data/lib/templates/erb/scaffold/show.html.erb +11 -0
- metadata +129 -84
- data/.codeclimate.yml +0 -30
- data/.gitignore +0 -17
- data/.rspec +0 -2
- data/.rubocop.yml +0 -1156
- data/.travis.yml +0 -20
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -113
- data/Guardfile +0 -31
- data/features/plugin.feature +0 -35
- data/features/step_definitions/plugin_steps.rb +0 -75
- data/features/support/env.rb +0 -1
- data/lib/odania/config.rb +0 -17
- data/lib/odania/config/backend.rb +0 -31
- data/lib/odania/config/backend_group.rb +0 -43
- data/lib/odania/config/domain.rb +0 -59
- data/lib/odania/config/duplicates.rb +0 -28
- data/lib/odania/config/global_config.rb +0 -210
- data/lib/odania/config/layout.rb +0 -30
- data/lib/odania/config/page.rb +0 -29
- data/lib/odania/config/page_base.rb +0 -47
- data/lib/odania/config/plugin_config.rb +0 -58
- data/lib/odania/config/style.rb +0 -36
- data/lib/odania/config/sub_domain.rb +0 -113
- data/lib/odania/config/subdomain_config.rb +0 -124
- data/lib/odania/consul.rb +0 -138
- data/lib/odania/plugin.rb +0 -103
- data/odania.gemspec +0 -34
- data/spec/fixtures/global_config.json +0 -135
- data/spec/fixtures/plugin_config_1.json +0 -102
- data/spec/lib/odania/config/global_config_spec.rb +0 -69
- data/spec/lib/odania/config/plugin_config_spec.rb +0 -31
- data/spec/lib/odania/plugin_spec.rb +0 -25
- data/spec/lib/odania_spec.rb +0 -10
- data/spec/spec_helper.rb +0 -19
- data/spec/support/consul_mock.rb +0 -123
- data/tasks/odania.rake +0 -8
- data/tasks/rspec.rake +0 -7
@@ -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});
|