'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
default currency symbol ($): {{amount | currency}}
custom currency identifier (USD$): {{amount | currency:"USD$"}}
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').attr('className')). toMatch(/ng-format-negative/); });
*/ 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 Enter number:
Default formatting: {{val | number}}
No fractions: {{val | number:0}}
Negative number: {{-val | number:4}}
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'); });
*/ var DECIMAL_SEP = '.'; angularFilter.number = function(number, fractionSize) { if (isNaN(number) || !isFinite(number)) return ''; 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) { 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}(?![+\-])/; var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/ var OPERA_TOSTRING_PATTERN = /^[\d].*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. 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 {{1288323623006 | date:'medium'}}: {{1288323623006 | date:'medium'}}
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
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)/); });
*/ 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:
{{ {'name':'value'} | json }}
it('should jsonify filtered objects', function() { expect(binding('| json')).toBe('{\n "name":"value"}'); });
* */ 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 Snippet:
Filter Source Rendered
html filter
<div ng:bind="snippet | html">
</div>
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
it('should sanitize the html snippet ', function(){ expect(using('#html-filter').binding('snippet | html')). toBe('

an html\nclick here\nsnippet

'); }); 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("

an html\n" + "click here\n" + "snippet

"); }); it('should update', function(){ input('snippet').enter('new text'); expect(using('#html-filter').binding('snippet | html')).toBe('new text'); expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); });
*/ 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 Snippet:
Filter Source Rendered
linky filter
<div ng:bind="snippet | linky">
</div>
no filter
<div ng:bind="snippet">
</div>
it('should linkify the snippet with urls', function(){ expect(using('#linky-filter').binding('snippet | linky')). toBe('Pretty text with some links:\n' + 'http://angularjs.org/,\n' + 'us@somewhere.org,\n' + 'another@somewhere.org,\n' + 'and one more: ftp://127.0.0.1/.'); }); 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 http://link.'); expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); });
*/ 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('')); };