mirror of
https://github.com/wahyd4/cdnjs.git
synced 2026-08-09 04:46:00 +10:00
add textAngular v1.3.0-pre13
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,728 @@
|
||||
/**
|
||||
* @license AngularJS v1.2.26
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {'use strict';
|
||||
|
||||
var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngSanitize
|
||||
* @description
|
||||
*
|
||||
* # ngSanitize
|
||||
*
|
||||
* The `ngSanitize` module provides functionality to sanitize HTML.
|
||||
*
|
||||
*
|
||||
* <div doc-module-components="ngSanitize"></div>
|
||||
*
|
||||
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
|
||||
*/
|
||||
|
||||
/*
|
||||
* HTML Parser By Misko Hevery (misko@hevery.com)
|
||||
* based on: HTML Parser By John Resig (ejohn.org)
|
||||
* Original code by Erik Arvidsson, Mozilla Public License
|
||||
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
||||
*
|
||||
* // Use like so:
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $sanitize
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
|
||||
* then serialized back to properly escaped html string. This means that no unsafe input can make
|
||||
* it into the returned string, however, since our parser is more strict than a typical browser
|
||||
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
|
||||
* browser, won't make it through the sanitizer.
|
||||
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
|
||||
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
|
||||
*
|
||||
* @param {string} html Html input.
|
||||
* @returns {string} Sanitized html.
|
||||
*
|
||||
* @example
|
||||
<example module="sanitizeExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('sanitizeExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
|
||||
$scope.snippet =
|
||||
'<p style="color:blue">an html\n' +
|
||||
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
||||
'snippet</p>';
|
||||
$scope.deliberatelyTrustDangerousSnippet = function() {
|
||||
return $sce.trustAsHtml($scope.snippet);
|
||||
};
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Directive</td>
|
||||
<td>How</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-sanitize">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Automatically uses $sanitize</td>
|
||||
<td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind-html="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-trust">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
|
||||
</div></pre>
|
||||
</td>
|
||||
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-default">
|
||||
<td>ng-bind</td>
|
||||
<td>Automatically escapes</td>
|
||||
<td><pre><div ng-bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should sanitize the html snippet by default', function() {
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
||||
});
|
||||
|
||||
it('should inline raw snippet if bound to a trusted value', function() {
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should escape snippet without any filter', function() {
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('new <b>text</b>');
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
|
||||
'new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
|
||||
"new <b onclick=\"alert(1)\">text</b>");
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
function $SanitizeProvider() {
|
||||
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
|
||||
return function(html) {
|
||||
var buf = [];
|
||||
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
|
||||
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
|
||||
}));
|
||||
return buf.join('');
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
function sanitizeText(chars) {
|
||||
var buf = [];
|
||||
var writer = htmlSanitizeWriter(buf, angular.noop);
|
||||
writer.chars(chars);
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var START_TAG_REGEXP =
|
||||
/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
|
||||
END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
|
||||
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
|
||||
BEGIN_TAG_REGEXP = /^</,
|
||||
BEGING_END_TAGE_REGEXP = /^<\//,
|
||||
COMMENT_REGEXP = /<!--(.*?)-->/g,
|
||||
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
|
||||
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
// Match everything outside of normal chars and " (quote character)
|
||||
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
|
||||
|
||||
|
||||
// Good source of info about elements and attributes
|
||||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||||
// http://simon.html5.org/html-elements
|
||||
|
||||
// Safe Void Elements - HTML5
|
||||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||||
var voidElements = makeMap("area,br,col,hr,img,wbr");
|
||||
|
||||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||||
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
|
||||
optionalEndTagInlineElements = makeMap("rp,rt"),
|
||||
optionalEndTagElements = angular.extend({},
|
||||
optionalEndTagInlineElements,
|
||||
optionalEndTagBlockElements);
|
||||
|
||||
// Safe Block Elements - HTML5
|
||||
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
|
||||
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
|
||||
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
|
||||
|
||||
// Inline Elements - HTML5
|
||||
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
|
||||
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
|
||||
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
|
||||
|
||||
|
||||
// Special Elements (can contain anything)
|
||||
var specialElements = makeMap("script,style");
|
||||
|
||||
var validElements = angular.extend({},
|
||||
voidElements,
|
||||
blockElements,
|
||||
inlineElements,
|
||||
optionalEndTagElements);
|
||||
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
|
||||
var validAttrs = angular.extend({}, uriAttrs, makeMap(
|
||||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
|
||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
|
||||
'id,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
|
||||
'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
|
||||
'valign,value,vspace,width'));
|
||||
|
||||
function makeMap(str) {
|
||||
var obj = {}, items = str.split(','), i;
|
||||
for (i = 0; i < items.length; i++) obj[items[i]] = true;
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @example
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
function htmlParser( html, handler ) {
|
||||
if (typeof html !== 'string') {
|
||||
if (html === null || typeof html === 'undefined') {
|
||||
html = '';
|
||||
} else {
|
||||
html = '' + html;
|
||||
}
|
||||
}
|
||||
var index, chars, match, stack = [], last = html, text;
|
||||
stack.last = function() { return stack[ stack.length - 1 ]; };
|
||||
|
||||
while ( html ) {
|
||||
text = '';
|
||||
chars = true;
|
||||
|
||||
// Make sure we're not in a script or style element
|
||||
if ( !stack.last() || !specialElements[ stack.last() ] ) {
|
||||
|
||||
// Comment
|
||||
if ( html.indexOf("<!--") === 0 ) {
|
||||
// comments containing -- are not allowed unless they terminate the comment
|
||||
index = html.indexOf("--", 4);
|
||||
|
||||
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
|
||||
if (handler.comment) handler.comment( html.substring( 4, index ) );
|
||||
html = html.substring( index + 3 );
|
||||
chars = false;
|
||||
}
|
||||
// DOCTYPE
|
||||
} else if ( DOCTYPE_REGEXP.test(html) ) {
|
||||
match = html.match( DOCTYPE_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.replace( match[0], '');
|
||||
chars = false;
|
||||
}
|
||||
// end tag
|
||||
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
|
||||
match = html.match( END_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( END_TAG_REGEXP, parseEndTag );
|
||||
chars = false;
|
||||
}
|
||||
|
||||
// start tag
|
||||
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
|
||||
match = html.match( START_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
// We only have a valid start-tag if there is a '>'.
|
||||
if ( match[4] ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( START_TAG_REGEXP, parseStartTag );
|
||||
}
|
||||
chars = false;
|
||||
} else {
|
||||
// no ending tag found --- this piece should be encoded as an entity.
|
||||
text += '<';
|
||||
html = html.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ( chars ) {
|
||||
index = html.indexOf("<");
|
||||
|
||||
text += index < 0 ? html : html.substring( 0, index );
|
||||
html = index < 0 ? "" : html.substring( index );
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
}
|
||||
|
||||
} else {
|
||||
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
|
||||
function(all, text){
|
||||
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
|
||||
if ( html == last ) {
|
||||
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
|
||||
"of html: {0}", html);
|
||||
}
|
||||
last = html;
|
||||
}
|
||||
|
||||
// Clean up any remaining tags
|
||||
parseEndTag();
|
||||
|
||||
function parseStartTag( tag, tagName, rest, unary ) {
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( blockElements[ tagName ] ) {
|
||||
while ( stack.last() && inlineElements[ stack.last() ] ) {
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
|
||||
parseEndTag( "", tagName );
|
||||
}
|
||||
|
||||
unary = voidElements[ tagName ] || !!unary;
|
||||
|
||||
if ( !unary )
|
||||
stack.push( tagName );
|
||||
|
||||
var attrs = {};
|
||||
|
||||
rest.replace(ATTR_REGEXP,
|
||||
function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
|
||||
var value = doubleQuotedValue
|
||||
|| singleQuotedValue
|
||||
|| unquotedValue
|
||||
|| '';
|
||||
|
||||
attrs[name] = decodeEntities(value);
|
||||
});
|
||||
if (handler.start) handler.start( tagName, attrs, unary );
|
||||
}
|
||||
|
||||
function parseEndTag( tag, tagName ) {
|
||||
var pos = 0, i;
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( tagName )
|
||||
// Find the closest opened tag of the same type
|
||||
for ( pos = stack.length - 1; pos >= 0; pos-- )
|
||||
if ( stack[ pos ] == tagName )
|
||||
break;
|
||||
|
||||
if ( pos >= 0 ) {
|
||||
// Close all the open elements, up the stack
|
||||
for ( i = stack.length - 1; i >= pos; i-- )
|
||||
if (handler.end) handler.end( stack[ i ] );
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenPre=document.createElement("pre");
|
||||
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
|
||||
/**
|
||||
* decodes all entities into regular string
|
||||
* @param value
|
||||
* @returns {string} A string with decoded entities.
|
||||
*/
|
||||
function decodeEntities(value) {
|
||||
if (!value) { return ''; }
|
||||
|
||||
// Note: IE8 does not preserve spaces at the start/end of innerHTML
|
||||
// so we must capture them and reattach them afterward
|
||||
var parts = spaceRe.exec(value);
|
||||
var spaceBefore = parts[1];
|
||||
var spaceAfter = parts[3];
|
||||
var content = parts[2];
|
||||
if (content) {
|
||||
hiddenPre.innerHTML=content.replace(/</g,"<");
|
||||
// innerText depends on styling as it doesn't display hidden elements.
|
||||
// Therefore, it's better to use textContent not to cause unnecessary
|
||||
// reflows. However, IE<9 don't support textContent so the innerText
|
||||
// fallback is necessary.
|
||||
content = 'textContent' in hiddenPre ?
|
||||
hiddenPre.textContent : hiddenPre.innerText;
|
||||
}
|
||||
return spaceBefore + content + spaceAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns {string} escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(SURROGATE_PAIR_REGEXP, function (value) {
|
||||
var hi = value.charCodeAt(0);
|
||||
var low = value.charCodeAt(1);
|
||||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||||
}).
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function(value){
|
||||
// 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
|
||||
// decimal values are: 0-31, 127-159, 173, 1536-1540, 1807, 6068, 6069, 8204-8207, 8232-8239, 8288-8303, 65279, 65520-65535
|
||||
var c = value.charCodeAt(0);
|
||||
// if unsafe character encode
|
||||
if(c <= 159 ||
|
||||
c == 173 ||
|
||||
(c >= 1536 && c <= 1540) ||
|
||||
c == 1807 ||
|
||||
c == 6068 ||
|
||||
c == 6069 ||
|
||||
(c >= 8204 && c <= 8207) ||
|
||||
(c >= 8232 && c <= 8239) ||
|
||||
(c >= 8288 && c <= 8303) ||
|
||||
c == 65279 ||
|
||||
(c >= 65520 && c <= 65535)) return '&#' + c + ';';
|
||||
return value; // avoids multilingual issues
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
var trim = (function() {
|
||||
// native trim is way faster: http://jsperf.com/angular-trim-test
|
||||
// but IE doesn't have it... :-(
|
||||
// TODO: we should move this into IE/ES5 polyfill
|
||||
if (!String.prototype.trim) {
|
||||
return function(value) {
|
||||
return angular.isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value;
|
||||
};
|
||||
}
|
||||
return function(value) {
|
||||
return angular.isString(value) ? value.trim() : value;
|
||||
};
|
||||
})();
|
||||
|
||||
// Custom logic for accepting certain style options only - textAngular
|
||||
// Currently allows only the color, background-color, text-align, float, width and height attributes
|
||||
// all other attributes should be easily done through classes.
|
||||
function validStyles(styleAttr){
|
||||
var result = '';
|
||||
var styleArray = styleAttr.split(';');
|
||||
angular.forEach(styleArray, function(value){
|
||||
var v = value.split(':');
|
||||
if(v.length == 2){
|
||||
var key = trim(angular.lowercase(v[0]));
|
||||
var value = trim(angular.lowercase(v[1]));
|
||||
if(
|
||||
(key === 'color' || key === 'background-color') && (
|
||||
value.match(/^rgb\([0-9%,\. ]*\)$/i)
|
||||
|| value.match(/^rgba\([0-9%,\. ]*\)$/i)
|
||||
|| value.match(/^hsl\([0-9%,\. ]*\)$/i)
|
||||
|| value.match(/^hsla\([0-9%,\. ]*\)$/i)
|
||||
|| value.match(/^#[0-9a-f]{3,6}$/i)
|
||||
|| value.match(/^[a-z]*$/i)
|
||||
)
|
||||
||
|
||||
key === 'text-align' && (
|
||||
value === 'left'
|
||||
|| value === 'right'
|
||||
|| value === 'center'
|
||||
|| value === 'justify'
|
||||
)
|
||||
||
|
||||
key === 'float' && (
|
||||
value === 'left'
|
||||
|| value === 'right'
|
||||
|| value === 'none'
|
||||
)
|
||||
||
|
||||
(key === 'width' || key === 'height') && (
|
||||
value.match(/[0-9\.]*(px|em|rem|%)/)
|
||||
)
|
||||
) result += key + ': ' + value + ';';
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// this function is used to manually allow specific attributes on specific tags with certain prerequisites
|
||||
function validCustomTag(tag, attrs, lkey, value){
|
||||
// catch the div placeholder for the iframe replacement
|
||||
if (tag === 'img' && attrs['ta-insert-video']){
|
||||
if(lkey === 'ta-insert-video' || lkey === 'allowfullscreen' || lkey === 'frameborder' || (lkey === 'contenteditble' && value === 'false')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* create an HTML/XML writer which writes to buffer
|
||||
* @param {Array} buf use buf.jain('') to get out sanitized html string
|
||||
* @returns {object} in the form of {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* }
|
||||
*/
|
||||
function htmlSanitizeWriter(buf, uriValidator){
|
||||
var ignore = false;
|
||||
var out = angular.bind(buf, buf.push);
|
||||
return {
|
||||
start: function(tag, attrs, unary){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && specialElements[tag]) {
|
||||
ignore = tag;
|
||||
}
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
angular.forEach(attrs, function(value, key){
|
||||
var lkey=angular.lowercase(key);
|
||||
var isImage=(tag === 'img' && lkey === 'src') || (lkey === 'background');
|
||||
if ((lkey === 'style' && (value = validStyles(value)) !== '') || validCustomTag(tag, attrs, lkey, value) || validAttrs[lkey] === true &&
|
||||
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
out(unary ? '/>' : '>');
|
||||
}
|
||||
},
|
||||
end: function(tag){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
}
|
||||
if (tag == ignore) {
|
||||
ignore = false;
|
||||
}
|
||||
},
|
||||
chars: function(chars){
|
||||
if (!ignore) {
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// define ngSanitize module and register $sanitize service
|
||||
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
||||
|
||||
/* global sanitizeText: false */
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name linky
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
|
||||
* plain email address links.
|
||||
*
|
||||
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
|
||||
*
|
||||
* @param {string} text Input text.
|
||||
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
|
||||
* @returns {string} Html-linkified text.
|
||||
*
|
||||
* @usage
|
||||
<span ng-bind-html="linky_expression | linky"></span>
|
||||
*
|
||||
* @example
|
||||
<example module="linkyExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('linkyExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.snippet =
|
||||
'Pretty text with some links:\n'+
|
||||
'http://angularjs.org/,\n'+
|
||||
'mailto:us@somewhere.org,\n'+
|
||||
'another@somewhere.org,\n'+
|
||||
'and one more: ftp://127.0.0.1/.';
|
||||
$scope.snippetWithTarget = 'http://angularjs.org/';
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="linky-filter">
|
||||
<td>linky filter</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippet | linky"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippet | linky"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="linky-target">
|
||||
<td>linky target</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng-bind="snippet"><br></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should linkify the snippet with urls', function() {
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
|
||||
});
|
||||
|
||||
it('should not linkify snippet without the linky filter', function() {
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new http://link.');
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('new http://link.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
|
||||
.toBe('new http://link.');
|
||||
});
|
||||
|
||||
it('should work with the target property', function() {
|
||||
expect(element(by.id('linky-target')).
|
||||
element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
|
||||
toBe('http://angularjs.org/');
|
||||
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
||||
var LINKY_URL_REGEXP =
|
||||
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
|
||||
MAILTO_REGEXP = /^mailto:/;
|
||||
|
||||
return function(text, target) {
|
||||
if (!text) return text;
|
||||
var match;
|
||||
var raw = text;
|
||||
var html = [];
|
||||
var url;
|
||||
var i;
|
||||
while ((match = raw.match(LINKY_URL_REGEXP))) {
|
||||
// We can not end in these as they are sometimes found at the end of the sentence
|
||||
url = match[0];
|
||||
// if we did not match ftp/http/mailto then assume mailto
|
||||
if (match[2] == match[3]) url = 'mailto:' + url;
|
||||
i = match.index;
|
||||
addText(raw.substr(0, i));
|
||||
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
|
||||
raw = raw.substring(i + match[0].length);
|
||||
}
|
||||
addText(raw);
|
||||
return $sanitize(html.join(''));
|
||||
|
||||
function addText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
html.push(sanitizeText(text));
|
||||
}
|
||||
|
||||
function addLink(url, text) {
|
||||
html.push('<a ');
|
||||
if (angular.isDefined(target)) {
|
||||
html.push('target="');
|
||||
html.push(target);
|
||||
html.push('" ');
|
||||
}
|
||||
html.push('href="');
|
||||
html.push(url);
|
||||
html.push('">');
|
||||
addText(text);
|
||||
html.push('</a>');
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
})(window, window.angular);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,683 @@
|
||||
/*
|
||||
@license textAngular
|
||||
Author : Austin Anderson
|
||||
License : 2013 MIT
|
||||
Version 1.3.0-pre13
|
||||
|
||||
See README.md or https://github.com/fraywing/textAngular/wiki for requirements and use.
|
||||
*/
|
||||
angular.module('textAngularSetup', [])
|
||||
|
||||
// Here we set up the global display defaults, to set your own use a angular $provider#decorator.
|
||||
.value('taOptions', {
|
||||
toolbar: [
|
||||
['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'],
|
||||
['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'],
|
||||
['justifyLeft','justifyCenter','justifyRight','indent','outdent'],
|
||||
['html', 'insertImage', 'insertLink', 'insertVideo', 'wordcount', 'charcount']
|
||||
],
|
||||
classes: {
|
||||
focussed: "focussed",
|
||||
toolbar: "btn-toolbar",
|
||||
toolbarGroup: "btn-group",
|
||||
toolbarButton: "btn btn-default",
|
||||
toolbarButtonActive: "active",
|
||||
disabled: "disabled",
|
||||
textEditor: 'form-control',
|
||||
htmlEditor: 'form-control'
|
||||
},
|
||||
setup: {
|
||||
// wysiwyg mode
|
||||
textEditorSetup: function($element){ /* Do some processing here */ },
|
||||
// raw html
|
||||
htmlEditorSetup: function($element){ /* Do some processing here */ }
|
||||
},
|
||||
defaultFileDropHandler:
|
||||
/* istanbul ignore next: untestable image processing */
|
||||
function(file, insertAction){
|
||||
var reader = new FileReader();
|
||||
if(file.type.substring(0, 5) === 'image'){
|
||||
reader.onload = function() {
|
||||
if(reader.result !== '') insertAction('insertImage', reader.result, true);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
// NOTE: For async procedures return a promise and resolve it when the editor should update the model.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
// This is the element selector string that is used to catch click events within a taBind, prevents the default and $emits a 'ta-element-select' event
|
||||
// these are individually used in an angular.element().find() call. What can go here depends on whether you have full jQuery loaded or just jQLite with angularjs.
|
||||
// div is only used as div.ta-insert-video caught in filter.
|
||||
.value('taSelectableElements', ['a','img'])
|
||||
|
||||
// This is an array of objects with the following options:
|
||||
// selector: <string> a jqLite or jQuery selector string
|
||||
// customAttribute: <string> an attribute to search for
|
||||
// renderLogic: <function(element)>
|
||||
// Both or one of selector and customAttribute must be defined.
|
||||
.value('taCustomRenderers', [
|
||||
{
|
||||
// Parse back out: '<div class="ta-insert-video" ta-insert-video src="' + urlLink + '" allowfullscreen="true" width="300" frameborder="0" height="250"></div>'
|
||||
// To correct video element. For now only support youtube
|
||||
selector: 'img',
|
||||
customAttribute: 'ta-insert-video',
|
||||
renderLogic: function(element){
|
||||
var iframe = angular.element('<iframe></iframe>');
|
||||
var attributes = element.prop("attributes");
|
||||
// loop through element attributes and apply them on iframe
|
||||
angular.forEach(attributes, function(attr) {
|
||||
iframe.attr(attr.name, attr.value);
|
||||
});
|
||||
iframe.attr('src', iframe.attr('ta-insert-video'));
|
||||
element.replaceWith(iframe);
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
.value('taTranslations', {
|
||||
// moved to sub-elements
|
||||
//toggleHTML: "Toggle HTML",
|
||||
//insertImage: "Please enter a image URL to insert",
|
||||
//insertLink: "Please enter a URL to insert",
|
||||
//insertVideo: "Please enter a youtube URL to embed",
|
||||
html: {
|
||||
buttontext: 'Toggle HTML',
|
||||
tooltip: 'Toggle html / Rich Text'
|
||||
},
|
||||
// tooltip for heading - might be worth splitting
|
||||
heading: {
|
||||
tooltip: 'Heading '
|
||||
},
|
||||
p: {
|
||||
tooltip: 'Paragraph'
|
||||
},
|
||||
pre: {
|
||||
tooltip: 'Preformatted text'
|
||||
},
|
||||
ul: {
|
||||
tooltip: 'Unordered List'
|
||||
},
|
||||
ol: {
|
||||
tooltip: 'Ordered List'
|
||||
},
|
||||
quote: {
|
||||
tooltip: 'Quote/unqoute selection or paragraph'
|
||||
},
|
||||
undo: {
|
||||
tooltip: 'Undo'
|
||||
},
|
||||
redo: {
|
||||
tooltip: 'Redo'
|
||||
},
|
||||
bold: {
|
||||
tooltip: 'Bold'
|
||||
},
|
||||
italic: {
|
||||
tooltip: 'Italic'
|
||||
},
|
||||
underline: {
|
||||
tooltip: 'Underline'
|
||||
},
|
||||
strikeThrough:{
|
||||
tooltip: 'Strikethrough'
|
||||
},
|
||||
justifyLeft: {
|
||||
tooltip: 'Align text left'
|
||||
},
|
||||
justifyRight: {
|
||||
tooltip: 'Align text right'
|
||||
},
|
||||
justifyCenter: {
|
||||
tooltip: 'Center'
|
||||
},
|
||||
indent: {
|
||||
tooltip: 'Increase indent'
|
||||
},
|
||||
outdent: {
|
||||
tooltip: 'Decrease indent'
|
||||
},
|
||||
clear: {
|
||||
tooltip: 'Clear formatting'
|
||||
},
|
||||
insertImage: {
|
||||
dialogPrompt: 'Please enter an image URL to insert',
|
||||
tooltip: 'Insert image',
|
||||
hotkey: 'the - possibly language dependent hotkey ... for some future implementation'
|
||||
},
|
||||
insertVideo: {
|
||||
tooltip: 'Insert video',
|
||||
dialogPrompt: 'Please enter a youtube URL to embed'
|
||||
},
|
||||
insertLink: {
|
||||
tooltip: 'Insert / edit link',
|
||||
dialogPrompt: "Please enter a URL to insert"
|
||||
},
|
||||
wordcount: {
|
||||
tooltip: 'Display words Count'
|
||||
},
|
||||
charcount: {
|
||||
tooltip: 'Display characters Count'
|
||||
}
|
||||
})
|
||||
.run(['taRegisterTool', '$window', 'taTranslations', 'taSelection', function(taRegisterTool, $window, taTranslations, taSelection){
|
||||
taRegisterTool("html", {
|
||||
buttontext: taTranslations.html.buttontext,
|
||||
tooltiptext: taTranslations.html.tooltip,
|
||||
action: function(){
|
||||
this.$editor().switchView();
|
||||
},
|
||||
activeState: function(){
|
||||
return this.$editor().showHtml;
|
||||
}
|
||||
});
|
||||
// add the Header tools
|
||||
// convenience functions so that the loop works correctly
|
||||
var _retActiveStateFunction = function(q){
|
||||
return function(){ return this.$editor().queryFormatBlockState(q); };
|
||||
};
|
||||
var headerAction = function(){
|
||||
return this.$editor().wrapSelection("formatBlock", "<" + this.name.toUpperCase() +">");
|
||||
};
|
||||
angular.forEach(['h1','h2','h3','h4','h5','h6'], function(h){
|
||||
taRegisterTool(h.toLowerCase(), {
|
||||
buttontext: h.toUpperCase(),
|
||||
tooltiptext: taTranslations.heading.tooltip + h.charAt(1),
|
||||
action: headerAction,
|
||||
activeState: _retActiveStateFunction(h.toLowerCase())
|
||||
});
|
||||
});
|
||||
taRegisterTool('p', {
|
||||
buttontext: 'P',
|
||||
tooltiptext: taTranslations.p.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("formatBlock", "<P>");
|
||||
},
|
||||
activeState: function(){ return this.$editor().queryFormatBlockState('p'); }
|
||||
});
|
||||
// key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext
|
||||
taRegisterTool('pre', {
|
||||
buttontext: 'pre',
|
||||
tooltiptext: taTranslations.pre.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("formatBlock", "<PRE>");
|
||||
},
|
||||
activeState: function(){ return this.$editor().queryFormatBlockState('pre'); }
|
||||
});
|
||||
taRegisterTool('ul', {
|
||||
iconclass: 'fa fa-list-ul',
|
||||
tooltiptext: taTranslations.ul.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("insertUnorderedList", null);
|
||||
},
|
||||
activeState: function(){ return this.$editor().queryCommandState('insertUnorderedList'); }
|
||||
});
|
||||
taRegisterTool('ol', {
|
||||
iconclass: 'fa fa-list-ol',
|
||||
tooltiptext: taTranslations.ol.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("insertOrderedList", null);
|
||||
},
|
||||
activeState: function(){ return this.$editor().queryCommandState('insertOrderedList'); }
|
||||
});
|
||||
taRegisterTool('quote', {
|
||||
iconclass: 'fa fa-quote-right',
|
||||
tooltiptext: taTranslations.quote.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("formatBlock", "<BLOCKQUOTE>");
|
||||
},
|
||||
activeState: function(){ return this.$editor().queryFormatBlockState('blockquote'); }
|
||||
});
|
||||
taRegisterTool('undo', {
|
||||
iconclass: 'fa fa-undo',
|
||||
tooltiptext: taTranslations.undo.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("undo", null);
|
||||
}
|
||||
});
|
||||
taRegisterTool('redo', {
|
||||
iconclass: 'fa fa-repeat',
|
||||
tooltiptext: taTranslations.redo.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("redo", null);
|
||||
}
|
||||
});
|
||||
taRegisterTool('bold', {
|
||||
iconclass: 'fa fa-bold',
|
||||
tooltiptext: taTranslations.bold.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("bold", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return this.$editor().queryCommandState('bold');
|
||||
},
|
||||
commandKeyCode: 98
|
||||
});
|
||||
taRegisterTool('justifyLeft', {
|
||||
iconclass: 'fa fa-align-left',
|
||||
tooltiptext: taTranslations.justifyLeft.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("justifyLeft", null);
|
||||
},
|
||||
activeState: function(commonElement){
|
||||
var result = false;
|
||||
if(commonElement) result =
|
||||
commonElement.css('text-align') === 'left' ||
|
||||
commonElement.attr('align') === 'left' ||
|
||||
(
|
||||
commonElement.css('text-align') !== 'right' &&
|
||||
commonElement.css('text-align') !== 'center' &&
|
||||
commonElement.css('text-align') !== 'justify' &&
|
||||
!this.$editor().queryCommandState('justifyRight') &&
|
||||
!this.$editor().queryCommandState('justifyCenter')
|
||||
) && !this.$editor().queryCommandState('justifyFull');
|
||||
result = result || this.$editor().queryCommandState('justifyLeft');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
taRegisterTool('justifyRight', {
|
||||
iconclass: 'fa fa-align-right',
|
||||
tooltiptext: taTranslations.justifyRight.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("justifyRight", null);
|
||||
},
|
||||
activeState: function(commonElement){
|
||||
var result = false;
|
||||
if(commonElement) result = commonElement.css('text-align') === 'right';
|
||||
result = result || this.$editor().queryCommandState('justifyRight');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
taRegisterTool('justifyCenter', {
|
||||
iconclass: 'fa fa-align-center',
|
||||
tooltiptext: taTranslations.justifyCenter.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("justifyCenter", null);
|
||||
},
|
||||
activeState: function(commonElement){
|
||||
var result = false;
|
||||
if(commonElement) result = commonElement.css('text-align') === 'center';
|
||||
result = result || this.$editor().queryCommandState('justifyCenter');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
taRegisterTool('indent', {
|
||||
iconclass: 'fa fa-indent',
|
||||
tooltiptext: taTranslations.indent.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("indent", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return this.$editor().queryFormatBlockState('blockquote');
|
||||
}
|
||||
});
|
||||
taRegisterTool('outdent', {
|
||||
iconclass: 'fa fa-outdent',
|
||||
tooltiptext: taTranslations.outdent.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("outdent", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return false;
|
||||
}
|
||||
});
|
||||
taRegisterTool('italics', {
|
||||
iconclass: 'fa fa-italic',
|
||||
tooltiptext: taTranslations.italic.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("italic", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return this.$editor().queryCommandState('italic');
|
||||
},
|
||||
commandKeyCode: 105
|
||||
});
|
||||
taRegisterTool('underline', {
|
||||
iconclass: 'fa fa-underline',
|
||||
tooltiptext: taTranslations.underline.tooltip,
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("underline", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return this.$editor().queryCommandState('underline');
|
||||
},
|
||||
commandKeyCode: 117
|
||||
});
|
||||
taRegisterTool('strikeThrough', {
|
||||
iconclass: 'fa fa-strikethrough',
|
||||
action: function(){
|
||||
return this.$editor().wrapSelection("strikeThrough", null);
|
||||
},
|
||||
activeState: function(){
|
||||
return document.queryCommandState('strikeThrough');
|
||||
}
|
||||
});
|
||||
taRegisterTool('clear', {
|
||||
iconclass: 'fa fa-ban',
|
||||
tooltiptext: taTranslations.clear.tooltip,
|
||||
action: function(deferred, restoreSelection){
|
||||
var i;
|
||||
this.$editor().wrapSelection("removeFormat", null);
|
||||
var possibleNodes = angular.element(taSelection.getSelectionElement());
|
||||
// remove lists
|
||||
var removeListElements = function(list){
|
||||
list = angular.element(list);
|
||||
var prevElement = list;
|
||||
angular.forEach(list.children(), function(liElem){
|
||||
var newElem = angular.element('<p></p>');
|
||||
newElem.html(angular.element(liElem).html());
|
||||
prevElement.after(newElem);
|
||||
prevElement = newElem;
|
||||
});
|
||||
list.remove();
|
||||
};
|
||||
angular.forEach(possibleNodes.find("ul"), removeListElements);
|
||||
angular.forEach(possibleNodes.find("ol"), removeListElements);
|
||||
if(possibleNodes[0].tagName.toLowerCase() === 'li'){
|
||||
var _list = possibleNodes[0].parentNode.childNodes;
|
||||
var _preLis = [], _postLis = [], _found = false;
|
||||
for(i = 0; i < _list.length; i++){
|
||||
if(_list[i] === possibleNodes[0]){
|
||||
_found = true;
|
||||
}else if(!_found) _preLis.push(_list[i]);
|
||||
else _postLis.push(_list[i]);
|
||||
}
|
||||
var _parent = angular.element(possibleNodes[0].parentNode);
|
||||
var newElem = angular.element('<p></p>');
|
||||
newElem.html(angular.element(possibleNodes[0]).html());
|
||||
if(_preLis.length === 0 || _postLis.length === 0){
|
||||
if(_postLis.length === 0) _parent.after(newElem);
|
||||
else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]);
|
||||
|
||||
if(_preLis.length === 0 && _postLis.length === 0) _parent.remove();
|
||||
else angular.element(possibleNodes[0]).remove();
|
||||
}else{
|
||||
var _firstList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
|
||||
var _secondList = angular.element('<'+_parent[0].tagName+'></'+_parent[0].tagName+'>');
|
||||
for(i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i]));
|
||||
for(i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i]));
|
||||
_parent.after(_secondList);
|
||||
_parent.after(newElem);
|
||||
_parent.after(_firstList);
|
||||
_parent.remove();
|
||||
}
|
||||
taSelection.setSelectionToElementEnd(newElem[0]);
|
||||
}
|
||||
// clear out all class attributes. These do not seem to be cleared via removeFormat
|
||||
var $editor = this.$editor();
|
||||
var recursiveRemoveClass = function(node){
|
||||
node = angular.element(node);
|
||||
if(node[0] !== $editor.displayElements.text[0]) node.removeAttr('class');
|
||||
angular.forEach(node.children(), recursiveRemoveClass);
|
||||
};
|
||||
angular.forEach(possibleNodes, recursiveRemoveClass);
|
||||
// check if in list. If not in list then use formatBlock option
|
||||
if(possibleNodes[0].tagName.toLowerCase() !== 'li' &&
|
||||
possibleNodes[0].tagName.toLowerCase() !== 'ol' &&
|
||||
possibleNodes[0].tagName.toLowerCase() !== 'ul') this.$editor().wrapSelection("formatBlock", "default");
|
||||
restoreSelection();
|
||||
}
|
||||
});
|
||||
|
||||
var imgOnSelectAction = function(event, $element, editorScope){
|
||||
// setup the editor toolbar
|
||||
// Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic/display
|
||||
var finishEdit = function(){
|
||||
editorScope.updateTaBindtaTextElement();
|
||||
editorScope.hidePopover();
|
||||
};
|
||||
event.preventDefault();
|
||||
editorScope.displayElements.popover.css('width', '375px');
|
||||
var container = editorScope.displayElements.popoverContainer;
|
||||
container.empty();
|
||||
var buttonGroup = angular.element('<div class="btn-group" style="padding-right: 6px;">');
|
||||
var fullButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">100% </button>');
|
||||
fullButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.css({
|
||||
'width': '100%',
|
||||
'height': ''
|
||||
});
|
||||
finishEdit();
|
||||
});
|
||||
var halfButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">50% </button>');
|
||||
halfButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.css({
|
||||
'width': '50%',
|
||||
'height': ''
|
||||
});
|
||||
finishEdit();
|
||||
});
|
||||
var quartButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">25% </button>');
|
||||
quartButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.css({
|
||||
'width': '25%',
|
||||
'height': ''
|
||||
});
|
||||
finishEdit();
|
||||
});
|
||||
var resetButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1">Reset</button>');
|
||||
resetButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.css({
|
||||
width: '',
|
||||
height: ''
|
||||
});
|
||||
finishEdit();
|
||||
});
|
||||
buttonGroup.append(fullButton);
|
||||
buttonGroup.append(halfButton);
|
||||
buttonGroup.append(quartButton);
|
||||
buttonGroup.append(resetButton);
|
||||
container.append(buttonGroup);
|
||||
|
||||
buttonGroup = angular.element('<div class="btn-group" style="padding-right: 6px;">');
|
||||
var floatLeft = 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>');
|
||||
floatLeft.on('click', function(event){
|
||||
event.preventDefault();
|
||||
// webkit
|
||||
$element.css('float', 'left');
|
||||
// firefox
|
||||
$element.css('cssFloat', 'left');
|
||||
// IE < 8
|
||||
$element.css('styleFloat', 'left');
|
||||
finishEdit();
|
||||
});
|
||||
var floatRight = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-right"></i></button>');
|
||||
floatRight.on('click', function(event){
|
||||
event.preventDefault();
|
||||
// webkit
|
||||
$element.css('float', 'right');
|
||||
// firefox
|
||||
$element.css('cssFloat', 'right');
|
||||
// IE < 8
|
||||
$element.css('styleFloat', 'right');
|
||||
finishEdit();
|
||||
});
|
||||
var floatNone = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" unselectable="on" tabindex="-1"><i class="fa fa-align-justify"></i></button>');
|
||||
floatNone.on('click', function(event){
|
||||
event.preventDefault();
|
||||
// webkit
|
||||
$element.css('float', '');
|
||||
// firefox
|
||||
$element.css('cssFloat', '');
|
||||
// IE < 8
|
||||
$element.css('styleFloat', '');
|
||||
finishEdit();
|
||||
});
|
||||
buttonGroup.append(floatLeft);
|
||||
buttonGroup.append(floatNone);
|
||||
buttonGroup.append(floatRight);
|
||||
container.append(buttonGroup);
|
||||
|
||||
buttonGroup = angular.element('<div class="btn-group">');
|
||||
var remove = 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>');
|
||||
remove.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.remove();
|
||||
finishEdit();
|
||||
});
|
||||
buttonGroup.append(remove);
|
||||
container.append(buttonGroup);
|
||||
|
||||
editorScope.showPopover($element);
|
||||
editorScope.showResizeOverlay($element);
|
||||
};
|
||||
|
||||
taRegisterTool('insertImage', {
|
||||
iconclass: 'fa fa-picture-o',
|
||||
tooltiptext: taTranslations.insertImage.tooltip,
|
||||
action: function(){
|
||||
var imageLink;
|
||||
imageLink = $window.prompt(taTranslations.insertImage.dialogPrompt, 'http://');
|
||||
if(imageLink && imageLink !== '' && imageLink !== 'http://'){
|
||||
return this.$editor().wrapSelection('insertImage', imageLink, true);
|
||||
}
|
||||
},
|
||||
onElementSelect: {
|
||||
element: 'img',
|
||||
action: imgOnSelectAction
|
||||
}
|
||||
});
|
||||
taRegisterTool('insertVideo', {
|
||||
iconclass: 'fa fa-youtube-play',
|
||||
tooltiptext: taTranslations.insertVideo.tooltip,
|
||||
action: function(){
|
||||
var urlPrompt;
|
||||
urlPrompt = $window.prompt(taTranslations.insertVideo.dialogPrompt, 'https://');
|
||||
if (urlPrompt && urlPrompt !== '' && urlPrompt !== 'https://') {
|
||||
// get the video ID
|
||||
var ids = urlPrompt.match(/(\?|&)v=[^&]*/);
|
||||
/* istanbul ignore else: if it's invalid don't worry - though probably should show some kind of error message */
|
||||
if(ids.length > 0){
|
||||
// create the embed link
|
||||
var urlLink = "https://www.youtube.com/embed/" + ids[0].substring(3);
|
||||
// create the HTML
|
||||
// for all options see: http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
||||
// maxresdefault.jpg seems to be undefined on some.
|
||||
var embed = '<img class="ta-insert-video" src="https://img.youtube.com/vi/' + ids[0].substring(3) + '/hqdefault.jpg" ta-insert-video="' + urlLink + '" contenteditable="false" src="" allowfullscreen="true" frameborder="0" />';
|
||||
// insert
|
||||
return this.$editor().wrapSelection('insertHTML', embed, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
onElementSelect: {
|
||||
element: 'img',
|
||||
onlyWithAttrs: ['ta-insert-video'],
|
||||
action: imgOnSelectAction
|
||||
}
|
||||
});
|
||||
taRegisterTool('insertLink', {
|
||||
tooltiptext: taTranslations.insertLink.tooltip,
|
||||
iconclass: 'fa fa-link',
|
||||
action: function(){
|
||||
var urlLink;
|
||||
urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, 'http://');
|
||||
if(urlLink && urlLink !== '' && urlLink !== 'http://'){
|
||||
return this.$editor().wrapSelection('createLink', urlLink, true);
|
||||
}
|
||||
},
|
||||
activeState: function(commonElement){
|
||||
if(commonElement) return commonElement[0].tagName === 'A';
|
||||
return false;
|
||||
},
|
||||
onElementSelect: {
|
||||
element: 'a',
|
||||
action: function(event, $element, editorScope){
|
||||
// setup the editor toolbar
|
||||
// Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic
|
||||
event.preventDefault();
|
||||
editorScope.displayElements.popover.css('width', '435px');
|
||||
var container = editorScope.displayElements.popoverContainer;
|
||||
container.empty();
|
||||
container.css('line-height', '28px');
|
||||
var link = angular.element('<a href="' + $element.attr('href') + '" target="_blank">' + $element.attr('href') + '</a>');
|
||||
link.css({
|
||||
'display': 'inline-block',
|
||||
'max-width': '200px',
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'white-space': 'nowrap',
|
||||
'vertical-align': 'middle'
|
||||
});
|
||||
container.append(link);
|
||||
var buttonGroup = angular.element('<div class="btn-group pull-right">');
|
||||
var reLinkButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on"><i class="fa fa-edit icon-edit"></i></button>');
|
||||
reLinkButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
var urlLink = $window.prompt(taTranslations.insertLink.dialogPrompt, $element.attr('href'));
|
||||
if(urlLink && urlLink !== '' && urlLink !== 'http://'){
|
||||
$element.attr('href', urlLink);
|
||||
editorScope.updateTaBindtaTextElement();
|
||||
}
|
||||
editorScope.hidePopover();
|
||||
});
|
||||
buttonGroup.append(reLinkButton);
|
||||
var unLinkButton = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on"><i class="fa fa-unlink icon-unlink"></i></button>');
|
||||
// directly before this click event is fired a digest is fired off whereby the reference to $element is orphaned off
|
||||
unLinkButton.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.replaceWith($element.contents());
|
||||
editorScope.updateTaBindtaTextElement();
|
||||
editorScope.hidePopover();
|
||||
});
|
||||
buttonGroup.append(unLinkButton);
|
||||
var targetToggle = angular.element('<button type="button" class="btn btn-default btn-sm btn-small" tabindex="-1" unselectable="on">Open in New Window</button>');
|
||||
if($element.attr('target') === '_blank'){
|
||||
targetToggle.addClass('active');
|
||||
}
|
||||
targetToggle.on('click', function(event){
|
||||
event.preventDefault();
|
||||
$element.attr('target', ($element.attr('target') === '_blank') ? '' : '_blank');
|
||||
targetToggle.toggleClass('active');
|
||||
editorScope.updateTaBindtaTextElement();
|
||||
});
|
||||
buttonGroup.append(targetToggle);
|
||||
container.append(buttonGroup);
|
||||
editorScope.showPopover($element);
|
||||
}
|
||||
}
|
||||
});
|
||||
taRegisterTool('wordcount', {
|
||||
display: '<div id="toolbarWC" style="display:block; min-width:100px;">Words:{{wordcount}}</div>',
|
||||
disabled: true,
|
||||
wordcount: 0,
|
||||
activeState: function(){ // this fires on keyup
|
||||
var textElement = this.$editor().displayElements.text;
|
||||
var workingHTML = textElement[0].innerHTML;
|
||||
var sourceText = workingHTML.replace(/(<[^>]*?>)/ig, ' '); // replace all html tags with spaces
|
||||
|
||||
// Caculate number of words
|
||||
var sourceTextMatches = sourceText.match(/\S+/g);
|
||||
var noOfWords = sourceTextMatches && sourceTextMatches.length || 0;
|
||||
|
||||
//Set current scope
|
||||
this.wordcount = noOfWords;
|
||||
//Set editor scope
|
||||
this.$editor().wordcount = noOfWords;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
taRegisterTool('charcount', {
|
||||
display: '<div id="toolbarCC" style="display:block; min-width:120px;">Characters:{{charcount}}</div>',
|
||||
disabled: true,
|
||||
charcount: 0,
|
||||
activeState: function(){ // this fires on keyup
|
||||
var textElement = this.$editor().displayElements.text;
|
||||
var sourceText = textElement[0].innerText || textElement[0].textContent; // to cover the non-jquery use case.
|
||||
|
||||
// Caculate number of chars
|
||||
var noOfChars = sourceText.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g,' ').replace(/\s+$/g, ' ').length;
|
||||
//Set current scope
|
||||
this.charcount = noOfChars;
|
||||
//Set editor scope
|
||||
this.$editor().charcount = noOfChars;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}]);
|
||||
Reference in New Issue
Block a user