mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-18 15:50:22 +00:00
544 lines
17 KiB
JavaScript
544 lines
17 KiB
JavaScript
/**
|
||
* @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).
|
||
*
|
||
* @param {number} amount Input to filter.
|
||
* @returns {string} Formated 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>
|
||
<input type="text" name="amount" value="1234.56"/> <br/>
|
||
{{amount | currency}}
|
||
</doc:source>
|
||
<doc:scenario>
|
||
it('should init with 1234.56', function(){
|
||
expect(binding('amount | currency')).toBe('$1,234.56');
|
||
});
|
||
it('should update', function(){
|
||
input('amount').enter('-1234');
|
||
expect(binding('amount | currency')).toBe('$-1,234.00');
|
||
expect(element('.doc-example-live .ng-binding').attr('className')).
|
||
toMatch(/ng-format-negative/);
|
||
});
|
||
</doc:scenario>
|
||
</doc:example>
|
||
*/
|
||
angularFilter.currency = function(amount){
|
||
this.$element.toggleClass('ng-format-negative', amount < 0);
|
||
return '$' + angularFilter.number.apply(this, [amount, 2]);
|
||
};
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc filter
|
||
* @name angular.filter.number
|
||
* @function
|
||
*
|
||
* @description
|
||
* Formats a number as text.
|
||
*
|
||
* If the input is not a number 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>
|
||
Enter number: <input name='val' value='1234.56789' /><br/>
|
||
Default formatting: {{val | number}}<br/>
|
||
No fractions: {{val | number:0}}<br/>
|
||
Negative number: {{-val | number:4}}
|
||
</doc:source>
|
||
<doc:scenario>
|
||
it('should format numbers', function(){
|
||
expect(binding('val | number')).toBe('1,234.57');
|
||
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.33');
|
||
expect(binding('val | number:0')).toBe('3,374');
|
||
expect(binding('-val | number:4')).toBe('-3,374.3330');
|
||
});
|
||
</doc:scenario>
|
||
</doc:example>
|
||
*/
|
||
angularFilter.number = function(number, fractionSize){
|
||
if (isNaN(number) || !isFinite(number)) {
|
||
return '';
|
||
}
|
||
fractionSize = isUndefined(fractionSize)? 2 : fractionSize;
|
||
|
||
var isNegative = number < 0,
|
||
pow = Math.pow(10, fractionSize),
|
||
whole = '' + number,
|
||
formatedText = '',
|
||
i;
|
||
|
||
if (whole.indexOf('e') > -1) return whole;
|
||
|
||
number = Math.round(number * pow) / pow;
|
||
fraction = ('' + number).split('.');
|
||
whole = fraction[0];
|
||
fraction = fraction[1] || '';
|
||
if (isNegative) {
|
||
formatedText = '-';
|
||
whole = whole.substring(1);
|
||
}
|
||
|
||
|
||
for (i = 0; i < whole.length; i++) {
|
||
if ((whole.length - i)%3 === 0 && i !== 0) {
|
||
formatedText += ',';
|
||
}
|
||
formatedText += whole.charAt(i);
|
||
}
|
||
if (fractionSize) {
|
||
while(fraction.length < fractionSize) {
|
||
fraction += '0';
|
||
}
|
||
formatedText += '.' + fraction.substring(0, fractionSize);
|
||
}
|
||
return formatedText;
|
||
};
|
||
|
||
|
||
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);
|
||
};
|
||
}
|
||
|
||
|
||
var DATE_FORMATS = {
|
||
yyyy: dateGetter('FullYear', 4),
|
||
yy: dateGetter('FullYear', 2, 0, 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),
|
||
a: function(date){return date.getHours() < 12 ? 'am' : 'pm';},
|
||
Z: function(date){
|
||
var offset = date.getTimezoneOffset();
|
||
return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2);
|
||
}
|
||
};
|
||
|
||
|
||
var DATE_FORMATS_SPLIT = /([^yMdHhmsaZ]*)(y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/;
|
||
var 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. 2010
|
||
* * `'yy'`: 2 digit representation of year, padded (00-99)
|
||
* * `'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)
|
||
* * `'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)
|
||
*
|
||
* @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. 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:'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:'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) {
|
||
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) : value;
|
||
});
|
||
}
|
||
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>
|
||
<input type="text" name="objTxt" value="{a:1, b:[]}"
|
||
ng:eval="obj = $eval(objTxt)"/>
|
||
<pre>{{ obj | json }}</pre>
|
||
</doc:source>
|
||
<doc:scenario>
|
||
it('should jsonify filtered objects', function() {
|
||
expect(binding('obj | json')).toBe('{\n "a":1,\n "b":[]}');
|
||
});
|
||
|
||
it('should update', function() {
|
||
input('objTxt').enter('[1, 2, 3]');
|
||
expect(binding('obj | json')).toBe('[1,2,3]');
|
||
});
|
||
</doc:scenario>
|
||
</doc:example>
|
||
*
|
||
*/
|
||
angularFilter.json = function(object) {
|
||
this.$element.addClass("ng-monospace");
|
||
return toJson(object, true);
|
||
};
|
||
|
||
|
||
/**
|
||
* @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>
|
||
Snippet: <textarea name="snippet" cols="60" rows="3">
|
||
<p style="color:blue">an html
|
||
<em onmouseover="this.textContent='PWN3D!'">click here</em>
|
||
snippet</p></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>
|
||
</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
|
||
* plane email address links.
|
||
*
|
||
* @param {string} text Input text.
|
||
* @returns {string} Html-linkified text.
|
||
*
|
||
* @example
|
||
<doc:example>
|
||
<doc:source>
|
||
Snippet: <textarea name="snippet" cols="60" rows="3">
|
||
Pretty text with some links:
|
||
http://angularjs.org/,
|
||
mailto:us@somewhere.org,
|
||
another@somewhere.org,
|
||
and one more: ftp://127.0.0.1/.</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>
|
||
*/
|
||
//TODO: externalize all regexps
|
||
angularFilter.linky = function(text){
|
||
if (!text) return text;
|
||
var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/;
|
||
var match;
|
||
var raw = text;
|
||
var html = [];
|
||
var writer = htmlSanitizeWriter(html);
|
||
var url;
|
||
var i;
|
||
while (match=raw.match(URL)) {
|
||
// 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:/, ''));
|
||
writer.end('a');
|
||
raw = raw.substring(i + match[0].length);
|
||
}
|
||
writer.chars(raw);
|
||
return new HTML(html.join(''));
|
||
};
|