From 0df93fd49c1687b2eddaa79faa1c0adbef82bf72 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 7 Apr 2010 10:17:15 -0700 Subject: clean up, fixes for app --- src/Angular.js | 17 ++- src/Compiler.js | 2 +- src/Filters.js | 320 ----------------------------------------------------- src/Formatters.js | 23 ---- src/Scope.js | 2 +- src/Validators.js | 113 ------------------- src/Widgets.js | 204 ---------------------------------- src/angular.prefix | 2 +- src/angular.suffix | 8 ++ src/directives.js | 1 + src/filters.js | 320 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/formatters.js | 23 ++++ src/validators.js | 113 +++++++++++++++++++ src/widgets.js | 229 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 713 insertions(+), 664 deletions(-) delete mode 100644 src/Filters.js delete mode 100644 src/Formatters.js delete mode 100644 src/Validators.js delete mode 100644 src/Widgets.js create mode 100644 src/filters.js create mode 100644 src/formatters.js create mode 100644 src/validators.js create mode 100644 src/widgets.js (limited to 'src') diff --git a/src/Angular.js b/src/Angular.js index 2d67b2cb..3b5e1c90 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -102,7 +102,7 @@ function isTextNode(node) { return nodeName(node) == '#text'; } function lowercase(value){ return isString(value) ? value.toLowerCase() : value; } function uppercase(value){ return isString(value) ? value.toUpperCase() : value; } function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; } -function nodeName(element) { return (element[0] || element || {}).nodeName; } +function nodeName(element) { return (element[0] || element).nodeName; } function map(obj, iterator, context) { var results = []; foreach(obj, function(value, index, list) { @@ -274,6 +274,8 @@ function escapeAttr(html) { } function bind(_this, _function) { + if (!isFunction(_function)) + throw "Not a function!"; var curryArgs = slice.call(arguments, 2, arguments.length); return function() { return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); @@ -347,3 +349,16 @@ function angularInit(config){ scope.$init(); } } + +function angularJsConfig(document) { + var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/, + scripts = document.getElementsByTagName("SCRIPT"), + match; + for(var j = 0; j < scripts.length; j++) { + match = (scripts[j].src || "").match(filename); + if (match) { + return match[5]; + } + } + return ""; +} diff --git a/src/Compiler.js b/src/Compiler.js index 67c22461..ae2bcdb6 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -1,5 +1,5 @@ /** -= * Template provides directions an how to bind to a given element. + * Template provides directions an how to bind to a given element. * It contains a list of init functions which need to be called to * bind to a new instance of elements. It also provides a list * of child paths which contain child templates diff --git a/src/Filters.js b/src/Filters.js deleted file mode 100644 index dac8d31d..00000000 --- a/src/Filters.js +++ /dev/null @@ -1,320 +0,0 @@ -angularFilter.Meta = function(obj){ - if (obj) { - for ( var key in obj) { - this[key] = obj[key]; - } - } -}; -angularFilter.Meta.get = function(obj, attr){ - attr = attr || 'text'; - switch(typeof obj) { - case "string": - return attr == "text" ? obj : undefined; - case "object": - if (obj && typeof obj[attr] !== "undefined") { - return obj[attr]; - } - return undefined; - default: - return obj; - } -}; - -var angularFilterGoogleChartApi; - -foreach({ - 'currency': function(amount){ - jQuery(this.$element).toggleClass('ng-format-negative', amount < 0); - return '$' + angularFilter['number'].apply(this, [amount, 2]); - }, - - 'number': function(amount, fractionSize){ - if (isNaN(amount) || !isFinite(amount)) { - return ''; - } - fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; - var isNegative = amount < 0; - amount = Math.abs(amount); - var pow = Math.pow(10, fractionSize); - var text = "" + Math.round(amount * pow); - var whole = text.substring(0, text.length - fractionSize); - whole = whole || '0'; - var frc = text.substring(text.length - fractionSize); - text = isNegative ? '-' : ''; - for (var i = 0; i < whole.length; i++) { - if ((whole.length - i)%3 === 0 && i !== 0) { - text += ','; - } - text += whole.charAt(i); - } - if (fractionSize > 0) { - for (var j = frc.length; j < fractionSize; j++) { - frc += '0'; - } - text += '.' + frc.substring(0, fractionSize); - } - return text; - }, - - 'date': function(amount) { - }, - - 'json': function(object) { - jQuery(this.$element).addClass("ng-monospace"); - return toJson(object, true); - }, - - 'trackPackage': (function(){ - var MATCHERS = [ - { name: "UPS", - url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=", - regexp: [ - /^1Z[0-9A-Z]{16}$/i]}, - { name: "FedEx", - url: "http://www.fedex.com/Tracking?tracknumbers=", - regexp: [ - /^96\d{10}?$/i, - /^96\d{17}?$/i, - /^96\d{20}?$/i, - /^\d{15}$/i, - /^\d{12}$/i]}, - { name: "USPS", - url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=", - regexp: [ - /^(91\d{20})$/i, - /^(91\d{18})$/i]}]; - return function(trackingNo, noMatch) { - trackingNo = trim(trackingNo); - var tNo = trackingNo.replace(/ /g, ''); - var returnValue; - foreach(MATCHERS, function(carrier){ - foreach(carrier.regexp, function(regexp){ - if (!returnValue && regexp.test(tNo)) { - var text = carrier.name + ": " + trackingNo; - var url = carrier.url + trackingNo; - returnValue = new angularFilter.Meta({ - text:text, - url:url, - html: '' + text + '', - trackingNo:trackingNo}); - } - }); - }); - if (returnValue) - return returnValue; - else if (trackingNo) - return noMatch || new angularFilter.Meta({text:trackingNo + " is not recognized"}); - else - return null; - };})(), - - 'link': function(obj, title) { - var text = title || angularFilter.Meta.get(obj); - var url = angularFilter.Meta.get(obj, "url") || angularFilter.Meta.get(obj); - if (url) { - if (angular.validator.email(url) === null) { - url = "mailto:" + url; - } - var html = '' + text + ''; - return new angularFilter.Meta({text:text, url:url, html:html}); - } - return obj; - }, - - - 'bytes': (function(){ - var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - return function(size) { - if(size === null) return ""; - - var suffix = 0; - while (size > 1000) { - size = size / 1024; - suffix++; - } - var txt = "" + size; - var dot = txt.indexOf('.'); - if (dot > -1 && dot + 2 < txt.length) { - txt = txt.substring(0, dot + 2); - } - return txt + " " + SUFFIX[suffix]; - }; - })(), - - 'image': function(obj, width, height) { - if (obj && obj.url) { - var style = ""; - if (width) { - style = ' style="max-width: ' + width + - 'px; max-height: ' + (height || width) + 'px;"'; - } - return new angularFilter.Meta({url:obj.url, text:obj.url, - html:''}); - } - return null; - }, - - 'lowercase': function (obj) { - var text = angularFilter.Meta.get(obj); - return text ? ("" + text).toLowerCase() : text; - }, - - 'uppercase': function (obj) { - var text = angularFilter.Meta.get(obj); - return text ? ("" + text).toUpperCase() : text; - }, - - 'linecount': function (obj) { - var text = angularFilter.Meta.get(obj); - if (text==='' || !text) return 1; - return text.split(/\n|\f/).length; - }, - - 'if': function (result, expression) { - return expression ? result : undefined; - }, - - 'unless': function (result, expression) { - return expression ? undefined : result; - }, - - 'googleChartApi': extend( - function(type, data, width, height) { - data = data || {}; - var chart = { - 'cht':type, - 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), - 'chtt':angularFilterGoogleChartApi['title'](data), - 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), - 'chd':angularFilterGoogleChartApi['values'](data), - 'chf':'bg,s,FFFFFF00' - }; - if (_.isArray(data['xLabels'])) { - chart['chxt']='x'; - chart['chxl']='0:|' + data.xLabels.join('|'); - } - return angularFilterGoogleChartApi['encode'](chart, width, height); - }, - { - 'values': function(data){ - var seriesValues = []; - foreach(data['series']||[], function(serie){ - var values = []; - foreach(serie['values']||[], function(value){ - values.push(value); - }); - seriesValues.push(values.join(',')); - }); - var values = seriesValues.join('|'); - return values === "" ? null : "t:" + values; - }, - - 'title': function(data){ - var titles = []; - var title = data['title'] || []; - foreach(_.isArray(title)?title:[title], function(text){ - titles.push(encodeURIComponent(text)); - }); - return titles.join('|'); - }, - - 'collect': function(data, key){ - var outterValues = []; - var count = 0; - foreach(data['series']||[], function(serie){ - var innerValues = []; - var value = serie[key] || []; - foreach(_.isArray(value)?value:[value], function(color){ - innerValues.push(encodeURIComponent(color)); - count++; - }); - outterValues.push(innerValues.join('|')); - }); - return count?outterValues.join(','):null; - }, - - 'encode': function(params, width, height) { - width = width || 200; - height = height || width; - var url = "http://chart.apis.google.com/chart?"; - var urlParam = []; - params['chs'] = width + "x" + height; - foreach(params, function(value, key){ - if (value) { - urlParam.push(key + "=" + value); - } - }); - urlParam.sort(); - url += urlParam.join("&"); - return new angularFilter.Meta({url:url, - html:''}); - } - } - ), - - - 'qrcode': function(value, width, height) { - return angularFilterGoogleChartApi['encode']({ - 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); - }, - 'chart': { - 'pie':function(data, width, height) { - return angularFilterGoogleChartApi('p', data, width, height); - }, - 'pie3d':function(data, width, height) { - return angularFilterGoogleChartApi('p3', data, width, height); - }, - 'pieConcentric':function(data, width, height) { - return angularFilterGoogleChartApi('pc', data, width, height); - }, - 'barHorizontalStacked':function(data, width, height) { - return angularFilterGoogleChartApi('bhs', data, width, height); - }, - 'barHorizontalGrouped':function(data, width, height) { - return angularFilterGoogleChartApi('bhg', data, width, height); - }, - 'barVerticalStacked':function(data, width, height) { - return angularFilterGoogleChartApi('bvs', data, width, height); - }, - 'barVerticalGrouped':function(data, width, height) { - return angularFilterGoogleChartApi('bvg', data, width, height); - }, - 'line':function(data, width, height) { - return angularFilterGoogleChartApi('lc', data, width, height); - }, - 'sparkline':function(data, width, height) { - return angularFilterGoogleChartApi('ls', data, width, height); - }, - 'scatter':function(data, width, height) { - return angularFilterGoogleChartApi('s', data, width, height); - } - }, - - 'html': function(html){ - return new angularFilter.Meta({html:html}); - }, - - 'linky': function(text){ - if (!text) return text; - function regExpEscape(text) { - return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); - } - var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; - var match; - var raw = text; - var html = []; - while (match=raw.match(URL)) { - var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); - var i = raw.indexOf(url); - html.push(escapeHtml(raw.substr(0, i))); - html.push(''); - html.push(url); - html.push(''); - raw = raw.substring(i + url.length); - } - html.push(escapeHtml(raw)); - return new angularFilter.Meta({text:text, html:html.join('')}); - } -}, function(v,k){angularFilter[k] = v;}); - -angularFilterGoogleChartApi = angularFilter['googleChartApi']; diff --git a/src/Formatters.js b/src/Formatters.js deleted file mode 100644 index ee63c1a5..00000000 --- a/src/Formatters.js +++ /dev/null @@ -1,23 +0,0 @@ -function formater(format, parse) {return {'format':format, 'parse':parse || format};} -function toString(obj) {return isDefined(obj) ? "" + obj : obj;} -extend(angularFormatter, { - 'noop':formater(identity, identity), - 'boolean':formater(toString, toBoolean), - 'number':formater(toString, function(obj){return 1*obj;}), - - 'list':formater( - function(obj) { return obj ? obj.join(", ") : obj; }, - function(value) { - var list = []; - foreach((value || '').split(','), function(item){ - item = trim(item); - if (item) list.push(item); - }); - return list; - } - ), - - 'trim':formater( - function(obj) { return obj ? trim("" + obj) : ""; } - ) -}); diff --git a/src/Scope.js b/src/Scope.js index b41f7436..0bc551c4 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -17,7 +17,7 @@ function getter(instance, path, unboundFn) { type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; var fn = type ? type[[key.substring(1)]] : undefined; if (fn) { - instance = bind(fn, lastInstance, lastInstance); + instance = bind(lastInstance, fn, lastInstance); return instance; } } diff --git a/src/Validators.js b/src/Validators.js deleted file mode 100644 index e3da0a81..00000000 --- a/src/Validators.js +++ /dev/null @@ -1,113 +0,0 @@ -foreach({ - 'noop': noop, - - 'regexp': function(value, regexp, msg) { - if (!value.match(regexp)) { - return msg || - "Value does not match expected format " + regexp + "."; - } else { - return null; - } - }, - - 'number': function(value, min, max) { - var num = 1 * value; - if (num == value) { - if (typeof min != 'undefined' && num < min) { - return "Value can not be less than " + min + "."; - } - if (typeof min != 'undefined' && num > max) { - return "Value can not be greater than " + max + "."; - } - return null; - } else { - return "Not a number"; - } - }, - - 'integer': function(value, min, max) { - var numberError = angularValidator['number'](value, min, max); - if (numberError) return numberError; - if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { - return "Not a whole number"; - } - return null; - }, - - 'date': function(value, min, max) { - if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { - return null; - } - return "Value is not a date. (Expecting format: 12/31/2009)."; - }, - - 'ssn': function(value) { - if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { - return null; - } - return "SSN needs to be in 999-99-9999 format."; - }, - - 'email': function(value) { - if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { - return null; - } - return "Email needs to be in username@host.com format."; - }, - - 'phone': function(value) { - if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { - return null; - } - if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) { - return null; - } - return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; - }, - - 'url': function(value) { - if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { - return null; - } - return "URL needs to be in http://server[:port]/path format."; - }, - - 'json': function(value) { - try { - fromJson(value); - return null; - } catch (e) { - return e.toString(); - } - }, - - 'asynchronous': function(text, asynchronousFn) { - var element = this['$element']; - var cache = element.data('$validateState'); - if (!cache) { - cache = { state: {}}; - element.data('$validateState', cache); - } - var state = cache.state[text]; - cache.lastKey = text; - if (state === undefined) { - // we have never seen this before, Request it - element.addClass('ng-input-indicator-wait'); - state = cache.state[text] = null; - (asynchronousFn || noop)(text, function(error){ - state = cache.state[text] = error ? error : false; - if (cache.state[cache.lastKey] !== null) { - element.removeClass('ng-input-indicator-wait'); - } - elementError(element, NG_VALIDATION_ERROR, error); - }); - } - - if (state === null){ - // request in flight, mark widget invalid, but don't show it to user - (this['$invalidWidgets']||[]).push(this.$element); - } - return state; - } - -}, function(v,k) {angularValidator[k] = v;}); diff --git a/src/Widgets.js b/src/Widgets.js deleted file mode 100644 index 8e668c8f..00000000 --- a/src/Widgets.js +++ /dev/null @@ -1,204 +0,0 @@ -function modelAccessor(scope, element) { - var expr = element.attr('name'), - farmatterName = element.attr('ng-format') || NOOP, - formatter = angularFormatter(farmatterName); - if (!expr) throw "Required field 'name' not found."; - if (!formatter) throw "Formatter named '" + farmatterName + "' not found."; - return { - get: function() { - return formatter['format'](scope.$eval(expr)); - }, - set: function(value) { - scope.$tryEval(expr + '=' + toJson(formatter['parse'](value)), element); - } - }; -} - -function compileValidator(expr) { - return new Parser(expr).validator()(); -} - -function valueAccessor(scope, element) { - var validatorName = element.attr('ng-validate') || NOOP, - validator = compileValidator(validatorName), - required = element.attr('ng-required'), - lastError; - required = required || required === ''; - if (!validator) throw "Validator named '" + validatorName + "' not found."; - function validate(value) { - var error = required && !trim(value) ? "Required" : validator({self:scope, scope:{get:scope.$get, set:scope.$set}}, value); - if (error !== lastError) { - elementError(element, NG_VALIDATION_ERROR, error); - lastError = error; - } - return value; - } - return { - get: function(){ return validate(element.val()); }, - set: function(value){ element.val(validate(value)); } - }; -} - -function checkedAccessor(scope, element) { - var domElement = element[0]; - return { - get: function(){ - return !!domElement.checked; - }, - set: function(value){ - domElement.checked = !!value; - } - }; -} - -function radioAccessor(scope, element) { - var domElement = element[0]; - return { - get: function(){ - return domElement.checked ? domElement.value : null; - }, - set: function(value){ - domElement.checked = value == domElement.value; - } - }; -} - -function optionsAccessor(scope, element) { - var options = element[0].options; - return { - get: function(){ - var values = []; - foreach(options, function(option){ - if (option.selected) values.push(option.value); - }); - return values; - }, - set: function(values){ - var keys = {}; - foreach(values, function(value){ keys[value] = true; }); - foreach(options, function(option){ - option.selected = keys[option.value]; - }); - } - }; -} - -function noopAccessor() { return { get: noop, set: noop }; } - -var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue('')), - buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), - INPUT_TYPE = { - 'text': textWidget, - 'textarea': textWidget, - 'hidden': textWidget, - 'password': textWidget, - 'button': buttonWidget, - 'submit': buttonWidget, - 'reset': buttonWidget, - 'image': buttonWidget, - 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, initWidgetValue(false)), - 'radio': inputWidget('click', modelAccessor, radioAccessor, radioInit), - 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)), - 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([])) -// 'file': fileWidget??? - }; - -function initWidgetValue(initValue) { - return function (model, view) { - var value = view.get() || copy(initValue); - if (isUndefined(model.get()) && isDefined(value)) - model.set(value); - }; -} - -function radioInit(model, view, element) { - var modelValue = model.get(), viewValue = view.get(), input = element[0]; - input.name = this.$id + '@' + input.name; - if (isUndefined(modelValue)) model.set(null); - if (viewValue !== null) model.set(viewValue); -} - -function inputWidget(events, modelAccessor, viewAccessor, initFn) { - return function(element) { - var scope = this, - model = modelAccessor(scope, element), - view = viewAccessor(scope, element), - action = element.attr('ng-change') || ''; - initFn.call(scope, model, view, element); - this.$eval(element.attr('ng-init')||''); - // Don't register a handler if we are a button (noopAccessor) and there is no action - if (action || modelAccessor !== noopAccessor) { - element.bind(events, function(){ - model.set(view.get()); - scope.$tryEval(action, element); - scope.$root.$eval(); - // if we have noop initFn than we are just a button, - // therefore we want to prevent default action - return initFn != noop; - }); - } - view.set(model.get()); - scope.$watch(model.get, view.set); - }; -} - -function inputWidgetSelector(element){ - this.directives(true); - return INPUT_TYPE[lowercase(element[0].type)] || noop; -} - -angularWidget('INPUT', inputWidgetSelector); -angularWidget('TEXTAREA', inputWidgetSelector); -angularWidget('BUTTON', inputWidgetSelector); -angularWidget('SELECT', function(element){ - this.descend(true); - return inputWidgetSelector.call(this, element); -}); - - -angularWidget('NG:INCLUDE', function(element){ - var compiler = this, - src = element.attr("src"); - return element.attr('switch-instance') ? null : function(element){ - var scope = this, childScope; - element.attr('switch-instance', 'compiled'); - scope.$browser.xhr('GET', src, function(code, response){ - element.html(response); - childScope = createScope(scope); - compiler.compile(element)(element, childScope); - childScope.$init(); - }); - scope.$onEval(function(){ if (childScope) childScope.$eval(); }); - }; -}); - -angularWidget('NG:SWITCH', function(element){ - var compiler = this, - watchExpr = element.attr("on"), - cases = []; - eachNode(element, function(caseElement){ - var when = caseElement.attr('ng-switch-when'); - if (when) { - cases.push({ - when: function(value){ return value == when; }, - element: caseElement, - template: compiler.compile(caseElement) - }); - } - }); - element.html(''); - return function(element){ - var scope = this; - this.$watch(watchExpr, function(value){ - element.html(''); - foreach(cases, function(switchCase){ - if (switchCase.when(value)) { - element.append(switchCase.element); - var childScope = createScope(scope); - switchCase.template(switchCase.element, childScope); - childScope.$init(); - } - }); - }); - }; -}); diff --git a/src/angular.prefix b/src/angular.prefix index 0552b2ed..a1b4e151 100644 --- a/src/angular.prefix +++ b/src/angular.prefix @@ -21,4 +21,4 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -(function(window, document, onLoadDelegate){ +(function(window, document, previousOnLoad){ diff --git a/src/angular.suffix b/src/angular.suffix index c5754df2..36d73df2 100644 --- a/src/angular.suffix +++ b/src/angular.suffix @@ -1 +1,9 @@ + + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + angularInit(parseKeyValue(angularJsConfig(document))); + }; + })(window, document, window.onload); diff --git a/src/directives.js b/src/directives.js index d1b1dba3..5cee0978 100644 --- a/src/directives.js +++ b/src/directives.js @@ -12,6 +12,7 @@ angularDirective("ng-controller", function(expression){ if (!isFunction(controller)) throw "Reference '"+expression+"' is not a class."; this.$become(controller); + (this.init || noop)(); }; }); diff --git a/src/filters.js b/src/filters.js new file mode 100644 index 00000000..dac8d31d --- /dev/null +++ b/src/filters.js @@ -0,0 +1,320 @@ +angularFilter.Meta = function(obj){ + if (obj) { + for ( var key in obj) { + this[key] = obj[key]; + } + } +}; +angularFilter.Meta.get = function(obj, attr){ + attr = attr || 'text'; + switch(typeof obj) { + case "string": + return attr == "text" ? obj : undefined; + case "object": + if (obj && typeof obj[attr] !== "undefined") { + return obj[attr]; + } + return undefined; + default: + return obj; + } +}; + +var angularFilterGoogleChartApi; + +foreach({ + 'currency': function(amount){ + jQuery(this.$element).toggleClass('ng-format-negative', amount < 0); + return '$' + angularFilter['number'].apply(this, [amount, 2]); + }, + + 'number': function(amount, fractionSize){ + if (isNaN(amount) || !isFinite(amount)) { + return ''; + } + fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; + var isNegative = amount < 0; + amount = Math.abs(amount); + var pow = Math.pow(10, fractionSize); + var text = "" + Math.round(amount * pow); + var whole = text.substring(0, text.length - fractionSize); + whole = whole || '0'; + var frc = text.substring(text.length - fractionSize); + text = isNegative ? '-' : ''; + for (var i = 0; i < whole.length; i++) { + if ((whole.length - i)%3 === 0 && i !== 0) { + text += ','; + } + text += whole.charAt(i); + } + if (fractionSize > 0) { + for (var j = frc.length; j < fractionSize; j++) { + frc += '0'; + } + text += '.' + frc.substring(0, fractionSize); + } + return text; + }, + + 'date': function(amount) { + }, + + 'json': function(object) { + jQuery(this.$element).addClass("ng-monospace"); + return toJson(object, true); + }, + + 'trackPackage': (function(){ + var MATCHERS = [ + { name: "UPS", + url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=", + regexp: [ + /^1Z[0-9A-Z]{16}$/i]}, + { name: "FedEx", + url: "http://www.fedex.com/Tracking?tracknumbers=", + regexp: [ + /^96\d{10}?$/i, + /^96\d{17}?$/i, + /^96\d{20}?$/i, + /^\d{15}$/i, + /^\d{12}$/i]}, + { name: "USPS", + url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=", + regexp: [ + /^(91\d{20})$/i, + /^(91\d{18})$/i]}]; + return function(trackingNo, noMatch) { + trackingNo = trim(trackingNo); + var tNo = trackingNo.replace(/ /g, ''); + var returnValue; + foreach(MATCHERS, function(carrier){ + foreach(carrier.regexp, function(regexp){ + if (!returnValue && regexp.test(tNo)) { + var text = carrier.name + ": " + trackingNo; + var url = carrier.url + trackingNo; + returnValue = new angularFilter.Meta({ + text:text, + url:url, + html: '' + text + '', + trackingNo:trackingNo}); + } + }); + }); + if (returnValue) + return returnValue; + else if (trackingNo) + return noMatch || new angularFilter.Meta({text:trackingNo + " is not recognized"}); + else + return null; + };})(), + + 'link': function(obj, title) { + var text = title || angularFilter.Meta.get(obj); + var url = angularFilter.Meta.get(obj, "url") || angularFilter.Meta.get(obj); + if (url) { + if (angular.validator.email(url) === null) { + url = "mailto:" + url; + } + var html = '' + text + ''; + return new angularFilter.Meta({text:text, url:url, html:html}); + } + return obj; + }, + + + 'bytes': (function(){ + var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + return function(size) { + if(size === null) return ""; + + var suffix = 0; + while (size > 1000) { + size = size / 1024; + suffix++; + } + var txt = "" + size; + var dot = txt.indexOf('.'); + if (dot > -1 && dot + 2 < txt.length) { + txt = txt.substring(0, dot + 2); + } + return txt + " " + SUFFIX[suffix]; + }; + })(), + + 'image': function(obj, width, height) { + if (obj && obj.url) { + var style = ""; + if (width) { + style = ' style="max-width: ' + width + + 'px; max-height: ' + (height || width) + 'px;"'; + } + return new angularFilter.Meta({url:obj.url, text:obj.url, + html:''}); + } + return null; + }, + + 'lowercase': function (obj) { + var text = angularFilter.Meta.get(obj); + return text ? ("" + text).toLowerCase() : text; + }, + + 'uppercase': function (obj) { + var text = angularFilter.Meta.get(obj); + return text ? ("" + text).toUpperCase() : text; + }, + + 'linecount': function (obj) { + var text = angularFilter.Meta.get(obj); + if (text==='' || !text) return 1; + return text.split(/\n|\f/).length; + }, + + 'if': function (result, expression) { + return expression ? result : undefined; + }, + + 'unless': function (result, expression) { + return expression ? undefined : result; + }, + + 'googleChartApi': extend( + function(type, data, width, height) { + data = data || {}; + var chart = { + 'cht':type, + 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), + 'chtt':angularFilterGoogleChartApi['title'](data), + 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), + 'chd':angularFilterGoogleChartApi['values'](data), + 'chf':'bg,s,FFFFFF00' + }; + if (_.isArray(data['xLabels'])) { + chart['chxt']='x'; + chart['chxl']='0:|' + data.xLabels.join('|'); + } + return angularFilterGoogleChartApi['encode'](chart, width, height); + }, + { + 'values': function(data){ + var seriesValues = []; + foreach(data['series']||[], function(serie){ + var values = []; + foreach(serie['values']||[], function(value){ + values.push(value); + }); + seriesValues.push(values.join(',')); + }); + var values = seriesValues.join('|'); + return values === "" ? null : "t:" + values; + }, + + 'title': function(data){ + var titles = []; + var title = data['title'] || []; + foreach(_.isArray(title)?title:[title], function(text){ + titles.push(encodeURIComponent(text)); + }); + return titles.join('|'); + }, + + 'collect': function(data, key){ + var outterValues = []; + var count = 0; + foreach(data['series']||[], function(serie){ + var innerValues = []; + var value = serie[key] || []; + foreach(_.isArray(value)?value:[value], function(color){ + innerValues.push(encodeURIComponent(color)); + count++; + }); + outterValues.push(innerValues.join('|')); + }); + return count?outterValues.join(','):null; + }, + + 'encode': function(params, width, height) { + width = width || 200; + height = height || width; + var url = "http://chart.apis.google.com/chart?"; + var urlParam = []; + params['chs'] = width + "x" + height; + foreach(params, function(value, key){ + if (value) { + urlParam.push(key + "=" + value); + } + }); + urlParam.sort(); + url += urlParam.join("&"); + return new angularFilter.Meta({url:url, + html:''}); + } + } + ), + + + 'qrcode': function(value, width, height) { + return angularFilterGoogleChartApi['encode']({ + 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); + }, + 'chart': { + 'pie':function(data, width, height) { + return angularFilterGoogleChartApi('p', data, width, height); + }, + 'pie3d':function(data, width, height) { + return angularFilterGoogleChartApi('p3', data, width, height); + }, + 'pieConcentric':function(data, width, height) { + return angularFilterGoogleChartApi('pc', data, width, height); + }, + 'barHorizontalStacked':function(data, width, height) { + return angularFilterGoogleChartApi('bhs', data, width, height); + }, + 'barHorizontalGrouped':function(data, width, height) { + return angularFilterGoogleChartApi('bhg', data, width, height); + }, + 'barVerticalStacked':function(data, width, height) { + return angularFilterGoogleChartApi('bvs', data, width, height); + }, + 'barVerticalGrouped':function(data, width, height) { + return angularFilterGoogleChartApi('bvg', data, width, height); + }, + 'line':function(data, width, height) { + return angularFilterGoogleChartApi('lc', data, width, height); + }, + 'sparkline':function(data, width, height) { + return angularFilterGoogleChartApi('ls', data, width, height); + }, + 'scatter':function(data, width, height) { + return angularFilterGoogleChartApi('s', data, width, height); + } + }, + + 'html': function(html){ + return new angularFilter.Meta({html:html}); + }, + + 'linky': function(text){ + if (!text) return text; + function regExpEscape(text) { + return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); + } + var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; + var match; + var raw = text; + var html = []; + while (match=raw.match(URL)) { + var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); + var i = raw.indexOf(url); + html.push(escapeHtml(raw.substr(0, i))); + html.push(''); + html.push(url); + html.push(''); + raw = raw.substring(i + url.length); + } + html.push(escapeHtml(raw)); + return new angularFilter.Meta({text:text, html:html.join('')}); + } +}, function(v,k){angularFilter[k] = v;}); + +angularFilterGoogleChartApi = angularFilter['googleChartApi']; diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 00000000..ee63c1a5 --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,23 @@ +function formater(format, parse) {return {'format':format, 'parse':parse || format};} +function toString(obj) {return isDefined(obj) ? "" + obj : obj;} +extend(angularFormatter, { + 'noop':formater(identity, identity), + 'boolean':formater(toString, toBoolean), + 'number':formater(toString, function(obj){return 1*obj;}), + + 'list':formater( + function(obj) { return obj ? obj.join(", ") : obj; }, + function(value) { + var list = []; + foreach((value || '').split(','), function(item){ + item = trim(item); + if (item) list.push(item); + }); + return list; + } + ), + + 'trim':formater( + function(obj) { return obj ? trim("" + obj) : ""; } + ) +}); diff --git a/src/validators.js b/src/validators.js new file mode 100644 index 00000000..e3da0a81 --- /dev/null +++ b/src/validators.js @@ -0,0 +1,113 @@ +foreach({ + 'noop': noop, + + 'regexp': function(value, regexp, msg) { + if (!value.match(regexp)) { + return msg || + "Value does not match expected format " + regexp + "."; + } else { + return null; + } + }, + + 'number': function(value, min, max) { + var num = 1 * value; + if (num == value) { + if (typeof min != 'undefined' && num < min) { + return "Value can not be less than " + min + "."; + } + if (typeof min != 'undefined' && num > max) { + return "Value can not be greater than " + max + "."; + } + return null; + } else { + return "Not a number"; + } + }, + + 'integer': function(value, min, max) { + var numberError = angularValidator['number'](value, min, max); + if (numberError) return numberError; + if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { + return "Not a whole number"; + } + return null; + }, + + 'date': function(value, min, max) { + if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { + return null; + } + return "Value is not a date. (Expecting format: 12/31/2009)."; + }, + + 'ssn': function(value) { + if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { + return null; + } + return "SSN needs to be in 999-99-9999 format."; + }, + + 'email': function(value) { + if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { + return null; + } + return "Email needs to be in username@host.com format."; + }, + + 'phone': function(value) { + if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { + return null; + } + if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) { + return null; + } + return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; + }, + + 'url': function(value) { + if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { + return null; + } + return "URL needs to be in http://server[:port]/path format."; + }, + + 'json': function(value) { + try { + fromJson(value); + return null; + } catch (e) { + return e.toString(); + } + }, + + 'asynchronous': function(text, asynchronousFn) { + var element = this['$element']; + var cache = element.data('$validateState'); + if (!cache) { + cache = { state: {}}; + element.data('$validateState', cache); + } + var state = cache.state[text]; + cache.lastKey = text; + if (state === undefined) { + // we have never seen this before, Request it + element.addClass('ng-input-indicator-wait'); + state = cache.state[text] = null; + (asynchronousFn || noop)(text, function(error){ + state = cache.state[text] = error ? error : false; + if (cache.state[cache.lastKey] !== null) { + element.removeClass('ng-input-indicator-wait'); + } + elementError(element, NG_VALIDATION_ERROR, error); + }); + } + + if (state === null){ + // request in flight, mark widget invalid, but don't show it to user + (this['$invalidWidgets']||[]).push(this.$element); + } + return state; + } + +}, function(v,k) {angularValidator[k] = v;}); diff --git a/src/widgets.js b/src/widgets.js new file mode 100644 index 00000000..3e9ba236 --- /dev/null +++ b/src/widgets.js @@ -0,0 +1,229 @@ +function modelAccessor(scope, element) { + var expr = element.attr('name'), + farmatterName = element.attr('ng-format') || NOOP, + formatter = angularFormatter(farmatterName); + if (!expr) throw "Required field 'name' not found."; + if (!formatter) throw "Formatter named '" + farmatterName + "' not found."; + return { + get: function() { + return formatter['format'](scope.$eval(expr)); + }, + set: function(value) { + scope.$tryEval(expr + '=' + toJson(formatter['parse'](value)), element); + } + }; +} + +function compileValidator(expr) { + return new Parser(expr).validator()(); +} + +function valueAccessor(scope, element) { + var validatorName = element.attr('ng-validate') || NOOP, + validator = compileValidator(validatorName), + required = element.attr('ng-required'), + lastError; + required = required || required === ''; + if (!validator) throw "Validator named '" + validatorName + "' not found."; + function validate(value) { + var error = required && !trim(value) ? "Required" : validator({self:scope, scope:{get:scope.$get, set:scope.$set}}, value); + if (error !== lastError) { + elementError(element, NG_VALIDATION_ERROR, error); + lastError = error; + } + return value; + } + return { + get: function(){ return validate(element.val()); }, + set: function(value){ element.val(validate(value)); } + }; +} + +function checkedAccessor(scope, element) { + var domElement = element[0]; + return { + get: function(){ + return !!domElement.checked; + }, + set: function(value){ + domElement.checked = !!value; + } + }; +} + +function radioAccessor(scope, element) { + var domElement = element[0]; + return { + get: function(){ + return domElement.checked ? domElement.value : null; + }, + set: function(value){ + domElement.checked = value == domElement.value; + } + }; +} + +function optionsAccessor(scope, element) { + var options = element[0].options; + return { + get: function(){ + var values = []; + foreach(options, function(option){ + if (option.selected) values.push(option.value); + }); + return values; + }, + set: function(values){ + var keys = {}; + foreach(values, function(value){ keys[value] = true; }); + foreach(options, function(option){ + option.selected = keys[option.value]; + }); + } + }; +} + +function noopAccessor() { return { get: noop, set: noop }; } + +var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue('')), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), + INPUT_TYPE = { + 'text': textWidget, + 'textarea': textWidget, + 'hidden': textWidget, + 'password': textWidget, + 'button': buttonWidget, + 'submit': buttonWidget, + 'reset': buttonWidget, + 'image': buttonWidget, + 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, initWidgetValue(false)), + 'radio': inputWidget('click', modelAccessor, radioAccessor, radioInit), + 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)), + 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([])) +// 'file': fileWidget??? + }; + +function initWidgetValue(initValue) { + return function (model, view) { + var value = view.get() || copy(initValue); + if (isUndefined(model.get()) && isDefined(value)) + model.set(value); + }; +} + +function radioInit(model, view, element) { + var modelValue = model.get(), viewValue = view.get(), input = element[0]; + input.name = this.$id + '@' + input.name; + if (isUndefined(modelValue)) model.set(null); + if (viewValue !== null) model.set(viewValue); +} + +function inputWidget(events, modelAccessor, viewAccessor, initFn) { + return function(element) { + var scope = this, + model = modelAccessor(scope, element), + view = viewAccessor(scope, element), + action = element.attr('ng-change') || ''; + initFn.call(scope, model, view, element); + this.$eval(element.attr('ng-init')||''); + // Don't register a handler if we are a button (noopAccessor) and there is no action + if (action || modelAccessor !== noopAccessor) { + element.bind(events, function(){ + model.set(view.get()); + scope.$tryEval(action, element); + scope.$root.$eval(); + // if we have noop initFn than we are just a button, + // therefore we want to prevent default action + return initFn != noop; + }); + } + view.set(model.get()); + scope.$watch(model.get, view.set); + }; +} + +function inputWidgetSelector(element){ + this.directives(true); + return INPUT_TYPE[lowercase(element[0].type)] || noop; +} + +angularWidget('INPUT', inputWidgetSelector); +angularWidget('TEXTAREA', inputWidgetSelector); +angularWidget('BUTTON', inputWidgetSelector); +angularWidget('SELECT', function(element){ + this.descend(true); + return inputWidgetSelector.call(this, element); +}); + + +angularWidget('NG:INCLUDE', function(element){ + var compiler = this, + src = element.attr("src"); + if (element.attr('switch-instance')) { + this.descend(true); + this.directives(true); + } else { + return function(element){ + var scope = this, childScope; + element.attr('switch-instance', 'compiled'); + scope.$browser.xhr('GET', src, function(code, response){ + element.html(response); + childScope = createScope(scope); + compiler.compile(element)(element, childScope); + childScope.$init(); + scope.$root.$eval(); + }); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + }; + } +}); + +angularWidget('NG:SWITCH', function ngSwitch(element){ + var compiler = this, + watchExpr = element.attr("on"), + whenFn = ngSwitch[element.attr("using") || 'equals']; + changeExpr = element.attr('change') || '', + cases = []; + if (!whenFn) throw "Using expression '" + usingExpr + "' unknown."; + eachNode(element, function(caseElement){ + var when = caseElement.attr('ng-switch-when'); + if (when) { + cases.push({ + when: function(scope, value){ + return whenFn.call(scope, value, when); + }, + change: changeExpr, + element: caseElement, + template: compiler.compile(caseElement) + }); + } + }); + element.html(''); + return function(element){ + var scope = this, childScope; + this.$watch(watchExpr, function(value){ + element.html(''); + childScope = null; + foreach(cases, function(switchCase){ + if (switchCase.when(childScope, value)) { + element.append(switchCase.element); + childScope = createScope(scope); + childScope.$tryEval(switchCase.change, element); + switchCase.template(switchCase.element, childScope); + childScope.$init(); + } + }); + }); + scope.$onEval(function(){ + if (childScope) childScope.$eval(); + }); + }; +}, { + equals: function(on, when) { + return on == when; + }, + route: function(on, when) { + } +}); -- cgit v1.2.3