diff options
Diffstat (limited to 'src/widget')
| -rw-r--r-- | src/widget/form.js | 235 | ||||
| -rw-r--r-- | src/widget/input.js | 1171 | ||||
| -rw-r--r-- | src/widget/select.js | 444 |
3 files changed, 0 insertions, 1850 deletions
diff --git a/src/widget/form.js b/src/widget/form.js deleted file mode 100644 index e3823f41..00000000 --- a/src/widget/form.js +++ /dev/null @@ -1,235 +0,0 @@ -'use strict'; - - -/** - * @ngdoc object - * @name angular.module.ng.$compileProvider.directive.form.FormController - * - * @property {boolean} pristine True if user has not interacted with the form yet. - * @property {boolean} dirty True if user has already interacted with the form. - * @property {boolean} valid True if all of the containg widgets are valid. - * @property {boolean} invalid True if at least one containing widget is invalid. - * - * @property {Object} error Is an object hash, containing references to all invalid widgets, where - * - * - keys are error ids (such as `REQUIRED`, `URL` or `EMAIL`), - * - values are arrays of widgets that are invalid with given error. - * - * @description - * `FormController` keeps track of all its widgets as well as state of them form, such as being valid/invalid or dirty/pristine. - * - * Each {@link angular.module.ng.$compileProvider.directive.form form} directive creates an instance - * of `FormController`. - * - */ -FormController.$inject = ['$scope', 'name']; -function FormController($scope, name) { - var form = this, - errors = form.error = {}; - - // publish the form into scope - name(this); - - $scope.$on('$destroy', function(event, widget) { - if (!widget) return; - - if (widget.widgetId) { - delete form[widget.widgetId]; - } - forEach(errors, removeWidget, widget); - }); - - $scope.$on('$valid', function(event, error, widget) { - removeWidget(errors[error], error, widget); - - if (equals(errors, {})) { - form.valid = true; - form.invalid = false; - } - }); - - $scope.$on('$invalid', function(event, error, widget) { - addWidget(error, widget); - - form.valid = false; - form.invalid = true; - }); - - $scope.$on('$viewTouch', function() { - form.dirty = true; - form.pristine = false; - }); - - // init state - form.dirty = false; - form.pristine = true; - form.valid = true; - form.invalid = false; - - function removeWidget(queue, errorKey, widget) { - if (queue) { - widget = widget || this; // so that we can be used in forEach; - for (var i = 0, length = queue.length; i < length; i++) { - if (queue[i] === widget) { - queue.splice(i, 1); - if (!queue.length) { - delete errors[errorKey]; - } - } - } - } - } - - function addWidget(errorKey, widget) { - var queue = errors[errorKey]; - if (queue) { - for (var i = 0, length = queue.length; i < length; i++) { - if (queue[i] === widget) { - return; - } - } - } else { - errors[errorKey] = queue = []; - } - queue.push(widget); - } -} - -/** - * @ngdoc function - * @name angular.module.ng.$compileProvider.directive.form.FormController#registerWidget - * @methodOf angular.module.ng.$compileProvider.directive.form.FormController - * @function - * - * @param {Object} widget Widget to register (controller of a widget) - * @param {string=} alias Name alias of the widget. - * (If specified, widget will be accesible as a form property) - * - * @description - * - */ -FormController.prototype.registerWidget = function(widget, alias) { - if (alias && !this.hasOwnProperty(alias)) { - widget.widgetId = alias; - this[alias] = widget; - } -}; - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.form - * - * @scope - * @description - * Directive that instantiates - * {@link angular.module.ng.$compileProvider.directive.form.FormController FormController}. - * - * If `name` attribute is specified, the controller is published to the scope as well. - * - * # Alias: `ng:form` - * - * In angular forms can be nested. This means that the outer form is valid when all of the child - * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this - * reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows - * element nesting. - * - * - * # CSS classes - * - `ng-valid` Is set if the form is valid. - * - `ng-invalid` Is set if the form is invalid. - * - `ng-pristine` Is set if the form is pristine. - * - `ng-dirty` Is set if the form is dirty. - * - * - * # Submitting a form and preventing default action - * - * Since the role of forms in client-side Angular applications is different than in classical - * roundtrip apps, it is desirable for the browser not to translate the form submission into a full - * page reload that sends the data to the server. Instead some javascript logic should be triggered - * to handle the form submission in application specific way. - * - * For this reason, Angular prevents the default action (form submission to the server) unless the - * `<form>` element has an `action` attribute specified. - * - * You can use one of the following two ways to specify what javascript method should be called when - * a form is submitted: - * - * - ng:submit on the form element (add link to ng:submit) - * - ng:click on the first button or input field of type submit (input[type=submit]) - * - * To prevent double execution of the handler, use only one of ng:submit or ng:click. This is - * because of the following form submission rules coming from the html spec: - * - * - If a form has only one input field then hitting enter in this field triggers form submit - * (`ng:submit`) - * - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter - * doesn't trigger submit - * - if a form has one or more input fields and one or more buttons or input[type=submit] then - * hitting enter in any of the input fields will trigger the click handler on the *first* button or - * input[type=submit] (`ng:click`) *and* a submit handler on the enclosing form (`ng:submit`) - * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.text = 'guest'; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - text: <input type="text" name="input" ng:model="text" required> - <span class="error" ng:show="myForm.input.error.REQUIRED">Required!</span> - <tt>text = {{text}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('text')).toEqual('guest'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ -var formDirective = [function() { - return { - name: 'form', - restrict: 'E', - scope: true, - inject: { - name: 'accessor' - }, - controller: FormController, - compile: function() { - return { - pre: function(scope, formElement, attr, controller) { - formElement.data('$form', controller); - formElement.bind('submit', function(event) { - if (!attr.action) event.preventDefault(); - }); - - forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) { - scope.$watch(function() { - return controller[name]; - }, function(value) { - formElement[value ? 'addClass' : 'removeClass']('ng-' + name); - }); - }); - } - }; - } - }; -}]; diff --git a/src/widget/input.js b/src/widget/input.js deleted file mode 100644 index af446c6b..00000000 --- a/src/widget/input.js +++ /dev/null @@ -1,1171 +0,0 @@ -'use strict'; - -var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; - -var inputType = { - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.text - * - * @description - * Standard HTML text input with angular data binding. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.text = 'guest'; - $scope.word = /^\w*$/; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - Single word: <input type="text" name="input" ng:model="text" - ng:pattern="word" required> - <span class="error" ng:show="myForm.input.error.REQUIRED"> - Required!</span> - <span class="error" ng:show="myForm.input.error.PATTERN"> - Single word only!</span> - - <tt>text = {{text}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('text')).toEqual('guest'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - - it('should be invalid if multi word', function() { - input('text').enter('hello world'); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ - 'text': textInputType, - - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.number - * - * @description - * Text input with number validation and transformation. Sets the `NUMBER` validation - * error if not a valid number. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`. - * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.value = 12; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - Number: <input type="number" name="input" ng:model="value" - min="0" max="99" required> - <span class="error" ng:show="myForm.list.error.REQUIRED"> - Required!</span> - <span class="error" ng:show="myForm.list.error.NUMBER"> - Not valid number!</span> - <tt>value = {{value}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('value')).toEqual('12'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('value').enter(''); - expect(binding('value')).toEqual(''); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - - it('should be invalid if over max', function() { - input('value').enter('123'); - expect(binding('value')).toEqual('12'); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ - 'number': numberInputType, - - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.url - * - * @description - * Text input with URL validation. Sets the `URL` validation error key if the content is not a - * valid URL. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.text = 'http://google.com'; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - URL: <input type="url" name="input" ng:model="text" required> - <span class="error" ng:show="myForm.input.error.REQUIRED"> - Required!</span> - <span class="error" ng:show="myForm.input.error.url"> - Not valid url!</span> - <tt>text = {{text}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - <tt>myForm.error.url = {{!!myForm.error.url}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('text')).toEqual('http://google.com'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - - it('should be invalid if not url', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ - 'url': urlInputType, - - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.email - * - * @description - * Text input with email validation. Sets the `EMAIL` validation error key if not a valid email - * address. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.text = 'me@example.com'; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - Email: <input type="email" name="input" ng:model="text" required> - <span class="error" ng:show="myForm.input.error.REQUIRED"> - Required!</span> - <span class="error" ng:show="myForm.input.error.EMAIL"> - Not valid email!</span> - <tt>text = {{text}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - <tt>myForm.error.EMAIL = {{!!myForm.error.EMAIL}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('text')).toEqual('me@example.com'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - - it('should be invalid if not email', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ - 'email': emailInputType, - - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.radio - * - * @description - * HTML radio button. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string} value The value to which the expression should be set when selected. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.color = 'blue'; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - <input type="radio" ng:model="color" value="red"> Red <br/> - <input type="radio" ng:model="color" value="green"> Green <br/> - <input type="radio" ng:model="color" value="blue"> Blue <br/> - <tt>color = {{color}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should change state', function() { - expect(binding('color')).toEqual('blue'); - - input('color').select('red'); - expect(binding('color')).toEqual('red'); - }); - </doc:scenario> - </doc:example> - */ - 'radio': radioInputType, - - - /** - * @ngdoc inputType - * @name angular.module.ng.$compileProvider.directive.input.checkbox - * - * @description - * HTML checkbox. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} ng:true-value The value to which the expression should be set when selected. - * @param {string=} ng:false-value The value to which the expression should be set when not selected. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.value1 = true; - $scope.value2 = 'YES' - } - </script> - <form name="myForm" ng:controller="Ctrl"> - Value1: <input type="checkbox" ng:model="value1"> <br/> - Value2: <input type="checkbox" ng:model="value2" - ng:true-value="YES" ng:false-value="NO"> <br/> - <tt>value1 = {{value1}}</tt><br/> - <tt>value2 = {{value2}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should change state', function() { - expect(binding('value1')).toEqual('true'); - expect(binding('value2')).toEqual('YES'); - - input('value1').check(); - input('value2').check(); - expect(binding('value1')).toEqual('false'); - expect(binding('value2')).toEqual('NO'); - }); - </doc:scenario> - </doc:example> - */ - 'checkbox': checkboxInputType, - - 'hidden': noop, - 'button': noop, - 'submit': noop, - 'reset': noop -}; - - -function isEmpty(value) { - return isUndefined(value) || value === '' || value === null || value !== value; -} - - -function textInputType(scope, element, attr, ctrl) { - element.bind('blur', function() { - var touched = ctrl.touch(), - value = trim(element.val()); - - if (ctrl.viewValue !== value) { - scope.$apply(function() { - ctrl.read(value); - }); - } else if (touched) { - scope.$apply(); - } - }); - - ctrl.render = function() { - element.val(isEmpty(ctrl.viewValue) ? '' : ctrl.viewValue); - }; - - // pattern validator - var pattern = attr.ngPattern, - patternValidator; - - var emit = function(regexp, value) { - if (isEmpty(value) || regexp.test(value)) { - ctrl.setValidity('PATTERN', true); - return value; - } else { - ctrl.setValidity('PATTERN', false); - return undefined; - } - }; - - if (pattern) { - if (pattern.match(/^\/(.*)\/$/)) { - pattern = new RegExp(pattern.substr(1, pattern.length - 2)); - patternValidator = function(value) { - return emit(pattern, value) - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); - - if (!patternObj || !patternObj.test) { - throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj); - } - return emit(patternObj, value); - }; - } - - ctrl.formatters.push(patternValidator); - ctrl.parsers.push(patternValidator); - } - - // min length validator - if (attr.ngMinlength) { - var minlength = parseInt(attr.ngMinlength, 10); - var minLengthValidator = function(value) { - if (!isEmpty(value) && value.length < minlength) { - ctrl.setValidity('MINLENGTH', false); - return undefined; - } else { - ctrl.setValidity('MINLENGTH', true); - return value; - } - }; - - ctrl.parsers.push(minLengthValidator); - ctrl.formatters.push(minLengthValidator); - } - - // max length validator - if (attr.ngMaxlength) { - var maxlength = parseInt(attr.ngMaxlength, 10); - var maxLengthValidator = function(value) { - if (!isEmpty(value) && value.length > maxlength) { - ctrl.setValidity('MAXLENGTH', false); - return undefined; - } else { - ctrl.setValidity('MAXLENGTH', true); - return value; - } - }; - - ctrl.parsers.push(maxLengthValidator); - ctrl.formatters.push(maxLengthValidator); - } -}; - -function numberInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); - - ctrl.parsers.push(function(value) { - var empty = isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.setValidity('NUMBER', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.setValidity('NUMBER', false); - return undefined; - } - }); - - ctrl.formatters.push(function(value) { - return isEmpty(value) ? '' : '' + value; - }); - - if (attr.min) { - var min = parseFloat(attr.min); - var minValidator = function(value) { - if (!isEmpty(value) && value < min) { - ctrl.setValidity('MIN', false); - return undefined; - } else { - ctrl.setValidity('MIN', true); - return value; - } - }; - - ctrl.parsers.push(minValidator); - ctrl.formatters.push(minValidator); - } - - if (attr.max) { - var max = parseFloat(attr.max); - var maxValidator = function(value) { - if (!isEmpty(value) && value > max) { - ctrl.setValidity('MAX', false); - return undefined; - } else { - ctrl.setValidity('MAX', true); - return value; - } - }; - - ctrl.parsers.push(maxValidator); - ctrl.formatters.push(maxValidator); - } - - ctrl.formatters.push(function(value) { - - if (isEmpty(value) || isNumber(value)) { - ctrl.setValidity('NUMBER', true); - return value; - } else { - ctrl.setValidity('NUMBER', false); - return undefined; - } - }); -} - -function urlInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); - - var urlValidator = function(value) { - if (isEmpty(value) || URL_REGEXP.test(value)) { - ctrl.setValidity('URL', true); - return value; - } else { - ctrl.setValidity('URL', false); - return undefined; - } - }; - - ctrl.formatters.push(urlValidator); - ctrl.parsers.push(urlValidator); -} - -function emailInputType(scope, element, attr, ctrl) { - textInputType(scope, element, attr, ctrl); - - var emailValidator = function(value) { - if (isEmpty(value) || EMAIL_REGEXP.test(value)) { - ctrl.setValidity('EMAIL', true); - return value; - } else { - ctrl.setValidity('EMAIL', false); - return undefined; - } - }; - - ctrl.formatters.push(emailValidator); - ctrl.parsers.push(emailValidator); -} - -function radioInputType(scope, element, attr, ctrl) { - // correct the name - element.attr('name', attr.id + '@' + attr.name); - - element.bind('click', function() { - if (element[0].checked) { - scope.$apply(function() { - ctrl.touch(); - ctrl.read(attr.value); - }); - }; - }); - - ctrl.render = function() { - var value = attr.value; - element[0].checked = isDefined(value) && (value == ctrl.viewValue); - }; -} - -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; - - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; - - element.bind('click', function() { - scope.$apply(function() { - ctrl.touch(); - ctrl.read(element[0].checked); - }); - }); - - ctrl.render = function() { - element[0].checked = ctrl.viewValue; - }; - - ctrl.formatters.push(function(value) { - return value === trueValue; - }); - - ctrl.parsers.push(function(value) { - return value ? trueValue : falseValue; - }); -} - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.textarea - * - * @description - * HTML textarea element widget with angular data-binding. The data-binding and validation - * properties of this element are exactly the same as those of the - * {@link angular.module.ng.$compileProvider.directive.input input element}. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - */ - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.input - * - * @description - * HTML input element widget with angular data-binding. Input widget follows HTML5 input types - * and polyfills the HTML5 validation behavior for older browsers. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets is published. - * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered. - * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than - * minlength. - * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than - * maxlength. - * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. - * @param {string=} ng:change Angular expression to be executed when input changes due to user - * interaction with the input element. - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.user = {name: 'guest', last: 'visitor'}; - } - </script> - <div ng:controller="Ctrl"> - <form name="myForm"> - User name: <input type="text" name="userName" ng:model="user.name" required> - <span class="error" ng:show="myForm.userName.error.REQUIRED"> - Required!</span><br> - Last name: <input type="text" name="lastName" ng:model="user.last" - ng:minlength="3" ng:maxlength="10"> - <span class="error" ng:show="myForm.lastName.error.MINLENGTH"> - Too short!</span> - <span class="error" ng:show="myForm.lastName.error.MAXLENGTH"> - Too long!</span><br> - </form> - <hr> - <tt>user = {{user}}</tt><br/> - <tt>myForm.userName.valid = {{myForm.userName.valid}}</tt><br> - <tt>myForm.userName.error = {{myForm.userName.error}}</tt><br> - <tt>myForm.lastName.valid = {{myForm.lastName.valid}}</tt><br> - <tt>myForm.userName.error = {{myForm.lastName.error}}</tt><br> - <tt>myForm.valid = {{myForm.valid}}</tt><br> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br> - <tt>myForm.error.MINLENGTH = {{!!myForm.error.MINLENGTH}}</tt><br> - <tt>myForm.error.MAXLENGTH = {{!!myForm.error.MAXLENGTH}}</tt><br> - </div> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}'); - expect(binding('myForm.userName.valid')).toEqual('true'); - expect(binding('myForm.valid')).toEqual('true'); - }); - - it('should be invalid if empty when required', function() { - input('user.name').enter(''); - expect(binding('user')).toEqual('{"last":"visitor","name":null}'); - expect(binding('myForm.userName.valid')).toEqual('false'); - expect(binding('myForm.valid')).toEqual('false'); - }); - - it('should be valid if empty when min length is set', function() { - input('user.last').enter(''); - expect(binding('user')).toEqual('{"last":"","name":"guest"}'); - expect(binding('myForm.lastName.valid')).toEqual('true'); - expect(binding('myForm.valid')).toEqual('true'); - }); - - it('should be invalid if less than required min length', function() { - input('user.last').enter('xx'); - expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}'); - expect(binding('myForm.lastName.valid')).toEqual('false'); - expect(binding('myForm.lastName.error')).toMatch(/MINLENGTH/); - expect(binding('myForm.valid')).toEqual('false'); - }); - - it('should be valid if longer than max length', function() { - input('user.last').enter('some ridiculously long name'); - expect(binding('user')) - .toEqual('{"last":"visitor","name":"guest"}'); - expect(binding('myForm.lastName.valid')).toEqual('false'); - expect(binding('myForm.lastName.error')).toMatch(/MAXLENGTH/); - expect(binding('myForm.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ -var inputDirective = [function() { - return { - restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl); - } - } - }; -}]; - - -/** - * @ngdoc object - * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController - * - * @property {string} viewValue Actual string value in the view. - * @property {*} modelValue The value in the model, that the widget is bound to. - * @property {Array.<Function>} parsers Whenever the widget reads value from the DOM, it executes - * all of these functions to sanitize / convert the value as well as validate. - * - * @property {Array.<Function>} formatters Wheneveer the model value changes, it executes all of - * these functions to convert the value as well as validate. - * - * @property {Object} error An bject hash with all errors as keys. - * - * @property {boolean} pristine True if user has not interacted with the widget yet. - * @property {boolean} dirty True if user has already interacted with the widget. - * @property {boolean} valid True if there is no error. - * @property {boolean} invalid True if at least one error on the widget. - * - * @description - * - */ -var NgModelController = ['$scope', '$exceptionHandler', 'ngModel', - function($scope, $exceptionHandler, ngModel) { - this.viewValue = Number.NaN; - this.modelValue = Number.NaN; - this.parsers = []; - this.formatters = []; - this.error = {}; - this.pristine = true; - this.dirty = false; - this.valid = true; - this.invalid = false; - this.render = noop; - - - /** - * @ngdoc function - * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#touch - * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController - * - * @return {boolean} Whether it did change state. - * - * @description - * This method should be called from within a DOM event handler. - * For example {@link angular.module.ng.$compileProvider.directive.input input} or - * {@link angular.module.ng.$compileProvider.directive.select select} directives call it. - * - * It changes state to `dirty` and emits `$viewTouch` event if the state was `pristine` before. - */ - this.touch = function() { - if (this.dirty) return false; - - this.dirty = true; - this.pristine = false; - try { - $scope.$emit('$viewTouch'); - } catch (e) { - $exceptionHandler(e); - } - return true; - }; - - - /** - * @ngdoc function - * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#setValidity - * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController - * - * @description - * Change the validity state, and notifies the form when the widget changes validity. (i.e. does - * not emit `$invalid` if given validator is already marked as invalid). - * - * This method should be called by validators - ie the parser or formatter method. - * - * @param {string} name Name of the validator. - * @param {boolean} isValid Whether it should $emit `$valid` (true) or `$invalid` (false) event. - */ - this.setValidity = function(name, isValid) { - - if (!isValid && this.error[name]) return; - if (isValid && !this.error[name]) return; - - if (isValid) { - delete this.error[name]; - if (equals(this.error, {})) { - this.valid = true; - this.invalid = false; - } - } else { - this.error[name] = true; - this.invalid = true; - this.valid = false; - } - - return $scope.$emit(isValid ? '$valid' : '$invalid', name, this); - }; - - - /** - * @ngdoc function - * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#read - * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController - * - * @description - * Read a value from view. - * - * This method should be called from within a DOM event handler. - * For example {@link angular.module.ng.$compileProvider.directive.input input} or - * {@link angular.module.ng.$compileProvider.directive.select select} directives call it. - * - * It internally calls all `formatters` and if resulted value is valid, update the model and emits - * `$viewChange` event afterwards. - * - * @param {string} value Value from the view - */ - this.read = function(value) { - this.viewValue = value; - - forEach(this.parsers, function(fn) { - value = fn(value); - }); - - if (isDefined(value) && this.model !== value) { - this.modelValue = value; - ngModel(value); - $scope.$emit('$viewChange', value, this); - } - }; - - // model -> value - var ctrl = this; - $scope.$watch(function() { - return ngModel(); - }, function(value) { - - // ignore change from view - if (ctrl.modelValue === value) return; - - var formatters = ctrl.formatters, - idx = formatters.length; - - ctrl.modelValue = value; - while(idx--) { - value = formatters[idx](value); - } - - if (isDefined(value) && ctrl.viewValue !== value) { - ctrl.viewValue = value; - ctrl.render(); - } - }); -}]; - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.ng:model - * - * @element input - * - * @description - * Is directive that tells Angular to do two-way data binding. It works together with `input`, - * `select`, `textarea`. You can easily write your own directives to use `ng:model` as well. - * - * `ng:model` is responsible for: - * - * - binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require, - * - providing validation behavior (i.e. required, number, email, url), - * - keeping state of the widget (valid/invalid, dirty/pristine, validation errors), - * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`), - * - register the widget with parent {@link angular.module.ng.$compileProvider.directive.form form}. - * - * For basic examples, how to use `ng:model`, see: - * - * - {@link angular.module.ng.$compileProvider.directive.input input} - * - {@link angular.module.ng.$compileProvider.directive.input.text text} - * - {@link angular.module.ng.$compileProvider.directive.input.checkbox checkbox} - * - {@link angular.module.ng.$compileProvider.directive.input.radio radio} - * - {@link angular.module.ng.$compileProvider.directive.input.number number} - * - {@link angular.module.ng.$compileProvider.directive.input.email email} - * - {@link angular.module.ng.$compileProvider.directive.input.url url} - * - {@link angular.module.ng.$compileProvider.directive.select select} - * - {@link angular.module.ng.$compileProvider.directive.textarea textarea} - * - */ -var ngModelDirective = [function() { - return { - inject: { - ngModel: 'accessor' - }, - require: ['ngModel', '^?form'], - controller: NgModelController, - link: function(scope, element, attr, controllers) { - var modelController = controllers[0], - formController = controllers[1]; - - if (formController) { - formController.registerWidget(modelController, attr.name); - } - - forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { - scope.$watch(function() { - return modelController[name]; - }, function(value) { - element[value ? 'addClass' : 'removeClass']('ng-' + name); - }); - }); - - element.bind('$destroy', function() { - scope.$emit('$destroy', modelController); - }); - } - }; -}]; - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.ng:change - * - * @description - * Evaluate given expression when user changes the input. - * The expression is not evaluated when the value change is coming from the model. - * - * Note, this directive requires `ng:model` to be present. - * - * @element input - * - * @example - * <doc:example> - * <doc:source> - * <script> - * function Controller($scope) { - * $scope.counter = 0; - * $scope.change = function() { - * $scope.counter++; - * }; - * } - * </script> - * <div ng:controller="Controller"> - * <input type="checkbox" ng:model="confirmed" ng:change="change()" id="ng-change-example1" /> - * <input type="checkbox" ng:model="confirmed" id="ng-change-example2" /> - * <label for="ng-change-example2">Confirmed</label><br /> - * debug = {{confirmed}}<br /> - * counter = {{counter}} - * </div> - * </doc:source> - * <doc:scenario> - * it('should evaluate the expression if changing from view', function() { - * expect(binding('counter')).toEqual('0'); - * element('#ng-change-example1').click(); - * expect(binding('counter')).toEqual('1'); - * expect(binding('confirmed')).toEqual('true'); - * }); - * - * it('should not evaluate the expression if changing from model', function() { - * element('#ng-change-example2').click(); - * expect(binding('counter')).toEqual('0'); - * expect(binding('confirmed')).toEqual('true'); - * }); - * </doc:scenario> - * </doc:example> - */ -var ngChangeDirective = valueFn({ - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - scope.$on('$viewChange', function(event, value, widget) { - if (ctrl === widget) scope.$eval(attr.ngChange); - }); - } -}); - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.ng:model-instant - * - * @element input - * - * @description - * By default, Angular udpates the model only on `blur` event - when the input looses focus. - * If you want to update after every key stroke, use `ng:model-instant`. - * - * @example - * <doc:example> - * <doc:source> - * First name: <input type="text" ng:model="firstName" /><br /> - * Last name: <input type="text" ng:model="lastName" ng:model-instant /><br /> - * - * First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}}) - * is updated immediately, because of using `ng:model-instant`. - * </doc:source> - * <doc:scenario> - * it('should update first name on blur', function() { - * input('firstName').enter('santa', 'blur'); - * expect(binding('firstName')).toEqual('santa'); - * }); - * - * it('should update last name immediately', function() { - * input('lastName').enter('santa', 'keydown'); - * expect(binding('lastName')).toEqual('santa'); - * }); - * </doc:scenario> - * </doc:example> - */ -var ngModelInstantDirective = ['$browser', function($browser) { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var handler = function() { - var touched = ctrl.touch(), - value = trim(element.val()); - - if (ctrl.viewValue !== value) { - scope.$apply(function() { - ctrl.read(value); - }); - } else if (touched) { - scope.$apply(); - } - }; - - var timeout; - element.bind('keydown', function(event) { - var key = event.keyCode; - - // command modifiers arrows - if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - - if (!timeout) { - timeout = $browser.defer(function() { - handler(); - timeout = null; - }); - } - }); - - element.bind('change input', handler); - } - }; -}]; - - -var requiredDirective = [function() { - return { - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - - var validator = function(value) { - if (attr.required && isEmpty(value)) { - ctrl.setValidity('REQUIRED', false); - return null; - } else { - ctrl.setValidity('REQUIRED', true); - return value; - } - }; - - ctrl.formatters.push(validator); - ctrl.parsers.unshift(validator); - - attr.$observe('required', function() { - validator(ctrl.viewValue); - }); - } - }; -}]; - - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.ng:list - * - * @description - * Text input that converts between comma-seperated string into an array of strings. - * - * @element input - * - * @example - <doc:example> - <doc:source> - <script> - function Ctrl($scope) { - $scope.names = ['igor', 'misko', 'vojta']; - } - </script> - <form name="myForm" ng:controller="Ctrl"> - List: <input name="input" ng:model="names" ng:list required> - <span class="error" ng:show="myForm.list.error.REQUIRED"> - Required!</span> - <tt>names = {{names}}</tt><br/> - <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/> - <tt>myForm.input.error = {{myForm.input.error}}</tt><br/> - <tt>myForm.valid = {{myForm.valid}}</tt><br/> - <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/> - </form> - </doc:source> - <doc:scenario> - it('should initialize to model', function() { - expect(binding('names')).toEqual('["igor","misko","vojta"]'); - expect(binding('myForm.input.valid')).toEqual('true'); - }); - - it('should be invalid if empty', function() { - input('names').enter(''); - expect(binding('names')).toEqual('[]'); - expect(binding('myForm.input.valid')).toEqual('false'); - }); - </doc:scenario> - </doc:example> - */ -var ngListDirective = function() { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var parse = function(viewValue) { - var list = []; - - if (viewValue) { - forEach(viewValue.split(/\s*,\s*/), function(value) { - if (value) list.push(value); - }); - } - - return list; - }; - - ctrl.parsers.push(parse); - ctrl.formatters.push(function(value) { - if (isArray(value) && !equals(parse(ctrl.viewValue), value)) { - return value.join(', '); - } - - return undefined; - }); - } - }; -}; diff --git a/src/widget/select.js b/src/widget/select.js deleted file mode 100644 index 5ed1367f..00000000 --- a/src/widget/select.js +++ /dev/null @@ -1,444 +0,0 @@ -'use strict'; - -/** - * @ngdoc directive - * @name angular.module.ng.$compileProvider.directive.select - * - * @description - * HTML `SELECT` element with angular data-binding. - * - * # `ng:options` - * - * Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>` - * elements for a `<select>` element using an array or an object obtained by evaluating the - * `ng:options` expression. - *˝˝ - * When an item in the select menu is select, the value of array element or object property - * represented by the selected option will be bound to the model identified by the `ng:model` attribute - * of the parent select element. - * - * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can - * be nested into the `<select>` element. This element will then represent `null` or "not selected" - * option. See example below for demonstration. - * - * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead - * of {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with - * `<option>` element because of the following reasons: - * - * * value attribute of the option element that we need to bind to requires a string, but the - * source of data for the iteration might be in a form of array containing objects instead of - * strings - * * {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat} unrolls after the select binds causing - * incorect rendering on most browsers. - * * binding to a value not in list confuses most browsers. - * - * @param {string} name assignable expression to data-bind to. - * @param {string=} required The widget is considered valid only if value is entered. - * @param {comprehension_expression=} ng:options in one of the following forms: - * - * * for array data sources: - * * `label` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * for object data sources: - * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`group by`** `group` - * **`for` `(`**`key`**`,`** `value`**`) in`** `object` - * - * Where: - * - * * `array` / `object`: an expression which evaluates to an array / object to iterate over. - * * `value`: local variable which will refer to each item in the `array` or each property value - * of `object` during iteration. - * * `key`: local variable which will refer to a property name in `object` during iteration. - * * `label`: The result of this expression will be the label for `<option>` element. The - * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). - * * `select`: The result of this expression will be bound to the model of the parent `<select>` - * element. If not specified, `select` expression will default to `value`. - * * `group`: The result of this expression will be used to group options using the `<optgroup>` - * DOM element. - * - * @example - <doc:example> - <doc:source> - <script> - function MyCntrl($scope) { - $scope.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - $scope.color = $scope.colors[2]; // red - } - </script> - <div ng:controller="MyCntrl"> - <ul> - <li ng:repeat="color in colors"> - Name: <input ng:model="color.name"> - [<a href ng:click="colors.$remove(color)">X</a>] - </li> - <li> - [<a href ng:click="colors.push({})">add</a>] - </li> - </ul> - <hr/> - Color (null not allowed): - <select ng:model="color" ng:options="c.name for c in colors"></select><br> - - Color (null allowed): - <div class="nullable"> - <select ng:model="color" ng:options="c.name for c in colors"> - <option value="">-- chose color --</option> - </select> - </div><br/> - - Color grouped by shade: - <select ng:model="color" ng:options="c.name group by c.shade for c in colors"> - </select><br/> - - - Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br> - <hr/> - Currently selected: {{ {selected_color:color} }} - <div style="border:solid 1px black; height:20px" - ng:style="{'background-color':color.name}"> - </div> - </div> - </doc:source> - <doc:scenario> - it('should check ng:options', function() { - expect(binding('{selected_color:color}')).toMatch('red'); - select('color').option('0'); - expect(binding('{selected_color:color}')).toMatch('black'); - using('.nullable').select('color').option(''); - expect(binding('{selected_color:color}')).toMatch('null'); - }); - </doc:scenario> - </doc:example> - */ - -var ngOptionsDirective = valueFn({ terminal: true }); -var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777 - var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/; - - return { - restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (!ctrl) return; - - var multiple = attr.multiple, - optionsExp = attr.ngOptions; - - // required validator - if (multiple && (attr.required || attr.ngRequired)) { - var requiredValidator = function(value) { - ctrl.setValidity('REQUIRED', !attr.required || (value && value.length)); - return value; - }; - - ctrl.parsers.push(requiredValidator); - ctrl.formatters.unshift(requiredValidator); - - attr.$observe('required', function() { - requiredValidator(ctrl.viewValue); - }); - } - - if (optionsExp) Options(scope, element, ctrl); - else if (multiple) Multiple(scope, element, ctrl); - else Single(scope, element, ctrl); - - - //////////////////////////// - - - - function Single(scope, selectElement, ctrl) { - ctrl.render = function() { - selectElement.val(ctrl.viewValue); - }; - - selectElement.bind('change', function() { - scope.$apply(function() { - ctrl.touch(); - ctrl.read(selectElement.val()); - }); - }); - } - - function Multiple(scope, selectElement, ctrl) { - ctrl.render = function() { - var items = new HashMap(ctrl.viewValue); - forEach(selectElement.children(), function(option) { - option.selected = isDefined(items.get(option.value)); - }); - }; - - selectElement.bind('change', function() { - scope.$apply(function() { - var array = []; - forEach(selectElement.children(), function(option) { - if (option.selected) { - array.push(option.value); - } - }); - ctrl.touch(); - ctrl.read(array); - }); - }); - } - - function Options(scope, selectElement, ctrl) { - var match; - - if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '" + optionsExp + "'."); - } - - var displayFn = $parse(match[2] || match[1]), - valueName = match[4] || match[6], - keyName = match[5], - groupByFn = $parse(match[3] || ''), - valueFn = $parse(match[2] ? match[1] : valueName), - valuesFn = $parse(match[7]), - // we can't just jqLite('<option>') since jqLite is not smart enough - // to create it in <select> and IE barfs otherwise. - optionTemplate = jqLite(document.createElement('option')), - optGroupTemplate = jqLite(document.createElement('optgroup')), - nullOption = false, // if false then user will not be able to select it - // This is an array of array of existing option groups in DOM. We try to reuse these if possible - // optionGroupsCache[0] is the options with no option group - // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element - optionGroupsCache = [[{element: selectElement, label:''}]]; - - // find existing special options - forEach(selectElement.children(), function(option) { - if (option.value == '') { - // developer declared null option, so user should be able to select it - nullOption = jqLite(option).remove(); - // compile the element since there might be bindings in it - $compile(nullOption)(scope); - } - }); - selectElement.html(''); // clear contents - - selectElement.bind('change', function() { - scope.$apply(function() { - var optionGroup, - collection = valuesFn(scope) || [], - locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength; - - if (multiple) { - value = []; - for (groupIndex = 0, groupLength = optionGroupsCache.length; - groupIndex < groupLength; - groupIndex++) { - // list of options for that group. (first item has the parent) - optionGroup = optionGroupsCache[groupIndex]; - - for(index = 1, length = optionGroup.length; index < length; index++) { - if ((optionElement = optionGroup[index].element)[0].selected) { - key = optionElement.val(); - if (keyName) locals[keyName] = key; - locals[valueName] = collection[key]; - value.push(valueFn(scope, locals)); - } - } - } - } else { - key = selectElement.val(); - if (key == '?') { - value = undefined; - } else if (key == ''){ - value = null; - } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); - } - } - ctrl.touch(); - - if (ctrl.viewValue !== value) { - ctrl.read(value); - } - }); - }); - - ctrl.render = render; - - // TODO(vojta): can't we optimize this ? - scope.$watch(render); - - function render() { - var optionGroups = {'':[]}, // Temporary location for the option groups before we render them - optionGroupNames = [''], - optionGroupName, - optionGroup, - option, - existingParent, existingOptions, existingOption, - modelValue = ctrl.modelValue, - values = valuesFn(scope) || [], - keys = keyName ? sortedKeys(values) : values, - groupLength, length, - groupIndex, index, - locals = {}, - selected, - selectedSet = false, // nothing is selected yet - lastElement, - element; - - if (multiple) { - selectedSet = new HashMap(modelValue); - } else if (modelValue === null || nullOption) { - // if we are not multiselect, and we are null then we have to add the nullOption - optionGroups[''].push({selected:modelValue === null, id:'', label:''}); - selectedSet = true; - } - - // We now build up the list of options we need (we merge later) - for (index = 0; length = keys.length, index < length; index++) { - locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; - optionGroupName = groupByFn(scope, locals) || ''; - if (!(optionGroup = optionGroups[optionGroupName])) { - optionGroup = optionGroups[optionGroupName] = []; - optionGroupNames.push(optionGroupName); - } - if (multiple) { - selected = selectedSet.remove(valueFn(scope, locals)) != undefined; - } else { - selected = modelValue === valueFn(scope, locals); - selectedSet = selectedSet || selected; // see if at least one item is selected - } - optionGroup.push({ - id: keyName ? keys[index] : index, // either the index into array or key from object - label: displayFn(scope, locals) || '', // what will be seen by the user - selected: selected // determine if we should be selected - }); - } - if (!multiple && !selectedSet) { - // nothing was selected, we have to insert the undefined item - optionGroups[''].unshift({id:'?', label:'', selected:true}); - } - - // Now we need to update the list of DOM nodes to match the optionGroups we computed above - for (groupIndex = 0, groupLength = optionGroupNames.length; - groupIndex < groupLength; - groupIndex++) { - // current option group name or '' if no group - optionGroupName = optionGroupNames[groupIndex]; - - // list of options for that group. (first item has the parent) - optionGroup = optionGroups[optionGroupName]; - - if (optionGroupsCache.length <= groupIndex) { - // we need to grow the optionGroups - existingParent = { - element: optGroupTemplate.clone().attr('label', optionGroupName), - label: optionGroup.label - }; - existingOptions = [existingParent]; - optionGroupsCache.push(existingOptions); - selectElement.append(existingParent.element); - } else { - existingOptions = optionGroupsCache[groupIndex]; - existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element - - // update the OPTGROUP label if not the same. - if (existingParent.label != optionGroupName) { - existingParent.element.attr('label', existingParent.label = optionGroupName); - } - } - - lastElement = null; // start at the begining - for(index = 0, length = optionGroup.length; index < length; index++) { - option = optionGroup[index]; - if ((existingOption = existingOptions[index+1])) { - // reuse elements - lastElement = existingOption.element; - if (existingOption.label !== option.label) { - lastElement.text(existingOption.label = option.label); - } - if (existingOption.id !== option.id) { - lastElement.val(existingOption.id = option.id); - } - if (existingOption.element.selected !== option.selected) { - lastElement.prop('selected', (existingOption.selected = option.selected)); - } - } else { - // grow elements - - // if it's a null option - if (option.id === '' && nullOption) { - // put back the pre-compiled element - element = nullOption; - } else { - // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but - // in this version of jQuery on some browser the .text() returns a string - // rather then the element. - (element = optionTemplate.clone()) - .val(option.id) - .attr('selected', option.selected) - .text(option.label); - } - - existingOptions.push(existingOption = { - element: element, - label: option.label, - id: option.id, - selected: option.selected - }); - if (lastElement) { - lastElement.after(element); - } else { - existingParent.element.append(element); - } - lastElement = element; - } - } - // remove any excessive OPTIONs in a group - index++; // increment since the existingOptions[0] is parent element not OPTION - while(existingOptions.length > index) { - existingOptions.pop().element.remove(); - } - } - // remove any excessive OPTGROUPs from select - while(optionGroupsCache.length > groupIndex) { - optionGroupsCache.pop()[0].element.remove(); - } - }; - } - } - } -}]; - -var optionDirective = ['$interpolate', function($interpolate) { - return { - restrict: 'E', - priority: 100, - compile: function(element, attr) { - if (isUndefined(attr.value)) { - var interpolateFn = $interpolate(element.text(), true); - if (interpolateFn) { - return function (scope, element, attr) { - scope.$watch(interpolateFn, function(value) { - attr.$set('value', value); - }); - } - } else { - attr.$set('value', element.text()); - } - } - } - } -}]; |
