mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
654 lines
21 KiB
JavaScript
654 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc overview
|
|
* @name angular.filter
|
|
* @description
|
|
*
|
|
* Filters are used for formatting data displayed to the user.
|
|
*
|
|
* The general syntax in templates is as follows:
|
|
*
|
|
* {{ expression | [ filter_name ] }}
|
|
*
|
|
* Following is the list of built-in angular filters:
|
|
*
|
|
* * {@link angular.filter.currency currency}
|
|
* * {@link angular.filter.date date}
|
|
* * {@link angular.filter.html html}
|
|
* * {@link angular.filter.json json}
|
|
* * {@link angular.filter.linky linky}
|
|
* * {@link angular.filter.lowercase lowercase}
|
|
* * {@link angular.filter.number number}
|
|
* * {@link angular.filter.uppercase uppercase}
|
|
*
|
|
* For more information about how angular filters work, and how to create your own filters, see
|
|
* {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer
|
|
* Guide.
|
|
*/
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.currency
|
|
* @function
|
|
*
|
|
* @description
|
|
* Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default
|
|
* symbol for current locale is used.
|
|
*
|
|
* @param {number} amount Input to filter.
|
|
* @param {string=} symbol Currency symbol or identifier to be displayed.
|
|
* @returns {string} Formatted number.
|
|
*
|
|
* @css ng-format-negative
|
|
* When the value is negative, this css class is applied to the binding making it (by default) red.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl() {
|
|
this.amount = 1234.56;
|
|
}
|
|
</script>
|
|
<div ng:controller="Ctrl">
|
|
<input type="number" ng:model="amount"/> <br/>
|
|
default currency symbol ($): {{amount | currency}}<br/>
|
|
custom currency identifier (USD$): {{amount | currency:"USD$"}}
|
|
</div>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should init with 1234.56', function() {
|
|
expect(binding('amount | currency')).toBe('$1,234.56');
|
|
expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56');
|
|
});
|
|
it('should update', function() {
|
|
input('amount').enter('-1234');
|
|
expect(binding('amount | currency')).toBe('($1,234.00)');
|
|
expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)');
|
|
expect(element('.doc-example-live .ng-binding').prop('className')).
|
|
toMatch(/ng-format-negative/);
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
angularFilter.currency = function(amount, currencySymbol){
|
|
var formats = this.$service('$locale').NUMBER_FORMATS;
|
|
this.$element.toggleClass('ng-format-negative', amount < 0);
|
|
if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM;
|
|
return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2).
|
|
replace(/\u00A4/g, currencySymbol);
|
|
};
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.number
|
|
* @function
|
|
*
|
|
* @description
|
|
* Formats a number as text.
|
|
*
|
|
* If the input is not a number an empty string is returned.
|
|
*
|
|
* @param {number|string} number Number to format.
|
|
* @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to.
|
|
* @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl() {
|
|
this.val = 1234.56789;
|
|
}
|
|
</script>
|
|
<div ng:controller="Ctrl">
|
|
Enter number: <input ng:model='val'><br/>
|
|
Default formatting: {{val | number}}<br/>
|
|
No fractions: {{val | number:0}}<br/>
|
|
Negative number: {{-val | number:4}}
|
|
</div>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should format numbers', function() {
|
|
expect(binding('val | number')).toBe('1,234.568');
|
|
expect(binding('val | number:0')).toBe('1,235');
|
|
expect(binding('-val | number:4')).toBe('-1,234.5679');
|
|
});
|
|
|
|
it('should update', function() {
|
|
input('val').enter('3374.333');
|
|
expect(binding('val | number')).toBe('3,374.333');
|
|
expect(binding('val | number:0')).toBe('3,374');
|
|
expect(binding('-val | number:4')).toBe('-3,374.3330');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
|
|
var DECIMAL_SEP = '.';
|
|
|
|
angularFilter.number = function(number, fractionSize) {
|
|
var formats = this.$service('$locale').NUMBER_FORMATS;
|
|
return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP,
|
|
formats.DECIMAL_SEP, fractionSize);
|
|
};
|
|
|
|
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
|
|
if (isNaN(number) || !isFinite(number)) return '';
|
|
|
|
var isNegative = number < 0;
|
|
number = Math.abs(number);
|
|
var numStr = number + '',
|
|
formatedText = '',
|
|
parts = [];
|
|
|
|
if (numStr.indexOf('e') !== -1) {
|
|
formatedText = numStr;
|
|
} else {
|
|
var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
|
|
|
|
// determine fractionSize if it is not specified
|
|
if (isUndefined(fractionSize)) {
|
|
fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
|
|
}
|
|
|
|
var pow = Math.pow(10, fractionSize);
|
|
number = Math.round(number * pow) / pow;
|
|
var fraction = ('' + number).split(DECIMAL_SEP);
|
|
var whole = fraction[0];
|
|
fraction = fraction[1] || '';
|
|
|
|
var pos = 0,
|
|
lgroup = pattern.lgSize,
|
|
group = pattern.gSize;
|
|
|
|
if (whole.length >= (lgroup + group)) {
|
|
pos = whole.length - lgroup;
|
|
for (var i = 0; i < pos; i++) {
|
|
if ((pos - i)%group === 0 && i !== 0) {
|
|
formatedText += groupSep;
|
|
}
|
|
formatedText += whole.charAt(i);
|
|
}
|
|
}
|
|
|
|
for (i = pos; i < whole.length; i++) {
|
|
if ((whole.length - i)%lgroup === 0 && i !== 0) {
|
|
formatedText += groupSep;
|
|
}
|
|
formatedText += whole.charAt(i);
|
|
}
|
|
|
|
// format fraction part.
|
|
while(fraction.length < fractionSize) {
|
|
fraction += '0';
|
|
}
|
|
|
|
if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize);
|
|
}
|
|
|
|
parts.push(isNegative ? pattern.negPre : pattern.posPre);
|
|
parts.push(formatedText);
|
|
parts.push(isNegative ? pattern.negSuf : pattern.posSuf);
|
|
return parts.join('');
|
|
}
|
|
|
|
function padNumber(num, digits, trim) {
|
|
var neg = '';
|
|
if (num < 0) {
|
|
neg = '-';
|
|
num = -num;
|
|
}
|
|
num = '' + num;
|
|
while(num.length < digits) num = '0' + num;
|
|
if (trim)
|
|
num = num.substr(num.length - digits);
|
|
return neg + num;
|
|
}
|
|
|
|
|
|
function dateGetter(name, size, offset, trim) {
|
|
return function(date) {
|
|
var value = date['get' + name]();
|
|
if (offset > 0 || value > -offset)
|
|
value += offset;
|
|
if (value === 0 && offset == -12 ) value = 12;
|
|
return padNumber(value, size, trim);
|
|
};
|
|
}
|
|
|
|
function dateStrGetter(name, shortForm) {
|
|
return function(date, formats) {
|
|
var value = date['get' + name]();
|
|
var get = uppercase(shortForm ? ('SHORT' + name) : name);
|
|
|
|
return formats[get][value];
|
|
};
|
|
}
|
|
|
|
function timeZoneGetter(date) {
|
|
var offset = date.getTimezoneOffset();
|
|
return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2);
|
|
}
|
|
|
|
function ampmGetter(date, formats) {
|
|
return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
|
|
}
|
|
|
|
var DATE_FORMATS = {
|
|
yyyy: dateGetter('FullYear', 4),
|
|
yy: dateGetter('FullYear', 2, 0, true),
|
|
y: dateGetter('FullYear', 1),
|
|
MMMM: dateStrGetter('Month'),
|
|
MMM: dateStrGetter('Month', true),
|
|
MM: dateGetter('Month', 2, 1),
|
|
M: dateGetter('Month', 1, 1),
|
|
dd: dateGetter('Date', 2),
|
|
d: dateGetter('Date', 1),
|
|
HH: dateGetter('Hours', 2),
|
|
H: dateGetter('Hours', 1),
|
|
hh: dateGetter('Hours', 2, -12),
|
|
h: dateGetter('Hours', 1, -12),
|
|
mm: dateGetter('Minutes', 2),
|
|
m: dateGetter('Minutes', 1),
|
|
ss: dateGetter('Seconds', 2),
|
|
s: dateGetter('Seconds', 1),
|
|
EEEE: dateStrGetter('Day'),
|
|
EEE: dateStrGetter('Day', true),
|
|
a: ampmGetter,
|
|
Z: timeZoneGetter
|
|
};
|
|
|
|
var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/,
|
|
DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
|
|
OPERA_TOSTRING_PATTERN = /^[\d].*Z$/,
|
|
NUMBER_STRING = /^\d+$/;
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.date
|
|
* @function
|
|
*
|
|
* @description
|
|
* Formats `date` to a string based on the requested `format`.
|
|
*
|
|
* `format` string can be composed of the following elements:
|
|
*
|
|
* * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010)
|
|
* * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10)
|
|
* * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199)
|
|
* * `'MMMM'`: Month in year (January-December)
|
|
* * `'MMM'`: Month in year (Jan-Dec)
|
|
* * `'MM'`: Month in year, padded (01-12)
|
|
* * `'M'`: Month in year (1-12)
|
|
* * `'dd'`: Day in month, padded (01-31)
|
|
* * `'d'`: Day in month (1-31)
|
|
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
|
|
* * `'EEE'`: Day in Week, (Sun-Sat)
|
|
* * `'HH'`: Hour in day, padded (00-23)
|
|
* * `'H'`: Hour in day (0-23)
|
|
* * `'hh'`: Hour in am/pm, padded (01-12)
|
|
* * `'h'`: Hour in am/pm, (1-12)
|
|
* * `'mm'`: Minute in hour, padded (00-59)
|
|
* * `'m'`: Minute in hour (0-59)
|
|
* * `'ss'`: Second in minute, padded (00-59)
|
|
* * `'s'`: Second in minute (0-59)
|
|
* * `'a'`: am/pm marker
|
|
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200)
|
|
*
|
|
* `format` string can also be one of the following predefined
|
|
* {@link guide/dev_guide.i18n localizable formats}:
|
|
*
|
|
* * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
|
|
* (e.g. Sep 3, 2010 12:05:08 pm)
|
|
* * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm)
|
|
* * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale
|
|
* (e.g. Friday, September 3, 2010)
|
|
* * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010
|
|
* * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
|
|
* * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
|
|
* * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm)
|
|
* * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm)
|
|
*
|
|
* `format` string can contain literal values. These need to be quoted with single quotes (e.g.
|
|
* `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence
|
|
* (e.g. `"h o''clock"`).
|
|
*
|
|
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
|
|
* number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ).
|
|
* @param {string=} format Formatting rules (see Description). If not specified,
|
|
* Date#toLocaleDateString is used.
|
|
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<span ng:non-bindable>{{1288323623006 | date:'medium'}}</span>:
|
|
{{1288323623006 | date:'medium'}}<br/>
|
|
<span ng:non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>:
|
|
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}<br/>
|
|
<span ng:non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
|
|
{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}<br/>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should format date', function() {
|
|
expect(binding("1288323623006 | date:'medium'")).
|
|
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
|
|
expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).
|
|
toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/);
|
|
expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).
|
|
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
angularFilter.date = function(date, format) {
|
|
var $locale = this.$service('$locale');
|
|
format = $locale.DATETIME_FORMATS[format] || format;
|
|
if (isString(date)) {
|
|
if (NUMBER_STRING.test(date)) {
|
|
date = parseInt(date, 10);
|
|
} else {
|
|
date = angularString.toDate(date);
|
|
}
|
|
}
|
|
|
|
if (isNumber(date)) {
|
|
date = new Date(date);
|
|
}
|
|
|
|
if (!isDate(date)) {
|
|
return date;
|
|
}
|
|
|
|
var text = date.toLocaleDateString(), fn;
|
|
if (format && isString(format)) {
|
|
text = '';
|
|
var parts = [], match;
|
|
while(format) {
|
|
match = DATE_FORMATS_SPLIT.exec(format);
|
|
if (match) {
|
|
parts = concat(parts, match, 1);
|
|
format = parts.pop();
|
|
} else {
|
|
parts.push(format);
|
|
format = null;
|
|
}
|
|
}
|
|
forEach(parts, function(value){
|
|
fn = DATE_FORMATS[value];
|
|
text += fn ? fn(date, $locale.DATETIME_FORMATS)
|
|
: value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
|
|
});
|
|
}
|
|
return text;
|
|
};
|
|
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.json
|
|
* @function
|
|
*
|
|
* @description
|
|
* Allows you to convert a JavaScript object into JSON string.
|
|
*
|
|
* This filter is mostly useful for debugging. When using the double curly {{value}} notation
|
|
* the binding is automatically converted to JSON.
|
|
*
|
|
* @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
|
|
* @returns {string} JSON string.
|
|
*
|
|
* @css ng-monospace Always applied to the encapsulating element.
|
|
*
|
|
* @example:
|
|
<doc:example>
|
|
<doc:source>
|
|
<pre>{{ {'name':'value'} | json }}</pre>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should jsonify filtered objects', function() {
|
|
expect(binding('| json')).toBe('{\n "name":"value"}');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*
|
|
*/
|
|
angularFilter.json = function(object) {
|
|
this.$element.addClass("ng-monospace");
|
|
return toJson(object, true, /^(\$|this$)/);
|
|
};
|
|
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.lowercase
|
|
* @function
|
|
*
|
|
* @see angular.lowercase
|
|
*/
|
|
angularFilter.lowercase = lowercase;
|
|
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.uppercase
|
|
* @function
|
|
*
|
|
* @see angular.uppercase
|
|
*/
|
|
angularFilter.uppercase = uppercase;
|
|
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.html
|
|
* @function
|
|
*
|
|
* @description
|
|
* Prevents the input from getting escaped by angular. By default the input is sanitized and
|
|
* inserted into the DOM as is.
|
|
*
|
|
* 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.
|
|
*
|
|
* If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses
|
|
* the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this
|
|
* option is strongly discouraged and should be used only if you absolutely trust the input being
|
|
* filtered and you can't get the content through the sanitizer.
|
|
*
|
|
* @param {string} html Html input.
|
|
* @param {string=} option If 'unsafe' then do not sanitize the HTML input.
|
|
* @returns {string} Sanitized or raw html.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl() {
|
|
this.snippet =
|
|
'<p style="color:blue">an html\n' +
|
|
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
|
'snippet</p>';
|
|
}
|
|
</script>
|
|
<div ng:controller="Ctrl">
|
|
Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
|
|
<table>
|
|
<tr>
|
|
<td>Filter</td>
|
|
<td>Source</td>
|
|
<td>Rendered</td>
|
|
</tr>
|
|
<tr id="html-filter">
|
|
<td>html filter</td>
|
|
<td>
|
|
<pre><div ng:bind="snippet | html"><br/></div></pre>
|
|
</td>
|
|
<td>
|
|
<div ng:bind="snippet | html"></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>
|
|
<tr id="html-unsafe-filter">
|
|
<td>unsafe html filter</td>
|
|
<td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></pre></td>
|
|
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should sanitize the html snippet ', function() {
|
|
expect(using('#html-filter').binding('snippet | html')).
|
|
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
|
});
|
|
|
|
it('should escape snippet without any filter', function() {
|
|
expect(using('#escaped-html').binding('snippet')).
|
|
toBe("<p style=\"color:blue\">an html\n" +
|
|
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
|
"snippet</p>");
|
|
});
|
|
|
|
it('should inline raw snippet if filtered as unsafe', function() {
|
|
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).
|
|
toBe("<p style=\"color:blue\">an html\n" +
|
|
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
|
"snippet</p>");
|
|
});
|
|
|
|
it('should update', function() {
|
|
input('snippet').enter('new <b>text</b>');
|
|
expect(using('#html-filter').binding('snippet | html')).toBe('new <b>text</b>');
|
|
expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>");
|
|
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new <b>text</b>');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
angularFilter.html = function(html, option){
|
|
return new HTML(html, option);
|
|
};
|
|
|
|
|
|
/**
|
|
* @workInProgress
|
|
* @ngdoc filter
|
|
* @name angular.filter.linky
|
|
* @function
|
|
*
|
|
* @description
|
|
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
|
|
* plain email address links.
|
|
*
|
|
* @param {string} text Input text.
|
|
* @returns {string} Html-linkified text.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl() {
|
|
this.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/.';
|
|
}
|
|
</script>
|
|
<div ng:controller="Ctrl">
|
|
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="snippet | linky"><br/></div></pre>
|
|
</td>
|
|
<td>
|
|
<div ng:bind="snippet | linky"></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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should linkify the snippet with urls', function() {
|
|
expect(using('#linky-filter').binding('snippet | linky')).
|
|
toBe('Pretty text with some links:\n' +
|
|
'<a href="http://angularjs.org/">http://angularjs.org/</a>,\n' +
|
|
'<a href="mailto:us@somewhere.org">us@somewhere.org</a>,\n' +
|
|
'<a href="mailto:another@somewhere.org">another@somewhere.org</a>,\n' +
|
|
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
|
|
});
|
|
|
|
it ('should not linkify snippet without the linky filter', function() {
|
|
expect(using('#escaped-html').binding('snippet')).
|
|
toBe("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/.");
|
|
});
|
|
|
|
it('should update', function() {
|
|
input('snippet').enter('new http://link.');
|
|
expect(using('#linky-filter').binding('snippet | linky')).
|
|
toBe('new <a href="http://link">http://link</a>.');
|
|
expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,
|
|
MAILTO_REGEXP = /^mailto:/;
|
|
|
|
angularFilter.linky = function(text) {
|
|
if (!text) return text;
|
|
var match;
|
|
var raw = text;
|
|
var html = [];
|
|
var writer = htmlSanitizeWriter(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;
|
|
writer.chars(raw.substr(0, i));
|
|
writer.start('a', {href:url});
|
|
writer.chars(match[0].replace(MAILTO_REGEXP, ''));
|
|
writer.end('a');
|
|
raw = raw.substring(i + match[0].length);
|
|
}
|
|
writer.chars(raw);
|
|
return new HTML(html.join(''));
|
|
};
|